From f0eefdd0b58b8f2a859637d3f1faeae50c60af09 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 6 Dec 2018 08:10:21 -0800 Subject: [PATCH 01/62] Replace the informal "array" subtype map with a more formal "SubtypeMap" object Summary: Ref T13222. Ref T12588. See PHI683. To make "Create Subtask..." fancier, we need slightly more logic around subtype maps. Upgrade the plain old array into a proper object so it can have relevant methods, notably "get a list of valid child subtypes for some parent subtype". Test Plan: Created and edited tasks, changed task subtypes. Grepped for affected symbols (`newEditEngineSubtypeMap`, `newSubtypeMap`). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T12588 Differential Revision: https://secure.phabricator.com/D19852 --- src/__phutil_library_map__.php | 2 + .../query/ManiphestTaskSearchEngine.php | 2 +- .../maniphest/storage/ManiphestTask.php | 2 +- .../storage/ManiphestTransaction.php | 5 ++- .../ManiphestTaskSubtypeDatasource.php | 2 +- .../maniphest/view/ManiphestTaskListView.php | 3 -- ...itEngineConfigurationSubtypeController.php | 3 +- .../PhabricatorEditEngineSubtype.php | 2 +- .../PhabricatorEditEngineSubtypeMap.php | 42 +++++++++++++++++++ ...habricatorApplicationTransactionEditor.php | 2 +- ...abricatorEditEngineConfigurationEditor.php | 2 +- .../PhabricatorSubtypeEditEngineExtension.php | 4 +- 12 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e4ffb74b0f..aeba5e18ae 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2976,6 +2976,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php', 'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php', 'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php', + 'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php', 'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php', 'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php', 'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php', @@ -8729,6 +8730,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineSubtype' => 'Phobject', + 'PhabricatorEditEngineSubtypeMap' => 'Phobject', 'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase', 'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditField' => 'Phobject', diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index 437362fa41..f4a1f2b344 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -47,7 +47,7 @@ final class ManiphestTaskSearchEngine // Hide the "Subtypes" constraint from the web UI if the install only // defines one task subtype, since it isn't of any use in this case. $subtype_map = id(new ManiphestTask())->newEditEngineSubtypeMap(); - $hide_subtypes = (count($subtype_map) == 1); + $hide_subtypes = ($subtype_map->getCount() == 1); return array( id(new PhabricatorOwnersSearchField()) diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index a93fe58c3f..cdac563a14 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -573,7 +573,7 @@ final class ManiphestTask extends ManiphestDAO public function newSubtypeObject() { $subtype_key = $this->getEditEngineSubtype(); $subtype_map = $this->newEditEngineSubtypeMap(); - return idx($subtype_map, $subtype_key); + return $subtype_map->getSubtype($subtype_key); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index e2739c1d09..297bb970d1 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -214,11 +214,12 @@ final class ManiphestTransaction public function renderSubtypeName($value) { $object = $this->getObject(); $map = $object->newEditEngineSubtypeMap(); - if (!isset($map[$value])) { + + if (!$map->isValidSubtype($value)) { return $value; } - return $map[$value]->getName(); + return $map->getSubtype($value)->getName(); } } diff --git a/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php b/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php index c061c694e4..cfa5592ccc 100644 --- a/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php +++ b/src/applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php @@ -28,7 +28,7 @@ final class ManiphestTaskSubtypeDatasource $results = array(); $subtype_map = id(new ManiphestTask())->newEditEngineSubtypeMap(); - foreach ($subtype_map as $key => $subtype) { + foreach ($subtype_map->getSubtypes() as $key => $subtype) { $result = id(new PhabricatorTypeaheadResult()) ->setIcon($subtype->getIcon()) diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php index ba17b8e25d..e7128fb850 100644 --- a/src/applications/maniphest/view/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/ManiphestTaskListView.php @@ -56,9 +56,6 @@ final class ManiphestTaskListView extends ManiphestView { Javelin::initBehavior('maniphest-list-editor'); } - $subtype_map = id(new ManiphestTask()) - ->newEditEngineSubtypeMap(); - foreach ($this->tasks as $task) { $item = id(new PHUIObjectItemView()) ->setUser($this->getUser()) diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php index 92bc34c72b..9ce8d7a0e5 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php @@ -61,8 +61,7 @@ Choose the object **subtype** that this form should create and edit. EOTEXT ); - $map = $engine->newSubtypeMap(); - $map = mpull($map, 'getName'); + $map = $engine->newSubtypeMap()->getDisplayMap(); $form = id(new AphrontFormView()) ->setUser($viewer) diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php index df367955af..23fac106d7 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php @@ -182,7 +182,7 @@ final class PhabricatorEditEngineSubtype $map[$key] = $subtype; } - return $map; + return new PhabricatorEditEngineSubtypeMap($map); } } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php new file mode 100644 index 0000000000..cd8e0674ae --- /dev/null +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php @@ -0,0 +1,42 @@ +subtypes = $subtypes; + } + + public function getDisplayMap() { + return mpull($this->subtypes, 'getName'); + } + + public function getCount() { + return count($this->subtypes); + } + + public function isValidSubtype($subtype_key) { + return isset($this->subtypes[$subtype_key]); + } + + public function getSubtypes() { + return $this->subtypes; + } + + public function getSubtype($subtype_key) { + if (!$this->isValidSubtype($subtype_key)) { + throw new Exception( + pht( + 'Subtype key "%s" does not identify a valid subtype.', + $subtype_key)); + } + + return $this->subtypes[$subtype_key]; + } + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index a77b7f84a4..f354de6a1a 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -2541,7 +2541,7 @@ abstract class PhabricatorApplicationTransactionEditor continue; } - if (!isset($map[$new])) { + if (!$map->isValidSubtype($new)) { $errors[] = new PhabricatorApplicationTransactionValidationError( $transaction_type, pht('Invalid'), diff --git a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php index d394e87266..ccadf9b819 100644 --- a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php +++ b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php @@ -64,7 +64,7 @@ final class PhabricatorEditEngineConfigurationEditor foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); - if (isset($map[$new])) { + if ($map->isValidSubtype($new)) { continue; } diff --git a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php index 7d32545416..d0b6d017f3 100644 --- a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php @@ -31,7 +31,7 @@ final class PhabricatorSubtypeEditEngineExtension $subtype_type = PhabricatorTransactions::TYPE_SUBTYPE; $map = $object->newEditEngineSubtypeMap(); - $options = mpull($map, 'getName'); + $options = $map->getDisplayMap(); $subtype_field = id(new PhabricatorSelectEditField()) ->setKey(self::EDITKEY) @@ -45,7 +45,7 @@ final class PhabricatorSubtypeEditEngineExtension // If subtypes are configured, enable changing them from the bulk editor // and comment action stack. - if (count($map) > 1) { + if ($map->getCount() > 1) { $subtype_field ->setBulkEditLabel(pht('Change subtype to')) ->setCommentActionLabel(pht('Change Subtype')) From b88a87c43a28a6948a4fd95e7f12a5e4ffdd22c0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 5 Dec 2018 05:49:09 -0800 Subject: [PATCH 02/62] Address a transaction issue with some audit actions not applying correctly Summary: See . In D19842, I changed `PhabricatorEditField->shouldGenerateTransactionsFromComment()`. - Previously, it bailed on `getIsConduitOnly()`. - After the patch, it bails on a missing `getCommentActionLabel()`. The old code was actually wrong, and it was previously possible to apply possibly-invalid actions in some cases (or, at least, sneak them through this layer: they would only actually apply if not validated properly). In practice, it let a different bug through: we sometimes loaded commits without loading their audit authority, so testing whether the viewer could "Accept" the commit or not (or take some other actions like "Raise Concern") would always fail and throw an exception: "Trying to access data not attached to this object..." Fixing the insufficiently-strict transaction generation code exposed the "authority not attached" bug, which caused some actions to fail to generate transactions. This appeared in the UI as either an unhelpful error ("You can't post an empty comment") or an action with no effect. The unhelpful error was because we show that error if you aren't taking any //other// actions, and we wouldn't generate an "Accept" action because of the interaction of these bugs, so the code thought you were just posting an empty comment. Test Plan: Without leaving comments, accepted and rejected commits. No more error messages, and actions took effect. Reviewers: amckinley Reviewed By: amckinley Subscribers: stephan.senkbeil, hskiba Differential Revision: https://secure.phabricator.com/D19845 --- .../audit/editor/PhabricatorAuditEditor.php | 4 - .../controller/DiffusionCommitController.php | 2 +- .../editor/DiffusionCommitEditEngine.php | 5 +- .../diffusion/query/DiffusionCommitQuery.php | 92 ++++++++++++++++++- .../storage/PhabricatorRepositoryCommit.php | 80 +++------------- 5 files changed, 108 insertions(+), 75 deletions(-) diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index cfa84b8bd7..3f65cc80f8 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -59,10 +59,6 @@ final class PhabricatorAuditEditor $this->oldAuditStatus = $object->getAuditStatus(); - $object->loadAndAttachAuditAuthority( - $this->getActor(), - $this->getActingAsPHID()); - return parent::expandTransactions($object, $xactions); } diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 283278fcf8..5621b1fa12 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -45,6 +45,7 @@ final class DiffusionCommitController extends DiffusionController { ->withIdentifiers(array($commit_identifier)) ->needCommitData(true) ->needAuditRequests(true) + ->needAuditAuthority(array($viewer)) ->setLimit(100) ->needIdentities(true) ->execute(); @@ -111,7 +112,6 @@ final class DiffusionCommitController extends DiffusionController { } $audit_requests = $commit->getAudits(); - $commit->loadAndAttachAuditAuthority($viewer); $commit_data = $commit->getCommitData(); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); diff --git a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php index 3470d02f92..8392504def 100644 --- a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php @@ -43,9 +43,12 @@ final class DiffusionCommitEditEngine } protected function newObjectQuery() { + $viewer = $this->getViewer(); + return id(new DiffusionCommitQuery()) ->needCommitData(true) - ->needAuditRequests(true); + ->needAuditRequests(true) + ->needAuditAuthority(array($viewer)); } protected function getEditorURI() { diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index cf8c25baf1..05072e07c7 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -17,6 +17,7 @@ final class DiffusionCommitQuery private $unreachable; private $needAuditRequests; + private $needAuditAuthority; private $auditIDs; private $auditorPHIDs; private $epochMin; @@ -121,6 +122,12 @@ final class DiffusionCommitQuery return $this; } + public function needAuditAuthority(array $users) { + assert_instances_of($users, 'PhabricatorUser'); + $this->needAuditAuthority = $users; + return $this; + } + public function withAuditIDs(array $ids) { $this->auditIDs = $ids; return $this; @@ -231,14 +238,27 @@ final class DiffusionCommitQuery } if (count($subqueries) > 1) { - foreach ($subqueries as $key => $subquery) { - $subqueries[$key] = '('.$subquery.')'; + $unions = null; + foreach ($subqueries as $subquery) { + if (!$unions) { + $unions = qsprintf( + $conn, + '(%Q)', + $subquery); + continue; + } + + $unions = qsprintf( + $conn, + '%Q UNION DISTINCT (%Q)', + $unions, + $subquery); } $query = qsprintf( $conn, '%Q %Q %Q', - implode(' UNION DISTINCT ', $subqueries), + $unions, $this->buildOrderClause($conn, true), $this->buildLimitClause($conn)); } else { @@ -423,6 +443,72 @@ final class DiffusionCommitQuery $commits); } + if ($this->needAuditAuthority) { + $authority_users = $this->needAuditAuthority; + + // NOTE: This isn't very efficient since we're running two queries per + // user, but there's currently no way to figure out authority for + // multiple users in one query. Today, we only ever request authority for + // a single user and single commit, so this has no practical impact. + + // NOTE: We're querying with the viewership of query viewer, not the + // actual users. If the viewer can't see a project or package, they + // won't be able to see who has authority on it. This is safer than + // showing them true authority, and should never matter today, but it + // also doesn't seem like a significant disclosure and might be + // reasonable to adjust later if it causes something weird or confusing + // to happen. + + $authority_map = array(); + foreach ($authority_users as $authority_user) { + $authority_phid = $authority_user->getPHID(); + if (!$authority_phid) { + continue; + } + + $result_phids = array(); + + // Users have authority over themselves. + $result_phids[] = $authority_phid; + + // Users have authority over packages they own. + $owned_packages = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withAuthorityPHIDs(array($authority_phid)) + ->execute(); + foreach ($owned_packages as $package) { + $result_phids[] = $package->getPHID(); + } + + // Users have authority over projects they're members of. + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($authority_phid)) + ->execute(); + foreach ($projects as $project) { + $result_phids[] = $project->getPHID(); + } + + $result_phids = array_fuse($result_phids); + + foreach ($commits as $commit) { + $attach_phids = $result_phids; + + // NOTE: When modifying your own commits, you act only on behalf of + // yourself, not your packages or projects. The idea here is that you + // can't accept your own commits. In the future, this might change or + // depend on configuration. + $author_phid = $commit->getAuthorPHID(); + if ($author_phid == $authority_phid) { + $attach_phids = array($author_phid); + $attach_phids = array_fuse($attach_phids); + } + + $commit->attachAuditAuthority($authority_user, $attach_phids); + } + } + } + return $commits; } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 5615b5231f..b5fe08b6e0 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -210,84 +210,32 @@ final class PhabricatorRepositoryCommit return $this->assertAttached($this->committerIdentity); } - public function loadAndAttachAuditAuthority( - PhabricatorUser $viewer, - $actor_phid = null) { + public function attachAuditAuthority( + PhabricatorUser $user, + array $authority) { - if ($actor_phid === null) { - $actor_phid = $viewer->getPHID(); + $user_phid = $user->getPHID(); + if (!$user->getPHID()) { + throw new Exception( + pht('You can not attach audit authority for a user with no PHID.')); } - // TODO: This method is a little weird and sketchy, but worlds better than - // what came before it. Eventually, this should probably live in a Query - // class. - - // Figure out which requests the actor has authority over: these are user - // requests where they are the auditor, and packages and projects they are - // a member of. - - if (!$actor_phid) { - $attach_key = $viewer->getCacheFragment(); - $phids = array(); - } else { - $attach_key = $actor_phid; - // At least currently, when modifying your own commits, you act only on - // behalf of yourself, not your packages/projects -- the idea being that - // you can't accept your own commits. This may change or depend on - // config. - $actor_is_author = ($actor_phid == $this->getAuthorPHID()); - if ($actor_is_author) { - $phids = array($actor_phid); - } else { - $phids = array(); - $phids[$actor_phid] = true; - - $owned_packages = id(new PhabricatorOwnersPackageQuery()) - ->setViewer($viewer) - ->withAuthorityPHIDs(array($actor_phid)) - ->execute(); - foreach ($owned_packages as $package) { - $phids[$package->getPHID()] = true; - } - - $projects = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withMemberPHIDs(array($actor_phid)) - ->execute(); - foreach ($projects as $project) { - $phids[$project->getPHID()] = true; - } - - $phids = array_keys($phids); - } - } - - $this->auditAuthorityPHIDs[$attach_key] = array_fuse($phids); + $this->auditAuthorityPHIDs[$user_phid] = $authority; return $this; } public function hasAuditAuthority( - PhabricatorUser $viewer, - PhabricatorRepositoryAuditRequest $audit, - $actor_phid = null) { + PhabricatorUser $user, + PhabricatorRepositoryAuditRequest $audit) { - if ($actor_phid === null) { - $actor_phid = $viewer->getPHID(); - } - - if (!$actor_phid) { - $attach_key = $viewer->getCacheFragment(); - } else { - $attach_key = $actor_phid; - } - - $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $attach_key); - - if (!$actor_phid) { + $user_phid = $user->getPHID(); + if (!$user_phid) { return false; } + $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $user_phid); + return isset($map[$audit->getAuditorPHID()]); } From 1029081b28051b7fb82d5eac89c6e40d1a6e6be4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 5 Dec 2018 06:12:08 -0800 Subject: [PATCH 03/62] Correct two straggling "%Q" + "implode(...)" callsites in Revision updates Summary: See . When creating or updating revisions, we do some manual query construction to update the affected path table. Update these queries to modern `qsprintf()`. Test Plan: Created and updated revisions affecting paths, no more logs in the webserver log. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19846 --- .../differential/editor/DifferentialTransactionEditor.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 7fa4cd61e2..853b024e27 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1367,9 +1367,9 @@ final class DifferentialTransactionEditor foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, - 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', + 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %LQ', $table->getTableName(), - implode(', ', $chunk)); + $chunk); } } @@ -1444,9 +1444,9 @@ final class DifferentialTransactionEditor if ($sql) { queryfx( $conn_w, - 'INSERT INTO %T (revisionID, type, hash) VALUES %Q', + 'INSERT INTO %T (revisionID, type, hash) VALUES %LQ', ArcanistDifferentialRevisionHash::TABLE_NAME, - implode(', ', $sql)); + $sql); } } From 1e4bdc39a11beb2fe39a53e13d35773786ef0306 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 5 Dec 2018 12:40:59 -0800 Subject: [PATCH 04/62] Add an "availaiblity" attachment for user.search Summary: Ref T13222. See PHI990. The older `user.query` supports availability information, but it isn't currently available in a modern way. Make it available. Test Plan: {F6048126} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19851 --- src/__phutil_library_map__.php | 2 + ...opleAvailabilitySearchEngineAttachment.php | 46 +++++++++++++++++++ .../people/storage/PhabricatorUser.php | 5 +- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index aeba5e18ae..cd260f497d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3761,6 +3761,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php', 'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php', 'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php', + 'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php', 'PhabricatorPeopleBadgesProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php', 'PhabricatorPeopleCommitsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php', 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', @@ -9640,6 +9641,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleApplication' => 'PhabricatorApplication', 'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorPeopleBadgesProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleCommitsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleController' => 'PhabricatorController', diff --git a/src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php b/src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php new file mode 100644 index 0000000000..f61ebe7f01 --- /dev/null +++ b/src/applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php @@ -0,0 +1,46 @@ +needAvailability(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + + $until = $object->getAwayUntil(); + if ($until) { + $until = (int)$until; + } else { + $until = null; + } + + $value = $object->getDisplayAvailability(); + if ($value === null) { + $value = PhabricatorCalendarEventInvitee::AVAILABILITY_AVAILABLE; + } + + $name = PhabricatorCalendarEventInvitee::getAvailabilityName($value); + $color = PhabricatorCalendarEventInvitee::getAvailabilityColor($value); + + $event_phid = $object->getAvailabilityEventPHID(); + + return array( + 'value' => $value, + 'until' => $until, + 'name' => $name, + 'color' => $color, + 'eventPHID' => $event_phid, + ); + } + +} diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 651d1b75a6..7717fbf18c 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1466,7 +1466,10 @@ final class PhabricatorUser } public function getConduitSearchAttachments() { - return array(); + return array( + id(new PhabricatorPeopleAvailabilitySearchEngineAttachment()) + ->setAttachmentKey('availability'), + ); } From bba4186005917d7325e9888a5ff7b989061889f9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 5 Dec 2018 11:51:15 -0800 Subject: [PATCH 05/62] Allow "bin/repository thaw" to accept "--all-repositories" instead of a list of repositories Summary: Ref T13222. See PHI992. If you've lost an entire cluster (or have lost a device and are willing to make broad assumptions about the state the device was in) you currently have to `xargs` to thaw everything or do something else creative. Since this workflow is broadly reasonable, provide an easier way to accomplish the goal. Test Plan: - Ran with `--all-repositories`, a list of repositories, both (error) and neither (error). - Saw a helpful new list of affected repositories. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19849 --- ...icatorRepositoryManagementThawWorkflow.php | 66 +++++++++++++++++-- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php index 4c4eed85b9..ed2e1df3e2 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php @@ -32,6 +32,12 @@ final class PhabricatorRepositoryManagementThawWorkflow 'name' => 'force', 'help' => pht('Run operations without asking for confirmation.'), ), + array( + 'name' => 'all-repositories', + 'help' => pht( + 'Apply the promotion or demotion to all repositories hosted '. + 'on the device.'), + ), array( 'name' => 'repositories', 'wildcard' => true, @@ -42,12 +48,6 @@ final class PhabricatorRepositoryManagementThawWorkflow 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 thaw.')); - } - $promote = $args->getArg('promote'); $demote = $args->getArg('demote'); @@ -72,6 +72,60 @@ final class PhabricatorRepositoryManagementThawWorkflow pht('No device "%s" exists.', $device_name)); } + $repository_names = $args->getArg('repositories'); + $all_repositories = $args->getArg('all-repositories'); + if ($repository_names && $all_repositories) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a list of repositories or "--all-repositories", '. + 'but not both.')); + } else if (!$repository_names && !$all_repositories) { + throw new PhutilArgumentUsageException( + pht( + 'Select repositories to affect by providing a list of repositories '. + 'or using the "--all-repositories" flag.')); + } + + if ($repository_names) { + $repositories = $this->loadRepositories($args, 'repositories'); + if (!$repositories) { + throw new PhutilArgumentUsageException( + pht('Specify one or more repositories to thaw.')); + } + } else { + $repositories = array(); + + $services = id(new AlmanacServiceQuery()) + ->setViewer($viewer) + ->withDevicePHIDs(array($device->getPHID())) + ->execute(); + if ($services) { + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withAlmanacServicePHIDs(mpull($services, 'getPHID')) + ->execute(); + } + + if (!$repositories) { + throw new PhutilArgumentUsageException( + pht('There are no repositories on the selected device.')); + } + } + + $display_list = new PhutilConsoleList(); + foreach ($repositories as $repository) { + $display_list->addItem( + pht( + '%s %s', + $repository->getMonogram(), + $repository->getName())); + } + + echo tsprintf( + "%s\n\n%B\n", + pht('These repositories will be thawed:'), + $display_list->drawConsoleString()); + if ($promote) { $risk_message = pht( 'Promoting a device can cause the loss of any repository data which '. From 1a6a0181a8985371fe1470cbea24463885850668 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 5 Dec 2018 12:22:14 -0800 Subject: [PATCH 06/62] Allow "bin/repository thaw --demote" to demote an entire service, not just a single device Summary: Ref T13222. See PHI992. If you lose an entire cluster, you may want to aggressively demote it out of existence. You currently need to `xargs` your way through this. Allow `--demote `, which demotes all devices in a service. Test Plan: Demoted with `--demote ` and `--demote `. Hit the `--promote service` error. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19850 --- ...icatorRepositoryManagementThawWorkflow.php | 263 ++++++++++-------- .../user/cluster/cluster_repositories.diviner | 12 + 2 files changed, 164 insertions(+), 111 deletions(-) diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php index ed2e1df3e2..3743045c34 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php @@ -15,10 +15,11 @@ final class PhabricatorRepositoryManagementThawWorkflow array( array( 'name' => 'demote', - 'param' => 'device', + 'param' => 'device/service', 'help' => pht( - 'Demote a device, discarding local changes. Clears stuck '. - 'write locks and recovers from lost leaders.'), + 'Demote a device (or all devices in a service) discarding '. + 'local changes. Clears stuck write locks and recovers from '. + 'lost leaders.'), ), array( 'name' => 'promote', @@ -61,15 +62,53 @@ final class PhabricatorRepositoryManagementThawWorkflow pht('Specify either --promote or --demote, but not both.')); } - $device_name = nonempty($promote, $demote); + $target_name = nonempty($promote, $demote); - $device = id(new AlmanacDeviceQuery()) + $devices = id(new AlmanacDeviceQuery()) ->setViewer($viewer) - ->withNames(array($device_name)) - ->executeOne(); - if (!$device) { - throw new PhutilArgumentUsageException( - pht('No device "%s" exists.', $device_name)); + ->withNames(array($target_name)) + ->execute(); + if (!$devices) { + $service = id(new AlmanacServiceQuery()) + ->setViewer($viewer) + ->withNames(array($target_name)) + ->executeOne(); + + if (!$service) { + throw new PhutilArgumentUsageException( + pht('No device or service named "%s" exists.', $target_name)); + } + + if ($promote) { + throw new PhutilArgumentUsageException( + pht( + 'You can not "--promote" an entire service ("%s"). Only a single '. + 'device may be promoted.', + $target_name)); + } + + $bindings = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withServicePHIDs(array($service->getPHID())) + ->execute(); + if (!$bindings) { + throw new PhutilArgumentUsageException( + pht( + 'Service "%s" is not bound to any devices.', + $target_name)); + } + + $interfaces = id(new AlmanacInterfaceQuery()) + ->setViewer($viewer) + ->withPHIDs(mpull($bindings, 'getInterfacePHID')) + ->execute(); + + $device_phids = mpull($interfaces, 'getDevicePHID'); + + $devices = id(new AlmanacDeviceQuery()) + ->setViewer($viewer) + ->withPHIDs($device_phids) + ->execute(); } $repository_names = $args->getArg('repositories'); @@ -97,7 +136,7 @@ final class PhabricatorRepositoryManagementThawWorkflow $services = id(new AlmanacServiceQuery()) ->setViewer($viewer) - ->withDevicePHIDs(array($device->getPHID())) + ->withDevicePHIDs(mpull($devices, 'getPHID')) ->execute(); if ($services) { $repositories = id(new PhabricatorRepositoryQuery()) @@ -108,7 +147,7 @@ final class PhabricatorRepositoryManagementThawWorkflow if (!$repositories) { throw new PhutilArgumentUsageException( - pht('There are no repositories on the selected device.')); + pht('There are no repositories on the selected device or service.')); } } @@ -150,126 +189,128 @@ final class PhabricatorRepositoryManagementThawWorkflow pht('User aborted the workflow.')); } - foreach ($repositories as $repository) { - $repository_phid = $repository->getPHID(); + foreach ($devices as $device) { + foreach ($repositories as $repository) { + $repository_phid = $repository->getPHID(); - $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock( - $repository_phid); + $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock( + $repository_phid); - echo tsprintf( - "%s\n", - pht( - 'Waiting to acquire write lock for "%s"...', - $repository->getDisplayName())); + echo tsprintf( + "%s\n", + pht( + 'Waiting to acquire write lock for "%s"...', + $repository->getDisplayName())); - $write_lock->lock(phutil_units('5 minutes in seconds')); - try { + $write_lock->lock(phutil_units('5 minutes in seconds')); + try { - $service = $repository->loadAlmanacService(); - if (!$service) { - throw new PhutilArgumentUsageException( - pht( - 'Repository "%s" is not a cluster repository: it is not '. - 'bound to an Almanac service.', - $repository->getDisplayName())); - } - - if ($promote) { - // You can only promote active devices. (You may demote active or - // inactive devices.) - $bindings = $service->getActiveBindings(); - $bindings = mpull($bindings, null, 'getDevicePHID'); - if (empty($bindings[$device->getPHID()])) { + $service = $repository->loadAlmanacService(); + if (!$service) { throw new PhutilArgumentUsageException( pht( - 'Repository "%s" has no active binding to device "%s". Only '. - 'actively bound devices can be promoted.', - $repository->getDisplayName(), - $device->getName())); + 'Repository "%s" is not a cluster repository: it is not '. + 'bound to an Almanac service.', + $repository->getDisplayName())); } - $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( - $repository->getPHID()); - $versions = mpull($versions, null, 'getDevicePHID'); - - // Before we promote, make sure there are no outstanding versions on - // devices with inactive bindings. If there are, you need to demote - // these first. - $inactive = array(); - foreach ($versions as $device_phid => $version) { - if (isset($bindings[$device_phid])) { - continue; + if ($promote) { + // You can only promote active devices. (You may demote active or + // inactive devices.) + $bindings = $service->getActiveBindings(); + $bindings = mpull($bindings, null, 'getDevicePHID'); + if (empty($bindings[$device->getPHID()])) { + throw new PhutilArgumentUsageException( + pht( + 'Repository "%s" has no active binding to device "%s". '. + 'Only actively bound devices can be promoted.', + $repository->getDisplayName(), + $device->getName())); } - $inactive[$device_phid] = $version; - } - if ($inactive) { - $handles = $viewer->loadHandles(array_keys($inactive)); + $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( + $repository->getPHID()); + $versions = mpull($versions, null, 'getDevicePHID'); - $handle_list = iterator_to_array($handles); - $handle_list = mpull($handle_list, 'getName'); - $handle_list = implode(', ', $handle_list); + // Before we promote, make sure there are no outstanding versions + // on devices with inactive bindings. If there are, you need to + // demote these first. + $inactive = array(); + foreach ($versions as $device_phid => $version) { + if (isset($bindings[$device_phid])) { + continue; + } + $inactive[$device_phid] = $version; + } - throw new PhutilArgumentUsageException( + if ($inactive) { + $handles = $viewer->loadHandles(array_keys($inactive)); + + $handle_list = iterator_to_array($handles); + $handle_list = mpull($handle_list, 'getName'); + $handle_list = implode(', ', $handle_list); + + throw new PhutilArgumentUsageException( + pht( + 'Repository "%s" has versions on inactive devices. Demote '. + '(or reactivate) these devices before promoting a new '. + 'leader: %s.', + $repository->getDisplayName(), + $handle_list)); + } + + // Now, make sure there are no outstanding versions on devices with + // active bindings. These also need to be demoted (or promoting is + // a mistake or already happened). + $active = array_select_keys($versions, array_keys($bindings)); + if ($active) { + $handles = $viewer->loadHandles(array_keys($active)); + + $handle_list = iterator_to_array($handles); + $handle_list = mpull($handle_list, 'getName'); + $handle_list = implode(', ', $handle_list); + + throw new PhutilArgumentUsageException( + pht( + 'Unable to promote "%s" for repository "%s" because this '. + 'cluster already has one or more unambiguous leaders: %s.', + $device->getName(), + $repository->getDisplayName(), + $handle_list)); + } + + PhabricatorRepositoryWorkingCopyVersion::updateVersion( + $repository->getPHID(), + $device->getPHID(), + 0); + + echo tsprintf( + "%s\n", pht( - 'Repository "%s" has versions on inactive devices. Demote '. - '(or reactivate) these devices before promoting a new '. - 'leader: %s.', - $repository->getDisplayName(), - $handle_list)); - } - - // Now, make sure there are no outstanding versions on devices with - // active bindings. These also need to be demoted (or promoting is a - // mistake or already happened). - $active = array_select_keys($versions, array_keys($bindings)); - if ($active) { - $handles = $viewer->loadHandles(array_keys($active)); - - $handle_list = iterator_to_array($handles); - $handle_list = mpull($handle_list, 'getName'); - $handle_list = implode(', ', $handle_list); - - throw new PhutilArgumentUsageException( - pht( - 'Unable to promote "%s" for repository "%s" because this '. - 'cluster already has one or more unambiguous leaders: %s.', + 'Promoted "%s" to become a leader for "%s".', $device->getName(), - $repository->getDisplayName(), - $handle_list)); + $repository->getDisplayName())); } - PhabricatorRepositoryWorkingCopyVersion::updateVersion( - $repository->getPHID(), - $device->getPHID(), - 0); + if ($demote) { + PhabricatorRepositoryWorkingCopyVersion::demoteDevice( + $repository->getPHID(), + $device->getPHID()); - echo tsprintf( - "%s\n", - pht( - 'Promoted "%s" to become a leader for "%s".', - $device->getName(), - $repository->getDisplayName())); + echo tsprintf( + "%s\n", + pht( + 'Demoted "%s" from leadership of repository "%s".', + $device->getName(), + $repository->getDisplayName())); + } + } catch (Exception $ex) { + $write_lock->unlock(); + throw $ex; } - if ($demote) { - PhabricatorRepositoryWorkingCopyVersion::demoteDevice( - $repository->getPHID(), - $device->getPHID()); - - echo tsprintf( - "%s\n", - pht( - 'Demoted "%s" from leadership of repository "%s".', - $device->getName(), - $repository->getDisplayName())); - } - } catch (Exception $ex) { $write_lock->unlock(); - throw $ex; } - - $write_lock->unlock(); } return 0; diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner index cdf2641933..13307d0b79 100644 --- a/src/docs/user/cluster/cluster_repositories.diviner +++ b/src/docs/user/cluster/cluster_repositories.diviner @@ -433,6 +433,18 @@ If you do this, **you will lose unreplicated data**. You will discard any changes on the affected leaders which have not replicated to other devices in the cluster. +If you have lost an entire cluster and replaced it with new devices that you +have restored from backups, you can aggressively wipe all memory of the old +devices by using `--demote ` and `--all-repositories`. **This is +dangerous and discards all unreplicated data in any repository on any device.** + +``` +phabricator/ $ ./bin/repository thaw --demote repo.corp.net --all-repositories +``` + +After you do this, continue below to promote a leader and restore the cluster +to service. + Ambiguous Leaders ================= From c3206476a303556f19e8dc4c058030005172ca9b Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 07:44:23 -0800 Subject: [PATCH 07/62] Give "Autoclose Only" repository detail proper getters/setters Summary: Ref T13222. See D19829. We're inconsistent about using `getDetail()/setDetail()` to do some ad-hoc reads. Put this stuff in proper accessor methods. Also a couple of text fixes from D19850. Test Plan: Set, edited, and removed autoclose branches from a repository. Got sensible persistence and rendering behavior. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19856 --- .../editor/DiffusionRepositoryEditEngine.php | 3 +-- .../DiffusionRepositoryBranchesManagementPanel.php | 6 ++++-- .../PhabricatorRepositoryManagementThawWorkflow.php | 6 +++--- .../repository/storage/PhabricatorRepository.php | 11 ++++++++++- .../PhabricatorRepositoryAutocloseOnlyTransaction.php | 4 ++-- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php index 29dc588b45..e570b6debe 100644 --- a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php @@ -215,8 +215,7 @@ final class DiffusionRepositoryEditEngine $track_value = $object->getDetail('branch-filter', array()); $track_value = array_keys($track_value); - $autoclose_value = $object->getDetail('close-commits-filter', array()); - $autoclose_value = array_keys($autoclose_value); + $autoclose_value = $object->getAutocloseOnlyRules(); $automation_instructions = pht( "Configure **Repository Automation** to allow Phabricator to ". diff --git a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php index e3ace3a5a5..77a13c07a1 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php @@ -24,7 +24,7 @@ final class DiffusionRepositoryBranchesManagementPanel $has_any = $repository->getDetail('default-branch') || $repository->getDetail('branch-filter') || - $repository->getDetail('close-commits-filter'); + $repository->getAutocloseOnlyRules(); if ($has_any) { return 'fa-code-fork'; @@ -83,8 +83,10 @@ final class DiffusionRepositoryBranchesManagementPanel phutil_tag('em', array(), pht('Track All Branches'))); $view->addProperty(pht('Track Only'), $track_only); + $autoclose_rules = $repository->getAutocloseOnlyRules(); + $autoclose_rules = implode(', ', $autoclose_rules); $autoclose_only = nonempty( - $repository->getHumanReadableDetail('close-commits-filter', array()), + $autoclose_rules, phutil_tag('em', array(), pht('Autoclose On All Branches'))); $autoclose_disabled = false; diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php index 3743045c34..7ca725de81 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php @@ -15,11 +15,11 @@ final class PhabricatorRepositoryManagementThawWorkflow array( array( 'name' => 'demote', - 'param' => 'device/service', + 'param' => 'device|service', 'help' => pht( 'Demote a device (or all devices in a service) discarding '. - 'local changes. Clears stuck write locks and recovers from '. - 'lost leaders.'), + 'unsynchronized changes. Clears stuck write locks and recovers '. + 'from lost leaders.'), ), array( 'name' => 'promote', diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index c961e06f27..302ef82a48 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -244,7 +244,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO switch ($key) { case 'branch-filter': - case 'close-commits-filter': $value = array_keys($value); $value = implode(', ', $value); break; @@ -1202,6 +1201,16 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return null; } + public function getAutocloseOnlyRules() { + return array_keys($this->getDetail('close-commits-filter', array())); + } + + public function setAutocloseOnlyRules(array $rules) { + $rules = array_fill_keys($rules, true); + $this->setDetail('close-commits-filter', $rules); + return $this; + } + /* -( Repository URI Management )------------------------------------------ */ diff --git a/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php index feacc73c44..5ff17c4b05 100644 --- a/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php +++ b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php @@ -6,11 +6,11 @@ final class PhabricatorRepositoryAutocloseOnlyTransaction const TRANSACTIONTYPE = 'repo:autoclose-only'; public function generateOldValue($object) { - return array_keys($object->getDetail('close-commits-filter', array())); + return $object->getAutocloseOnlyRules(); } public function applyInternalEffects($object, $value) { - $object->setDetail('close-commits-filter', array_fill_keys($value, true)); + $object->setAutocloseOnlyRules($value); } public function getTitle() { From bf6c534b567a5defa33119de7b2c501d17fc0624 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 08:05:13 -0800 Subject: [PATCH 08/62] Give "Track Only" repository detail proper getters/setters Summary: Depends on D19856. Ref T13222. See D19829. Make access to "Track Only" slightly cleaner and more consistent.. Test Plan: Set, edited, and removed "Track Only" settings for a repository. Saw sensible persistence and display behaviors. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19857 --- .../editor/DiffusionRepositoryEditEngine.php | 4 +--- ...usionRepositoryBranchesManagementPanel.php | 8 ++++--- ...ionRepositorySubversionManagementPanel.php | 2 +- .../storage/PhabricatorRepository.php | 23 ++++++++----------- .../PhabricatorRepositoryTestCase.php | 6 +---- ...bricatorRepositoryTrackOnlyTransaction.php | 4 ++-- 6 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php index e570b6debe..8907a22ce5 100644 --- a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php @@ -212,9 +212,7 @@ final class DiffusionRepositoryEditEngine ->setObject($object) ->execute(); - $track_value = $object->getDetail('branch-filter', array()); - $track_value = array_keys($track_value); - + $track_value = $object->getTrackOnlyRules(); $autoclose_value = $object->getAutocloseOnlyRules(); $automation_instructions = pht( diff --git a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php index 77a13c07a1..bdd0a3f71d 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php @@ -23,7 +23,7 @@ final class DiffusionRepositoryBranchesManagementPanel $has_any = $repository->getDetail('default-branch') || - $repository->getDetail('branch-filter') || + $repository->getTrackOnlyRules() || $repository->getAutocloseOnlyRules(); if ($has_any) { @@ -74,12 +74,14 @@ final class DiffusionRepositoryBranchesManagementPanel ->setViewer($viewer); $default_branch = nonempty( - $repository->getHumanReadableDetail('default-branch'), + $repository->getDetail('default-branch'), phutil_tag('em', array(), $repository->getDefaultBranch())); $view->addProperty(pht('Default Branch'), $default_branch); + $track_only_rules = $repository->getTrackOnlyRules(); + $track_only_rules = implode(', ', $track_only_rules); $track_only = nonempty( - $repository->getHumanReadableDetail('branch-filter', array()), + $track_only_rules, phutil_tag('em', array(), pht('Track All Branches'))); $view->addProperty(pht('Track Only'), $track_only); diff --git a/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php index f87a5d9b85..2d95bc2292 100644 --- a/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php @@ -68,7 +68,7 @@ final class DiffusionRepositorySubversionManagementPanel ->setViewer($viewer); $default_branch = nonempty( - $repository->getHumanReadableDetail('svn-subpath'), + $repository->getDetail('svn-subpath'), phutil_tag('em', array(), pht('Import Entire Repository'))); $view->addProperty(pht('Import Only'), $default_branch); diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 302ef82a48..12c2b9e8a3 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -239,19 +239,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return idx($this->details, $key, $default); } - public function getHumanReadableDetail($key, $default = null) { - $value = $this->getDetail($key, $default); - - switch ($key) { - case 'branch-filter': - $value = array_keys($value); - $value = implode(', ', $value); - break; - } - - return $value; - } - public function setDetail($key, $value) { $this->details[$key] = $value; return $this; @@ -1211,6 +1198,16 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return $this; } + public function getTrackOnlyRules() { + return array_keys($this->getDetail('branch-filter', array())); + } + + public function setTrackOnlyRules(array $rules) { + $rules = array_fill_keys($rules, true); + $this->setDetail('branch-filter', $rules); + return $this; + } + /* -( Repository URI Management )------------------------------------------ */ diff --git a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php index f78c3374ed..ea83aa9d56 100644 --- a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php +++ b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php @@ -37,11 +37,7 @@ final class PhabricatorRepositoryTestCase $repo->shouldTrackBranch('imaginary'), pht('Track all branches by default.')); - $repo->setDetail( - 'branch-filter', - array( - 'master' => true, - )); + $repo->setTrackOnlyRules(array('master')); $this->assertTrue( $repo->shouldTrackBranch('master'), diff --git a/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php index 20ff1baf08..5fdc26e792 100644 --- a/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php +++ b/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php @@ -6,11 +6,11 @@ final class PhabricatorRepositoryTrackOnlyTransaction const TRANSACTIONTYPE = 'repo:track-only'; public function generateOldValue($object) { - return array_keys($object->getDetail('branch-filter', array())); + return $object->getTrackOnlyRules(); } public function applyInternalEffects($object, $value) { - $object->setDetail('branch-filter', array_fill_keys($value, true)); + $object->setTrackOnlyRules($value); } public function getTitle() { From 00a7071e2d1d3f25ec445430708b7a426298b2a1 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Mon, 10 Dec 2018 12:47:54 -0800 Subject: [PATCH 09/62] Fix handling of Phriction conduit edits Summary: See https://discourse.phabricator-community.org/t/conduit-method-phriction-edit-requires-title-while-the-docs-say-its-optional/2176. Make code consistent with documentation by not requiring either `content` or `title`. Test Plan: Hit the method via the UI and no longer got an error on missing `content` or `title` fields. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19862 --- .../conduit/PhrictionEditConduitAPIMethod.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php index 9d97ed7f8c..d1e400a485 100644 --- a/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php +++ b/src/applications/phriction/conduit/PhrictionEditConduitAPIMethod.php @@ -41,12 +41,19 @@ final class PhrictionEditConduitAPIMethod extends PhrictionConduitAPIMethod { } $xactions = array(); - $xactions[] = id(new PhrictionTransaction()) - ->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE) - ->setNewValue($request->getValue('title')); - $xactions[] = id(new PhrictionTransaction()) - ->setTransactionType(PhrictionDocumentContentTransaction::TRANSACTIONTYPE) - ->setNewValue($request->getValue('content')); + if ($request->getValue('title')) { + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType( + PhrictionDocumentTitleTransaction::TRANSACTIONTYPE) + ->setNewValue($request->getValue('title')); + } + + if ($request->getValue('content')) { + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType( + PhrictionDocumentContentTransaction::TRANSACTIONTYPE) + ->setNewValue($request->getValue('content')); + } $editor = id(new PhrictionTransactionEditor()) ->setActor($request->getUser()) From da4341cf8be5551832597c5e6611112984998088 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Mon, 10 Dec 2018 13:40:37 -0800 Subject: [PATCH 10/62] Make it less confusing to create root-level Phriction doc Summary: Without an existing root document, Phriction shows a nice little "fake" document as the landing page, which has its own nice "Edit this document" button. When showing that page, don't also render the standard "New Document" breadcrumb in the top right. That button always prompts first for a slug name, which is silly when the root document doesn't exist (because the slug name is required to be ''). Test Plan: Loaded Phriction with and without a root document. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19863 --- .../controller/PhrictionController.php | 22 ++++++++++++++----- .../PhrictionDocumentController.php | 5 +++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/applications/phriction/controller/PhrictionController.php b/src/applications/phriction/controller/PhrictionController.php index 51ffbcab1f..06f0102526 100644 --- a/src/applications/phriction/controller/PhrictionController.php +++ b/src/applications/phriction/controller/PhrictionController.php @@ -2,6 +2,14 @@ abstract class PhrictionController extends PhabricatorController { + private $showingWelcomeDocument = false; + + public function setShowingWelcomeDocument($show_welcome) { + $this->showingWelcomeDocument = $show_welcome; + return $this; + + } + public function buildSideNavView($for_app = false) { $user = $this->getRequest()->getUser(); @@ -37,12 +45,14 @@ abstract class PhrictionController extends PhabricatorController { ->setIcon('fa-home')); } - $crumbs->addAction( - id(new PHUIListItemView()) - ->setName(pht('New Document')) - ->setHref('/phriction/new/?slug='.$this->getDocumentSlug()) - ->setWorkflow(true) - ->setIcon('fa-plus-square')); + if (!$this->showingWelcomeDocument) { + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('New Document')) + ->setHref('/phriction/new/?slug='.$this->getDocumentSlug()) + ->setWorkflow(true) + ->setIcon('fa-plus-square')); + } return $crumbs; } diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 8da9807064..bf7282b35a 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -9,6 +9,7 @@ final class PhrictionDocumentController return true; } + public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $this->slug = $request->getURIData('slug'); @@ -35,15 +36,15 @@ final class PhrictionDocumentController ->needContent(true) ->executeOne(); if (!$document) { - $document = PhrictionDocument::initializeNewDocument($viewer, $slug); - if ($slug == '/') { $title = pht('Welcome to Phriction'); $subtitle = pht('Phriction is a simple and easy to use wiki for '. 'keeping track of documents and their changes.'); $page_title = pht('Welcome'); $create_text = pht('Edit this Document'); + $this->setShowingWelcomeDocument(true); + } else { $title = pht('No Document Here'); From d1bcdaeda467570066a7e9e7ddd565eee51bc50b Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 6 Dec 2018 08:25:25 -0800 Subject: [PATCH 11/62] Allow the "Create Subtask" workflow to prompt for a subtype selection, and prepare for customizable options Summary: Ref T13222. Ref T12588. See PHI683. Currently, "Create Subtask" always uses the first edit form that the user has access to for the same task subtype. (For example, if you "Create Subtask" from a "Bug", you get the first edit form for "Bugs".) I didn't want to go too crazy with the initial subtype implementation, but it seems like we're generally on firm ground and it's working fairly well: user requests are for more flexibility in using the system as implemented, not changes to the system or confusion/difficulty with any of the tradeoffs. Thus, I'm generally comfortable continuing to build it out in the same direction. To improve flexibility, I want to make the options from "Create Subtask" more flexible/configurable. I plan to let you specify that a given subtype (say, "Quest") prompts you with creation options for a set of other subtypes (say, "Objective"), or prompts you with a particular set of forms. If we end up with a single option, we just go into the current flow (directly to the edit form). If we end up with more than one option, we prompt the user to choose between them. This change is a first step toward this: - When building "Create Subtask", query for multiple forms. - The default behavior is now "prompt user to choose among create forms of the same subtype". Previously, it was "use the first edit form of the same subtype". This is a behavioral change. - The next change will make the selected forms configurable. - (I also plan to make the dialog itself less rough.) Test Plan: {F6051067} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T12588 Differential Revision: https://secure.phabricator.com/D19853 --- src/__phutil_library_map__.php | 2 + .../PhabricatorManiphestApplication.php | 1 + .../ManiphestTaskDetailController.php | 38 ++++++---- .../ManiphestTaskSubtaskController.php | 76 +++++++++++++++++++ .../editengine/PhabricatorEditEngine.php | 2 +- .../PhabricatorEditEngineSubtypeMap.php | 43 +++++++++++ 6 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 src/applications/maniphest/controller/ManiphestTaskSubtaskController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index cd260f497d..184c2a3d7c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1760,6 +1760,7 @@ phutil_register_library_map(array( 'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php', 'ManiphestTaskStatusTransaction' => 'applications/maniphest/xaction/ManiphestTaskStatusTransaction.php', 'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php', + 'ManiphestTaskSubtaskController' => 'applications/maniphest/controller/ManiphestTaskSubtaskController.php', 'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php', 'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php', 'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php', @@ -7347,6 +7348,7 @@ phutil_register_library_map(array( 'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase', 'ManiphestTaskStatusTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType', + 'ManiphestTaskSubtaskController' => 'ManiphestController', 'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskTestCase' => 'PhabricatorTestCase', 'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField', diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 1ed4096660..35c1efb6e8 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -52,6 +52,7 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { 'task/' => array( $this->getEditRoutePattern('edit/') => 'ManiphestTaskEditController', + 'subtask/(?P[1-9]\d*)/' => 'ManiphestTaskSubtaskController', ), 'subpriority/' => 'ManiphestSubpriorityController', ), diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index a11cc4d85f..0f96d76b91 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -281,29 +281,39 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setDisabled(!$can_edit) ->setWorkflow($workflow_edit)); - $edit_config = $edit_engine->loadDefaultEditConfiguration($task); - $can_create = (bool)$edit_config; + $subtype_map = $task->newEditEngineSubtypeMap(); + $subtask_options = $subtype_map->getCreateFormsForSubtype( + $edit_engine, + $task); - if ($can_create) { - $form_key = $edit_config->getIdentifier(); - $edit_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) + // If no forms are available, we want to show the user an error. + // If one form is available, we take them user directly to the form. + // If two or more forms are available, we give the user a choice. + + // The "subtask" controller handles the first case (no forms) and the + // third case (more than one form). In the case of one form, we link + // directly to the form. + $subtask_uri = "/task/subtask/{$id}/"; + $subtask_workflow = true; + + if (count($subtask_options) == 1) { + $subtask_form = head($subtask_options); + $form_key = $subtask_form->getIdentifier(); + $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) ->setQueryParam('parent', $id) ->setQueryParam('template', $id) ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); - $edit_uri = $this->getApplicationURI($edit_uri); - } else { - // TODO: This will usually give us a somewhat-reasonable error page, but - // could be a bit cleaner. - $edit_uri = "/task/edit/{$id}/"; - $edit_uri = $this->getApplicationURI($edit_uri); + $subtask_workflow = false; } + $subtask_uri = $this->getApplicationURI($subtask_uri); + $subtask_item = id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) - ->setHref($edit_uri) + ->setHref($subtask_uri) ->setIcon('fa-level-down') - ->setDisabled(!$can_create) - ->setWorkflow(!$can_create); + ->setDisabled(!$subtask_options) + ->setWorkflow($subtask_workflow); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, diff --git a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php new file mode 100644 index 0000000000..9da0ed4206 --- /dev/null +++ b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php @@ -0,0 +1,76 @@ +getViewer(); + $id = $request->getURIData('id'); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$task) { + return new Aphront404Response(); + } + + $cancel_uri = $task->getURI(); + + $edit_engine = id(new ManiphestEditEngine()) + ->setViewer($viewer) + ->setTargetObject($task); + + $subtype_map = $task->newEditEngineSubtypeMap(); + + $subtype_options = $subtype_map->getCreateFormsForSubtype( + $edit_engine, + $task); + + if (!$subtype_options) { + return $this->newDialog() + ->setTitle(pht('No Forms')) + ->appendParagraph( + pht( + 'You do not have access to any forms which can be used to '. + 'create a subtask.')) + ->addCancelButton($cancel_uri, pht('Close')); + } + + if ($request->isFormPost()) { + $form_key = $request->getStr('formKey'); + if (isset($subtype_options[$form_key])) { + $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) + ->setQueryParam('parent', $id) + ->setQueryParam('template', $id) + ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); + $subtask_uri = $this->getApplicationURI($subtask_uri); + + return id(new AphrontRedirectResponse()) + ->setURI($subtask_uri); + } + } + + $control = id(new AphrontFormRadioButtonControl()) + ->setName('formKey') + ->setLabel(pht('Subtype')); + + foreach ($subtype_options as $key => $subtype_form) { + $control->addButton( + $key, + $subtype_form->getDisplayName(), + null); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl($control); + + return $this->newDialog() + ->setTitle(pht('Choose Subtype')) + ->appendForm($form) + ->addSubmitButton(pht('Continue')) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 4e189df164..86cc014102 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -358,7 +358,7 @@ abstract class PhabricatorEditEngine return $this->editEngineConfiguration; } - private function newConfigurationQuery() { + public function newConfigurationQuery() { return id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($this->getViewer()) ->withEngineKeys(array($this->getEngineKey())); diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php index cd8e0674ae..c76911dbc3 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php @@ -39,4 +39,47 @@ final class PhabricatorEditEngineSubtypeMap return $this->subtypes[$subtype_key]; } + public function getCreateFormsForSubtype( + PhabricatorEditEngine $edit_engine, + PhabricatorEditEngineSubtypeInterface $object) { + + $subtype_key = $object->getEditEngineSubtype(); + $subtype = $this->getSubtype($subtype_key); + + // TODO: Allow subtype configuration to specify that children should be + // created from particular forms or subtypes. + $select_ids = array(); + $select_subtypes = array(); + + $query = $edit_engine->newConfigurationQuery() + ->withIsDisabled(false); + + if ($select_ids) { + $query->withIDs($select_ids); + } else { + // If we're selecting by subtype rather than selecting specific forms, + // only select create forms. + $query->withIsDefault(true); + + if ($select_subtypes) { + $query->withSubtypes($select_subtypes); + } else { + $query->withSubtypes(array($subtype_key)); + } + } + + $forms = $query->execute(); + $forms = mpull($forms, null, 'getIdentifier'); + + // If we're selecting by ID, respect the order specified in the + // constraint. Otherwise, use the create form sort order. + if ($select_ids) { + $forms = array_select_keys($forms, $select_ids) + $forms; + } else { + $forms = msort($forms, 'getCreateSortKey'); + } + + return $forms; + } + } From a6632f8c188a0b0b460b35003ccc0e41a45bb90e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 6 Dec 2018 11:42:53 -0800 Subject: [PATCH 12/62] Allow "maniphest.subtypes" to configure which options are presented by "Create Subtask" Summary: Ref T13222. Ref T12588. See PHI683. After D19853, "Create Subtask" may pop a dialog to let you choose between multiple forms. Allow users to configure which forms are available by using `maniphest.subtypes` to choose available children for each subtype. Users may either specify particular subtypes or specific forms. Test Plan: Configured "Quest" tasks to have "Objective" children, got appropriate prompt behavior. Used "subtypes" and "forms" to select forms; used "forms" to reorder forms. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T12588 Differential Revision: https://secure.phabricator.com/D19854 --- .../PhabricatorManiphestConfigOptions.php | 50 +++++++++++++++++ .../PhabricatorEditEngineSubtype.php | 54 +++++++++++++++++++ .../PhabricatorEditEngineSubtypeMap.php | 14 +++-- ...torEditEngineConfigurationSearchEngine.php | 33 +++++++++--- 4 files changed, 135 insertions(+), 16 deletions(-) diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index 4458dc3636..609db0d1b6 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -338,6 +338,8 @@ dictionary with these keys: - `tag` //Optional string.// Tag text for this subtype. - `color` //Optional string.// Display color for this subtype. - `icon` //Optional string.// Icon for the subtype. + - `children` //Optional map.// Configure options shown to the user when + they "Create Subtask". See below. Each subtype must have a unique key, and you must define a subtype with the key "%s", which is used as a default subtype. @@ -345,6 +347,54 @@ the key "%s", which is used as a default subtype. The tag text (`tag`) is used to set the text shown in the subtype tag on list views and workboards. If you do not configure it, the default subtype will have no subtype tag and other subtypes will use their name as tag text. + +The `children` key allows you to configure which options are presented to the +user when they "Create Subtask" from a task of this subtype. You can specify +these keys: + + - `subtypes`: //Optional list.// Show users creation forms for these + task subtypes. + - `forms`: //Optional list.// Show users these specific forms, + in order. + +If you don't specify either constraint, users will be shown creation forms +for the same subtype. + +For example, if you have a "quest" subtype and do not configure `children`, +users who click "Create Subtask" will be presented with all create forms for +"quest" tasks. + +If you want to present them with forms for a different task subtype or set of +subtypes instead, use `subtypes`: + +``` + { + ... + "children": { + "subtypes": ["objective", "boss", "reward"] + } + ... + } +``` + +If you want to present them with specific forms, use `forms` and specify form +IDs: + +``` + { + ... + "children": { + "forms": [12, 16] + } + ... + } +``` + +When specifying forms by ID explicitly, the order you specify the forms in will +be used when presenting options to the user. + +If only one option would be presented, the user will be taken directly to the +appropriate form instead of being prompted to choose a form. EOTEXT , $subtype_default_key)); diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php index 23fac106d7..7aad1c9509 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php @@ -11,6 +11,8 @@ final class PhabricatorEditEngineSubtype private $icon; private $tagText; private $color; + private $childSubtypes = array(); + private $childIdentifiers = array(); public function setKey($key) { $this->key = $key; @@ -57,6 +59,24 @@ final class PhabricatorEditEngineSubtype return $this->color; } + public function setChildSubtypes(array $child_subtypes) { + $this->childSubtypes = $child_subtypes; + return $this; + } + + public function getChildSubtypes() { + return $this->childSubtypes; + } + + public function setChildFormIdentifiers(array $child_identifiers) { + $this->childIdentifiers = $child_identifiers; + return $this; + } + + public function getChildFormIdentifiers() { + return $this->childIdentifiers; + } + public function hasTagView() { return (bool)strlen($this->getTagText()); } @@ -118,6 +138,7 @@ final class PhabricatorEditEngineSubtype 'tag' => 'optional string', 'color' => 'optional string', 'icon' => 'optional string', + 'children' => 'optional map', )); $key = $value['key']; @@ -141,6 +162,27 @@ final class PhabricatorEditEngineSubtype 'no name. Subtypes must have a name.', $key)); } + + $children = idx($value, 'children'); + if ($children) { + PhutilTypeSpec::checkMap( + $children, + array( + 'subtypes' => 'optional list', + 'forms' => 'optional list', + )); + + $child_subtypes = idx($children, 'subtypes'); + $child_forms = idx($children, 'forms'); + + if ($child_subtypes && $child_forms) { + throw new Exception( + pht( + 'Subtype configuration is invalid: subtype with key "%s" '. + 'specifies both child subtypes and child forms. Specify one '. + 'or the other, but not both.')); + } + } } if (!isset($map[self::SUBTYPE_DEFAULT])) { @@ -179,6 +221,18 @@ final class PhabricatorEditEngineSubtype $subtype->setColor($color); } + $children = idx($entry, 'children', array()); + $child_subtypes = idx($children, 'subtypes'); + $child_forms = idx($children, 'forms'); + + if ($child_subtypes) { + $subtype->setChildSubtypes($child_subtypes); + } + + if ($child_forms) { + $subtype->setChildFormIdentifiers($child_forms); + } + $map[$key] = $subtype; } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php index c76911dbc3..638b665184 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php @@ -46,16 +46,14 @@ final class PhabricatorEditEngineSubtypeMap $subtype_key = $object->getEditEngineSubtype(); $subtype = $this->getSubtype($subtype_key); - // TODO: Allow subtype configuration to specify that children should be - // created from particular forms or subtypes. - $select_ids = array(); - $select_subtypes = array(); + $select_identifiers = $subtype->getChildFormIdentifiers(); + $select_subtypes = $subtype->getChildSubtypes(); $query = $edit_engine->newConfigurationQuery() ->withIsDisabled(false); - if ($select_ids) { - $query->withIDs($select_ids); + if ($select_identifiers) { + $query->withIdentifiers($select_identifiers); } else { // If we're selecting by subtype rather than selecting specific forms, // only select create forms. @@ -73,8 +71,8 @@ final class PhabricatorEditEngineSubtypeMap // If we're selecting by ID, respect the order specified in the // constraint. Otherwise, use the create form sort order. - if ($select_ids) { - $forms = array_select_keys($forms, $select_ids) + $forms; + if ($select_identifiers) { + $forms = array_select_keys($forms, $select_identifiers) + $forms; } else { $forms = msort($forms, 'getCreateSortKey'); } diff --git a/src/applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php b/src/applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php index 52d48f528b..694b4c48b6 100644 --- a/src/applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php +++ b/src/applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php @@ -115,14 +115,6 @@ final class PhabricatorEditEngineConfigurationSearchEngine ->setHeader($config->getDisplayName()); $id = $config->getID(); - if ($id) { - $item->addIcon('fa-file-text-o bluegrey', pht('Form %d', $id)); - $key = $id; - } else { - $item->addIcon('fa-file-text bluegrey', pht('Builtin')); - $key = $config->getBuiltinKey(); - } - $item->setHref("/transactions/editengine/{$engine_key}/view/{$key}/"); if ($config->getIsDefault()) { $item->addAttribute(pht('Default Create Form')); @@ -139,6 +131,31 @@ final class PhabricatorEditEngineConfigurationSearchEngine $item->setStatusIcon('fa-file-text-o green', pht('Enabled')); } + $subtype_key = $config->getSubtype(); + if ($subtype_key !== PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT) { + $engine = $config->getEngine(); + if ($engine->supportsSubtypes()) { + $map = $engine->newSubtypeMap(); + if ($map->isValidSubtype($subtype_key)) { + $subtype = $map->getSubtype($subtype_key); + + $icon = $subtype->getIcon(); + $color = $subtype->getColor(); + + $item->addIcon("{$icon} {$color}", $subtype->getName()); + } + } + } + + if ($id) { + $item->setObjectName(pht('Form %d', $id)); + $key = $id; + } else { + $item->addIcon('fa-file-text bluegrey', pht('Builtin')); + $key = $config->getBuiltinKey(); + } + $item->setHref("/transactions/editengine/{$engine_key}/view/{$key}/"); + $list->addItem($item); } From 68b1dee1390d59fbfce55988f8e4d3810a7da2a7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 7 Dec 2018 06:04:07 -0800 Subject: [PATCH 13/62] Replace the "Choose Subtype" radio buttons dialog with a simpler "big stuff you click" sort of UI Summary: Ref T13222. Fixes T12588. See PHI683. In several cases, we present the user with a choice between multiple major options: Alamnac service types, Drydock blueprint types, Repository VCS types, Herald rule types, etc. Today, we generally do this with radio buttons and a "Submit" button. This isn't terrible, but often it means users have to click twice (once on the radio; once on submit) when a single click would be sufficient. The radio click target can also be small. In other cases, we have a container with a link and we'd like to link the entire container: notifications, the `/drydock/` console, etc. We'd like to just link the entire container, but this causes some problems: - It's not legal to link block eleements like `
...
` and some browsers actually get upset about it. - We can ` ... ` instead, then turn the `` into a block element with CSS -- and this sometimes works, but also has some drawbacks: - It's not great to do that for screenreaders, since the readable text in the link isn't necessarily very meaningful. - We can't have any other links inside the element (e.g., details or documentation). - We can `
` instead, but this has its own set of problems: - You can't right-click to interact with a button in the same way you can with a link. - Also not great for screenreaders. Instead, try adding a `linked-container` behavior which just means "when users click this element, pretend they clicked the first link inside it". This gives us natural HTML (real, legal HTML with actual `` tags) and good screenreader behavior, but allows the effective link target to be visually larger than just the link. If no issues crop up with this, I'd plan to eventually use this technique in more places (Repositories, Herald, Almanac, Drydock, Notifications menu, etc). Test Plan: {F6053035} - Left-clicked and command-left-clicked the new JS fanciness, got sensible behaviors. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13222, T12588 Differential Revision: https://secure.phabricator.com/D19855 --- resources/celerity/map.php | 18 ++++--- .../controller/DrydockConsoleController.php | 4 ++ .../ManiphestTaskSubtaskController.php | 51 +++++++++---------- .../PhabricatorEditEngineSubtype.php | 5 ++ src/view/AphrontDialogView.php | 2 +- src/view/phui/PHUIObjectItemView.php | 28 ++++++---- .../css/phui/object-item/phui-oi-big-ui.css | 13 +++++ .../rsrc/js/core/behavior-linked-container.js | 47 +++++++++++++++++ 8 files changed, 123 insertions(+), 45 deletions(-) create mode 100644 webroot/rsrc/js/core/behavior-linked-container.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index e5eb50bb58..4223d94095 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => 'cff4ff6f', + 'core.pkg.css' => '9d1148a4', 'core.pkg.js' => '4bde473b', 'differential.pkg.css' => '06dc617c', 'differential.pkg.js' => 'ef0b989b', @@ -127,7 +127,7 @@ return array( 'rsrc/css/phui/calendar/phui-calendar-list.css' => '576be600', 'rsrc/css/phui/calendar/phui-calendar-month.css' => '21154caf', 'rsrc/css/phui/calendar/phui-calendar.css' => 'f1ddf11c', - 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '628f59de', + 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '7a7c22af', 'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6', @@ -469,6 +469,7 @@ return array( 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0', 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a', 'rsrc/js/core/behavior-line-linker.js' => '66a62306', + 'rsrc/js/core/behavior-linked-container.js' => '291da458', 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', @@ -616,6 +617,7 @@ return array( 'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-lightbox-attachments' => '6b31879a', 'javelin-behavior-line-chart' => 'e4232876', + 'javelin-behavior-linked-container' => '291da458', 'javelin-behavior-maniphest-batch-selector' => 'ad54037e', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '71237763', @@ -832,7 +834,7 @@ return array( 'phui-lightbox-css' => '0a035e40', 'phui-list-view-css' => '38f8c9bd', 'phui-object-box-css' => '9cff003c', - 'phui-oi-big-ui-css' => '628f59de', + 'phui-oi-big-ui-css' => '7a7c22af', 'phui-oi-color-css' => 'cd2b9b77', 'phui-oi-drag-ui-css' => '08f4ccc3', 'phui-oi-flush-ui-css' => '9d9685d6', @@ -1027,6 +1029,10 @@ return array( 'javelin-uri', 'phabricator-notification', ), + '291da458' => array( + 'javelin-behavior', + 'javelin-dom', + ), '2926fff2' => array( 'javelin-behavior', 'javelin-dom', @@ -1348,9 +1354,6 @@ return array( 'javelin-magical-init', 'javelin-util', ), - '628f59de' => array( - 'phui-oi-list-view-css', - ), '62dfea03' => array( 'javelin-install', 'javelin-util', @@ -1508,6 +1511,9 @@ return array( 'owners-path-editor', 'javelin-behavior', ), + '7a7c22af' => array( + 'phui-oi-list-view-css', + ), '7cbe244b' => array( 'javelin-install', 'javelin-util', diff --git a/src/applications/drydock/controller/DrydockConsoleController.php b/src/applications/drydock/controller/DrydockConsoleController.php index 54adcf5ca5..d0ae358768 100644 --- a/src/applications/drydock/controller/DrydockConsoleController.php +++ b/src/applications/drydock/controller/DrydockConsoleController.php @@ -34,6 +34,7 @@ final class DrydockConsoleController extends DrydockController { ->setHeader(pht('Blueprints')) ->setImageIcon('fa-map-o') ->setHref($this->getApplicationURI('blueprint/')) + ->setClickable(true) ->addAttribute( pht( 'Configure blueprints so Drydock can build resources, like '. @@ -44,6 +45,7 @@ final class DrydockConsoleController extends DrydockController { ->setHeader(pht('Resources')) ->setImageIcon('fa-map') ->setHref($this->getApplicationURI('resource/')) + ->setClickable(true) ->addAttribute( pht('View and manage resources Drydock has built, like hosts.'))); @@ -52,6 +54,7 @@ final class DrydockConsoleController extends DrydockController { ->setHeader(pht('Leases')) ->setImageIcon('fa-link') ->setHref($this->getApplicationURI('lease/')) + ->setClickable(true) ->addAttribute(pht('Manage leases on resources.'))); $menu->addItem( @@ -59,6 +62,7 @@ final class DrydockConsoleController extends DrydockController { ->setHeader(pht('Repository Operations')) ->setImageIcon('fa-fighter-jet') ->setHref($this->getApplicationURI('operation/')) + ->setClickable(true) ->addAttribute(pht('Review the repository operation queue.'))); $crumbs = $this->buildApplicationCrumbs(); diff --git a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php index 9da0ed4206..5256c1bd26 100644 --- a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php +++ b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php @@ -37,39 +37,34 @@ final class ManiphestTaskSubtaskController ->addCancelButton($cancel_uri, pht('Close')); } - if ($request->isFormPost()) { - $form_key = $request->getStr('formKey'); - if (isset($subtype_options[$form_key])) { - $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) - ->setQueryParam('parent', $id) - ->setQueryParam('template', $id) - ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); - $subtask_uri = $this->getApplicationURI($subtask_uri); + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); - return id(new AphrontRedirectResponse()) - ->setURI($subtask_uri); - } + foreach ($subtype_options as $form_key => $subtype_form) { + $subtype_key = $subtype_form->getSubtype(); + $subtype = $subtype_map->getSubtype($subtype_key); + + $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) + ->setQueryParam('parent', $id) + ->setQueryParam('template', $id) + ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); + $subtask_uri = $this->getApplicationURI($subtask_uri); + + $item = id(new PHUIObjectItemView()) + ->setHeader($subtype_form->getDisplayName()) + ->setHref($subtask_uri) + ->setClickable(true) + ->setImageIcon($subtype->newIconView()) + ->addAttribute($subtype->getName()); + + $menu->addItem($item); } - $control = id(new AphrontFormRadioButtonControl()) - ->setName('formKey') - ->setLabel(pht('Subtype')); - - foreach ($subtype_options as $key => $subtype_form) { - $control->addButton( - $key, - $subtype_form->getDisplayName(), - null); - } - - $form = id(new AphrontFormView()) - ->setViewer($viewer) - ->appendControl($control); - return $this->newDialog() ->setTitle(pht('Choose Subtype')) - ->appendForm($form) - ->addSubmitButton(pht('Continue')) + ->appendChild($menu) ->addCancelButton($cancel_uri); } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php index 7aad1c9509..9e754a3ca8 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php @@ -239,4 +239,9 @@ final class PhabricatorEditEngineSubtype return new PhabricatorEditEngineSubtypeMap($map); } + public function newIconView() { + return id(new PHUIIconView()) + ->setIcon($this->getIcon(), $this->getColor()); + } + } diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 12bec92843..52681845d2 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -404,7 +404,7 @@ final class AphrontDialogView $header), phutil_tag('div', array( - 'class' => 'aphront-dialog-body phabricator-remarkup grouped', + 'class' => 'aphront-dialog-body grouped', ), $children), $tail, diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 0657086c49..565b9c2165 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -28,6 +28,7 @@ final class PHUIObjectItemView extends AphrontTagView { private $sideColumn; private $coverImage; private $description; + private $clickable; private $selectableName; private $selectableValue; @@ -179,6 +180,15 @@ final class PHUIObjectItemView extends AphrontTagView { return $this; } + public function setClickable($clickable) { + $this->clickable = $clickable; + return $this; + } + + public function getClickable() { + return $this->clickable; + } + public function setEpoch($epoch) { $date = phabricator_datetime($epoch, $this->getUser()); $this->addIcon('none', $date); @@ -332,6 +342,13 @@ final class PHUIObjectItemView extends AphrontTagView { $item_classes[] = 'phui-oi-with-image-icon'; } + if ($this->getClickable()) { + Javelin::initBehavior('linked-container'); + + $item_classes[] = 'phui-oi-linked-container'; + $sigils[] = 'linked-container'; + } + return array( 'class' => $item_classes, 'sigil' => $sigils, @@ -443,15 +460,6 @@ final class PHUIObjectItemView extends AphrontTagView { ), $spec['label']); - if (isset($spec['attributes']['href'])) { - $icon_href = phutil_tag( - 'a', - array('href' => $spec['attributes']['href']), - array($icon, $label)); - } else { - $icon_href = array($icon, $label); - } - $classes = array(); $classes[] = 'phui-oi-icon'; if (isset($spec['attributes']['class'])) { @@ -463,7 +471,7 @@ final class PHUIObjectItemView extends AphrontTagView { array( 'class' => implode(' ', $classes), ), - $icon_href); + $icon); } $icons[] = phutil_tag( diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css index 67ce566733..6f60560a2e 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css @@ -59,3 +59,16 @@ background-color: {$hoverblue}; border-radius: 3px; } + +.device-desktop .phui-oi-linked-container { + cursor: pointer; +} + +.device-desktop .phui-oi-linked-container:hover { + background-color: {$hoverblue}; + border-radius: 3px; +} + +.device-desktop .phui-oi-linked-container a:hover { + text-decoration: none; +} diff --git a/webroot/rsrc/js/core/behavior-linked-container.js b/webroot/rsrc/js/core/behavior-linked-container.js new file mode 100644 index 0000000000..9721f91b22 --- /dev/null +++ b/webroot/rsrc/js/core/behavior-linked-container.js @@ -0,0 +1,47 @@ +/** + * @provides javelin-behavior-linked-container + * @requires javelin-behavior javelin-dom + */ + +JX.behavior('linked-container', function() { + + JX.Stratcom.listen( + 'click', + 'linked-container', + function(e) { + + // If the user clicked some link inside the container, bail out and just + // click the link. + if (e.getNode('tag:a')) { + return; + } + + // If this is some sort of unusual click, bail out. Note that we'll + // handle "Left Click" and "Command + Left Click" differently, below. + if (!e.isLeftButton()) { + return; + } + + var container = e.getNode('linked-container'); + + // Find the first link in the container. We're going to pretend the user + // clicked it. + var link = JX.DOM.scry(container, 'a')[0]; + if (!link) { + return; + } + + // If the click is a "Command + Left Click", change the target of the + // link so we open it in a new tab. + var is_command = !!e.getRawEvent().metaKey; + if (is_command) { + var old_target = link.target; + link.target = '_blank'; + link.click(); + link.target = old_target; + } else { + link.click(); + } + }); + +}); From 46feccdfcf1963d7d8d38b2c282b1e702f059a76 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 08:20:22 -0800 Subject: [PATCH 14/62] Share more inline "Done" code between Differential and Diffusion Summary: Ref T13222. See PHI995. Before making a change to inline rendering, consolidate this code for generating the "alice added inlines comments." and "alice marked X inlines as done." transactions. Both Differential and Diffusion have four very similar chunks of code. Merge them into shared methods and reduce code duplication across the methods. (In the next change, I plan to hide the "done" story when the mark affects your own inline, since users marking their own inlines as "done" is generally not very interesting or useful.) Test Plan: As author and reviewer/auditor, added inlines in Differential and Diffusion. As author, marked own and others inlines as done and undone. Got sensible transaction rendering and persistence of "Done". Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19858 --- .../audit/editor/PhabricatorAuditEditor.php | 45 ++---------- .../editor/DifferentialRevisionEditEngine.php | 37 +++------- .../editor/DifferentialTransactionEditor.php | 48 ++----------- .../editor/DiffusionCommitEditEngine.php | 37 +++------- ...habricatorApplicationTransactionEditor.php | 71 +++++++++++++++++++ 5 files changed, 103 insertions(+), 135 deletions(-) diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 3f65cc80f8..165f8ef84f 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -251,47 +251,16 @@ final class PhabricatorAuditEditor case PhabricatorTransactions::TYPE_COMMENT: $this->didExpandInlineState = true; - $actor_phid = $this->getActingAsPHID(); - $author_phid = $object->getAuthorPHID(); - $actor_is_author = ($actor_phid == $author_phid); + $query_template = id(new DiffusionDiffInlineCommentQuery()) + ->withCommitPHIDs(array($object->getPHID())); - $state_map = PhabricatorTransactions::getInlineStateMap(); + $state_xaction = $this->newInlineStateTransaction( + $object, + $query_template); - $query = id(new DiffusionDiffInlineCommentQuery()) - ->setViewer($this->getActor()) - ->withCommitPHIDs(array($object->getPHID())) - ->withFixedStates(array_keys($state_map)); - - $inlines = array(); - - $inlines[] = id(clone $query) - ->withAuthorPHIDs(array($actor_phid)) - ->withHasTransaction(false) - ->execute(); - - if ($actor_is_author) { - $inlines[] = id(clone $query) - ->withHasTransaction(true) - ->execute(); + if ($state_xaction) { + $xactions[] = $state_xaction; } - - $inlines = array_mergev($inlines); - - if (!$inlines) { - break; - } - - $old_value = mpull($inlines, 'getFixedState', 'getPHID'); - $new_value = array(); - foreach ($old_value as $key => $state) { - $new_value[$key] = $state_map[$state]; - } - - $xactions[] = id(new PhabricatorAuditTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) - ->setIgnoreOnNoEffect(true) - ->setOldValue($old_value) - ->setNewValue($new_value); break; } } diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php index ffae7fab16..07b693044f 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php +++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php @@ -279,36 +279,17 @@ final class DifferentialRevisionEditEngine $object); $inlines = msort($inlines, 'getID'); - foreach ($inlines as $inline) { - $xactions[] = id(new DifferentialTransaction()) - ->setTransactionType(DifferentialTransaction::TYPE_INLINE) - ->attachComment($inline); - } + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer); - $viewer_phid = $viewer->getPHID(); - $viewer_is_author = ($object->getAuthorPHID() == $viewer_phid); - if ($viewer_is_author) { - $state_map = PhabricatorTransactions::getInlineStateMap(); + $query_template = id(new DifferentialDiffInlineCommentQuery()) + ->withRevisionPHIDs(array($object->getPHID())); - $inlines = id(new DifferentialDiffInlineCommentQuery()) - ->setViewer($viewer) - ->withRevisionPHIDs(array($object->getPHID())) - ->withFixedStates(array_keys($state_map)) - ->execute(); - if ($inlines) { - $old_value = mpull($inlines, 'getFixedState', 'getPHID'); - $new_value = array(); - foreach ($old_value as $key => $state) { - $new_value[$key] = $state_map[$state]; - } - - $xactions[] = id(new DifferentialTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) - ->setIgnoreOnNoEffect(true) - ->setOldValue($old_value) - ->setNewValue($new_value); - } - } + $xactions = $editor->newAutomaticInlineTransactions( + $object, + $inlines, + DifferentialTransaction::TYPE_INLINE, + $query_template); return $xactions; } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 853b024e27..8072c19e9a 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -247,50 +247,16 @@ final class DifferentialTransactionEditor case DifferentialTransaction::TYPE_INLINE: $this->didExpandInlineState = true; - $actor_phid = $this->getActingAsPHID(); - $author_phid = $object->getAuthorPHID(); - $actor_is_author = ($actor_phid == $author_phid); + $query_template = id(new DifferentialDiffInlineCommentQuery()) + ->withRevisionPHIDs(array($object->getPHID())); - $state_map = PhabricatorTransactions::getInlineStateMap(); + $state_xaction = $this->newInlineStateTransaction( + $object, + $query_template); - $query = id(new DifferentialDiffInlineCommentQuery()) - ->setViewer($this->getActor()) - ->withRevisionPHIDs(array($object->getPHID())) - ->withFixedStates(array_keys($state_map)); - - $inlines = array(); - - // We're going to undraft any "done" marks on your own inlines. - $inlines[] = id(clone $query) - ->withAuthorPHIDs(array($actor_phid)) - ->withHasTransaction(false) - ->execute(); - - // If you're the author, we also undraft any "done" marks on other - // inlines. - if ($actor_is_author) { - $inlines[] = id(clone $query) - ->withHasTransaction(true) - ->execute(); + if ($state_xaction) { + $results[] = $state_xaction; } - - $inlines = array_mergev($inlines); - - if (!$inlines) { - break; - } - - $old_value = mpull($inlines, 'getFixedState', 'getPHID'); - $new_value = array(); - foreach ($old_value as $key => $state) { - $new_value[$key] = $state_map[$state]; - } - - $results[] = id(new DifferentialTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) - ->setIgnoreOnNoEffect(true) - ->setOldValue($old_value) - ->setNewValue($new_value); break; } } diff --git a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php index 8392504def..667af46550 100644 --- a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php @@ -170,36 +170,17 @@ final class DiffusionCommitEditEngine $raw = true); $inlines = msort($inlines, 'getID'); - foreach ($inlines as $inline) { - $xactions[] = $object->getApplicationTransactionTemplate() - ->setTransactionType(PhabricatorAuditActionConstants::INLINE) - ->attachComment($inline); - } + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer); - $viewer_phid = $viewer->getPHID(); - $viewer_is_author = ($object->getAuthorPHID() == $viewer_phid); - if ($viewer_is_author) { - $state_map = PhabricatorTransactions::getInlineStateMap(); + $query_template = id(new DiffusionDiffInlineCommentQuery()) + ->withCommitPHIDs(array($object->getPHID())); - $inlines = id(new DiffusionDiffInlineCommentQuery()) - ->setViewer($viewer) - ->withCommitPHIDs(array($object->getPHID())) - ->withFixedStates(array_keys($state_map)) - ->execute(); - if ($inlines) { - $old_value = mpull($inlines, 'getFixedState', 'getPHID'); - $new_value = array(); - foreach ($old_value as $key => $state) { - $new_value[$key] = $state_map[$state]; - } - - $xactions[] = $object->getApplicationTransactionTemplate() - ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) - ->setIgnoreOnNoEffect(true) - ->setOldValue($old_value) - ->setNewValue($new_value); - } - } + $xactions = $editor->newAutomaticInlineTransactions( + $object, + $inlines, + PhabricatorAuditActionConstants::INLINE, + $query_template); return $xactions; } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index f354de6a1a..634b4b92db 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -4594,4 +4594,75 @@ abstract class PhabricatorApplicationTransactionEditor return $mail; } + public function newAutomaticInlineTransactions( + PhabricatorLiskDAO $object, + array $inlines, + $transaction_type, + PhabricatorCursorPagedPolicyAwareQuery $query_template) { + + $xactions = array(); + + foreach ($inlines as $inline) { + $xactions[] = $object->getApplicationTransactionTemplate() + ->setTransactionType($transaction_type) + ->attachComment($inline); + } + + $state_xaction = $this->newInlineStateTransaction( + $object, + $query_template); + + if ($state_xaction) { + $xactions[] = $state_xaction; + } + + return $xactions; + } + + protected function newInlineStateTransaction( + PhabricatorLiskDAO $object, + PhabricatorCursorPagedPolicyAwareQuery $query_template) { + + $actor_phid = $this->getActingAsPHID(); + $author_phid = $object->getAuthorPHID(); + $actor_is_author = ($actor_phid == $author_phid); + + $state_map = PhabricatorTransactions::getInlineStateMap(); + + $query = id(clone $query_template) + ->setViewer($this->getActor()) + ->withFixedStates(array_keys($state_map)); + + $inlines = array(); + + $inlines[] = id(clone $query) + ->withAuthorPHIDs(array($actor_phid)) + ->withHasTransaction(false) + ->execute(); + + if ($actor_is_author) { + $inlines[] = id(clone $query) + ->withHasTransaction(true) + ->execute(); + } + + $inlines = array_mergev($inlines); + + if (!$inlines) { + return null; + } + + $old_value = mpull($inlines, 'getFixedState', 'getPHID'); + $new_value = array(); + foreach ($old_value as $key => $state) { + $new_value[$key] = $state_map[$state]; + } + + return $object->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) + ->setIgnoreOnNoEffect(true) + ->setOldValue($old_value) + ->setNewValue($new_value); + } + } From 508df60a621715778ee9d1f8064874da6dd770cd Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 10:58:51 -0800 Subject: [PATCH 15/62] When users mark their own inline comments as "Done", suppress the timeline/mail stories Summary: Depends on D19858. Ref T13222. See PHI995. In D19635 and related revisions, inline behavior changed to allow you to pre-mark your own inlines as done (as a reviewer) and to pre-mark your inlines for you (as an author). These actions generate low-value stories in the timeline, like "alice marked 3 comments done." when an author adds some notes to their own revision. These aren't helpful and can be a little misleading. Instead, just don't count it when someone marks their own inlines as "done". If we throw away all the marks after throwing away the self-marks, hide the whole story. This happens in three cases: # You comment on your own revision, and don't uncheck the "Done" checkbox. # You comment on someone else's revision, and check the "Done" checkbox before submitting. # You leave a not-"Done" inline on your own revision, then "Done" it later. Cases (1) and (2) seem unambiguously good/clear. Case (3) is a little more questionable, but I think this still isn't very useful for reviewers. If there's still a clarity issue around case (3), we could change the story text to "alice marked 3 inline comments by other users as done.", but I think this is probably needlessly verbose and that no one will be confused by the behavior as written here. (Also note that this story is never shown in feed.) Test Plan: Created and marked a bunch of inlines as "Done" in Differential and Diffusion, as the author and reviewer/auditor. My own marks didn't generate timeline stories; marking others' comments still does. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19859 --- ...habricatorApplicationTransactionEditor.php | 12 ++++ .../PhabricatorApplicationTransaction.php | 63 ++++++++++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 634b4b92db..378b96adb4 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -4658,9 +4658,21 @@ abstract class PhabricatorApplicationTransactionEditor $new_value[$key] = $state_map[$state]; } + // See PHI995. Copy some information about the inlines into the transaction + // so we can tailor rendering behavior. In particular, we don't want to + // render transactions about users marking their own inlines as "Done". + + $inline_details = array(); + foreach ($inlines as $inline) { + $inline_details[$inline->getPHID()] = array( + 'authorPHID' => $inline->getAuthorPHID(), + ); + } + return $object->getApplicationTransactionTemplate() ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) + ->setMetadataValue('inline.details', $inline_details) ->setOldValue($old_value) ->setNewValue($new_value); } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index c65adcac6b..5fc3385d27 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -664,6 +664,16 @@ abstract class PhabricatorApplicationTransaction break; } break; + + case PhabricatorTransactions::TYPE_INLINESTATE: + list($done, $undone) = $this->getInterestingInlineStateChangeCounts(); + + if (!$done && !$undone) { + return true; + } + + break; + } return false; @@ -1007,15 +1017,7 @@ abstract class PhabricatorApplicationTransaction } case PhabricatorTransactions::TYPE_INLINESTATE: - $done = 0; - $undone = 0; - foreach ($new as $phid => $state) { - if ($state == PhabricatorInlineCommentInterface::STATE_DONE) { - $done++; - } else { - $undone++; - } - } + list($done, $undone) = $this->getInterestingInlineStateChangeCounts(); if ($done && $undone) { return pht( '%s marked %s inline comment(s) as done and %s inline comment(s) '. @@ -1582,6 +1584,49 @@ abstract class PhabricatorApplicationTransaction return $moves; } + private function getInterestingInlineStateChangeCounts() { + // See PHI995. Newer inline state transactions have additional details + // which we use to tailor the rendering behavior. These details are not + // present on older transactions. + $details = $this->getMetadataValue('inline.details', array()); + + $new = $this->getNewValue(); + + $done = 0; + $undone = 0; + foreach ($new as $phid => $state) { + $is_done = ($state == PhabricatorInlineCommentInterface::STATE_DONE); + + // See PHI995. If you're marking your own inline comments as "Done", + // don't count them when rendering a timeline story. In the case where + // you're only affecting your own comments, this will hide the + // "alice marked X comments as done" story entirely. + + // Usually, this happens when you pre-mark inlines as "done" and submit + // them yourself. We'll still generate an "alice added inline comments" + // story (in most cases/contexts), but the state change story is largely + // just clutter and slightly confusing/misleading. + + $inline_details = idx($details, $phid, array()); + $inline_author_phid = idx($inline_details, 'authorPHID'); + if ($inline_author_phid) { + if ($inline_author_phid == $this->getAuthorPHID()) { + if ($is_done) { + continue; + } + } + } + + if ($is_done) { + $done++; + } else { + $undone++; + } + } + + return array($done, $undone); + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ From 55a1ef339f4534ab1a8e1a0f82818f78f3408fab Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 11:39:43 -0800 Subject: [PATCH 16/62] Fix a bad method call signature throwing exceptions in newer Node Summary: Ref T13222. See PHI996. Ref T10743. For context, perhaps see T12171. Node changed some signatures, behaviors, and error handling here in recent versions. As far as I can tell: - The `script.runInNewContext(...)` method has never taken a `path` parameter, and passing the path has always been wrong. - The `script.runInNewContext(...)` method started taking an `[options]` parameter at some point, and validating it, so the bad `path` parameter now throws. - `vm.createScript(...)` is "soft deprecated" but basically fine, and keeping it looks more compatible. This seems like the smallest and most compatible correct change. Test Plan: Under Node 10, started Aphlict. Before: fatal error on bad `options` parameter to `runInNewContext()` (expected dictionary). After: notification server starts. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T10743 Differential Revision: https://secure.phabricator.com/D19860 --- resources/celerity/map.php | 2 +- webroot/rsrc/externals/javelin/core/init_node.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4223d94095..3b86ebeeb4 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -214,7 +214,7 @@ return array( 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', 'rsrc/externals/javelin/core/init.js' => '8d83d2a1', - 'rsrc/externals/javelin/core/init_node.js' => 'c234aded', + 'rsrc/externals/javelin/core/init_node.js' => 'f7732951', 'rsrc/externals/javelin/core/install.js' => '05270951', 'rsrc/externals/javelin/core/util.js' => '93cc50d6', 'rsrc/externals/javelin/docs/Base.js' => '74676256', diff --git a/webroot/rsrc/externals/javelin/core/init_node.js b/webroot/rsrc/externals/javelin/core/init_node.js index 19834664b4..1a7999e253 100644 --- a/webroot/rsrc/externals/javelin/core/init_node.js +++ b/webroot/rsrc/externals/javelin/core/init_node.js @@ -49,7 +49,7 @@ JX.require = function(thing) { } vm.createScript(content, path) - .runInNewContext(sandbox, path); + .runInNewContext(sandbox); }; exports.JX = JX; From ba833805656e724aea17463618874e75bcd8612f Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 11:32:13 -0800 Subject: [PATCH 17/62] Update the "Notification Test" workflow to use more modern mechanisms Summary: Depends on D19860. Ref T13222. Ref T10743. See PHI996. Long ago, there were different types of feed stories. Over time, there was less and less need for this, and nowadays basically everything is a "transaction" feed story. Each story renders differently, but they're fundamentally all about transactions. The Notification test controller still uses a custom type of feed story to send notifications. Move away from this, and apply a transaction against the user instead. This has the same ultimate effect, but involves less weird custom code from ages long forgotten. This doesn't fix the actual problem with these things showing up in feed. Currently, stories always use the same rendering for feed and notifications, and there need to be some additional changes to fix this. So no behavioral change yet, just slightly more reasonable code. Test Plan: Clicked the button and got some test notifications, with Aphlict running. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T10743 Differential Revision: https://secure.phabricator.com/D19861 --- src/__phutil_library_map__.php | 2 + .../PhabricatorNotificationTestController.php | 51 +++++++++---------- ...abricatorPeopleProfileManageController.php | 6 +++ .../PhabricatorUserTransactionEditor.php | 14 +++++ .../PhabricatorUserDisableTransaction.php | 17 ++----- .../PhabricatorUserNotifyTransaction.php | 34 +++++++++++++ ...habricatorApplicationTransactionEditor.php | 25 +++++++-- .../PhabricatorApplicationTransaction.php | 9 ++++ 8 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 src/applications/people/xaction/PhabricatorUserNotifyTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 184c2a3d7c..e2586354e8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4623,6 +4623,7 @@ phutil_register_library_map(array( 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php', 'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php', + 'PhabricatorUserNotifyTransaction' => 'applications/people/xaction/PhabricatorUserNotifyTransaction.php', 'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php', 'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php', 'PhabricatorUserPreferencesCacheType' => 'applications/people/cache/PhabricatorUserPreferencesCacheType.php', @@ -10676,6 +10677,7 @@ phutil_register_library_map(array( 'PhabricatorUserLogView' => 'AphrontView', 'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType', + 'PhabricatorUserNotifyTransaction' => 'PhabricatorUserTransactionType', 'PhabricatorUserPHIDResolver' => 'PhabricatorPHIDResolver', 'PhabricatorUserPreferences' => array( 'PhabricatorUserDAO', diff --git a/src/applications/notification/controller/PhabricatorNotificationTestController.php b/src/applications/notification/controller/PhabricatorNotificationTestController.php index 5c76e53357..f49f9d31a1 100644 --- a/src/applications/notification/controller/PhabricatorNotificationTestController.php +++ b/src/applications/notification/controller/PhabricatorNotificationTestController.php @@ -6,34 +6,31 @@ final class PhabricatorNotificationTestController public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $story_type = 'PhabricatorNotificationTestFeedStory'; - $story_data = array( - 'title' => pht( - 'This is a test notification, sent at %s.', - phabricator_datetime(time(), $viewer)), - ); - - $viewer_phid = $viewer->getPHID(); - - // NOTE: Because we don't currently show you your own notifications, make - // sure this comes from a different PHID. - $application_phid = id(new PhabricatorNotificationsApplication()) - ->getPHID(); - - // TODO: When it's easier to get these buttons to render as forms, this - // would be slightly nicer as a more standard isFormPost() check. - if ($request->validateCSRF()) { - id(new PhabricatorFeedStoryPublisher()) - ->setStoryType($story_type) - ->setStoryData($story_data) - ->setStoryTime(time()) - ->setStoryAuthorPHID($application_phid) - ->setRelatedPHIDs(array($viewer_phid)) - ->setPrimaryObjectPHID($viewer_phid) - ->setSubscribedPHIDs(array($viewer_phid)) - ->setNotifyAuthor(true) - ->publish(); + $message_text = pht( + 'This is a test notification, sent at %s.', + phabricator_datetime(time(), $viewer)); + + // NOTE: Currently, the FeedStoryPublisher explicitly filters out + // notifications about your own actions. Send this notification from + // a different actor to get around this. + $application_phid = id(new PhabricatorNotificationsApplication()) + ->getPHID(); + + $xactions = array(); + + $xactions[] = id(new PhabricatorUserTransaction()) + ->setTransactionType( + PhabricatorUserNotifyTransaction::TRANSACTIONTYPE) + ->setNewValue($message_text) + ->setForceNotifyPHIDs(array($viewer->getPHID())); + + $editor = id(new PhabricatorUserTransactionEditor()) + ->setActor($viewer) + ->setActingAsPHID($application_phid) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($viewer, $xactions); } return id(new AphrontAjaxResponse()); diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php index 9759a375c7..55f9311ada 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php @@ -43,6 +43,11 @@ final class PhabricatorPeopleProfileManageController ->setCurtain($curtain) ->addPropertySection(pht('Details'), $properties); + $timeline = $this->buildTransactionTimeline( + $user, + new PhabricatorPeopleTransactionQuery()); + $timeline->setShouldTerminate(true); + return $this->newPage() ->setTitle( array( @@ -54,6 +59,7 @@ final class PhabricatorPeopleProfileManageController ->appendChild( array( $manage, + $timeline, )); } diff --git a/src/applications/people/editor/PhabricatorUserTransactionEditor.php b/src/applications/people/editor/PhabricatorUserTransactionEditor.php index c0dfa941af..929b2e224c 100644 --- a/src/applications/people/editor/PhabricatorUserTransactionEditor.php +++ b/src/applications/people/editor/PhabricatorUserTransactionEditor.php @@ -11,4 +11,18 @@ final class PhabricatorUserTransactionEditor return pht('Users'); } + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function getMailTo(PhabricatorLiskDAO $object) { + return array(); + } + + protected function getMailCC(PhabricatorLiskDAO $object) { + return array(); + } + } diff --git a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php index ab399a28e0..629d538dee 100644 --- a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php @@ -35,19 +35,10 @@ final class PhabricatorUserDisableTransaction } } - public function getTitleForFeed() { - $new = $this->getNewValue(); - if ($new) { - return pht( - '%s disabled %s.', - $this->renderAuthor(), - $this->renderObject()); - } else { - return pht( - '%s enabled %s.', - $this->renderAuthor(), - $this->renderObject()); - } + public function shouldHideForFeed() { + // Don't publish feed stories about disabling users, since this can be + // a sensitive action. + return true; } public function validateTransactions($object, array $xactions) { diff --git a/src/applications/people/xaction/PhabricatorUserNotifyTransaction.php b/src/applications/people/xaction/PhabricatorUserNotifyTransaction.php new file mode 100644 index 0000000000..bc5657d22e --- /dev/null +++ b/src/applications/people/xaction/PhabricatorUserNotifyTransaction.php @@ -0,0 +1,34 @@ +renderAuthor()); + } + + public function getTitleForFeed() { + return $this->getNewValue(); + } + + public function shouldHideForFeed() { + return false; + } + + public function shouldHideForMail() { + return true; + } + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 378b96adb4..9e946a249e 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3378,9 +3378,28 @@ abstract class PhabricatorApplicationTransactionEditor PhabricatorLiskDAO $object, array $xactions) { - return array_unique(array_merge( - $this->getMailTo($object), - $this->getMailCC($object))); + // If some transactions are forcing notification delivery, add the forced + // recipients to the notify list. + $force_list = array(); + foreach ($xactions as $xaction) { + $force_phids = $xaction->getForceNotifyPHIDs(); + + if (!$force_phids) { + continue; + } + + foreach ($force_phids as $force_phid) { + $force_list[] = $force_phid; + } + } + + $to_list = $this->getMailTo($object); + $cc_list = $this->getMailCC($object); + + $full_list = array_merge($force_list, $to_list, $cc_list); + $full_list = array_fuse($full_list); + + return array_keys($full_list); } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 5fc3385d27..9e024b43aa 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1662,6 +1662,15 @@ abstract class PhabricatorApplicationTransaction return null; } + public function setForceNotifyPHIDs(array $phids) { + $this->setMetadataValue('notify.force', $phids); + return $this; + } + + public function getForceNotifyPHIDs() { + return $this->getMetadataValue('notify.force', array()); + } + /* -( PhabricatorDestructibleInterface )----------------------------------- */ From 773b4eaa9ea0999fd86fb9d883b186d867f60a09 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 12:40:24 -0800 Subject: [PATCH 18/62] Separate "feed" and "notifications" better, allow stories to appear in notifications only Summary: Depends on D19861. Ref T13222. See PHI996. Fixes T10743. Currently, notifications only work if a story also has a feed rendering. Separate "visible in feed" and "visible in notifications", and make notifications query only notifications and vice versa. Then, set the test notification stories to be visible in notifications only, not feed. This could be refined a bit (there's no way to have the two views render different values today, for example) but since the only actual use case we have right now is test notifications I don't want to go //too// crazy future-proofing it. I could imagine doing some more of this kind of stuff in Conpherence eventually, though, perhaps. Test Plan: Sent myself test notifications, saw them appear on my profile timeline and in the JS popup, and in my notifications menu, but not in feed. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T10743 Differential Revision: https://secure.phabricator.com/D19864 --- .../feed/query/PhabricatorFeedQuery.php | 10 ++++- .../feed/story/PhabricatorFeedStory.php | 8 ++++ .../query/PhabricatorNotificationQuery.php | 20 ++++++--- .../PhabricatorUserNotifyTransaction.php | 6 ++- .../PhabricatorNotificationsSettingsPanel.php | 2 +- ...habricatorApplicationTransactionEditor.php | 14 +++++- ...ricatorApplicationTransactionFeedStory.php | 44 ++++++++++++++++--- .../PhabricatorApplicationTransaction.php | 4 ++ .../storage/PhabricatorModularTransaction.php | 11 +++++ .../PhabricatorModularTransactionType.php | 4 ++ 10 files changed, 107 insertions(+), 16 deletions(-) diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php index ce472a6026..a35f14da57 100644 --- a/src/applications/feed/query/PhabricatorFeedQuery.php +++ b/src/applications/feed/query/PhabricatorFeedQuery.php @@ -34,7 +34,15 @@ final class PhabricatorFeedQuery } protected function willFilterPage(array $data) { - return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer()); + $stories = PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer()); + + foreach ($stories as $key => $story) { + if (!$story->isVisibleInFeed()) { + unset($stories[$key]); + } + } + + return $stories; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php index c3984573c7..e0c65c7dc2 100644 --- a/src/applications/feed/story/PhabricatorFeedStory.php +++ b/src/applications/feed/story/PhabricatorFeedStory.php @@ -418,6 +418,14 @@ abstract class PhabricatorFeedStory return array(); } + public function isVisibleInFeed() { + return true; + } + + public function isVisibleInNotifications() { + return true; + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/src/applications/notification/query/PhabricatorNotificationQuery.php b/src/applications/notification/query/PhabricatorNotificationQuery.php index b40bdee066..0063c06e45 100644 --- a/src/applications/notification/query/PhabricatorNotificationQuery.php +++ b/src/applications/notification/query/PhabricatorNotificationQuery.php @@ -53,13 +53,13 @@ final class PhabricatorNotificationQuery $data = queryfx_all( $conn, - 'SELECT story.*, notif.hasViewed FROM %T notif - JOIN %T story ON notif.chronologicalKey = story.chronologicalKey + 'SELECT story.*, notif.hasViewed FROM %R notif + JOIN %R story ON notif.chronologicalKey = story.chronologicalKey %Q ORDER BY notif.chronologicalKey DESC %Q', - $notification_table->getTableName(), - $story_table->getTableName(), + $notification_table, + $story_table, $this->buildWhereClause($conn), $this->buildLimitClause($conn)); @@ -93,7 +93,7 @@ final class PhabricatorNotificationQuery (int)!$this->unread); } - if ($this->keys) { + if ($this->keys !== null) { $where[] = qsprintf( $conn, 'notif.chronologicalKey IN (%Ls)', @@ -103,6 +103,16 @@ final class PhabricatorNotificationQuery return $where; } + protected function willFilterPage(array $stories) { + foreach ($stories as $key => $story) { + if (!$story->isVisibleInNotifications()) { + unset($stories[$key]); + } + } + + return $stories; + } + protected function getResultCursor($item) { return $item->getChronologicalKey(); } diff --git a/src/applications/people/xaction/PhabricatorUserNotifyTransaction.php b/src/applications/people/xaction/PhabricatorUserNotifyTransaction.php index bc5657d22e..4a527f8265 100644 --- a/src/applications/people/xaction/PhabricatorUserNotifyTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserNotifyTransaction.php @@ -23,10 +23,14 @@ final class PhabricatorUserNotifyTransaction return $this->getNewValue(); } - public function shouldHideForFeed() { + public function shouldHideForNotifications() { return false; } + public function shouldHideForFeed() { + return true; + } + public function shouldHideForMail() { return true; } diff --git a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php index e75c2b99ee..797bcafcb3 100644 --- a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php @@ -143,7 +143,7 @@ final class PhabricatorNotificationsSettingsPanel ->setCaption( pht( 'Phabricator can send real-time notifications to your web browser '. - 'or to your desktop. Select where you\'d want to receive these '. + 'or to your desktop. Select where you want to receive these '. 'real-time updates.')) ->initBehavior( 'desktop-notifications-control', diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 9e946a249e..738fcf098b 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3428,7 +3428,19 @@ abstract class PhabricatorApplicationTransactionEditor array $xactions, array $mailed_phids) { - $xactions = mfilter($xactions, 'shouldHideForFeed', true); + // Remove transactions which don't publish feed stories or notifications. + // These never show up anywhere, so we don't need to do anything with them. + foreach ($xactions as $key => $xaction) { + if (!$xaction->shouldHideForFeed()) { + continue; + } + + if (!$xaction->shouldHideForNotifications()) { + continue; + } + + unset($xactions[$key]); + } if (!$xactions) { return; diff --git a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php index 305e38ab09..0e8531a22d 100644 --- a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php +++ b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php @@ -36,12 +36,7 @@ class PhabricatorApplicationTransactionFeedStory // time because it's cheap and gets us better results when things change // by letting the changes apply retroactively. - $xaction_phids = $this->getValue('transactionPHIDs'); - - $xactions = array(); - foreach ($xaction_phids as $xaction_phid) { - $xactions[] = $this->getObject($xaction_phid); - } + $xactions = $this->getTransactions(); foreach ($xactions as $key => $xaction) { if ($xaction->shouldHideForFeed()) { @@ -52,7 +47,7 @@ class PhabricatorApplicationTransactionFeedStory if ($xactions) { $primary_phid = head($xactions)->getPHID(); } else { - $primary_phid = head($xaction_phids); + $primary_phid = head($this->getValue('transactionPHIDs')); } $this->primaryTransactionPHID = $primary_phid; @@ -61,6 +56,41 @@ class PhabricatorApplicationTransactionFeedStory return $this->primaryTransactionPHID; } + public function isVisibleInNotifications() { + $xactions = $this->getTransactions(); + + foreach ($xactions as $key => $xaction) { + if (!$xaction->shouldHideForNotifications()) { + return true; + } + } + + return false; + } + + public function isVisibleInFeed() { + $xactions = $this->getTransactions(); + + foreach ($xactions as $key => $xaction) { + if (!$xaction->shouldHideForFeed()) { + return true; + } + } + + return false; + } + + private function getTransactions() { + $xaction_phids = $this->getValue('transactionPHIDs'); + + $xactions = array(); + foreach ($xaction_phids as $xaction_phid) { + $xactions[] = $this->getObject($xaction_phid); + } + + return $xactions; + } + public function getPrimaryTransaction() { return $this->getObject($this->getPrimaryTransactionPHID()); } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 9e024b43aa..f38c56acb3 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -759,6 +759,10 @@ abstract class PhabricatorApplicationTransaction return $this->shouldHide(); } + public function shouldHideForNotifications() { + return $this->shouldHideForFeed(); + } + public function getTitleForMail() { return id(clone $this)->setRenderingTarget('text')->getTitle(); } diff --git a/src/applications/transactions/storage/PhabricatorModularTransaction.php b/src/applications/transactions/storage/PhabricatorModularTransaction.php index eef4b8e5d9..f6aece2a7f 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransaction.php +++ b/src/applications/transactions/storage/PhabricatorModularTransaction.php @@ -108,6 +108,17 @@ abstract class PhabricatorModularTransaction return parent::shouldHideForMail($xactions); } + final public function shouldHideForNotifications() { + $hide = $this->getTransactionImplementation()->shouldHideForNotifications(); + + // Returning "null" means "use the default behavior". + if ($hide === null) { + return parent::shouldHideForNotifications(); + } + + return $hide; + } + /* final */ public function getIcon() { $icon = $this->getTransactionImplementation()->getIcon(); if ($icon !== null) { diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php index ca82af1c66..35dd3ac197 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -59,6 +59,10 @@ abstract class PhabricatorModularTransactionType return false; } + public function shouldHideForNotifications() { + return null; + } + public function getIcon() { return null; } From e43f9124f8d4c2b0bc7950b8faea5f9058c5df86 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 14:24:30 -0800 Subject: [PATCH 19/62] Remove obsolete "NotifyTest" feed story Summary: Depends on D19864. Ref T13222. See PHI996. This is no longer used by anything, so get rid of it. Test Plan: Grepped; viewed a feed with these stories in it to make sure nothing crashed/exploded. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19865 --- src/__phutil_library_map__.php | 2 -- .../PhabricatorNotificationBuilder.php | 9 ------- .../PhabricatorNotificationTestFeedStory.php | 27 ------------------- 3 files changed, 38 deletions(-) delete mode 100644 src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e2586354e8..9d7b783091 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3504,7 +3504,6 @@ phutil_register_library_map(array( 'PhabricatorNotificationServersConfigType' => 'applications/notification/config/PhabricatorNotificationServersConfigType.php', 'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php', 'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php', - 'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php', 'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php', 'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php', 'PhabricatorNotificationsSetting' => 'applications/settings/setting/PhabricatorNotificationsSetting.php', @@ -9323,7 +9322,6 @@ phutil_register_library_map(array( 'PhabricatorNotificationServersConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorNotificationStatusView' => 'AphrontTagView', 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', - 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory', 'PhabricatorNotificationUIExample' => 'PhabricatorUIExample', 'PhabricatorNotificationsApplication' => 'PhabricatorApplication', 'PhabricatorNotificationsSetting' => 'PhabricatorInternalSetting', diff --git a/src/applications/notification/builder/PhabricatorNotificationBuilder.php b/src/applications/notification/builder/PhabricatorNotificationBuilder.php index 026f55149c..b9a79be3f3 100644 --- a/src/applications/notification/builder/PhabricatorNotificationBuilder.php +++ b/src/applications/notification/builder/PhabricatorNotificationBuilder.php @@ -160,15 +160,6 @@ final class PhabricatorNotificationBuilder extends Phobject { 'href' => $story->getURI(), 'icon' => $story->getImageURI(), ); - } else if ($story instanceof PhabricatorNotificationTestFeedStory) { - $dict[] = array( - 'showAnyNotification' => $web_ready, - 'showDesktopNotification' => $desktop_ready, - 'title' => pht('Test Notification'), - 'body' => $story->renderText(), - 'href' => null, - 'icon' => PhabricatorUser::getDefaultProfileImageURI(), - ); } else { $dict[] = array( 'showWebNotification' => false, diff --git a/src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php b/src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php deleted file mode 100644 index ad984431ba..0000000000 --- a/src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php +++ /dev/null @@ -1,27 +0,0 @@ -getAuthorPHID(); - } - - public function renderView() { - $data = $this->getStoryData(); - - $author_phid = $data->getAuthorPHID(); - - $view = $this->newStoryView(); - - $view->setTitle($data->getValue('title')); - $view->setImage($this->getHandle($author_phid)->getImageURI()); - - return $view; - } - - public function renderText() { - $data = $this->getStoryData(); - return $data->getValue('title'); - } - -} From 05900a4cc990b0fc06b5b9c55dda99ef68b21d32 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 14:33:23 -0800 Subject: [PATCH 20/62] Add a CLI workflow for testing that notifications are being delivered Summary: Depends on D19865. Ref T13222. See PHI996. Provide a `bin/aphlict notify --user ... --message ...` workflow for sending test notifications from the CLI. Test Plan: {F6058287} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19866 --- src/__phutil_library_map__.php | 2 + ...ricatorAphlictManagementNotifyWorkflow.php | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9d7b783091..8bb0b89d0e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2085,6 +2085,7 @@ phutil_register_library_map(array( 'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php', 'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php', 'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php', + 'PhabricatorAphlictManagementNotifyWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php', 'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php', 'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php', 'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php', @@ -7702,6 +7703,7 @@ phutil_register_library_map(array( 'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorAnchorView' => 'AphrontView', 'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow', + 'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow', diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php new file mode 100644 index 0000000000..a7653e74b5 --- /dev/null +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php @@ -0,0 +1,81 @@ +setName('notify') + ->setSynopsis(pht('Send a notification to a user.')) + ->setArguments( + array( + array( + 'name' => 'user', + 'param' => 'username', + 'help' => pht('User to notify.'), + ), + array( + 'name' => 'message', + 'param' => 'text', + 'help' => pht('Message to send.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $username = $args->getArg('user'); + if (!strlen($username)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a user to notify with "--user".')); + } + + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withUsernames(array($username)) + ->executeOne(); + + if (!$user) { + throw new PhutilArgumentUsageException( + pht( + 'No user with username "%s" exists.', + $username)); + } + + $message = $args->getArg('message'); + if (!strlen($message)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a message to send with "--message".')); + } + + $application_phid = id(new PhabricatorNotificationsApplication()) + ->getPHID(); + + $content_source = $this->newContentSource(); + + $xactions = array(); + + $xactions[] = id(new PhabricatorUserTransaction()) + ->setTransactionType( + PhabricatorUserNotifyTransaction::TRANSACTIONTYPE) + ->setNewValue($message) + ->setForceNotifyPHIDs(array($user->getPHID())); + + $editor = id(new PhabricatorUserTransactionEditor()) + ->setActor($viewer) + ->setActingAsPHID($application_phid) + ->setContentSource($content_source); + + $editor->applyTransactions($user, $xactions); + + echo tsprintf( + "%s\n", + pht('Sent notification.')); + + return 0; + } + +} From 0e067213fbb17e5f06d78fcb2081e04f3907e94c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 10 Dec 2018 16:13:54 -0800 Subject: [PATCH 21/62] Make viewing a user's profile page clear notifications about that user Summary: Ref T13222. See PHI996. This is a general correctness improvement, but also allows you to clear test notifications by clicking on them (since their default destination is the recipient's profile page). Test Plan: Clicked a test notification, got taken to my profile page, saw notification marked as read. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19867 --- .../controller/PhabricatorPeopleProfileViewController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 5dce872c4b..0c32075932 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -74,6 +74,10 @@ final class PhabricatorPeopleProfileViewController ->setTitle($user->getUsername()) ->setNavigation($nav) ->setCrumbs($crumbs) + ->setPageObjectPHIDs( + array( + $user->getPHID(), + )) ->appendChild( array( $home, From d8e2bb9f0f51c73b1d13427eb494a1d2a3c583ba Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 05:20:36 -0800 Subject: [PATCH 22/62] Fix some straggling qsprintf() warnings in repository import Summary: Ref T13217. See . Hunt down and fix two more `qsprintf()` things. I just converted the "performance optimization" into a normal, safe call since we're dealing with far less SVN stuff nowadays and the actual issue has been lost in the mists of time. If it resurfaces, we can take another look. Test Plan: Imported some commits, no longer saw these warnings in the daemon logs. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13217 Differential Revision: https://secure.phabricator.com/D19869 --- .../pathchange/DiffusionPathChangeQuery.php | 3 ++- ...atorRepositoryCommitChangeParserWorker.php | 24 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php b/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php index 8ae9b3d203..850c06f105 100644 --- a/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php +++ b/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php @@ -43,12 +43,13 @@ final class DiffusionPathChangeQuery extends Phobject { $conn_r = $repository->establishConnection('r'); - $limit = ''; if ($this->limit) { $limit = qsprintf( $conn_r, 'LIMIT %d', $this->limit + 1); + } else { + $limit = qsprintf($conn_r, ''); } $raw_changes = queryfx_all( diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php index b544228561..f5d3af7fd8 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php @@ -114,40 +114,36 @@ abstract class PhabricatorRepositoryCommitChangeParserWorker PhabricatorRepositoryCommit $commit, array $changes) { + $conn = $repository->establishConnection('w'); + $repository_id = (int)$repository->getID(); $commit_id = (int)$commit->getID(); - // NOTE: This SQL is being built manually instead of with qsprintf() - // because some SVN changes affect an enormous number of paths (millions) - // and this showed up as significantly slow on a profile at some point. - $changes_sql = array(); foreach ($changes as $change) { - $values = array( + $changes_sql[] = qsprintf( + $conn, + '(%d, %d, %d, %nd, %nd, %d, %d, %d, %d)', $repository_id, (int)$change->getPathID(), $commit_id, - nonempty((int)$change->getTargetPathID(), 'null'), - nonempty((int)$change->getTargetCommitID(), 'null'), + nonempty((int)$change->getTargetPathID(), null), + nonempty((int)$change->getTargetCommitID(), null), (int)$change->getChangeType(), (int)$change->getFileType(), (int)$change->getIsDirect(), - (int)$change->getCommitSequence(), - ); - $changes_sql[] = '('.implode(', ', $values).')'; + (int)$change->getCommitSequence()); } - $conn_w = $repository->establishConnection('w'); - queryfx( - $conn_w, + $conn, 'DELETE FROM %T WHERE commitID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit_id); foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) { queryfx( - $conn_w, + $conn, 'INSERT INTO %T (repositoryID, pathID, commitID, targetPathID, targetCommitID, changeType, fileType, isDirect, commitSequence) From 66ff6d4a2ca00b7d4fd15eda140c140735205159 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 08:09:13 -0800 Subject: [PATCH 23/62] Fix an issue with creating tasks directly into workboard columns Summary: See . The recent `setIsConduitOnly()` / `setIsFormField()` change (in D19842) disrupted creating tasks directly into a column from the workboard UI. This field //is// a form field, it just doesn't render a visible control. Test Plan: - Created a task directly into a workboard column. Before: column selection ignored. After: appeared in correct column. - Used "move on workboard" comment action. - Edited tasks; edited forms for tasks. Didn't observe any collateral damage (weird "Column" fields being present). Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19870 --- src/applications/maniphest/editor/ManiphestEditEngine.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 0b0b4d6758..dc9c56f840 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -169,7 +169,9 @@ EODOCS ->setConduitDocumentation($column_documentation) ->setAliases(array('columnPHID', 'columns', 'columnPHIDs')) ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) - ->setIsFormField(false) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) ->setCommentActionLabel(pht('Move on Workboard')) ->setCommentActionOrder(2000) ->setColumnMap($column_map), From 265f1f9c4d8a73e26ed20693a364d3940420c55a Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 09:29:27 -0800 Subject: [PATCH 24/62] Fix an issue with item list view icon labels (including Differential date updated times) not appearing in the UI Summary: In D19855, I removed a no-longer-necessary link around icons in some cases, but incorrectly discarded labels in other cases. Restore labels. Test Plan: Viewed Differential revision list, saw date stamps again. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19871 --- src/view/phui/PHUIObjectItemView.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 565b9c2165..e1c67c7f32 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -471,7 +471,10 @@ final class PHUIObjectItemView extends AphrontTagView { array( 'class' => implode(' ', $classes), ), - $icon); + array( + $icon, + $label, + )); } $icons[] = phutil_tag( From 2814d340367c4f369aa56e1a51480206adb44702 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 10:57:21 -0800 Subject: [PATCH 25/62] Fix a stray qsprintf() in the Herald rules engine when recording rule application to objects Summary: Ref T13217. See PHI1006. Test Plan: Touched an object with associated Herald rules. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13217 Differential Revision: https://secure.phabricator.com/D19872 --- src/applications/herald/engine/HeraldEngine.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php index 739e83e4e8..d853e3eb9a 100644 --- a/src/applications/herald/engine/HeraldEngine.php +++ b/src/applications/herald/engine/HeraldEngine.php @@ -243,9 +243,9 @@ final class HeraldEngine extends Phobject { } queryfx( $conn_w, - 'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q', + 'INSERT IGNORE INTO %T (phid, ruleID) VALUES %LQ', HeraldRule::TABLE_RULE_APPLIED, - implode(', ', $sql)); + $sql); } } } From 5cb462d511c7d938af8bfc2f6da70e7f72c0bdc7 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 12 Dec 2018 13:02:52 -0800 Subject: [PATCH 26/62] Show more of UTC offset when user's TZ is not an integer number of hours offset Summary: See https://discourse.phabricator-community.org/t/personal-timezone-setting-mismatch-cleared-and-more-specific-cases/1680. The code has always worked correctly, but the resulting timezone mismatch warning messsage wasn't specific enough when the mismatch is by a non-integer number of hours. Test Plan: Set timezone locally to Asia/Vladivostok and in Phabricator to Australia/Adelaide (which as of today's date are 30 minutes apart) and observed a more precise error message: F6061330 Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19873 --- .../PhabricatorSettingsTimezoneController.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php index 2e2e174e0f..51f1747b9f 100644 --- a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php +++ b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php @@ -63,7 +63,7 @@ final class PhabricatorSettingsTimezoneController $server_offset = $viewer->getTimeZoneOffset(); - if ($client_offset == $server_offset || $did_calibrate) { + if (($client_offset == $server_offset) || $did_calibrate) { return $this->newDialog() ->setTitle(pht('Timezone Calibrated')) ->appendParagraph( @@ -113,12 +113,13 @@ final class PhabricatorSettingsTimezoneController } private function formatOffset($offset) { - $offset = $offset / 60; - - if ($offset >= 0) { - return pht('UTC-%d', $offset); + $hours = $offset / 60; + // Non-integer number of hours off UTC? + if ($offset % 60) { + $minutes = abs($offset % 60); + return pht('UTC%+d:%02d', $hours, $minutes); } else { - return pht('UTC+%d', -$offset); + return pht('UTC%+d', $hours); } } From aba99459238fcb04e095c7ddead2271f95cacbdc Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 12 Dec 2018 15:38:06 -0800 Subject: [PATCH 27/62] Move user approval to modular transactions Summary: See https://discourse.phabricator-community.org/t/how-to-approve-user-via-conduit-api/2189. This particular use case doesn't seem very compelling, but moving this logic out of `PhabricatorUserEditor` is a win anyway. Test Plan: Registered a new user, approved/unapproved them conduit, approved from the UI. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19877 --- src/__phutil_library_map__.php | 2 + .../PhabricatorPeopleApproveController.php | 34 +++---- .../editor/PhabricatorUserEditEngine.php | 10 ++ .../people/editor/PhabricatorUserEditor.php | 39 -------- .../PhabricatorUserApproveTransaction.php | 96 +++++++++++++++++++ .../PhabricatorUserDisableTransaction.php | 2 + 6 files changed, 121 insertions(+), 62 deletions(-) create mode 100644 src/applications/people/xaction/PhabricatorUserApproveTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8bb0b89d0e..f4c6faa4f1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4597,6 +4597,7 @@ phutil_register_library_map(array( 'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', + 'PhabricatorUserApproveTransaction' => 'applications/people/xaction/PhabricatorUserApproveTransaction.php', 'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php', 'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php', 'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php', @@ -10645,6 +10646,7 @@ phutil_register_library_map(array( 'PhabricatorConduitResultInterface', 'PhabricatorAuthPasswordHashInterface', ), + 'PhabricatorUserApproveTransaction' => 'PhabricatorUserTransactionType', 'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', 'PhabricatorUserCache' => 'PhabricatorUserDAO', diff --git a/src/applications/people/controller/PhabricatorPeopleApproveController.php b/src/applications/people/controller/PhabricatorPeopleApproveController.php index 0e97ad6ee6..013f4371f6 100644 --- a/src/applications/people/controller/PhabricatorPeopleApproveController.php +++ b/src/applications/people/controller/PhabricatorPeopleApproveController.php @@ -24,30 +24,18 @@ final class PhabricatorPeopleApproveController } if ($request->isFormPost()) { - id(new PhabricatorUserEditor()) + $xactions = array(); + + $xactions[] = id(new PhabricatorUserTransaction()) + ->setTransactionType(PhabricatorUserApproveTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + id(new PhabricatorUserTransactionEditor()) ->setActor($viewer) - ->approveUser($user, true); - - $title = pht( - 'Phabricator Account "%s" Approved', - $user->getUsername()); - - $body = sprintf( - "%s\n\n %s\n\n", - pht( - 'Your Phabricator account (%s) has been approved by %s. You can '. - 'login here:', - $user->getUsername(), - $viewer->getUsername()), - PhabricatorEnv::getProductionURI('/')); - - $mail = id(new PhabricatorMetaMTAMail()) - ->addTos(array($user->getPHID())) - ->addCCs(array($viewer->getPHID())) - ->setSubject('[Phabricator] '.$title) - ->setForceDelivery(true) - ->setBody($body) - ->saveAndSend(); + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->applyTransactions($user, $xactions); return id(new AphrontRedirectResponse())->setURI($done_uri); } diff --git a/src/applications/people/editor/PhabricatorUserEditEngine.php b/src/applications/people/editor/PhabricatorUserEditEngine.php index c4c1abf1e3..4b3a60a2fc 100644 --- a/src/applications/people/editor/PhabricatorUserEditEngine.php +++ b/src/applications/people/editor/PhabricatorUserEditEngine.php @@ -75,6 +75,16 @@ final class PhabricatorUserEditEngine ->setConduitDescription(pht('Disable or enable the user.')) ->setConduitTypeDescription(pht('True to disable the user.')) ->setValue($object->getIsDisabled()), + id(new PhabricatorBoolEditField()) + ->setKey('approved') + ->setOptions(pht('Approved'), pht('Unapproved')) + ->setLabel(pht('Approved')) + ->setDescription(pht('Approve the user.')) + ->setTransactionType(PhabricatorUserApproveTransaction::TRANSACTIONTYPE) + ->setIsFormField(false) + ->setConduitDescription(pht('Approve or reject the user.')) + ->setConduitTypeDescription(pht('True to approve the user.')) + ->setValue($object->getIsApproved()), ); } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 58c2ba1ff1..a64c1cc2a9 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -293,45 +293,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - /** - * @task role - */ - public function approveUser(PhabricatorUser $user, $approve) { - $actor = $this->requireActor(); - - if (!$user->getID()) { - throw new Exception(pht('User has not been created yet!')); - } - - $user->openTransaction(); - $user->beginWriteLocking(); - - $user->reload(); - if ($user->getIsApproved() == $approve) { - $user->endWriteLocking(); - $user->killTransaction(); - return $this; - } - - $log = PhabricatorUserLog::initializeNewLog( - $actor, - $user->getPHID(), - PhabricatorUserLog::ACTION_APPROVE); - $log->setOldValue($user->getIsApproved()); - $log->setNewValue($approve); - - $user->setIsApproved($approve); - $user->save(); - - $log->save(); - - $user->endWriteLocking(); - $user->saveTransaction(); - - return $this; - } - - /* -( Adding, Removing and Changing Email )-------------------------------- */ diff --git a/src/applications/people/xaction/PhabricatorUserApproveTransaction.php b/src/applications/people/xaction/PhabricatorUserApproveTransaction.php new file mode 100644 index 0000000000..e458c5822c --- /dev/null +++ b/src/applications/people/xaction/PhabricatorUserApproveTransaction.php @@ -0,0 +1,96 @@ +getIsApproved(); + } + + public function generateNewValue($object, $value) { + return (bool)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsApproved((int)$value); + } + + 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( + 'Phabricator Account "%s" Approved', + $user->getUsername()); + + $body = sprintf( + "%s\n\n %s\n\n", + pht( + 'Your Phabricator account (%s) has been approved by %s. You can '. + 'login here:', + $user->getUsername(), + $actor->getUsername()), + PhabricatorEnv::getProductionURI('/')); + + $mail = id(new PhabricatorMetaMTAMail()) + ->addTos(array($user->getPHID())) + ->addCCs(array($actor->getPHID())) + ->setSubject('[Phabricator] '.$title) + ->setForceDelivery(true) + ->setBody($body) + ->saveAndSend(); + } + + public function getTitle() { + $new = $this->getNewValue(); + if ($new) { + return pht( + '%s approved this user.', + $this->renderAuthor()); + } else { + return pht( + '%s rejected this user.', + $this->renderAuthor()); + } + } + + public function shouldHideForFeed() { + return true; + } + + public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + $errors = array(); + + foreach ($xactions as $xaction) { + $is_approved = (bool)$object->getIsApproved(); + + if ((bool)$xaction->getNewValue() === $is_approved) { + continue; + } + + if (!$actor->getIsAdmin()) { + $errors[] = $this->newInvalidError( + pht('You must be an administrator to approve users.')); + } + } + + return $errors; + } + + public function getRequiredCapabilities( + $object, + PhabricatorApplicationTransaction $xaction) { + + // Unlike normal user edits, approvals require admin permissions, which + // is enforced by validateTransactions(). + + return null; + } +} diff --git a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php index 629d538dee..7a8a1c7966 100644 --- a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php @@ -15,7 +15,9 @@ final class PhabricatorUserDisableTransaction public function applyInternalEffects($object, $value) { $object->setIsDisabled((int)$value); + } + public function applyExternalEffects($object, $value) { $this->newUserLog(PhabricatorUserLog::ACTION_DISABLE) ->setOldValue((bool)$object->getIsDisabled()) ->setNewValue((bool)$value) From 5c99163b7c8050e1ec62cd57b25573a3fa4812b4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 15:37:51 -0800 Subject: [PATCH 28/62] Remove application callers to "LiskDAO->loadRelatives()" Summary: Ref T13218. See that task for some discussion. `loadRelatives()` is like `loadAllWhere(...)` except that it does enormous amounts of weird magic which we've moved away from. Test Plan: Did not test whatsoever since these changes are in Releeph. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13218 Differential Revision: https://secure.phabricator.com/D19874 --- .../conduit/ReleephGetBranchesConduitAPIMethod.php | 8 +++----- .../ReleephDiffSizeFieldSpecification.php | 13 +++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php b/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php index 6e225ec8e3..469713ce39 100644 --- a/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php +++ b/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php @@ -30,11 +30,9 @@ final class ReleephGetBranchesConduitAPIMethod extends ReleephConduitAPIMethod { foreach ($projects as $project) { $repository = $project->getRepository(); - $branches = $project->loadRelatives( - id(new ReleephBranch()), - 'releephProjectID', - 'getID', - 'isActive = 1'); + $branches = id(new ReleephBranch())->loadAllWhere( + 'releephProjectID = %d AND isActive = 1', + $project->getID()); foreach ($branches as $branch) { $full_branch_name = $branch->getName(); diff --git a/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php b/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php index 5975f208e6..20c8d5e226 100644 --- a/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php +++ b/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php @@ -22,16 +22,17 @@ final class ReleephDiffSizeFieldSpecification } $diff_rev = $requested_object; - $diffs = $diff_rev->loadRelatives( - new DifferentialDiff(), - 'revisionID', - 'getID', - 'creationMethod <> "commit"'); + $diffs = id(new DifferentialDiff())->loadAllWhere( + 'revisionID = %d AND creationMethod != %s', + $diff_rev->getID(), + 'commit'); $all_changesets = array(); $most_recent_changesets = null; foreach ($diffs as $diff) { - $changesets = $diff->loadRelatives(new DifferentialChangeset(), 'diffID'); + $changesets = id(new DifferentialChangeset())->loadAllWhere( + 'diffID = %d', + $diff->getID()); $all_changesets += $changesets; $most_recent_changesets = $changesets; } From 793f185d292400e8b9ab713cd8c96ffe6b130e28 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 15:47:17 -0800 Subject: [PATCH 29/62] Remove application callsites to "LiskDAO->loadOneRelative()" Summary: Ref T13218. This is like `loadOneWhere(...)` but with more dark magic. Get rid of it. Test Plan: - Forced `20130219.commitsummarymig.php` to hit this code and ran it with `bin/storage upgrade --force --apply ...`. - Ran `20130409.commitdrev.php` with `bin/storage upgrade --force --apply ...`. - Called `user.search` to indirectly get primary email information. - Did not test Releeph at all. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13218 Differential Revision: https://secure.phabricator.com/D19876 --- resources/sql/patches/20130219.commitsummarymig.php | 6 +++--- resources/sql/patches/20130409.commitdrev.php | 4 +++- src/applications/people/storage/PhabricatorUser.php | 11 +++-------- .../conduit/ReleephGetBranchesConduitAPIMethod.php | 7 +++---- src/applications/releeph/storage/ReleephRequest.php | 13 ++++++------- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/resources/sql/patches/20130219.commitsummarymig.php b/resources/sql/patches/20130219.commitsummarymig.php index 60bdd1542c..f47016804d 100644 --- a/resources/sql/patches/20130219.commitsummarymig.php +++ b/resources/sql/patches/20130219.commitsummarymig.php @@ -12,9 +12,9 @@ foreach ($commits as $commit) { continue; } - $data = $commit->loadOneRelative( - new PhabricatorRepositoryCommitData(), - 'commitID'); + $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( + 'commitID = %d', + $commit->getID()); if (!$data) { continue; diff --git a/resources/sql/patches/20130409.commitdrev.php b/resources/sql/patches/20130409.commitdrev.php index fb556f1846..a264e8edeb 100644 --- a/resources/sql/patches/20130409.commitdrev.php +++ b/resources/sql/patches/20130409.commitdrev.php @@ -8,7 +8,9 @@ $commit_table->establishConnection('w'); $edges = 0; foreach (new LiskMigrationIterator($commit_table) as $commit) { - $data = $commit->loadOneRelative($data_table, 'commitID'); + $data = $data_table->loadOneWhere( + 'commitID = %d', + $commit->getID()); if (!$data) { continue; } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 7717fbf18c..5d310378e0 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -458,14 +458,9 @@ final class PhabricatorUser } public function loadPrimaryEmail() { - $email = new PhabricatorUserEmail(); - $conn = $email->establishConnection('r'); - - return $this->loadOneRelative( - $email, - 'userPHID', - 'getPHID', - qsprintf($conn, '(isPrimary = 1)')); + return id(new PhabricatorUserEmail())->loadOneWhere( + 'userPHID = %s AND isPrimary = 1', + $this->getPHID()); } diff --git a/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php b/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php index 469713ce39..5516b4edb6 100644 --- a/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php +++ b/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php @@ -37,10 +37,9 @@ final class ReleephGetBranchesConduitAPIMethod extends ReleephConduitAPIMethod { foreach ($branches as $branch) { $full_branch_name = $branch->getName(); - $cut_point_commit = $branch->loadOneRelative( - id(new PhabricatorRepositoryCommit()), - 'phid', - 'getCutPointCommitPHID'); + $cut_point_commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( + 'phid = %s', + $branch->getCutPointCommitPHID()); $results[] = array( 'project' => $project->getName(), diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index c071600ec7..a877192107 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -257,18 +257,17 @@ final class ReleephRequest extends ReleephDAO /* -( Loading external objects )------------------------------------------- */ public function loadPhabricatorRepositoryCommit() { - return $this->loadOneRelative( - new PhabricatorRepositoryCommit(), - 'phid', - 'getRequestCommitPHID'); + return id(new PhabricatorRepositoryCommit())->loadOneWhere( + 'phid = %s', + $this->getRequestCommitPHID()); } public function loadPhabricatorRepositoryCommitData() { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { - return $commit->loadOneRelative( - new PhabricatorRepositoryCommitData(), - 'commitID'); + return id(new PhabricatorRepositoryCommitData())->loadOneWhere( + 'commitID = %d', + $commit->getID()); } } From 02933acbd5ae8c843020600c2125590cded1897e Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 16:12:11 -0800 Subject: [PATCH 30/62] Remove all application callers to "putInSet()" Summary: Ref T13218. This is the last public-facing API call for `loadRelatives/loadOneRelative`. This just "primed" objects to make the other calls work and had no direct effects. Test Plan: - Ran `bin/fact analyze`. - Used `bin/storage upgrade -f --apply` to apply `20181031.board.01.queryreset.php`, which uses `LiskMigrationIterator`. - Browsed user list. Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13218 Differential Revision: https://secure.phabricator.com/D19878 --- .../fact/extract/PhabricatorFactUpdateIterator.php | 7 +------ src/applications/people/query/PhabricatorPeopleQuery.php | 9 +-------- .../storage/lisk/LiskMigrationIterator.php | 8 +++----- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/applications/fact/extract/PhabricatorFactUpdateIterator.php b/src/applications/fact/extract/PhabricatorFactUpdateIterator.php index df77f4dd33..c0d229084f 100644 --- a/src/applications/fact/extract/PhabricatorFactUpdateIterator.php +++ b/src/applications/fact/extract/PhabricatorFactUpdateIterator.php @@ -12,11 +12,8 @@ final class PhabricatorFactUpdateIterator extends PhutilBufferedIterator { private $position; private $ignoreUpdatesDuration = 15; - private $set; - public function __construct(LiskDAO $object) { - $this->set = new LiskDAOSet(); - $this->object = $object->putInSet($this->set); + $this->object = $object; } public function setPosition($position) { @@ -41,8 +38,6 @@ final class PhabricatorFactUpdateIterator extends PhutilBufferedIterator { } protected function loadPage() { - $this->set->clearSet(); - if ($this->object->hasProperty('dateModified')) { if ($this->cursor) { list($after_epoch, $after_id) = explode(':', $this->cursor); diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index 131dc7c588..542b685e29 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -163,14 +163,7 @@ final class PhabricatorPeopleQuery } protected function loadPage() { - $table = new PhabricatorUser(); - $data = $this->loadStandardPageRows($table); - - if ($this->needPrimaryEmail) { - $table->putInSet(new LiskDAOSet()); - } - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function didFilterPage(array $users) { diff --git a/src/infrastructure/storage/lisk/LiskMigrationIterator.php b/src/infrastructure/storage/lisk/LiskMigrationIterator.php index a46512f866..edd31c8123 100644 --- a/src/infrastructure/storage/lisk/LiskMigrationIterator.php +++ b/src/infrastructure/storage/lisk/LiskMigrationIterator.php @@ -17,11 +17,9 @@ final class LiskMigrationIterator extends PhutilBufferedIterator { private $object; private $cursor; - private $set; public function __construct(LiskDAO $object) { - $this->set = new LiskDAOSet(); - $this->object = $object->putInSet($this->set); + $this->object = $object; } protected function didRewind() { @@ -33,15 +31,15 @@ final class LiskMigrationIterator extends PhutilBufferedIterator { } protected function loadPage() { - $this->set->clearSet(); - $results = $this->object->loadAllWhere( 'id > %d ORDER BY id ASC LIMIT %d', $this->cursor, $this->getPageSize()); + if ($results) { $this->cursor = last($results)->getID(); } + return $results; } From 9aa5a52fbd1b4265633df88b337eb95acee9bd85 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 16:15:19 -0800 Subject: [PATCH 31/62] Completely remove "LiskDAOSet" and "loadRelatives/loadOneRelative" Summary: Fixes T13218. We have no more callers to any of this and can get rid of it forever. Test Plan: Grepped for all four API methods, `LiskDAOSet`, and `inSet`. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13218 Differential Revision: https://secure.phabricator.com/D19879 --- src/__phutil_library_map__.php | 2 - src/infrastructure/storage/lisk/LiskDAO.php | 170 ------------------ .../storage/lisk/LiskDAOSet.php | 101 ----------- 3 files changed, 273 deletions(-) delete mode 100644 src/infrastructure/storage/lisk/LiskDAOSet.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f4c6faa4f1..09430367d9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1633,7 +1633,6 @@ phutil_register_library_map(array( 'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php', 'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php', 'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php', - 'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php', 'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php', 'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php', 'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php', @@ -7200,7 +7199,6 @@ phutil_register_library_map(array( 'Phobject', 'AphrontDatabaseTableRefInterface', ), - 'LiskDAOSet' => 'Phobject', 'LiskDAOTestCase' => 'PhabricatorTestCase', 'LiskEphemeralObjectException' => 'Exception', 'LiskFixtureTestCase' => 'PhabricatorTestCase', diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index aa12f8e614..81005ab30d 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -193,8 +193,6 @@ abstract class LiskDAO extends Phobject private static $connections = array(); - private $inSet = null; - protected $id; protected $phid; protected $dateCreated; @@ -659,179 +657,11 @@ abstract class LiskDAO extends Phobject } else { $result[] = $obj->loadFromArray($row); } - if ($this->inSet) { - $this->inSet->addToSet($obj); - } } return $result; } - /** - * This method helps to prevent the 1+N queries problem. It happens when you - * execute a query for each row in a result set. Like in this code: - * - * COUNTEREXAMPLE, name=Easy to write but expensive to execute - * $diffs = id(new DifferentialDiff())->loadAllWhere( - * 'revisionID = %d', - * $revision->getID()); - * foreach ($diffs as $diff) { - * $changesets = id(new DifferentialChangeset())->loadAllWhere( - * 'diffID = %d', - * $diff->getID()); - * // Do something with $changesets. - * } - * - * One can solve this problem by reading all the dependent objects at once and - * assigning them later: - * - * COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain - * $diffs = id(new DifferentialDiff())->loadAllWhere( - * 'revisionID = %d', - * $revision->getID()); - * $all_changesets = id(new DifferentialChangeset())->loadAllWhere( - * 'diffID IN (%Ld)', - * mpull($diffs, 'getID')); - * $all_changesets = mgroup($all_changesets, 'getDiffID'); - * foreach ($diffs as $diff) { - * $changesets = idx($all_changesets, $diff->getID(), array()); - * // Do something with $changesets. - * } - * - * The method @{method:loadRelatives} abstracts this approach which allows - * writing a code which is simple and efficient at the same time: - * - * name=Easy to write and cheap to execute - * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); - * foreach ($diffs as $diff) { - * $changesets = $diff->loadRelatives( - * new DifferentialChangeset(), - * 'diffID'); - * // Do something with $changesets. - * } - * - * This will load dependent objects for all diffs in the first call of - * @{method:loadRelatives} and use this result for all following calls. - * - * The method supports working with set of sets, like in this code: - * - * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); - * foreach ($diffs as $diff) { - * $changesets = $diff->loadRelatives( - * new DifferentialChangeset(), - * 'diffID'); - * foreach ($changesets as $changeset) { - * $hunks = $changeset->loadRelatives( - * new DifferentialHunk(), - * 'changesetID'); - * // Do something with hunks. - * } - * } - * - * This code will execute just three queries - one to load all diffs, one to - * load all their related changesets and one to load all their related hunks. - * You can try to write an equivalent code without using this method as - * a homework. - * - * The method also supports retrieving referenced objects, for example authors - * of all diffs (using shortcut @{method:loadOneRelative}): - * - * foreach ($diffs as $diff) { - * $author = $diff->loadOneRelative( - * new PhabricatorUser(), - * 'phid', - * 'getAuthorPHID'); - * // Do something with author. - * } - * - * It is also possible to specify additional conditions for the `WHERE` - * clause. Similarly to @{method:loadAllWhere}, you can specify everything - * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is - * allowed to pass only a constant string (`%` doesn't have a special - * meaning). This is intentional to avoid mistakes with using data from one - * row in retrieving other rows. Example of a correct usage: - * - * $status = $author->loadOneRelative( - * new PhabricatorCalendarEvent(), - * 'userPHID', - * 'getPHID', - * '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'); - * - * @param LiskDAO Type of objects to load. - * @param string Name of the column in target table. - * @param string Method name in this table. - * @param string Additional constraints on returned rows. It supports no - * placeholders and requires putting the WHERE part into - * parentheses. It's not possible to use LIMIT. - * @return list Objects of type $object. - * - * @task load - */ - public function loadRelatives( - LiskDAO $object, - $foreign_column, - $key_method = 'getID', - $where = '') { - - if (!$this->inSet) { - id(new LiskDAOSet())->addToSet($this); - } - $relatives = $this->inSet->loadRelatives( - $object, - $foreign_column, - $key_method, - $where); - return idx($relatives, $this->$key_method(), array()); - } - - /** - * Load referenced row. See @{method:loadRelatives} for details. - * - * @param LiskDAO Type of objects to load. - * @param string Name of the column in target table. - * @param string Method name in this table. - * @param string Additional constraints on returned rows. It supports no - * placeholders and requires putting the WHERE part into - * parentheses. It's not possible to use LIMIT. - * @return LiskDAO Object of type $object or null if there's no such object. - * - * @task load - */ - final public function loadOneRelative( - LiskDAO $object, - $foreign_column, - $key_method = 'getID', - $where = '') { - - $relatives = $this->loadRelatives( - $object, - $foreign_column, - $key_method, - $where); - - if (!$relatives) { - return null; - } - - if (count($relatives) > 1) { - throw new AphrontCountQueryException( - pht( - 'More than one result from %s!', - __FUNCTION__.'()')); - } - - return reset($relatives); - } - - final public function putInSet(LiskDAOSet $set) { - $this->inSet = $set; - return $this; - } - - final protected function getInSet() { - return $this->inSet; - } - /* -( Examining Objects )-------------------------------------------------- */ diff --git a/src/infrastructure/storage/lisk/LiskDAOSet.php b/src/infrastructure/storage/lisk/LiskDAOSet.php deleted file mode 100644 index 90eba708ea..0000000000 --- a/src/infrastructure/storage/lisk/LiskDAOSet.php +++ /dev/null @@ -1,101 +0,0 @@ -addToSet($author); - * foreach ($reviewers as $reviewer) { - * $users->addToSet($reviewer); - * } - * foreach ($ccs as $cc) { - * $users->addToSet($cc); - * } - * // Preload e-mails of all involved users and return e-mails of author. - * $author_emails = $author->loadRelatives( - * new PhabricatorUserEmail(), - * 'userPHID', - * 'getPHID'); - */ -final class LiskDAOSet extends Phobject { - private $daos = array(); - private $relatives = array(); - private $subsets = array(); - - public function addToSet(LiskDAO $dao) { - if ($this->relatives) { - throw new Exception( - pht( - "Don't call %s after loading data!", - __FUNCTION__.'()')); - } - $this->daos[] = $dao; - $dao->putInSet($this); - return $this; - } - - /** - * The main purpose of this method is to break cyclic dependency. - * It removes all objects from this set and all subsets created by it. - */ - public function clearSet() { - $this->daos = array(); - $this->relatives = array(); - foreach ($this->subsets as $set) { - $set->clearSet(); - } - $this->subsets = array(); - return $this; - } - - - /** - * See @{method:LiskDAO::loadRelatives}. - */ - public function loadRelatives( - LiskDAO $object, - $foreign_column, - $key_method = 'getID', - $where = '') { - - $relatives = &$this->relatives[ - get_class($object)."-{$foreign_column}-{$key_method}-{$where}"]; - - if ($relatives === null) { - $ids = array(); - foreach ($this->daos as $dao) { - $id = $dao->$key_method(); - if ($id !== null) { - $ids[$id] = $id; - } - } - if (!$ids) { - $relatives = array(); - } else { - $set = new LiskDAOSet(); - $this->subsets[] = $set; - - $conn = $object->establishConnection('r'); - - if (strlen($where)) { - $where_clause = qsprintf($conn, 'AND %Q', $where); - } else { - $where_clause = qsprintf($conn, ''); - } - - $relatives = $object->putInSet($set)->loadAllWhere( - '%C IN (%Ls) %Q', - $foreign_column, - $ids, - $where_clause); - $relatives = mgroup($relatives, 'get'.$foreign_column); - } - } - - return $relatives; - } - -} From ecae936d97011293737f39a44feaefaf4583fa77 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Dec 2018 16:55:32 -0800 Subject: [PATCH 32/62] Fix another qsprintf() straggler in "Has Open Subtasks" Summary: See . Test Plan: Queried for "With Open Subtasks" and "With No Open Subtasks". Reviewers: amckinley, joshuaspence Reviewed By: joshuaspence Subscribers: joshuaspence Differential Revision: https://secure.phabricator.com/D19880 --- src/applications/maniphest/query/ManiphestTaskQuery.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index d1932a7597..fc5097f4d3 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -641,9 +641,9 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { if ($this->hasOpenSubtasks !== null) { if ($this->hasOpenSubtasks) { - $join_type = 'JOIN'; + $join_type = qsprintf($conn, 'JOIN'); } else { - $join_type = 'LEFT JOIN'; + $join_type = qsprintf($conn, 'LEFT JOIN'); } $joins[] = qsprintf( From c58506aeaace694534a13b40c558a670aa8230c5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Dec 2018 10:13:56 -0800 Subject: [PATCH 33/62] Give sessions real PHIDs and slightly modernize session queries Summary: Ref T13222. See PHI873. I'm preparing to introduce a new MFA "Challenge" table which stores state about challenges we've issued (to bind challenges to sessions and prevent most challenge reuse). This table will reference sessions (since each challenge will be bound to a particular session) but sessions currently don't have PHIDs. Give them PHIDs and slightly modernize some related code. Test Plan: - Ran migrations. - Verified table got PHIDs. - Used `var_dump()` to dump an organic user session. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19881 --- .../20181213.auth.01.sessionphid.sql | 2 + .../20181213.auth.02.populatephid.php | 18 +++++++++ .../autopatches/20181213.auth.03.phidkey.sql | 2 + src/__phutil_library_map__.php | 2 + .../engine/PhabricatorAuthSessionEngine.php | 1 + .../phid/PhabricatorAuthSessionPHIDType.php | 34 +++++++++++++++++ .../query/PhabricatorAuthSessionQuery.php | 38 ++++++++++--------- .../auth/storage/PhabricatorAuthSession.php | 5 +++ 8 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 resources/sql/autopatches/20181213.auth.01.sessionphid.sql create mode 100644 resources/sql/autopatches/20181213.auth.02.populatephid.php create mode 100644 resources/sql/autopatches/20181213.auth.03.phidkey.sql create mode 100644 src/applications/auth/phid/PhabricatorAuthSessionPHIDType.php diff --git a/resources/sql/autopatches/20181213.auth.01.sessionphid.sql b/resources/sql/autopatches/20181213.auth.01.sessionphid.sql new file mode 100644 index 0000000000..34b5aa5bf6 --- /dev/null +++ b/resources/sql/autopatches/20181213.auth.01.sessionphid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.phabricator_session + ADD phid VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20181213.auth.02.populatephid.php b/resources/sql/autopatches/20181213.auth.02.populatephid.php new file mode 100644 index 0000000000..314eaf87a3 --- /dev/null +++ b/resources/sql/autopatches/20181213.auth.02.populatephid.php @@ -0,0 +1,18 @@ +establishConnection('w'); + +foreach ($iterator as $session) { + if (strlen($session->getPHID())) { + continue; + } + + queryfx( + $conn, + 'UPDATE %R SET phid = %s WHERE id = %d', + $table, + $session->generatePHID(), + $session->getID()); +} diff --git a/resources/sql/autopatches/20181213.auth.03.phidkey.sql b/resources/sql/autopatches/20181213.auth.03.phidkey.sql new file mode 100644 index 0000000000..6bc11b3e55 --- /dev/null +++ b/resources/sql/autopatches/20181213.auth.03.phidkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.phabricator_session + ADD UNIQUE KEY `key_phid` (phid); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 09430367d9..420457f525 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2296,6 +2296,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionEngineExtensionModule' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtensionModule.php', 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', 'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php', + 'PhabricatorAuthSessionPHIDType' => 'applications/auth/phid/PhabricatorAuthSessionPHIDType.php', 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', 'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php', 'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php', @@ -7948,6 +7949,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthSessionInfo' => 'Phobject', + 'PhabricatorAuthSessionPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController', diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 4ce86e8f97..ada4162067 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -119,6 +119,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $conn_r, 'SELECT s.id AS s_id, + s.phid AS s_phid, s.sessionExpires AS s_sessionExpires, s.sessionStart AS s_sessionStart, s.highSecurityUntil AS s_highSecurityUntil, diff --git a/src/applications/auth/phid/PhabricatorAuthSessionPHIDType.php b/src/applications/auth/phid/PhabricatorAuthSessionPHIDType.php new file mode 100644 index 0000000000..e031c4ae88 --- /dev/null +++ b/src/applications/auth/phid/PhabricatorAuthSessionPHIDType.php @@ -0,0 +1,34 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + return; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthSessionQuery.php b/src/applications/auth/query/PhabricatorAuthSessionQuery.php index 25928e72c1..a2101201e2 100644 --- a/src/applications/auth/query/PhabricatorAuthSessionQuery.php +++ b/src/applications/auth/query/PhabricatorAuthSessionQuery.php @@ -4,6 +4,7 @@ final class PhabricatorAuthSessionQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $ids; + private $phids; private $identityPHIDs; private $sessionKeys; private $sessionTypes; @@ -28,19 +29,17 @@ final class PhabricatorAuthSessionQuery return $this; } + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthSession(); + } + protected function loadPage() { - $table = new PhabricatorAuthSession(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $sessions) { @@ -65,8 +64,8 @@ final class PhabricatorAuthSessionQuery return $sessions; } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( @@ -75,6 +74,13 @@ final class PhabricatorAuthSessionQuery $this->ids); } + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + if ($this->identityPHIDs !== null) { $where[] = qsprintf( $conn, @@ -100,9 +106,7 @@ final class PhabricatorAuthSessionQuery $this->sessionTypes); } - $where[] = $this->buildPagingClause($conn); - - return $this->formatWhereClause($conn, $where); + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/auth/storage/PhabricatorAuthSession.php b/src/applications/auth/storage/PhabricatorAuthSession.php index cf707a053d..c4731b669b 100644 --- a/src/applications/auth/storage/PhabricatorAuthSession.php +++ b/src/applications/auth/storage/PhabricatorAuthSession.php @@ -20,6 +20,7 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO protected function getConfiguration() { return array( self::CONFIG_TIMESTAMPS => false, + self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'type' => 'text32', 'sessionKey' => 'bytes40', @@ -74,6 +75,10 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO } } + public function getPHIDType() { + return PhabricatorAuthSessionPHIDType::TYPECONST; + } + public function isHighSecuritySession() { $until = $this->getHighSecurityUntil(); From 1d34238dc94555466e15039ff6991b371ae294ef Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Dec 2018 10:52:54 -0800 Subject: [PATCH 34/62] Upgrade sessions digests to HMAC256, retaining compatibility with old digests Summary: Ref T13222. Ref T13225. We store a digest of the session key in the session table (not the session key itself) so that users with access to this table can't easily steal sessions by just setting their cookies to values from the table. Users with access to the database can //probably// do plenty of other bad stuff (e.g., T13134 mentions digesting Conduit tokens) but there's very little cost to storing digests instead of live tokens. We currently digest session keys with HMAC-SHA1. This is fine, but HMAC-SHA256 is better. Upgrade: - Always write new digests. - We still match sessions with either digest. - When we read a session with an old digest, upgrade it to a new digest. In a few months we can throw away the old code. When we do, installs that skip upgrades for a long time may suffer a one-time logout, but I'll note this in the changelog. We could avoid this by storing `hmac256(hmac1(key))` instead and re-hashing in a migration, but I think the cost of a one-time logout for some tiny subset of users is very low, and worth keeping things simpler in the long run. Test Plan: - Hit a page with an old session, got a session upgrade. - Reviewed sessions in Settings. - Reviewed user logs. - Logged out. - Logged in. - Terminated other sessions individually. - Terminated all other sessions. - Spot checked session table for general sanity. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13225, T13222 Differential Revision: https://secure.phabricator.com/D19883 --- .../20181213.auth.04.longerhashes.sql | 2 + .../20181213.auth.05.longerloghashes.sql | 2 + .../sql/patches/20130530.sessionhash.php | 25 ++-------- ...bricatorAuthTerminateSessionController.php | 5 +- .../PhabricatorAuthUnlinkController.php | 3 +- .../engine/PhabricatorAuthSessionEngine.php | 49 ++++++++++++++----- .../query/PhabricatorAuthSessionQuery.php | 3 +- .../auth/storage/PhabricatorAuthSession.php | 10 +++- .../people/storage/PhabricatorUserLog.php | 2 +- .../PhabricatorMultiFactorSettingsPanel.php | 3 +- .../PhabricatorPasswordSettingsPanel.php | 3 +- .../PhabricatorSessionsSettingsPanel.php | 5 +- src/infrastructure/util/PhabricatorHash.php | 14 +++++- 13 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 resources/sql/autopatches/20181213.auth.04.longerhashes.sql create mode 100644 resources/sql/autopatches/20181213.auth.05.longerloghashes.sql diff --git a/resources/sql/autopatches/20181213.auth.04.longerhashes.sql b/resources/sql/autopatches/20181213.auth.04.longerhashes.sql new file mode 100644 index 0000000000..2bffb4c4a8 --- /dev/null +++ b/resources/sql/autopatches/20181213.auth.04.longerhashes.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.phabricator_session + CHANGE sessionKey sessionKey VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20181213.auth.05.longerloghashes.sql b/resources/sql/autopatches/20181213.auth.05.longerloghashes.sql new file mode 100644 index 0000000000..dc8638d91c --- /dev/null +++ b/resources/sql/autopatches/20181213.auth.05.longerloghashes.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.user_log + CHANGE session session VARBINARY(64); diff --git a/resources/sql/patches/20130530.sessionhash.php b/resources/sql/patches/20130530.sessionhash.php index 771dac61e3..1e09ee32fd 100644 --- a/resources/sql/patches/20130530.sessionhash.php +++ b/resources/sql/patches/20130530.sessionhash.php @@ -1,22 +1,7 @@ openTransaction(); -$conn = $table->establishConnection('w'); - -$sessions = queryfx_all( - $conn, - 'SELECT userPHID, type, sessionKey FROM %T FOR UPDATE', - PhabricatorUser::SESSION_TABLE); - -foreach ($sessions as $session) { - queryfx( - $conn, - 'UPDATE %T SET sessionKey = %s WHERE userPHID = %s AND type = %s', - PhabricatorUser::SESSION_TABLE, - PhabricatorHash::weakDigest($session['sessionKey']), - $session['userPHID'], - $session['type']); -} - -$table->saveTransaction(); +// See T13225. Long ago, this upgraded session key storage from unhashed to +// HMAC-SHA1 here. We later upgraded storage to HMAC-SHA256, so this is initial +// upgrade is now fairly pointless. Dropping this migration entirely only logs +// users out of installs that waited more than 5 years to upgrade, which seems +// like a reasonable behavior. diff --git a/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php b/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php index fa58977c90..413d0306b9 100644 --- a/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php +++ b/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php @@ -16,8 +16,9 @@ final class PhabricatorAuthTerminateSessionController $query->withIDs(array($id)); } - $current_key = PhabricatorHash::weakDigest( - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); + $current_key = PhabricatorAuthSession::newSessionDigest( + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); $sessions = $query->execute(); foreach ($sessions as $key => $session) { diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php index 6211e78110..e6e1493e5a 100644 --- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php @@ -56,7 +56,8 @@ final class PhabricatorAuthUnlinkController id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $viewer, - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); return id(new AphrontRedirectResponse())->setURI($this->getDoneURI()); } diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index ada4162067..2020e4a542 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -109,14 +109,19 @@ final class PhabricatorAuthSessionEngine extends Phobject { $session_table = new PhabricatorAuthSession(); $user_table = new PhabricatorUser(); - $conn_r = $session_table->establishConnection('r'); - $session_key = PhabricatorHash::weakDigest($session_token); + $conn = $session_table->establishConnection('r'); - $cache_parts = $this->getUserCacheQueryParts($conn_r); + // TODO: See T13225. We're moving sessions to a more modern digest + // algorithm, but still accept older cookies for compatibility. + $session_key = PhabricatorAuthSession::newSessionDigest( + new PhutilOpaqueEnvelope($session_token)); + $weak_key = PhabricatorHash::weakDigest($session_token); + + $cache_parts = $this->getUserCacheQueryParts($conn); list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts; $info = queryfx_one( - $conn_r, + $conn, 'SELECT s.id AS s_id, s.phid AS s_phid, @@ -125,21 +130,28 @@ final class PhabricatorAuthSessionEngine extends Phobject { s.highSecurityUntil AS s_highSecurityUntil, s.isPartial AS s_isPartial, s.signedLegalpadDocuments as s_signedLegalpadDocuments, + IF(s.sessionKey = %P, 1, 0) as s_weak, u.* %Q - FROM %T u JOIN %T s ON u.phid = s.userPHID - AND s.type = %s AND s.sessionKey = %P %Q', + FROM %R u JOIN %R s ON u.phid = s.userPHID + AND s.type = %s AND s.sessionKey IN (%P, %P) %Q', + new PhutilOpaqueEnvelope($weak_key), $cache_selects, - $user_table->getTableName(), - $session_table->getTableName(), + $user_table, + $session_table, $session_type, new PhutilOpaqueEnvelope($session_key), + new PhutilOpaqueEnvelope($weak_key), $cache_joins); if (!$info) { return null; } + // TODO: Remove this, see T13225. + $is_weak = (bool)$info['s_weak']; + unset($info['s_weak']); + $session_dict = array( 'userPHID' => $info['phid'], 'sessionKey' => $session_key, @@ -202,6 +214,19 @@ final class PhabricatorAuthSessionEngine extends Phobject { unset($unguarded); } + // TODO: Remove this, see T13225. + if ($is_weak) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $conn_w = $session_table->establishConnection('w'); + queryfx( + $conn_w, + 'UPDATE %T SET sessionKey = %P WHERE id = %d', + $session->getTableName(), + new PhutilOpaqueEnvelope($session_key), + $session->getID()); + unset($unguarded); + } + $user->attachSession($session); return $user; } @@ -241,7 +266,8 @@ final class PhabricatorAuthSessionEngine extends Phobject { // This has a side effect of validating the session type. $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type); - $digest_key = PhabricatorHash::weakDigest($session_key); + $digest_key = PhabricatorAuthSession::newSessionDigest( + new PhutilOpaqueEnvelope($session_key)); // Logging-in users don't have CSRF stuff yet, so we have to unguard this // write. @@ -299,7 +325,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { */ public function terminateLoginSessions( PhabricatorUser $user, - $except_session = null) { + PhutilOpaqueEnvelope $except_session = null) { $sessions = id(new PhabricatorAuthSessionQuery()) ->setViewer($user) @@ -307,7 +333,8 @@ final class PhabricatorAuthSessionEngine extends Phobject { ->execute(); if ($except_session !== null) { - $except_session = PhabricatorHash::weakDigest($except_session); + $except_session = PhabricatorAuthSession::newSessionDigest( + $except_session); } foreach ($sessions as $key => $session) { diff --git a/src/applications/auth/query/PhabricatorAuthSessionQuery.php b/src/applications/auth/query/PhabricatorAuthSessionQuery.php index a2101201e2..00a663e964 100644 --- a/src/applications/auth/query/PhabricatorAuthSessionQuery.php +++ b/src/applications/auth/query/PhabricatorAuthSessionQuery.php @@ -91,7 +91,8 @@ final class PhabricatorAuthSessionQuery if ($this->sessionKeys !== null) { $hashes = array(); foreach ($this->sessionKeys as $session_key) { - $hashes[] = PhabricatorHash::weakDigest($session_key); + $hashes[] = PhabricatorAuthSession::newSessionDigest( + new PhutilOpaqueEnvelope($session_key)); } $where[] = qsprintf( $conn, diff --git a/src/applications/auth/storage/PhabricatorAuthSession.php b/src/applications/auth/storage/PhabricatorAuthSession.php index c4731b669b..6d54dda781 100644 --- a/src/applications/auth/storage/PhabricatorAuthSession.php +++ b/src/applications/auth/storage/PhabricatorAuthSession.php @@ -6,6 +6,8 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO const TYPE_WEB = 'web'; const TYPE_CONDUIT = 'conduit'; + const SESSION_DIGEST_KEY = 'session.digest'; + protected $userPHID; protected $type; protected $sessionKey; @@ -17,13 +19,19 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO private $identityObject = self::ATTACHABLE; + public static function newSessionDigest(PhutilOpaqueEnvelope $session_token) { + return PhabricatorHash::digestWithNamedKey( + $session_token->openEnvelope(), + self::SESSION_DIGEST_KEY); + } + protected function getConfiguration() { return array( self::CONFIG_TIMESTAMPS => false, self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'type' => 'text32', - 'sessionKey' => 'bytes40', + 'sessionKey' => 'text64', 'sessionStart' => 'epoch', 'sessionExpires' => 'epoch', 'highSecurityUntil' => 'epoch?', diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index 82819f8f5b..12cb4cb626 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -150,7 +150,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO 'actorPHID' => 'phid?', 'action' => 'text64', 'remoteAddr' => 'text64', - 'session' => 'bytes40?', + 'session' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'actorPHID' => array( diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index ae653e0f70..5fada0bbed 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -193,7 +193,8 @@ final class PhabricatorMultiFactorSettingsPanel id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $user, - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?id='.$config->getID())); diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php index 9fb8838cf7..79d7610f2f 100644 --- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php @@ -121,7 +121,8 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $user, - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); return id(new AphrontRedirectResponse())->setURI($next); } diff --git a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php index eab18002a1..314d68f69d 100644 --- a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php @@ -44,8 +44,9 @@ final class PhabricatorSessionsSettingsPanel extends PhabricatorSettingsPanel { ->withPHIDs($identity_phids) ->execute(); - $current_key = PhabricatorHash::weakDigest( - $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); + $current_key = PhabricatorAuthSession::newSessionDigest( + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); $rows = array(); $rowc = array(); diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index ce48b0966b..d717778eb3 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -187,6 +187,16 @@ final class PhabricatorHash extends Phobject { } public static function digestHMACSHA256($message, $key) { + if (!is_string($message)) { + throw new Exception( + pht('HMAC-SHA256 can only digest strings.')); + } + + if (!is_string($key)) { + throw new Exception( + pht('HMAC-SHA256 keys must be strings.')); + } + if (!strlen($key)) { throw new Exception( pht('HMAC-SHA256 requires a nonempty key.')); @@ -194,7 +204,9 @@ final class PhabricatorHash extends Phobject { $result = hash_hmac('sha256', $message, $key, $raw_output = false); - if ($result === false) { + // Although "hash_hmac()" is documented as returning `false` when it fails, + // it can also return `null` if you pass an object as the "$message". + if ($result === false || $result === null) { throw new Exception( pht('Unable to compute HMAC-SHA256 digest of message.')); } From 080fb1985f29909bb2710fd25f9918253b1a5c04 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Dec 2018 12:14:33 -0800 Subject: [PATCH 35/62] Upgrade an old "weakDigest()" inside TOTP synchronization code Summary: Ref T13222. Ref T12509. When you add a new MFA TOTP authenticator, we generate a temporary token to make sure you're actually adding the key we generated and not picking your own key. That is, if we just put inputs in the form like `key=123, response=456`, users could pick their own keys by changing the value of `key` and then generating the correct `response`. That's probably fine, but maybe attackers could somehow force users to pick known keys in combination with other unknown vulnerabilities that might exist in the future. Instead, we generate a random key and keep track of it to make sure nothing funny is afoot. As an additional barrier, we do the standard "store the digest, not the real key" sort of thing so you can't force a known value even if you can read the database (although this is mostly pointless since you can just read TOTP secrets directly if you can read the database). But it's pretty standard and doesn't hurt anything. Update this from SHA1 to SHA256. This will break any TOTP factors which someone was in the middle of adding during a Phabricator upgrade, but that seems reasonable. They'll get a sensible failure mode. Test Plan: Added a new TOTP factor. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T12509 Differential Revision: https://secure.phabricator.com/D19884 --- .../auth/factor/PhabricatorTOTPAuthFactor.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 7d7aec921f..ae3608d525 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -2,6 +2,8 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { + const DIGEST_TEMPORARY_KEY = 'mfa.totp.sync'; + public function getFactorKey() { return 'totp'; } @@ -34,12 +36,16 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { // (We store and verify the hash of the key, not the key itself, to limit // how useful the data in the table is to an attacker.) + $token_code = PhabricatorHash::digestWithNamedKey( + $key, + self::DIGEST_TEMPORARY_KEY); + $temporary_token = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer($user) ->withTokenResources(array($user->getPHID())) ->withTokenTypes(array($totp_token_type)) ->withExpired(false) - ->withTokenCodes(array(PhabricatorHash::weakDigest($key))) + ->withTokenCodes(array($token_code)) ->executeOne(); if (!$temporary_token) { // If we don't have a matching token, regenerate the key below. @@ -53,12 +59,16 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { // Mark this key as one we generated, so the user is allowed to submit // a response for it. + $token_code = PhabricatorHash::digestWithNamedKey( + $key, + self::DIGEST_TEMPORARY_KEY); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); id(new PhabricatorAuthTemporaryToken()) ->setTokenResource($user->getPHID()) ->setTokenType($totp_token_type) ->setTokenExpires(time() + phutil_units('1 hour in seconds')) - ->setTokenCode(PhabricatorHash::weakDigest($key)) + ->setTokenCode($token_code) ->save(); unset($unguarded); } From d23cc4b862aac68caf6fc68835884b2d2dae735b Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Thu, 13 Dec 2018 16:13:07 -0800 Subject: [PATCH 36/62] Move user renames to modular transactions Summary: Cleaning up more super-old code from `PhabricatorUserEditor`. Also fix user logging in approve transactions. I'm not sure how it worked at all previously. Test Plan: Created new users, renamed them, checked DB for sanity. Entered invalid names, duplicate names, and empty names, got appropriate error messages. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19887 --- src/__phutil_library_map__.php | 2 + .../PhabricatorPeopleRenameController.php | 56 +++++------ .../people/editor/PhabricatorUserEditor.php | 47 ---------- .../PhabricatorUserUsernameTransaction.php | 92 +++++++++++++++++++ 4 files changed, 116 insertions(+), 81 deletions(-) create mode 100644 src/applications/people/xaction/PhabricatorUserUsernameTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 420457f525..b0408e127d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4646,6 +4646,7 @@ phutil_register_library_map(array( 'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php', 'PhabricatorUserTransactionEditor' => 'applications/people/editor/PhabricatorUserTransactionEditor.php', 'PhabricatorUserTransactionType' => 'applications/people/xaction/PhabricatorUserTransactionType.php', + 'PhabricatorUserUsernameTransaction' => 'applications/people/xaction/PhabricatorUserUsernameTransaction.php', 'PhabricatorUsersEditField' => 'applications/transactions/editfield/PhabricatorUsersEditField.php', 'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php', 'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php', @@ -10706,6 +10707,7 @@ phutil_register_library_map(array( 'PhabricatorUserTransaction' => 'PhabricatorModularTransaction', 'PhabricatorUserTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorUserTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorUserUsernameTransaction' => 'PhabricatorUserTransactionType', 'PhabricatorUsersEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField', diff --git a/src/applications/people/controller/PhabricatorPeopleRenameController.php b/src/applications/people/controller/PhabricatorPeopleRenameController.php index cd552717e0..42ff2e7988 100644 --- a/src/applications/people/controller/PhabricatorPeopleRenameController.php +++ b/src/applications/people/controller/PhabricatorPeopleRenameController.php @@ -22,36 +22,29 @@ final class PhabricatorPeopleRenameController $request, $done_uri); - $errors = array(); - - $v_username = $user->getUsername(); - $e_username = true; + $validation_exception = null; + $username = $user->getUsername(); if ($request->isFormPost()) { - $v_username = $request->getStr('username'); + $username = $request->getStr('username'); + $xactions = array(); - if (!strlen($v_username)) { - $e_username = pht('Required'); - $errors[] = pht('New username is required.'); - } else if ($v_username == $user->getUsername()) { - $e_username = pht('Invalid'); - $errors[] = pht('New username must be different from old username.'); - } else if (!PhabricatorUser::validateUsername($v_username)) { - $e_username = pht('Invalid'); - $errors[] = PhabricatorUser::describeValidUsername(); + $xactions[] = id(new PhabricatorUserTransaction()) + ->setTransactionType( + PhabricatorUserUsernameTransaction::TRANSACTIONTYPE) + ->setNewValue($username); + + $editor = id(new PhabricatorUserTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true); + + try { + $editor->applyTransactions($user, $xactions); + return id(new AphrontRedirectResponse())->setURI($done_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; } - if (!$errors) { - try { - id(new PhabricatorUserEditor()) - ->setActor($viewer) - ->changeUsername($user, $v_username); - - return id(new AphrontRedirectResponse())->setURI($done_uri); - } catch (AphrontDuplicateKeyQueryException $ex) { - $e_username = pht('Not Unique'); - $errors[] = pht('Another user already has that username.'); - } - } } $inst1 = pht( @@ -87,18 +80,13 @@ final class PhabricatorPeopleRenameController ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('New Username')) - ->setValue($v_username) - ->setName('username') - ->setError($e_username)); - - if ($errors) { - $errors = id(new PHUIInfoView())->setErrors($errors); - } + ->setValue($username) + ->setName('username')); return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Change Username')) - ->appendChild($errors) + ->setValidationException($validation_exception) ->appendParagraph($inst1) ->appendParagraph($inst2) ->appendParagraph($inst3) diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index a64c1cc2a9..8092824a0c 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -129,53 +129,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { } - /** - * @task edit - */ - public function changeUsername(PhabricatorUser $user, $username) { - $actor = $this->requireActor(); - - if (!$user->getID()) { - throw new Exception(pht('User has not been created yet!')); - } - - if (!PhabricatorUser::validateUsername($username)) { - $valid = PhabricatorUser::describeValidUsername(); - throw new Exception(pht('Username is invalid! %s', $valid)); - } - - $old_username = $user->getUsername(); - - $user->openTransaction(); - $user->reload(); - $user->setUsername($username); - - try { - $user->save(); - } catch (AphrontDuplicateKeyQueryException $ex) { - $user->setUsername($old_username); - $user->killTransaction(); - throw $ex; - } - - $log = PhabricatorUserLog::initializeNewLog( - $actor, - $user->getPHID(), - PhabricatorUserLog::ACTION_CHANGE_USERNAME); - $log->setOldValue($old_username); - $log->setNewValue($username); - $log->save(); - - $user->saveTransaction(); - - // The SSH key cache currently includes usernames, so dirty it. See T12554 - // for discussion. - PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache(); - - $user->sendUsernameChangeEmail($actor, $old_username); - } - - /* -( Editing Roles )------------------------------------------------------ */ diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php new file mode 100644 index 0000000000..db134a5c78 --- /dev/null +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -0,0 +1,92 @@ +getUsername(); + } + + public function generateNewValue($object, $value) { + return $value; + } + + public function applyInternalEffects($object, $value) { + $object->setUsername($value); + } + + public function applyExternalEffects($object, $value) { + $user = $object; + + $this->newUserLog(PhabricatorUserLog::ACTION_CHANGE_USERNAME) + ->setOldValue($this->getOldValue()) + ->setNewValue($value) + ->save(); + + // The SSH key cache currently includes usernames, so dirty it. See T12554 + // for discussion. + PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache(); + + $user->sendUsernameChangeEmail($this->getActor(), $this->getOldValue()); + } + + public function getTitle() { + return pht( + '%s renamed this user from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + $errors = array(); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + $old = $xaction->getOldValue(); + + if ($old === $new) { + continue; + } + + if (!$actor->getIsAdmin()) { + $errors[] = $this->newInvalidError( + pht('You must be an administrator to rename users.')); + } + + if (!strlen($new)) { + $errors[] = $this->newRequiredError( + pht('New username is required.'), $xaction); + } else if (!PhabricatorUser::validateUsername($new)) { + $errors[] = $this->newInvalidError( + PhabricatorUser::describeValidUsername(), $xaction); + } + + $user = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withUsernames(array($new)) + ->executeOne(); + + if ($user) { + $errors[] = $this->newInvalidError( + pht('Another user already has that username.'), $xaction); + } + + } + + return $errors; + } + + public function getRequiredCapabilities( + $object, + PhabricatorApplicationTransaction $xaction) { + + // Unlike normal user edits, renames require admin permissions, which + // is enforced by validateTransactions(). + + return null; + } +} From 54b952df5d1483a61ceabaf44f92a35df4e0e983 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 14 Dec 2018 15:18:11 -0800 Subject: [PATCH 37/62] Fix weird gap/spacing on user "Manage" page Summary: I added this recently for debugging test notifications, but goofed up the markup, thought it was just some weird layout issue, and never got back to it. Test Plan: {F6063455} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D19892 --- ...abricatorPeopleProfileManageController.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php index 55f9311ada..046726f39e 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php @@ -36,17 +36,18 @@ final class PhabricatorPeopleProfileManageController $crumbs->addTextCrumb(pht('Manage')); $crumbs->setBorder(true); + $timeline = $this->buildTransactionTimeline( + $user, + new PhabricatorPeopleTransactionQuery()); + $timeline->setShouldTerminate(true); + $manage = id(new PHUITwoColumnView()) ->setHeader($header) ->addClass('project-view-home') ->addClass('project-view-people-home') ->setCurtain($curtain) - ->addPropertySection(pht('Details'), $properties); - - $timeline = $this->buildTransactionTimeline( - $user, - new PhabricatorPeopleTransactionQuery()); - $timeline->setShouldTerminate(true); + ->addPropertySection(pht('Details'), $properties) + ->setMainColumn($timeline); return $this->newPage() ->setTitle( @@ -56,11 +57,7 @@ final class PhabricatorPeopleProfileManageController )) ->setNavigation($nav) ->setCrumbs($crumbs) - ->appendChild( - array( - $manage, - $timeline, - )); + ->appendChild($manage); } private function buildPropertyView(PhabricatorUser $user) { From c731508d748aeda7073c3c8be7d073a47d29be4b Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Dec 2018 12:34:12 -0800 Subject: [PATCH 38/62] Require MFA implementations to return a formal result object when validating factors Summary: Ref T13222. See PHI873. Currently, MFA implementations return this weird sort of ad-hoc dictionary from validation, which is later used to render form/control stuff. I want to make this more formal to handle token reuse / session binding cases, and let MFA factors share more code around challenges. Formalize this into a proper object instead of an ad-hoc bundle of properties. Test Plan: - Answered a TOTP MFA prompt wrong (nothing, bad value). - Answered a TOTP MFA prompt properly. - Added new TOTP MFA, survived enrollment. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19885 --- src/__phutil_library_map__.php | 2 + .../engine/PhabricatorAuthSessionEngine.php | 20 +++++++-- ...catorAuthHighSecurityRequiredException.php | 1 + .../auth/factor/PhabricatorAuthFactor.php | 8 +--- .../factor/PhabricatorAuthFactorResult.php | 37 ++++++++++++++++ .../auth/factor/PhabricatorTOTPAuthFactor.php | 43 +++++++++++-------- 6 files changed, 82 insertions(+), 29 deletions(-) create mode 100644 src/applications/auth/factor/PhabricatorAuthFactorResult.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b0408e127d..614479c072 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2198,6 +2198,7 @@ phutil_register_library_map(array( 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', + 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', @@ -7833,6 +7834,7 @@ phutil_register_library_map(array( 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', + 'PhabricatorAuthFactorResult' => 'Phobject', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO', diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 2020e4a542..8754709258 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -496,14 +496,25 @@ final class PhabricatorAuthSessionEngine extends Phobject { $id = $factor->getID(); $impl = $factor->requireImplementation(); - $validation_results[$id] = $impl->processValidateFactorForm( + $validation_result = $impl->processValidateFactorForm( $factor, $viewer, $request); - if (!$impl->isFactorValid($factor, $validation_results[$id])) { + if (!($validation_result instanceof PhabricatorAuthFactorResult)) { + throw new Exception( + pht( + 'Expected "processValidateFactorForm()" to return an object '. + 'of class "%s"; got something else (from "%s").', + 'PhabricatorAuthFactorResult', + get_class($impl))); + } + + if (!$validation_result->getIsValid()) { $ok = false; } + + $validation_results[$id] = $validation_result; } if ($ok) { @@ -595,17 +606,20 @@ final class PhabricatorAuthSessionEngine extends Phobject { array $validation_results, PhabricatorUser $viewer, AphrontRequest $request) { + assert_instances_of($validation_results, 'PhabricatorAuthFactorResult'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions(''); foreach ($factors as $factor) { + $result = idx($validation_results, $factor->getID()); + $factor->requireImplementation()->renderValidateFactorForm( $factor, $form, $viewer, - idx($validation_results, $factor->getID())); + $result); } $form->appendRemarkupInstructions(''); diff --git a/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php index 56a4f9fc89..9f37d36a44 100644 --- a/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php +++ b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php @@ -7,6 +7,7 @@ final class PhabricatorAuthHighSecurityRequiredException extends Exception { private $factorValidationResults; public function setFactorValidationResults(array $results) { + assert_instances_of($results, 'PhabricatorAuthFactorResult'); $this->factorValidationResults = $results; return $this; } diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 7cddc2758c..21c861921f 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -14,19 +14,13 @@ abstract class PhabricatorAuthFactor extends Phobject { PhabricatorAuthFactorConfig $config, AphrontFormView $form, PhabricatorUser $viewer, - $validation_result); + PhabricatorAuthFactorResult $validation_result = null); abstract public function processValidateFactorForm( PhabricatorAuthFactorConfig $config, PhabricatorUser $viewer, AphrontRequest $request); - public function isFactorValid( - PhabricatorAuthFactorConfig $config, - $validation_result) { - return (idx($validation_result, 'valid') === true); - } - public function getParameterName( PhabricatorAuthFactorConfig $config, $name) { diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php new file mode 100644 index 0000000000..80d719063d --- /dev/null +++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php @@ -0,0 +1,37 @@ +isValid = $is_valid; + return $this; + } + + public function getIsValid() { + return $this->isValid; + } + + public function setHint($hint) { + $this->hint = $hint; + return $this; + } + + public function getHint() { + return $this->hint; + } + + public function setValue($value) { + $this->value = $value; + return $this; + } + + public function getValue() { + return $this->value; + } + +} diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index ae3608d525..3658f050e2 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -154,10 +154,14 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { PhabricatorAuthFactorConfig $config, AphrontFormView $form, PhabricatorUser $viewer, - $validation_result) { + PhabricatorAuthFactorResult $validation_result = null) { - if (!$validation_result) { - $validation_result = array(); + if ($validation_result) { + $value = $validation_result->getValue(); + $hint = $validation_result->getHint(); + } else { + $value = null; + $hint = true; } $form->appendChild( @@ -166,8 +170,8 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { ->setLabel(pht('App Code')) ->setDisableAutocomplete(true) ->setCaption(pht('Factor Name: %s', $config->getFactorName())) - ->setValue(idx($validation_result, 'value')) - ->setError(idx($validation_result, 'error', true))); + ->setValue($value) + ->setError($hint)); } public function processValidateFactorForm( @@ -178,21 +182,22 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $code = $request->getStr($this->getParameterName($config, 'totpcode')); $key = new PhutilOpaqueEnvelope($config->getFactorSecret()); - if (self::verifyTOTPCode($viewer, $key, $code)) { - return array( - 'error' => null, - 'value' => $code, - 'valid' => true, - ); - } else { - return array( - 'error' => strlen($code) ? pht('Invalid') : pht('Required'), - 'value' => $code, - 'valid' => false, - ); - } - } + $result = id(new PhabricatorAuthFactorResult()) + ->setValue($code); + if (self::verifyTOTPCode($viewer, $key, $code)) { + $result->setIsValid(true); + } else { + if (strlen($code)) { + $hint = pht('Invalid'); + } else { + $hint = pht('Required'); + } + $result->setHint($hint); + } + + return $result; + } public static function generateNewTOTPKey() { return strtoupper(Filesystem::readRandomCharacters(32)); From b8cbfda07ce6b7d921465d407d10a5cce45a8c6c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Dec 2018 10:13:56 -0800 Subject: [PATCH 39/62] Track MFA "challenges" so we can bind challenges to sessions and support SMS and other push MFA Summary: Ref T13222. See PHI873. Ref T9770. Currently, we support only TOTP MFA. For some MFA (SMS and "push-to-app"-style MFA) we may need to keep track of MFA details (e.g., the code we SMS'd you). There isn't much support for that yet. We also currently allow free reuse of TOTP responses across sessions and workflows. This hypothetically enables some "spyglass" attacks where you look at someone's phone and type the code in before they do. T9770 discusses this in more detail, but is focused on an attack window starting when the user submits the form. I claim the attack window opens when the TOTP code is shown on their phone, and the window between the code being shown and being submitted is //much// more interesting than the window after it is submitted. To address both of these cases, start tracking MFA "Challenges". These are basically a record that we asked you to give us MFA credentials. For TOTP, the challenge binds a particular timestep to a given session, so an attacker can't look at your phone and type the code into their browser before (or after) you do -- they have a different session. For now, this means that codes are reusable in the same session, but that will be refined in the future. For SMS / push, the "Challenge" would store the code we sent you so we could validate it. This is mostly a step on the way toward one-shot MFA, ad-hoc MFA in comment action stacks, and figuring out what's going on with Duo. Test Plan: - Passed MFA normally. - Passed MFA normally, simultaneously, as two different users. - With two different sessions for the same user: - Opened MFA in A, opened MFA in B. B got a "wait". - Submitted MFA in A. - Clicked "Wait" a bunch in B. - Submitted MFA in B when prompted. - Passed MFA normally, then passed MFA normally again with the same code in the same session. (This change does not prevent code reuse.) Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13222, T9770 Differential Revision: https://secure.phabricator.com/D19886 --- .../20181213.auth.06.challenge.sql | 12 ++ src/__phutil_library_map__.php | 9 ++ ...torHighSecurityRequestExceptionHandler.php | 19 ++- .../engine/PhabricatorAuthSessionEngine.php | 92 ++++++++++-- .../auth/factor/PhabricatorAuthFactor.php | 134 ++++++++++++++++- .../factor/PhabricatorAuthFactorResult.php | 31 +++- .../auth/factor/PhabricatorTOTPAuthFactor.php | 142 +++++++++++++++--- .../phid/PhabricatorAuthChallengePHIDType.php | 32 ++++ .../query/PhabricatorAuthChallengeQuery.php | 99 ++++++++++++ .../auth/storage/PhabricatorAuthChallenge.php | 54 +++++++ 10 files changed, 573 insertions(+), 51 deletions(-) create mode 100644 resources/sql/autopatches/20181213.auth.06.challenge.sql create mode 100644 src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php create mode 100644 src/applications/auth/query/PhabricatorAuthChallengeQuery.php create mode 100644 src/applications/auth/storage/PhabricatorAuthChallenge.php diff --git a/resources/sql/autopatches/20181213.auth.06.challenge.sql b/resources/sql/autopatches/20181213.auth.06.challenge.sql new file mode 100644 index 0000000000..0e5eeb35f0 --- /dev/null +++ b/resources/sql/autopatches/20181213.auth.06.challenge.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_challenge ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + userPHID VARBINARY(64) NOT NULL, + factorPHID VARBINARY(64) NOT NULL, + sessionPHID VARBINARY(64) NOT NULL, + challengeKey VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + challengeTTL INT UNSIGNED NOT NULL, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 614479c072..85f54f3cac 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2187,6 +2187,9 @@ phutil_register_library_map(array( 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', + 'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php', + 'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php', + 'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php', 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', @@ -7823,6 +7826,12 @@ phutil_register_library_map(array( 'PhabricatorAuthApplication' => 'PhabricatorApplication', 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthChallenge' => array( + 'PhabricatorAuthDAO', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', diff --git a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php index f8d522711f..1cbf4c6e4f 100644 --- a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php +++ b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php @@ -29,13 +29,28 @@ final class PhabricatorHighSecurityRequestExceptionHandler $throwable) { $viewer = $this->getViewer($request); + $results = $throwable->getFactorValidationResults(); $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( $throwable->getFactors(), - $throwable->getFactorValidationResults(), + $results, $viewer, $request); + $is_wait = false; + foreach ($results as $result) { + if ($result->getIsWait()) { + $is_wait = true; + break; + } + } + + if ($is_wait) { + $submit = pht('Wait Patiently'); + } else { + $submit = pht('Enter High Security'); + } + $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Entering High Security')) @@ -62,7 +77,7 @@ final class PhabricatorHighSecurityRequestExceptionHandler 'actions, you should leave high security.')) ->setSubmitURI($request->getPath()) ->addCancelButton($throwable->getCancelURI()) - ->addSubmitButton(pht('Enter High Security')); + ->addSubmitButton($submit); $request_parameters = $request->getPassthroughRequestParameters( $respect_quicksand = true); diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 8754709258..acd16f690f 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -480,7 +480,59 @@ final class PhabricatorAuthSessionEngine extends Phobject { new PhabricatorAuthTryFactorAction(), 0); + $now = PhabricatorTime::getNow(); + + // We need to do challenge validation first, since this happens whether you + // submitted responses or not. You can't get a "bad response" error before + // you actually submit a response, but you can get a "wait, we can't + // issue a challenge yet" response. Load all issued challenges which are + // currently valid. + $challenges = id(new PhabricatorAuthChallengeQuery()) + ->setViewer($viewer) + ->withFactorPHIDs(mpull($factors, 'getPHID')) + ->withUserPHIDs(array($viewer->getPHID())) + ->withChallengeTTLBetween($now, null) + ->execute(); + $challenge_map = mgroup($challenges, 'getFactorPHID'); + $validation_results = array(); + $ok = true; + + // Validate each factor against issued challenges. For example, this + // prevents you from receiving or responding to a TOTP challenge if another + // challenge was recently issued to a different session. + foreach ($factors as $factor) { + $factor_phid = $factor->getPHID(); + $issued_challenges = idx($challenge_map, $factor_phid, array()); + $impl = $factor->requireImplementation(); + + $new_challenges = $impl->getNewIssuedChallenges( + $factor, + $viewer, + $issued_challenges); + + foreach ($new_challenges as $new_challenge) { + $issued_challenges[] = $new_challenge; + } + $challenge_map[$factor_phid] = $issued_challenges; + + if (!$issued_challenges) { + continue; + } + + $result = $impl->getResultFromIssuedChallenges( + $factor, + $viewer, + $issued_challenges); + + if (!$result) { + continue; + } + + $ok = false; + $validation_results[$factor_phid] = $result; + } + if ($request->isHTTPPost()) { $request->validateCSRF(); if ($request->getExists(AphrontRequest::TYPE_HISEC)) { @@ -491,30 +543,28 @@ final class PhabricatorAuthSessionEngine extends Phobject { new PhabricatorAuthTryFactorAction(), 1); - $ok = true; foreach ($factors as $factor) { - $id = $factor->getID(); + $factor_phid = $factor->getPHID(); + + // If we already have a validation result from previously issued + // challenges, skip validating this factor. + if (isset($validation_results[$factor_phid])) { + continue; + } + $impl = $factor->requireImplementation(); - $validation_result = $impl->processValidateFactorForm( + $validation_result = $impl->getResultFromChallengeResponse( $factor, $viewer, - $request); - - if (!($validation_result instanceof PhabricatorAuthFactorResult)) { - throw new Exception( - pht( - 'Expected "processValidateFactorForm()" to return an object '. - 'of class "%s"; got something else (from "%s").', - 'PhabricatorAuthFactorResult', - get_class($impl))); - } + $request, + $issued_challenges); if (!$validation_result->getIsValid()) { $ok = false; } - $validation_results[$id] = $validation_result; + $validation_results[$factor_phid] = $validation_result; } if ($ok) { @@ -566,6 +616,18 @@ final class PhabricatorAuthSessionEngine extends Phobject { return $token; } + // If we don't have a validation result for some factors yet, fill them + // in with an empty result so form rendering doesn't have to care if the + // results exist or not. This happens when you first load the form and have + // not submitted any responses yet. + foreach ($factors as $factor) { + $factor_phid = $factor->getPHID(); + if (isset($validation_results[$factor_phid])) { + continue; + } + $validation_results[$factor_phid] = new PhabricatorAuthFactorResult(); + } + throw id(new PhabricatorAuthHighSecurityRequiredException()) ->setCancelURI($cancel_uri) ->setFactors($factors) @@ -613,7 +675,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { ->appendRemarkupInstructions(''); foreach ($factors as $factor) { - $result = idx($validation_results, $factor->getID()); + $result = $validation_results[$factor->getPHID()]; $factor->requireImplementation()->renderValidateFactorForm( $factor, diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 21c861921f..2b8ec486e2 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -14,12 +14,7 @@ abstract class PhabricatorAuthFactor extends Phobject { PhabricatorAuthFactorConfig $config, AphrontFormView $form, PhabricatorUser $viewer, - PhabricatorAuthFactorResult $validation_result = null); - - abstract public function processValidateFactorForm( - PhabricatorAuthFactorConfig $config, - PhabricatorUser $viewer, - AphrontRequest $request); + PhabricatorAuthFactorResult $validation_result); public function getParameterName( PhabricatorAuthFactorConfig $config, @@ -40,4 +35,131 @@ abstract class PhabricatorAuthFactor extends Phobject { ->setFactorKey($this->getFactorKey()); } + protected function newResult() { + return new PhabricatorAuthFactorResult(); + } + + protected function newChallenge( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer) { + + return id(new PhabricatorAuthChallenge()) + ->setUserPHID($viewer->getPHID()) + ->setSessionPHID($viewer->getSession()->getPHID()) + ->setFactorPHID($config->getPHID()); + } + + final public function getNewIssuedChallenges( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + array $challenges) { + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); + + $now = PhabricatorTime::getNow(); + + $new_challenges = $this->newIssuedChallenges( + $config, + $viewer, + $challenges); + + assert_instances_of($new_challenges, 'PhabricatorAuthChallenge'); + + foreach ($new_challenges as $new_challenge) { + $ttl = $new_challenge->getChallengeTTL(); + if (!$ttl) { + throw new Exception( + pht('Newly issued MFA challenges must have a valid TTL!')); + } + + if ($ttl < $now) { + throw new Exception( + pht( + 'Newly issued MFA challenges must have a future TTL. This '. + 'factor issued a bad TTL ("%s"). (Did you use a relative '. + 'time instead of an epoch?)', + $ttl)); + } + } + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + foreach ($new_challenges as $challenge) { + $challenge->save(); + } + unset($unguarded); + + return $new_challenges; + } + + abstract protected function newIssuedChallenges( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + array $challenges); + + final public function getResultFromIssuedChallenges( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + array $challenges) { + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); + + $result = $this->newResultFromIssuedChallenges( + $config, + $viewer, + $challenges); + + if ($result === null) { + return $result; + } + + if (!($result instanceof PhabricatorAuthFactorResult)) { + throw new Exception( + pht( + 'Expected "newResultFromIssuedChallenges()" to return null or '. + 'an object of class "%s"; got something else (in "%s").', + 'PhabricatorAuthFactorResult', + get_class($this))); + } + + $result->setIssuedChallenges($challenges); + + return $result; + } + + abstract protected function newResultFromIssuedChallenges( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + array $challenges); + + final public function getResultFromChallengeResponse( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); + + $result = $this->newResultFromChallengeResponse( + $config, + $viewer, + $request, + $challenges); + + if (!($result instanceof PhabricatorAuthFactorResult)) { + throw new Exception( + pht( + 'Expected "newResultFromChallengeResponse()" to return an object '. + 'of class "%s"; got something else (in "%s").', + 'PhabricatorAuthFactorResult', + get_class($this))); + } + + $result->setIssuedChallenges($challenges); + + return $result; + } + + abstract protected function newResultFromChallengeResponse( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges); + } diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php index 80d719063d..d75480747d 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactorResult.php +++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php @@ -4,8 +4,10 @@ final class PhabricatorAuthFactorResult extends Phobject { private $isValid = false; - private $hint; + private $isWait = false; + private $errorMessage; private $value; + private $issuedChallenges = array(); public function setIsValid($is_valid) { $this->isValid = $is_valid; @@ -16,13 +18,22 @@ final class PhabricatorAuthFactorResult return $this->isValid; } - public function setHint($hint) { - $this->hint = $hint; + public function setIsWait($is_wait) { + $this->isWait = $is_wait; return $this; } - public function getHint() { - return $this->hint; + public function getIsWait() { + return $this->isWait; + } + + public function setErrorMessage($error_message) { + $this->errorMessage = $error_message; + return $this; + } + + public function getErrorMessage() { + return $this->errorMessage; } public function setValue($value) { @@ -34,4 +45,14 @@ final class PhabricatorAuthFactorResult return $this->value; } + public function setIssuedChallenges(array $issued_challenges) { + assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge'); + $this->issuedChallenges = $issued_challenges; + return $this; + } + + public function getIssuedChallenges() { + return $this->issuedChallenges; + } + } diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 3658f050e2..7f426d0138 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -77,7 +77,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $e_code = true; if ($request->getExists('totp')) { - $okay = self::verifyTOTPCode( + $okay = $this->verifyTOTPCode( $user, new PhutilOpaqueEnvelope($key), $code); @@ -150,50 +150,131 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { } + protected function newIssuedChallenges( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + array $challenges) { + + $now = $this->getCurrentTimestep(); + + // If we already issued a valid challenge, don't issue a new one. + if ($challenges) { + return array(); + } + + // Otherwise, generate a new challenge for the current timestep. It TTLs + // after it would fall off the bottom of the window. + $timesteps = $this->getAllowedTimesteps(); + $min_step = min($timesteps); + + $step_duration = $this->getTimestepDuration(); + $ttl_steps = ($now - $min_step) + 1; + $ttl_seconds = ($ttl_steps * $step_duration); + + return array( + $this->newChallenge($config, $viewer) + ->setChallengeKey($now) + ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds), + ); + } + public function renderValidateFactorForm( PhabricatorAuthFactorConfig $config, AphrontFormView $form, PhabricatorUser $viewer, - PhabricatorAuthFactorResult $validation_result = null) { + PhabricatorAuthFactorResult $result) { - if ($validation_result) { - $value = $validation_result->getValue(); - $hint = $validation_result->getHint(); + $value = $result->getValue(); + $error = $result->getErrorMessage(); + $is_wait = $result->getIsWait(); + + if ($is_wait) { + $control = id(new AphrontFormMarkupControl()) + ->setValue($error) + ->setError(pht('Wait')); } else { - $value = null; - $hint = true; + $control = id(new PHUIFormNumberControl()) + ->setName($this->getParameterName($config, 'totpcode')) + ->setDisableAutocomplete(true) + ->setValue($value) + ->setError($error); } - $form->appendChild( - id(new PHUIFormNumberControl()) - ->setName($this->getParameterName($config, 'totpcode')) - ->setLabel(pht('App Code')) - ->setDisableAutocomplete(true) - ->setCaption(pht('Factor Name: %s', $config->getFactorName())) - ->setValue($value) - ->setError($hint)); + $control + ->setLabel(pht('App Code')) + ->setCaption(pht('Factor Name: %s', $config->getFactorName())); + + $form->appendChild($control); } - public function processValidateFactorForm( + protected function newResultFromIssuedChallenges( PhabricatorAuthFactorConfig $config, PhabricatorUser $viewer, - AphrontRequest $request) { + array $challenges) { + + // If we've already issued a challenge at the current timestep or any + // nearby timestep, require that it was issued to the current session. + // This is defusing attacks where you (broadly) look at someone's phone + // and type the code in more quickly than they do. + + $step_duration = $this->getTimestepDuration(); + $now = $this->getCurrentTimestep(); + $timesteps = $this->getAllowedTimesteps(); + $timesteps = array_fuse($timesteps); + $min_step = min($timesteps); + + $session_phid = $viewer->getSession()->getPHID(); + + foreach ($challenges as $challenge) { + $challenge_timestep = (int)$challenge->getChallengeKey(); + + // This challenge isn't for one of the timesteps you'd be able to respond + // to if you submitted the form right now, so we're good to keep going. + if (!isset($timesteps[$challenge_timestep])) { + continue; + } + + // This is the number of timesteps you need to wait for the problem + // timestep to leave the window, rounded up. + $wait_steps = ($challenge_timestep - $min_step) + 1; + $wait_duration = ($wait_steps * $step_duration); + + if ($challenge->getSessionPHID() !== $session_phid) { + return $this->newResult() + ->setIsWait(true) + ->setErrorMessage( + pht( + 'This factor recently issued a challenge to a different login '. + 'session. Wait %s seconds for the code to cycle, then try '. + 'again.', + new PhutilNumber($wait_duration))); + } + } + + return null; + } + + protected function newResultFromChallengeResponse( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { $code = $request->getStr($this->getParameterName($config, 'totpcode')); $key = new PhutilOpaqueEnvelope($config->getFactorSecret()); - $result = id(new PhabricatorAuthFactorResult()) + $result = $this->newResult() ->setValue($code); - if (self::verifyTOTPCode($viewer, $key, $code)) { + if ($this->verifyTOTPCode($viewer, $key, (string)$code)) { $result->setIsValid(true); } else { if (strlen($code)) { - $hint = pht('Invalid'); + $error_message = pht('Invalid'); } else { - $hint = pht('Required'); + $error_message = pht('Required'); } - $result->setHint($hint); + $result->setErrorMessage($error_message); } return $result; @@ -203,7 +284,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return strtoupper(Filesystem::readRandomCharacters(32)); } - public static function verifyTOTPCode( + private function verifyTOTPCode( PhabricatorUser $user, PhutilOpaqueEnvelope $key, $code) { @@ -318,4 +399,19 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $rows); } + private function getTimestepDuration() { + return 30; + } + + private function getCurrentTimestep() { + $duration = $this->getTimestepDuration(); + return (int)(PhabricatorTime::getNow() / $duration); + } + + private function getAllowedTimesteps() { + $now = $this->getCurrentTimestep(); + return range($now - 2, $now + 2); + } + + } diff --git a/src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php b/src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php new file mode 100644 index 0000000000..2d2fea26b6 --- /dev/null +++ b/src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php @@ -0,0 +1,32 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withUserPHIDs(array $user_phids) { + $this->userPHIDs = $user_phids; + return $this; + } + + public function withFactorPHIDs(array $factor_phids) { + $this->factorPHIDs = $factor_phids; + return $this; + } + + public function withChallengeTTLBetween($challenge_min, $challenge_max) { + $this->challengeTTLMin = $challenge_min; + $this->challengeTTLMax = $challenge_max; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthChallenge(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->userPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'userPHID IN (%Ls)', + $this->userPHIDs); + } + + if ($this->factorPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'factorPHID IN (%Ls)', + $this->factorPHIDs); + } + + if ($this->challengeTTLMin !== null) { + $where[] = qsprintf( + $conn, + 'challengeTTL >= %d', + $this->challengeTTLMin); + } + + if ($this->challengeTTLMax !== null) { + $where[] = qsprintf( + $conn, + 'challengeTTL <= %d', + $this->challengeTTLMax); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php new file mode 100644 index 0000000000..883aad475a --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php @@ -0,0 +1,54 @@ + array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'challengeKey' => 'text255', + 'challengeTTL' => 'epoch', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_issued' => array( + 'columns' => array('userPHID', 'challengeTTL'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorAuthChallengePHIDType::TYPECONST; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::POLICY_NOONE; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return ($viewer->getPHID() === $this->getUserPHID()); + } + +} From 5e94343c7d1ac87f1c4621c503373e88ddc892e3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 14 Dec 2018 05:26:51 -0800 Subject: [PATCH 40/62] Add a garbage collector for MFA challenges Summary: Depends on D19886. Ref T13222. Clean up MFA challenges after they expire. (There's maybe some argument to keeping these around for a little while for debugging/forensics, but I suspect it would never actually be valuable and figure we can cross that bridge if we come to it.) Test Plan: - Ran `bin/garbage collect --collector ...` and saw old MFA challenges collected. - Triggered a new challenge, GC'd again, saw it survive GC while still active. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19888 --- src/__phutil_library_map__.php | 2 ++ ...abricatorAuthChallengeGarbageCollector.php | 28 +++++++++++++++++++ .../auth/storage/PhabricatorAuthChallenge.php | 3 ++ 3 files changed, 33 insertions(+) create mode 100644 src/applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 85f54f3cac..8b545f3767 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2188,6 +2188,7 @@ phutil_register_library_map(array( 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', 'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php', + 'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php', 'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php', 'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php', 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', @@ -7830,6 +7831,7 @@ phutil_register_library_map(array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', ), + 'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', diff --git a/src/applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php b/src/applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php new file mode 100644 index 0000000000..8a715cb178 --- /dev/null +++ b/src/applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php @@ -0,0 +1,28 @@ +establishConnection('w'); + + queryfx( + $conn, + 'DELETE FROM %R WHERE challengeTTL < UNIX_TIMESTAMP() LIMIT 100', + $challenge_table); + + return ($conn->getAffectedRows() == 100); + } + +} diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php index 883aad475a..4ef2a7054f 100644 --- a/src/applications/auth/storage/PhabricatorAuthChallenge.php +++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php @@ -25,6 +25,9 @@ final class PhabricatorAuthChallenge 'key_issued' => array( 'columns' => array('userPHID', 'challengeTTL'), ), + 'key_collection' => array( + 'columns' => array('challengeTTL'), + ), ), ) + parent::getConfiguration(); } From b6999c7ef41dac8699ed07063872ea36ef3390f6 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Fri, 14 Dec 2018 15:19:56 -0800 Subject: [PATCH 41/62] Move admin promotions to modular transactions Summary: Continue clean up of super-old code. I am pretty proud of "defrocked", but would also consider "dethroned", "ousted", "unseated", "unmade", or "disenfranchised". I feel like there's a word for being kicked out of Hogwarts and having your wizarding powers revoked, but it is not leaping to mind. Test Plan: Promoted/demoted users to/from admin, attempted to demote myself and observed preserved witty text, checked user timelines, checked feed, checked DB for sanity, including `user_logs`. I didn't test exposing this via Conduit to attempt promoting a user without having admin access. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19891 --- src/__phutil_library_map__.php | 2 + .../PhabricatorPeopleEmpowerController.php | 31 +++--- .../people/editor/PhabricatorUserEditor.php | 39 -------- .../PhabricatorUserEmpowerTransaction.php | 95 +++++++++++++++++++ 4 files changed, 115 insertions(+), 52 deletions(-) create mode 100644 src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8b545f3767..8478bb45d5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4622,6 +4622,7 @@ phutil_register_library_map(array( 'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php', 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', 'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php', + 'PhabricatorUserEmpowerTransaction' => 'applications/people/xaction/PhabricatorUserEmpowerTransaction.php', 'PhabricatorUserFerretEngine' => 'applications/people/search/PhabricatorUserFerretEngine.php', 'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php', 'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php', @@ -10683,6 +10684,7 @@ phutil_register_library_map(array( 'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase', 'PhabricatorUserEmail' => 'PhabricatorUserDAO', 'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase', + 'PhabricatorUserEmpowerTransaction' => 'PhabricatorUserTransactionType', 'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine', 'PhabricatorUserFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorUserIconField' => 'PhabricatorUserCustomField', diff --git a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php index a49f8b3d1d..09021bf73e 100644 --- a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php +++ b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php @@ -22,22 +22,26 @@ final class PhabricatorPeopleEmpowerController $request, $done_uri); - if ($user->getPHID() == $viewer->getPHID()) { - return $this->newDialog() - ->setTitle(pht('Your Way is Blocked')) - ->appendParagraph( - pht( - 'After a time, your efforts fail. You can not adjust your own '. - 'status as an administrator.')) - ->addCancelButton($done_uri, pht('Accept Fate')); - } + $validation_exception = null; if ($request->isFormPost()) { - id(new PhabricatorUserEditor()) - ->setActor($viewer) - ->makeAdminUser($user, !$user->getIsAdmin()); + $xactions = array(); + $xactions[] = id(new PhabricatorUserTransaction()) + ->setTransactionType( + PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE) + ->setNewValue(!$user->getIsAdmin()); - return id(new AphrontRedirectResponse())->setURI($done_uri); + $editor = id(new PhabricatorUserTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true); + + try { + $editor->applyTransactions($user, $xactions); + return id(new AphrontRedirectResponse())->setURI($done_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + } } if ($user->getIsAdmin()) { @@ -60,6 +64,7 @@ final class PhabricatorPeopleEmpowerController } return $this->newDialog() + ->setValidationException($validation_exception) ->setTitle($title) ->setShortTitle($short) ->appendParagraph($body) diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 8092824a0c..c8068858da 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -131,45 +131,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { /* -( Editing Roles )------------------------------------------------------ */ - - /** - * @task role - */ - public function makeAdminUser(PhabricatorUser $user, $admin) { - $actor = $this->requireActor(); - - if (!$user->getID()) { - throw new Exception(pht('User has not been created yet!')); - } - - $user->openTransaction(); - $user->beginWriteLocking(); - - $user->reload(); - if ($user->getIsAdmin() == $admin) { - $user->endWriteLocking(); - $user->killTransaction(); - return $this; - } - - $log = PhabricatorUserLog::initializeNewLog( - $actor, - $user->getPHID(), - PhabricatorUserLog::ACTION_ADMIN); - $log->setOldValue($user->getIsAdmin()); - $log->setNewValue($admin); - - $user->setIsAdmin((int)$admin); - $user->save(); - - $log->save(); - - $user->endWriteLocking(); - $user->saveTransaction(); - - return $this; - } - /** * @task role */ diff --git a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php new file mode 100644 index 0000000000..12d6dc7523 --- /dev/null +++ b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php @@ -0,0 +1,95 @@ +getIsAdmin(); + } + + public function generateNewValue($object, $value) { + return (bool)$value; + } + + public function applyInternalEffects($object, $value) { + $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(); + + $errors = array(); + foreach ($xactions as $xaction) { + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + + if ($old === $new) { + continue; + } + + if ($user->getPHID() === $actor->getPHID()) { + $errors[] = $this->newInvalidError( + pht('After a time, your efforts fail. You can not adjust your own '. + 'status as an administrator.'), $xaction); + } + + if (!$actor->getIsAdmin()) { + $errors[] = $this->newInvalidError( + pht('You must be an administrator to create administrators.'), + $xaction); + } + } + + return $errors; + } + + public function getTitle() { + $new = $this->getNewValue(); + if ($new) { + return pht( + '%s empowered this user as an administrator.', + $this->renderAuthor()); + } else { + return pht( + '%s defrocked this user.', + $this->renderAuthor()); + } + } + + public function getTitleForFeed() { + $new = $this->getNewValue(); + if ($new) { + return pht( + '%s empowered %s as an administrator.', + $this->renderAuthor(), + $this->renderObject()); + } else { + return pht( + '%s defrocked %s.', + $this->renderAuthor(), + $this->renderObject()); + } + } + + public function getRequiredCapabilities( + $object, + PhabricatorApplicationTransaction $xaction) { + + // Unlike normal user edits, admin promotions require admin + // permissions, which is enforced by validateTransactions(). + + return null; + } +} From 46052878b1de10f33feab0895c8c90d2e87c20ec Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 14 Dec 2018 05:39:53 -0800 Subject: [PATCH 42/62] Bind MFA challenges to particular workflows, like signing a specific Legalpad document Summary: Depends on D19888. Ref T13222. When we issue an MFA challenge, prevent the user from responding to it in the context of a different workflow: if you ask for MFA to do something minor (award a token) you can't use the same challenge to do something more serious (launch nukes). This defuses highly-hypothetical attacks where the attacker: - already controls the user's session (since the challenge is already bound to the session); and - can observe MFA codes. One version of this attack is the "spill coffee on the victim when the code is shown on their phone, then grab their phone" attack. This whole vector really strains the bounds of plausibility, but it's easy to lock challenges to a workflow and it's possible that there's some more clever version of the "spill coffee" attack available to more sophisticated social engineers or with future MFA factors which we don't yet support. The "spill coffee" attack, in detail, is: - Go over to the victim's desk. - Ask them to do something safe and nonsuspicious that requires MFA (sign `L123 Best Friendship Agreement`). - When they unlock their phone, spill coffee all over them. - Urge them to go to the bathroom to clean up immediately, leaving their phone and computer in your custody. - Type the MFA code shown on the phone into a dangerous MFA prompt (sign `L345 Eternal Declaration of War`). - When they return, they may not suspect anything (it would be normal for the MFA token to have expired), or you can spill more coffee on their computer now to destroy it, and blame it on the earlier spill. Test Plan: - Triggered signatures for two different documents. - Got prompted in one, got a "wait" in the other. - Backed out of the good prompt, returned, still prompted. - Answered the good prompt. - Waited for the bad prompt to expire. - Went through the bad prompt again, got an actual prompt this time. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19889 --- .../20181214.auth.01.workflowkey.sql | 2 ++ .../engine/PhabricatorAuthSessionEngine.php | 24 +++++++++++++++++++ .../auth/factor/PhabricatorAuthFactor.php | 5 +++- .../auth/factor/PhabricatorTOTPAuthFactor.php | 14 +++++++++++ .../auth/storage/PhabricatorAuthChallenge.php | 2 ++ .../storage/PhabricatorAuthFactorConfig.php | 15 ++++++++++++ .../LegalpadDocumentSignController.php | 5 ++++ 7 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 resources/sql/autopatches/20181214.auth.01.workflowkey.sql diff --git a/resources/sql/autopatches/20181214.auth.01.workflowkey.sql b/resources/sql/autopatches/20181214.auth.01.workflowkey.sql new file mode 100644 index 0000000000..538778e218 --- /dev/null +++ b/resources/sql/autopatches/20181214.auth.01.workflowkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_challenge + ADD workflowKey VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index acd16f690f..f3814b949d 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -46,6 +46,26 @@ final class PhabricatorAuthSessionEngine extends Phobject { const ONETIME_USERNAME = 'rename'; + private $workflowKey; + + public function setWorkflowKey($workflow_key) { + $this->workflowKey = $workflow_key; + return $this; + } + + public function getWorkflowKey() { + + // TODO: A workflow key should become required in order to issue an MFA + // challenge, but allow things to keep working for now until we can update + // callsites. + if ($this->workflowKey === null) { + return 'legacy'; + } + + return $this->workflowKey; + } + + /** * Get the session kind (e.g., anonymous, user, external account) from a * session token. Returns a `KIND_` constant. @@ -473,6 +493,10 @@ final class PhabricatorAuthSessionEngine extends Phobject { return $this->issueHighSecurityToken($session, true); } + foreach ($factors as $factor) { + $factor->setSessionEngine($this); + } + // Check for a rate limit without awarding points, so the user doesn't // get partway through the workflow only to get blocked. PhabricatorSystemActionEngine::willTakeAction( diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 2b8ec486e2..be99df9c79 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -43,10 +43,13 @@ abstract class PhabricatorAuthFactor extends Phobject { PhabricatorAuthFactorConfig $config, PhabricatorUser $viewer) { + $engine = $config->getSessionEngine(); + return id(new PhabricatorAuthChallenge()) ->setUserPHID($viewer->getPHID()) ->setSessionPHID($viewer->getSession()->getPHID()) - ->setFactorPHID($config->getPHID()); + ->setFactorPHID($config->getPHID()) + ->setWorkflowKey($engine->getWorkflowKey()); } final public function getNewIssuedChallenges( diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 7f426d0138..373745e244 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -225,6 +225,9 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $session_phid = $viewer->getSession()->getPHID(); + $engine = $config->getSessionEngine(); + $workflow_key = $engine->getWorkflowKey(); + foreach ($challenges as $challenge) { $challenge_timestep = (int)$challenge->getChallengeKey(); @@ -249,6 +252,17 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { 'again.', new PhutilNumber($wait_duration))); } + + if ($challenge->getWorkflowKey() !== $workflow_key) { + return $this->newResult() + ->setIsWait(true) + ->setErrorMessage( + pht( + 'This factor recently issued a challenge for a different '. + 'workflow. Wait %s seconds for the code to cycle, then try '. + 'again.', + new PhutilNumber($wait_duration))); + } } return null; diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php index 4ef2a7054f..63d2092e49 100644 --- a/src/applications/auth/storage/PhabricatorAuthChallenge.php +++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php @@ -7,6 +7,7 @@ final class PhabricatorAuthChallenge protected $userPHID; protected $factorPHID; protected $sessionPHID; + protected $workflowKey; protected $challengeKey; protected $challengeTTL; protected $properties = array(); @@ -20,6 +21,7 @@ final class PhabricatorAuthChallenge self::CONFIG_COLUMN_SCHEMA => array( 'challengeKey' => 'text255', 'challengeTTL' => 'epoch', + 'workflowKey' => 'text255', ), self::CONFIG_KEY_SCHEMA => array( 'key_issued' => array( diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php index 8420ea9ba7..2bed939402 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -8,6 +8,8 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO { protected $factorSecret; protected $properties = array(); + private $sessionEngine; + protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( @@ -49,4 +51,17 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO { return $impl; } + public function setSessionEngine(PhabricatorAuthSessionEngine $engine) { + $this->sessionEngine = $engine; + return $this; + } + + public function getSessionEngine() { + if (!$this->sessionEngine) { + throw new PhutilInvalidStateException('setSessionEngine'); + } + + return $this->sessionEngine; + } + } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php index 8ef35e3493..27769432c2 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -154,7 +154,12 @@ final class LegalpadDocumentSignController extends LegalpadController { // Require two-factor auth to sign legal documents. if ($viewer->isLoggedIn()) { + $workflow_key = sprintf( + 'legalpad.sign(%s)', + $document->getPHID()); + $hisec_token = id(new PhabricatorAuthSessionEngine()) + ->setWorkflowKey($workflow_key) ->requireHighSecurityToken( $viewer, $request, From 95ea4f11b94f7ce9cc26a91423374684e5b8d2bf Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Tue, 18 Dec 2018 12:06:29 -0800 Subject: [PATCH 43/62] Fix sorting bug in ProjectDatasource Summary: See https://discourse.phabricator-community.org/t/typeahead-returning-only-archived-results/2220. Ref T12538. If a user has more than 100 disabled projects matching their search term, only disabled projects will be returned in the typeahead search results. Test Plan: Harcoded hard limit in `PhabricatorTypeaheadModularDatasourceController` to force truncation of search results, observed active project on top of results as expected. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T12538 Differential Revision: https://secure.phabricator.com/D19907 --- src/applications/project/query/PhabricatorProjectQuery.php | 5 +++++ .../project/typeahead/PhabricatorProjectDatasource.php | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 9b051c00dd..293378355e 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -187,6 +187,11 @@ final class PhabricatorProjectQuery 'column' => 'milestoneNumber', 'type' => 'int', ), + 'status' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'status', + 'type' => 'int', + ), ); } diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index d0abcad2ca..e5b24335cf 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -26,7 +26,8 @@ final class PhabricatorProjectDatasource $query = id(new PhabricatorProjectQuery()) ->needImages(true) - ->needSlugs(true); + ->needSlugs(true) + ->setOrderVector(array('-status', 'id')); if ($this->getPhase() == self::PHASE_PREFIX) { $prefix = $this->getPrefixQuery(); From 38083f6f9efe1389e5d12bef971a9fb25e58798b Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Dec 2018 15:14:44 -0800 Subject: [PATCH 44/62] Slightly modernize PholioImageQuery Summary: Ref T11351. My end goal is to remove `applyInitialEffects()` from Pholio to clear the way for D19897. Start with some query modernization. Test Plan: Browsed Pholio, nothing appeared to have changed. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19910 --- .../pholio/query/PholioImageQuery.php | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/applications/pholio/query/PholioImageQuery.php b/src/applications/pholio/query/PholioImageQuery.php index 79ffdc56d7..5f541f1768 100644 --- a/src/applications/pholio/query/PholioImageQuery.php +++ b/src/applications/pholio/query/PholioImageQuery.php @@ -44,43 +44,32 @@ final class PholioImageQuery return $this->mockCache; } - protected function loadPage() { - $table = new PholioImage(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - $images = $table->loadAllFromArray($data); - - return $images; + public function newResultObject() { + return new PholioImage(); } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } - $where[] = $this->buildPagingClause($conn); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->mockIDs) { + if ($this->mockIDs !== null) { $where[] = qsprintf( $conn, 'mockID IN (%Ld)', @@ -94,7 +83,7 @@ final class PholioImageQuery $this->obsolete); } - return $this->formatWhereClause($conn, $where); + return $where; } protected function willFilterPage(array $images) { From 1d84b5b86b848d7b084d82e010a36aaa7a00d0ab Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Dec 2018 15:23:07 -0800 Subject: [PATCH 45/62] Give Pholio images a more modern initializer method Summary: Depends on D19910. Ref T11351. Minor changes to make this behave in a more modern way. Test Plan: - Destroyed a mock. - Lipsum'd a mock. - Poked around, edited/viewed mocks. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19911 --- .../controller/PholioImageUploadController.php | 2 +- .../controller/PholioMockEditController.php | 4 ++-- .../PhabricatorPholioMockTestDataGenerator.php | 9 +++++---- .../pholio/storage/PholioImage.php | 18 ++++++++++++------ src/applications/pholio/storage/PholioMock.php | 7 ++++--- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/applications/pholio/controller/PholioImageUploadController.php b/src/applications/pholio/controller/PholioImageUploadController.php index 0329d3eb1d..39dd661a4a 100644 --- a/src/applications/pholio/controller/PholioImageUploadController.php +++ b/src/applications/pholio/controller/PholioImageUploadController.php @@ -22,7 +22,7 @@ final class PholioImageUploadController extends PholioController { $title = $file->getName(); } - $image = id(new PholioImage()) + $image = PholioImage::initializeNewImage() ->attachFile($file) ->setName($title) ->setDescription($description) diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index 89d1fe2a50..9a09c9bc67 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -140,7 +140,7 @@ final class PholioMockEditController extends PholioController { $sequence = $sequence_map[$file_phid]; if ($replaces_image_phid) { - $replace_image = id(new PholioImage()) + $replace_image = PholioImage::initializeNewImage() ->setReplacesImagePHID($replaces_image_phid) ->setFilePhid($file_phid) ->attachFile($file) @@ -153,7 +153,7 @@ final class PholioMockEditController extends PholioController { ->setNewValue($replace_image); $posted_mock_images[] = $replace_image; } else if (!$existing_image) { // this is an add - $add_image = id(new PholioImage()) + $add_image = PholioImage::initializeNewImage() ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) diff --git a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php index 039b0ddeef..c038fcf922 100644 --- a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php +++ b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php @@ -41,10 +41,11 @@ final class PhabricatorPholioMockTestDataGenerator $sequence = 0; $images = array(); foreach ($files as $file) { - $image = new PholioImage(); - $image->setFilePHID($file->getPHID()); - $image->setSequence($sequence++); - $image->attachMock($mock); + $image = PholioImage::initializeNewImage() + ->setFilePHID($file->getPHID()) + ->setSequence($sequence++) + ->attachMock($mock); + $images[] = $image; } diff --git a/src/applications/pholio/storage/PholioImage.php b/src/applications/pholio/storage/PholioImage.php index 70f6e8b8c4..e0b55bae10 100644 --- a/src/applications/pholio/storage/PholioImage.php +++ b/src/applications/pholio/storage/PholioImage.php @@ -9,16 +9,23 @@ final class PholioImage extends PholioDAO protected $mockID; protected $filePHID; - protected $name = ''; - protected $description = ''; + protected $name; + protected $description; protected $sequence; - protected $isObsolete = 0; + protected $isObsolete; protected $replacesImagePHID = null; private $inlineComments = self::ATTACHABLE; private $file = self::ATTACHABLE; private $mock = self::ATTACHABLE; + public static function initializeNewImage() { + return id(new self()) + ->setName('') + ->setDescription('') + ->setIsObsolete(0); + } + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -43,8 +50,8 @@ final class PholioImage extends PholioDAO ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID(PholioImagePHIDType::TYPECONST); + public function getPHIDType() { + return PholioImagePHIDType::TYPECONST; } public function attachFile(PhabricatorFile $file) { @@ -67,7 +74,6 @@ final class PholioImage extends PholioDAO return $this->mock; } - public function attachInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PholioTransactionComment'); $this->inlineComments = $inline_comments; diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 569513cb46..7ce4ece479 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -295,9 +295,10 @@ final class PholioMock extends PholioDAO PhabricatorDestructionEngine $engine) { $this->openTransaction(); - $images = id(new PholioImage())->loadAllWhere( - 'mockID = %d', - $this->getID()); + $images = id(new PholioImageQuery()) + ->setViewer($engine->getViewer()) + ->withMockIDs(array($this->getID())) + ->execute(); foreach ($images as $image) { $image->delete(); } From c4c5d8a21093ddf9a325319c08bad2f3429036bc Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Dec 2018 15:38:00 -0800 Subject: [PATCH 46/62] Un-implement MarkupInterface from Mocks and Images in Pholio Summary: Depends on D19911. Ref T11351. `MarkupInterface` has mostly been replaced with `PHUIRemarkupView`, and isn't really doing anything for us here. Get rid of it to simplify the code. Test Plan: Viewed various mocks with descriptions and image descriptions, saw remarkup presented properly. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19912 --- src/__phutil_library_map__.php | 2 - .../controller/PholioMockViewController.php | 7 +--- .../pholio/storage/PholioImage.php | 28 +------------- .../pholio/storage/PholioMock.php | 38 ------------------- .../pholio/view/PholioMockImagesView.php | 16 +------- 5 files changed, 4 insertions(+), 87 deletions(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8478bb45d5..deaaae0e98 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -10946,7 +10946,6 @@ phutil_register_library_map(array( 'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PholioImage' => array( 'PholioDAO', - 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', ), 'PholioImageDescriptionTransaction' => 'PholioImageTransactionType', @@ -10962,7 +10961,6 @@ phutil_register_library_map(array( 'PholioInlineListController' => 'PholioController', 'PholioMock' => array( 'PholioDAO', - 'PhabricatorMarkupInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', 'PhabricatorTokenReceiverInterface', diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index 2ab31d0d96..fd601f0a32 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -37,10 +37,6 @@ final class PholioMockViewController extends PholioController { PholioMockHasTaskEdgeType::EDGECONST); $this->setManiphestTaskPHIDs($phids); - $engine = id(new PhabricatorMarkupEngine()) - ->setViewer($viewer); - $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); - $title = $mock->getName(); if ($mock->isClosed()) { @@ -62,8 +58,7 @@ final class PholioMockViewController extends PholioController { $timeline = $this->buildTransactionTimeline( $mock, - new PholioTransactionQuery(), - $engine); + new PholioTransactionQuery()); $timeline->setMock($mock); $curtain = $this->buildCurtainView($mock); diff --git a/src/applications/pholio/storage/PholioImage.php b/src/applications/pholio/storage/PholioImage.php index e0b55bae10..92694fb806 100644 --- a/src/applications/pholio/storage/PholioImage.php +++ b/src/applications/pholio/storage/PholioImage.php @@ -2,11 +2,8 @@ final class PholioImage extends PholioDAO implements - PhabricatorMarkupInterface, PhabricatorPolicyInterface { - const MARKUP_FIELD_DESCRIPTION = 'markup:description'; - protected $mockID; protected $filePHID; protected $name; @@ -86,32 +83,9 @@ final class PholioImage extends PholioDAO } -/* -( PhabricatorMarkupInterface )----------------------------------------- */ - - - public function getMarkupFieldKey($field) { - $content = $this->getMarkupText($field); - return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); - } - - public function newMarkupEngine($field) { - return PhabricatorMarkupEngine::newMarkupEngine(array()); - } - - public function getMarkupText($field) { - return $this->getDescription(); - } - - public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { - return $output; - } - - public function shouldUseMarkupCache($field) { - return (bool)$this->getID(); - } - /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ + public function getCapabilities() { return $this->getMock()->getCapabilities(); } diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 7ce4ece479..cf32a34131 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -2,7 +2,6 @@ final class PholioMock extends PholioDAO implements - PhabricatorMarkupInterface, PhabricatorPolicyInterface, PhabricatorSubscribableInterface, PhabricatorTokenReceiverInterface, @@ -15,8 +14,6 @@ final class PholioMock extends PholioDAO PhabricatorFulltextInterface, PhabricatorFerretInterface { - const MARKUP_FIELD_DESCRIPTION = 'markup:description'; - const STATUS_OPEN = 'open'; const STATUS_CLOSED = 'closed'; @@ -216,41 +213,6 @@ final class PholioMock extends PholioDAO } -/* -( PhabricatorMarkupInterface )----------------------------------------- */ - - - public function getMarkupFieldKey($field) { - $content = $this->getMarkupText($field); - return PhabricatorMarkupEngine::digestRemarkupContent($this, $content); - } - - public function newMarkupEngine($field) { - return PhabricatorMarkupEngine::newMarkupEngine(array()); - } - - public function getMarkupText($field) { - if ($this->getDescription()) { - return $this->getDescription(); - } - - return null; - } - - public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { - require_celerity_resource('phabricator-remarkup-css'); - return phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - $output); - } - - public function shouldUseMarkupCache($field) { - return (bool)$this->getID(); - } - - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index c95d8827d3..d5ac41cd03 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -72,13 +72,6 @@ final class PholioMockImagesView extends AphrontView { $default = PhabricatorFile::loadBuiltin($viewer, 'image-100x100.png'); - $engine = id(new PhabricatorMarkupEngine()) - ->setViewer($this->getUser()); - foreach ($mock->getAllImages() as $image) { - $engine->addObject($image, 'default'); - } - $engine->process(); - $images = array(); $current_set = 0; foreach ($mock->getAllImages() as $image) { @@ -92,14 +85,9 @@ final class PholioMockImagesView extends AphrontView { $current_set++; } - $description = $engine->getOutput($image, 'default'); + $description = $image->getDescription(); if (strlen($description)) { - $description = phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - $description); + $description = new PHUIRemarkupView($viewer, $description); } $history_uri = '/pholio/image/history/'.$image->getID().'/'; From aa3b2ec5dcd0ba91e36d5a0b59f37f27ec78b57c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Dec 2018 15:54:27 -0800 Subject: [PATCH 47/62] Give Pholio Images an authorPHID and use ExtendedPolicies to implement policy behavior Summary: Depends on D19912. Ref T11351. Images currently use `getMock()->getPolicy()` stuff to define policies. This causes bugs with object policies like "Subscribers", since the policy engine tries to evaluate the subscribers //for the image// when the intent is to evaluate the subscribers for the mock. Move this to ExtendedPolicies to fix the behavior, and give Images sensible policy behavior when they aren't attached to a mock (specifically: only the user who created the image can see it). Test Plan: Applied migrations, created and edited mocks and images without anything blowing up. Set mock visibility to "Subscribers", everything worked great. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19913 --- .../20181218.pholio.01.imageauthor.sql | 2 + src/__phutil_library_map__.php | 1 + .../PholioImageUploadController.php | 1 + .../controller/PholioMockEditController.php | 2 + .../controller/PholioMockViewController.php | 19 ++++---- ...PhabricatorPholioMockTestDataGenerator.php | 1 + .../pholio/storage/PholioImage.php | 46 +++++++++++++++---- 7 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 resources/sql/autopatches/20181218.pholio.01.imageauthor.sql diff --git a/resources/sql/autopatches/20181218.pholio.01.imageauthor.sql b/resources/sql/autopatches/20181218.pholio.01.imageauthor.sql new file mode 100644 index 0000000000..4ff0a16258 --- /dev/null +++ b/resources/sql/autopatches/20181218.pholio.01.imageauthor.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_pholio.pholio_image + ADD authorPHID VARBINARY(64) NOT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index deaaae0e98..7767a93561 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -10947,6 +10947,7 @@ phutil_register_library_map(array( 'PholioImage' => array( 'PholioDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', ), 'PholioImageDescriptionTransaction' => 'PholioImageTransactionType', 'PholioImageFileTransaction' => 'PholioImageTransactionType', diff --git a/src/applications/pholio/controller/PholioImageUploadController.php b/src/applications/pholio/controller/PholioImageUploadController.php index 39dd661a4a..0ff5e061f5 100644 --- a/src/applications/pholio/controller/PholioImageUploadController.php +++ b/src/applications/pholio/controller/PholioImageUploadController.php @@ -23,6 +23,7 @@ final class PholioImageUploadController extends PholioController { } $image = PholioImage::initializeNewImage() + ->setAuthorPHID($viewer->getPHID()) ->attachFile($file) ->setName($title) ->setDescription($description) diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index 9a09c9bc67..fd1df77a8e 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -141,6 +141,7 @@ final class PholioMockEditController extends PholioController { if ($replaces_image_phid) { $replace_image = PholioImage::initializeNewImage() + ->setAuthorPHID($viewer->getPHID()) ->setReplacesImagePHID($replaces_image_phid) ->setFilePhid($file_phid) ->attachFile($file) @@ -154,6 +155,7 @@ final class PholioMockEditController extends PholioController { $posted_mock_images[] = $replace_image; } else if (!$existing_image) { // this is an add $add_image = PholioImage::initializeNewImage() + ->setAuthorPHID($viewer->getPHID()) ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index fd601f0a32..e24889875a 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -82,7 +82,7 @@ final class PholioMockViewController extends PholioController { $add_comment = $this->buildAddCommentView($mock, $comment_form_id); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()); + $crumbs->addTextCrumb($mock->getMonogram(), $mock->getURI()); $crumbs->setBorder(true); $thumb_grid = id(new PholioMockThumbGridView()) @@ -92,16 +92,17 @@ final class PholioMockViewController extends PholioController { $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) - ->setMainColumn(array( - $output, - $thumb_grid, - $details, - $timeline, - $add_comment, - )); + ->setMainColumn( + array( + $output, + $thumb_grid, + $details, + $timeline, + $add_comment, + )); return $this->newPage() - ->setTitle('M'.$mock->getID().' '.$title) + ->setTitle(pht('%s %s', $mock->getMonogram(), $title)) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($mock->getPHID())) ->addQuicksandConfig( diff --git a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php index c038fcf922..041e14fcbf 100644 --- a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php +++ b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php @@ -42,6 +42,7 @@ final class PhabricatorPholioMockTestDataGenerator $images = array(); foreach ($files as $file) { $image = PholioImage::initializeNewImage() + ->setAuthorPHID($author_phid) ->setFilePHID($file->getPHID()) ->setSequence($sequence++) ->attachMock($mock); diff --git a/src/applications/pholio/storage/PholioImage.php b/src/applications/pholio/storage/PholioImage.php index 92694fb806..0f800bbb0f 100644 --- a/src/applications/pholio/storage/PholioImage.php +++ b/src/applications/pholio/storage/PholioImage.php @@ -2,8 +2,10 @@ final class PholioImage extends PholioDAO implements - PhabricatorPolicyInterface { + PhabricatorPolicyInterface, + PhabricatorExtendedPolicyInterface { + protected $authorPHID; protected $mockID; protected $filePHID; protected $name; @@ -57,8 +59,7 @@ final class PholioImage extends PholioDAO } public function getFile() { - $this->assertAttached($this->file); - return $this->file; + return $this->assertAttached($this->file); } public function attachMock(PholioMock $mock) { @@ -67,8 +68,7 @@ final class PholioImage extends PholioDAO } public function getMock() { - $this->assertAttached($this->mock); - return $this->mock; + return $this->assertAttached($this->mock); } public function attachInlineComments(array $inline_comments) { @@ -83,20 +83,46 @@ final class PholioImage extends PholioDAO } -/* -( PhabricatorPolicyInterface Implementation )-------------------------- */ +/* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { - return $this->getMock()->getCapabilities(); + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); } public function getPolicy($capability) { - return $this->getMock()->getPolicy($capability); + // If the image is attached to a mock, we use an extended policy to match + // the mock's permissions. + if ($this->getMockID()) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + // If the image is not attached to a mock, only the author can see it. + return $this->getAuthorPHID(); } - // really the *mock* controls who can see an image public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return $this->getMock()->hasAutomaticCapability($capability, $viewer); + return false; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->getMockID()) { + return array( + array( + $this->getMock(), + $capability, + ), + ); + } + + return array(); } } From 979187132d7b3d6750ba9e3b4b1bb7efbab501d7 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 19 Dec 2018 11:31:02 -0800 Subject: [PATCH 48/62] Update accountadmin to use new admin empowerment code Summary: Fixes https://discourse.phabricator-community.org/t/admin-account-creation-fails-call-to-undefined-method-phabricatorusereditor-makeadminuser/2227. This callsite got skipped when updating the EmpowerController to use the new transactional admin approval code. Test Plan: Invoked `accountadmin` to promote a user, no longer got an exception. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19915 --- scripts/user/account_admin.php | 21 ++++++++++++++++++- .../PhabricatorAuthRegisterController.php | 21 ++++++++++++++++++- .../PhabricatorUserEmpowerTransaction.php | 5 ++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php index 9e01896637..4ad722e125 100755 --- a/scripts/user/account_admin.php +++ b/scripts/user/account_admin.php @@ -200,9 +200,28 @@ $user->openTransaction(); $editor->updateUser($user, $verify_email); } - $editor->makeAdminUser($user, $set_admin); $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) + ->setContinueOnMissingFields(true); + + $transaction_editor->applyTransactions($user, $xactions); + $user->saveTransaction(); echo pht('Saved changes.')."\n"; diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 1138d52b33..9e1aef592c 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -416,7 +416,26 @@ final class PhabricatorAuthRegisterController } if ($is_setup) { - $editor->makeAdminUser($user, true); + $xactions = array(); + $xactions[] = id(new PhabricatorUserTransaction()) + ->setTransactionType( + PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $actor = PhabricatorUser::getOmnipotentUser(); + $content_source = PhabricatorContentSource::newFromRequest( + $request); + + $people_application_phid = id(new PhabricatorPeopleApplication()) + ->getPHID(); + + $transaction_editor = id(new PhabricatorUserTransactionEditor()) + ->setActor($actor) + ->setActingAsPHID($people_application_phid) + ->setContentSource($content_source) + ->setContinueOnMissingFields(true); + + $transaction_editor->applyTransactions($user, $xactions); } $account->setUserPHID($user->getPHID()); diff --git a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php index 12d6dc7523..1b561d3236 100644 --- a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php @@ -45,7 +45,10 @@ final class PhabricatorUserEmpowerTransaction 'status as an administrator.'), $xaction); } - if (!$actor->getIsAdmin()) { + $is_admin = $actor->getIsAdmin(); + $is_omnipotent = $actor->isOmnipotent(); + + if (!$is_admin && !$is_omnipotent) { $errors[] = $this->newInvalidError( pht('You must be an administrator to create administrators.'), $xaction); From c72d29f401e2dfa37d8077cf25cb2f013a11e2b4 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 19 Dec 2018 16:24:12 -0800 Subject: [PATCH 49/62] Cleanup some clustering rough edges Summary: Suppress an unhelpful Almanac transaction and document the location of the secret clustering management capability. I thought maybe implementing `shouldHide` and checking for `isCreate` would work, but the binding apparently gets created before an interface is bound to it. Test Plan: Looked at a fresh binding and didn't see "Unknown Object(??)", ran bin/diviner and saw expected output. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19917 --- .../AlmanacBindingInterfaceTransaction.php | 21 ++++++++++++++----- .../user/cluster/cluster_repositories.diviner | 13 +++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php b/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php index f43fcdfa86..03effc028e 100644 --- a/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php +++ b/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php @@ -55,11 +55,22 @@ final class AlmanacBindingInterfaceTransaction } public function getTitle() { - return pht( - '%s changed the interface for this binding from %s to %s.', - $this->renderAuthor(), - $this->renderOldHandle(), - $this->renderNewHandle()); + if ($this->getOldValue() === null) { + return pht( + '%s set the interface for this binding to %s.', + $this->renderAuthor(), + $this->renderNewHandle()); + } else if ($this->getNewValue() == null) { + return pht( + '%s removed the interface for this binding.', + $this->renderAuthor()); + } else { + return pht( + '%s changed the interface for this binding from %s to %s.', + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } } public function validateTransactions($object, array $xactions) { diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner index 13307d0b79..9e4b324a58 100644 --- a/src/docs/user/cluster/cluster_repositories.diviner +++ b/src/docs/user/cluster/cluster_repositories.diviner @@ -98,7 +98,7 @@ repository to retrieve the data it needs. It will use the result of this query to respond to the user. -Setting up a Cluster Services +Setting up Cluster Services ============================= To set up clustering, first register the devices that you want to use as part @@ -107,6 +107,17 @@ of the cluster with Almanac. For details, see @{article:Cluster: Devices}. NOTE: Once you create a service, new repositories will immediately allocate on it. You may want to disable repository creation during initial setup. +NOTE: To create clustered services, your account must have the "Can Manage +Cluster Services" capability. By default, no accounts have this capability, +and you must enable it by changing the configuration of the Almanac +application. Navigate to the Alamanc application configuration as follows: +{nav icon=home, name=Home > +Applications > +Almanac > +Configure > +Edit Policies > +Can Manage Cluster Services } + Once the hosts are registered as devices, you can create a new service in Almanac: From 2de632d4fe98d7a0369d49c4669a7961122f6f16 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Thu, 20 Dec 2018 13:50:15 -0800 Subject: [PATCH 50/62] Update continue/break for php 7.3 Summary: Fixes https://discourse.phabricator-community.org/t/error-on-project-creation-or-edition-with-php7-3/2236 I didn't actually repro this because I don't have php 7.3 installed. I'm also not sure if the `break; break` was intentional or not, since I'm not sure you could ever reach two consecutive break statements. Test Plan: Created some projects. Didn't actually try to hit the code that fires if you're making a project both a subproject and a milestone. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D19925 --- .../project/editor/PhabricatorProjectTransactionEditor.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 581ec23153..ee2c087085 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -55,12 +55,12 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectParentTransaction::TRANSACTIONTYPE: case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE: if ($xaction->getNewValue() === null) { - continue; + continue 2; } if (!$parent_xaction) { $parent_xaction = $xaction; - continue; + continue 2; } $errors[] = new PhabricatorApplicationTransactionValidationError( @@ -71,8 +71,7 @@ final class PhabricatorProjectTransactionEditor 'project or milestone project. A project can not be both a '. 'subproject and a milestone.'), $xaction); - break; - break; + break 2; } } From 0673e79d6d96fcb5baa88062291a617cd3b56eda Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 14 Dec 2018 07:50:29 -0800 Subject: [PATCH 51/62] Simplify and correct some challenge TTL lockout code Summary: Depends on D19889. Ref T13222. Some of this logic is either not-quite-right or a little more complicated than it needs to be. Currently, we TTL TOTP challenges after three timesteps -- once the current code could no longer be used. But we actually have to TTL it after five timesteps -- once the most-future acceptable code could no longer be used. Otherwise, you can enter the most-future code now (perhaps the attacker compromises NTP and skews the server clock back by 75 seconds) and then an attacker can re-use it in three timesteps. Generally, simplify things a bit and trust TTLs more. This also makes the "wait" dialog friendlier since we can give users an exact number of seconds. The overall behavior here is still a little odd because we don't actually require you to respond to the challenge you were issued (right now, we check that the response is valid whenever you submit it, not that it's a valid response to the challenge we issued), but that will change in a future diff. This is just moving us generally in the right direction, and doesn't yet lock everything down properly. Test Plan: - Added a little snippet to the control caption to list all the valid codes to make this easier: ``` $key = new PhutilOpaqueEnvelope($config->getFactorSecret()); $valid = array(); foreach ($this->getAllowedTimesteps() as $step) { $valid[] = self::getTOTPCode($key, $step); } $control->setCaption( pht( 'Valid Codes: '.implode(', ', $valid))); ``` - Used the most-future code to sign `L3`. - Verified that `L4` did not unlock until the code for `L3` left the activation window. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19890 --- .../action/PhabricatorAuthTryFactorAction.php | 2 +- .../auth/factor/PhabricatorTOTPAuthFactor.php | 65 ++++++++++--------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/applications/auth/action/PhabricatorAuthTryFactorAction.php b/src/applications/auth/action/PhabricatorAuthTryFactorAction.php index 246298567b..40e7c858d1 100644 --- a/src/applications/auth/action/PhabricatorAuthTryFactorAction.php +++ b/src/applications/auth/action/PhabricatorAuthTryFactorAction.php @@ -9,7 +9,7 @@ final class PhabricatorAuthTryFactorAction extends PhabricatorSystemAction { } public function getScoreThreshold() { - return 10 / phutil_units('1 hour in seconds'); + return 100 / phutil_units('1 hour in seconds'); } public function getLimitExplanation() { diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 373745e244..471fe89ed7 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -155,25 +155,43 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { PhabricatorUser $viewer, array $challenges) { - $now = $this->getCurrentTimestep(); + $current_step = $this->getCurrentTimestep(); // If we already issued a valid challenge, don't issue a new one. if ($challenges) { return array(); } - // Otherwise, generate a new challenge for the current timestep. It TTLs - // after it would fall off the bottom of the window. - $timesteps = $this->getAllowedTimesteps(); - $min_step = min($timesteps); + // Otherwise, generate a new challenge for the current timestep and compute + // the TTL. + // When computing the TTL, note that we accept codes within a certain + // window of the challenge timestep to account for clock skew and users + // needing time to enter codes. + + // We don't want this challenge to expire until after all valid responses + // to it are no longer valid responses to any other challenge we might + // issue in the future. If the challenge expires too quickly, we may issue + // a new challenge which can accept the same TOTP code response. + + // This means that we need to keep this challenge alive for double the + // window size: if we're currently at timestep 3, the user might respond + // with the code for timestep 5. This is valid, since timestep 5 is within + // the window for timestep 3. + + // But the code for timestep 5 can be used to respond at timesteps 3, 4, 5, + // 6, and 7. To prevent any valid response to this challenge from being + // used again, we need to keep this challenge active until timestep 8. + + $window_size = $this->getTimestepWindowSize(); $step_duration = $this->getTimestepDuration(); - $ttl_steps = ($now - $min_step) + 1; + + $ttl_steps = ($window_size * 2) + 1; $ttl_seconds = ($ttl_steps * $step_duration); return array( $this->newChallenge($config, $viewer) - ->setChallengeKey($now) + ->setChallengeKey($current_step) ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds), ); } @@ -216,31 +234,15 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { // nearby timestep, require that it was issued to the current session. // This is defusing attacks where you (broadly) look at someone's phone // and type the code in more quickly than they do. - - $step_duration = $this->getTimestepDuration(); - $now = $this->getCurrentTimestep(); - $timesteps = $this->getAllowedTimesteps(); - $timesteps = array_fuse($timesteps); - $min_step = min($timesteps); - $session_phid = $viewer->getSession()->getPHID(); + $now = PhabricatorTime::getNow(); $engine = $config->getSessionEngine(); $workflow_key = $engine->getWorkflowKey(); foreach ($challenges as $challenge) { $challenge_timestep = (int)$challenge->getChallengeKey(); - - // This challenge isn't for one of the timesteps you'd be able to respond - // to if you submitted the form right now, so we're good to keep going. - if (!isset($timesteps[$challenge_timestep])) { - continue; - } - - // This is the number of timesteps you need to wait for the problem - // timestep to leave the window, rounded up. - $wait_steps = ($challenge_timestep - $min_step) + 1; - $wait_duration = ($wait_steps * $step_duration); + $wait_duration = ($challenge->getChallengeTTL() - $now) + 1; if ($challenge->getSessionPHID() !== $session_phid) { return $this->newResult() @@ -248,7 +250,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { ->setErrorMessage( pht( 'This factor recently issued a challenge to a different login '. - 'session. Wait %s seconds for the code to cycle, then try '. + 'session. Wait %s second(s) for the code to cycle, then try '. 'again.', new PhutilNumber($wait_duration))); } @@ -259,7 +261,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { ->setErrorMessage( pht( 'This factor recently issued a challenge for a different '. - 'workflow. Wait %s seconds for the code to cycle, then try '. + 'workflow. Wait %s second(s) for the code to cycle, then try '. 'again.', new PhutilNumber($wait_duration))); } @@ -423,8 +425,13 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { } private function getAllowedTimesteps() { - $now = $this->getCurrentTimestep(); - return range($now - 2, $now + 2); + $current_step = $this->getCurrentTimestep(); + $window = $this->getTimestepWindowSize(); + return range($current_step - $window, $current_step + $window); + } + + private function getTimestepWindowSize() { + return 2; } From 657f3c380608abc0fc59088d979457f1d8826f06 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Dec 2018 08:40:43 -0800 Subject: [PATCH 52/62] When accepting a TOTP response, require it respond explicitly to a specific challenge Summary: Depends on D19890. Ref T13222. See PHI873. Currently, we only validate TOTP responses against the current (realtime) timestep. Instead, also validate them against a specific challenge. This mostly just moves us toward more specifically preventing responses from being reused, and supporting flows which must look more like this (SMS/push). One rough edge here is that during the T+3 and T+4 windows (you request a prompt, then wait 60-120 seconds to respond) only past responses actually work (the current code on your device won't). For example: - At T+0, you request MFA. We issue a T+0 challenge that accepts codes T-2, T-1, T+0, T+1, and T+2. The challenge locks out T+3 and T+4 to prevent the window from overlapping with the next challenge we may issue (see D19890). - If you wait 60 seconds until T+3 to actually submit a code, the realtime valid responses are T+1, T+2, T+3, T+4, T+5. The challenge valid responses are T-2, T-1, T+0, T+1, and T+2. Only T+1 and T+2 are in the intersection. Your device is showing T+3 if the clock is right, so if you type in what's shown on your device it won't be accepted. - This //may// get refined in future changes, but, in the worst case, it's probably fine if it doesn't. Beyond 120s you'll get a new challenge and a full [-2, ..., +2] window to respond, so this lockout is temporary even if you manage to hit it. - If this //doesn't// get refined, I'll change the UI to say "This factor recently issued a challenge which has expired, wait N seconds." to smooth this over a bit. Test Plan: - Went through MFA. - Added a new TOTP factor. - Hit some error cases on purpose. - Tried to use an old code a moment after it expired, got rejected. - Waited 60+ seconds, tried to use the current displayed factor, got rejected (this isn't great, but currently expected). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19893 --- .../auth/factor/PhabricatorTOTPAuthFactor.php | 100 ++++++++++++------ 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 471fe89ed7..e2f0dc74b3 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -77,10 +77,10 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $e_code = true; if ($request->getExists('totp')) { - $okay = $this->verifyTOTPCode( - $user, + $okay = (bool)$this->getTimestepAtWhichResponseIsValid( + $this->getAllowedTimesteps($this->getCurrentTimestep()), new PhutilOpaqueEnvelope($key), - $code); + (string)$code); if ($okay) { $config = $this->newConfigForUser($user) @@ -240,6 +240,8 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $engine = $config->getSessionEngine(); $workflow_key = $engine->getWorkflowKey(); + $current_timestep = $this->getCurrentTimestep(); + foreach ($challenges as $challenge) { $challenge_timestep = (int)$challenge->getChallengeKey(); $wait_duration = ($challenge->getChallengeTTL() - $now) + 1; @@ -265,6 +267,22 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { 'again.', new PhutilNumber($wait_duration))); } + + // If the current realtime timestep isn't a valid response to the current + // challenge but the challenge hasn't expired yet, we're locking out + // the factor to prevent challenge windows from overlapping. Let the user + // know that they should wait for a new challenge. + $challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep); + if (!isset($challenge_timesteps[$current_timestep])) { + return $this->newResult() + ->setIsWait(true) + ->setErrorMessage( + pht( + 'This factor recently issued a challenge which has expired. '. + 'A new challenge can not be issued yet. Wait %s second(s) for '. + 'the code to cycle, then try again.', + new PhutilNumber($wait_duration))); + } } return null; @@ -277,12 +295,36 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { array $challenges) { $code = $request->getStr($this->getParameterName($config, 'totpcode')); - $key = new PhutilOpaqueEnvelope($config->getFactorSecret()); $result = $this->newResult() ->setValue($code); - if ($this->verifyTOTPCode($viewer, $key, (string)$code)) { + // We expect to reach TOTP validation with exactly one valid challenge. + if (count($challenges) !== 1) { + throw new Exception( + pht( + 'Reached TOTP challenge validation with an unexpected number of '. + 'unexpired challenges (%d), expected exactly one.', + phutil_count($challenges))); + } + + $challenge = head($challenges); + + $challenge_timestep = (int)$challenge->getChallengeKey(); + $current_timestep = $this->getCurrentTimestep(); + + $challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep); + $current_timesteps = $this->getAllowedTimesteps($current_timestep); + + // We require responses be both valid for the challenge and valid for the + // current timestep. A longer challenge TTL doesn't let you use older + // codes for a longer period of time. + $valid_timestep = $this->getTimestepAtWhichResponseIsValid( + array_intersect_key($challenge_timesteps, $current_timesteps), + new PhutilOpaqueEnvelope($config->getFactorSecret()), + (string)$code); + + if ($valid_timestep) { $result->setIsValid(true); } else { if (strlen($code)) { @@ -300,29 +342,6 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return strtoupper(Filesystem::readRandomCharacters(32)); } - private function verifyTOTPCode( - PhabricatorUser $user, - PhutilOpaqueEnvelope $key, - $code) { - - $now = (int)(time() / 30); - - // Allow the user to enter a code a few minutes away on either side, in - // case the server or client has some clock skew. - for ($offset = -2; $offset <= 2; $offset++) { - $real = self::getTOTPCode($key, $now + $offset); - if (phutil_hashes_are_identical($real, $code)) { - return true; - } - } - - // TODO: After validating a code, this should mark it as used and prevent - // it from being reused. - - return false; - } - - public static function base32Decode($buf) { $buf = strtoupper($buf); @@ -424,15 +443,34 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return (int)(PhabricatorTime::getNow() / $duration); } - private function getAllowedTimesteps() { - $current_step = $this->getCurrentTimestep(); + private function getAllowedTimesteps($at_timestep) { $window = $this->getTimestepWindowSize(); - return range($current_step - $window, $current_step + $window); + $range = range($at_timestep - $window, $at_timestep + $window); + return array_fuse($range); } private function getTimestepWindowSize() { + // The user is allowed to provide a code from the recent past or the + // near future to account for minor clock skew between the client + // and server, and the time it takes to actually enter a code. return 2; } + private function getTimestepAtWhichResponseIsValid( + array $timesteps, + PhutilOpaqueEnvelope $key, + $code) { + + foreach ($timesteps as $timestep) { + $expect_code = self::getTOTPCode($key, $timestep); + if (phutil_hashes_are_identical($code, $expect_code)) { + return $timestep; + } + } + + return null; + } + + } From ce953ea4479052c7e671c8c75af6833252ebefd4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Dec 2018 08:40:57 -0800 Subject: [PATCH 53/62] Explicitly mark MFA challenges as "answered" and "completed" Summary: Depends on D19893. Ref T13222. See PHI873. A challenge is "answered" if you provide a valid response. A challenge is "completed" if we let you through the MFA check and do whatever actual action the check is protecting. If you only have one MFA factor, challenges will be "completed" immediately after they are "answered". However, if you have two or more factors, it's possible to "answer" one or more prompts, but fewer than all of the prompts, and end up with "answered" challenges that are not "completed". In the future, it may also be possible to answer all the challenges but then have an error occur before they are marked "completed" (for example, a unique key collision in the transaction code). For now, nothing interesting happens between "answered" and "completed". This would take the form of the caller explicitly providing flags like "wait to mark the challenges as completed until I do something" and "okay, mark the challenges as completed now". This change prevents all token reuse, even on the same workflow. Future changes will let the answered challenges "stick" to the client form so you don't have to re-answer challenges for a short period of time if you hit a unique key collision. Test Plan: - Used a token to get through an MFA gate. - Tried to go through another gate, was told to wait for a long time for the next challenge window. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19894 --- .../autopatches/20181217.auth.01.digest.sql | 2 + .../sql/autopatches/20181217.auth.02.ttl.sql | 2 + .../20181217.auth.03.completed.sql | 2 + .../engine/PhabricatorAuthSessionEngine.php | 12 ++ .../auth/factor/PhabricatorAuthFactor.php | 2 +- .../factor/PhabricatorAuthFactorResult.php | 25 +++- .../auth/factor/PhabricatorTOTPAuthFactor.php | 22 +++- .../auth/storage/PhabricatorAuthChallenge.php | 113 ++++++++++++++++++ 8 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 resources/sql/autopatches/20181217.auth.01.digest.sql create mode 100644 resources/sql/autopatches/20181217.auth.02.ttl.sql create mode 100644 resources/sql/autopatches/20181217.auth.03.completed.sql diff --git a/resources/sql/autopatches/20181217.auth.01.digest.sql b/resources/sql/autopatches/20181217.auth.01.digest.sql new file mode 100644 index 0000000000..8e30143e8f --- /dev/null +++ b/resources/sql/autopatches/20181217.auth.01.digest.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_challenge + ADD responseDigest VARCHAR(255) COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20181217.auth.02.ttl.sql b/resources/sql/autopatches/20181217.auth.02.ttl.sql new file mode 100644 index 0000000000..c8e883dbea --- /dev/null +++ b/resources/sql/autopatches/20181217.auth.02.ttl.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_challenge + ADD responseTTL INT UNSIGNED; diff --git a/resources/sql/autopatches/20181217.auth.03.completed.sql b/resources/sql/autopatches/20181217.auth.03.completed.sql new file mode 100644 index 0000000000..22ca6e21ff --- /dev/null +++ b/resources/sql/autopatches/20181217.auth.03.completed.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_challenge + ADD isCompleted BOOL NOT NULL; diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index f3814b949d..74183f4ffa 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -576,6 +576,8 @@ final class PhabricatorAuthSessionEngine extends Phobject { continue; } + $issued_challenges = idx($challenge_map, $factor_phid, array()); + $impl = $factor->requireImplementation(); $validation_result = $impl->getResultFromChallengeResponse( @@ -592,6 +594,16 @@ final class PhabricatorAuthSessionEngine extends Phobject { } if ($ok) { + // We're letting you through, so mark all the challenges you + // responded to as completed. These challenges can never be used + // again, even by the same session and workflow: you can't use the + // same response to take two different actions, even if those actions + // are of the same type. + foreach ($validation_results as $validation_result) { + $challenge = $validation_result->getAnsweredChallenge() + ->markChallengeAsCompleted(); + } + // Give the user a credit back for a successful factor verification. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index be99df9c79..8cd61f089d 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -45,7 +45,7 @@ abstract class PhabricatorAuthFactor extends Phobject { $engine = $config->getSessionEngine(); - return id(new PhabricatorAuthChallenge()) + return PhabricatorAuthChallenge::initializeNewChallenge() ->setUserPHID($viewer->getPHID()) ->setSessionPHID($viewer->getSession()->getPHID()) ->setFactorPHID($config->getPHID()) diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php index d75480747d..faa25b4f42 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactorResult.php +++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php @@ -3,19 +3,36 @@ final class PhabricatorAuthFactorResult extends Phobject { - private $isValid = false; + private $answeredChallenge; private $isWait = false; private $errorMessage; private $value; private $issuedChallenges = array(); - public function setIsValid($is_valid) { - $this->isValid = $is_valid; + public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) { + if (!$challenge->getIsAnsweredChallenge()) { + throw new PhutilInvalidStateException('markChallengeAsAnswered'); + } + + if ($challenge->getIsCompleted()) { + throw new Exception( + pht( + 'A completed challenge was provided as an answered challenge. '. + 'The underlying factor is implemented improperly, challenges '. + 'may not be reused.')); + } + + $this->answeredChallenge = $challenge; + return $this; } + public function getAnsweredChallenge() { + return $this->answeredChallenge; + } + public function getIsValid() { - return $this->isValid; + return (bool)$this->getAnsweredChallenge(); } public function setIsWait($is_wait) { diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index e2f0dc74b3..0984347197 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -283,6 +283,17 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { 'the code to cycle, then try again.', new PhutilNumber($wait_duration))); } + + if ($challenge->getIsReusedChallenge()) { + return $this->newResult() + ->setIsWait(true) + ->setErrorMessage( + pht( + 'You recently provided a response to this factor. Responses '. + 'may not be reused. Wait %s second(s) for the code to cycle, '. + 'then try again.', + new PhutilNumber($wait_duration))); + } } return null; @@ -325,7 +336,16 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { (string)$code); if ($valid_timestep) { - $result->setIsValid(true); + $now = PhabricatorTime::getNow(); + $step_duration = $this->getTimestepDuration(); + $step_window = $this->getTimestepWindowSize(); + $ttl = $now + ($step_duration * $step_window); + + $challenge + ->setProperty('totp.timestep', $valid_timestep) + ->markChallengeAsAnswered($ttl); + + $result->setAnsweredChallenge($challenge); } else { if (strlen($code)) { $error_message = pht('Invalid'); diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php index 63d2092e49..89ffd67ef7 100644 --- a/src/applications/auth/storage/PhabricatorAuthChallenge.php +++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php @@ -10,8 +10,20 @@ final class PhabricatorAuthChallenge protected $workflowKey; protected $challengeKey; protected $challengeTTL; + protected $responseDigest; + protected $responseTTL; + protected $isCompleted; protected $properties = array(); + private $responseToken; + + const TOKEN_DIGEST_KEY = 'auth.challenge.token'; + + public static function initializeNewChallenge() { + return id(new self()) + ->setIsCompleted(0); + } + protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( @@ -22,6 +34,9 @@ final class PhabricatorAuthChallenge 'challengeKey' => 'text255', 'challengeTTL' => 'epoch', 'workflowKey' => 'text255', + 'responseDigest' => 'text255?', + 'responseTTL' => 'epoch?', + 'isCompleted' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_issued' => array( @@ -38,6 +53,104 @@ final class PhabricatorAuthChallenge return PhabricatorAuthChallengePHIDType::TYPECONST; } + public function getIsReusedChallenge() { + if ($this->getIsCompleted()) { + return true; + } + + // TODO: A challenge is "reused" if it has been answered previously and + // the request doesn't include proof that the client provided the answer. + // Since we aren't tracking client responses yet, any answered challenge + // is always a reused challenge for now. + + return $this->getIsAnsweredChallenge(); + } + + public function getIsAnsweredChallenge() { + return (bool)$this->getResponseDigest(); + } + + public function markChallengeAsAnswered($ttl) { + $token = Filesystem::readRandomCharacters(32); + $token = new PhutilOpaqueEnvelope($token); + + return $this + ->setResponseToken($token, $ttl) + ->save(); + } + + public function markChallengeAsCompleted() { + return $this + ->setIsCompleted(true) + ->save(); + } + + public function setResponseToken(PhutilOpaqueEnvelope $token, $ttl) { + if (!$this->getUserPHID()) { + throw new PhutilInvalidStateException('setUserPHID'); + } + + if ($this->responseToken) { + throw new Exception( + pht( + 'This challenge already has a response token; you can not '. + 'set a new response token.')); + } + + $now = PhabricatorTime::getNow(); + if ($ttl < $now) { + throw new Exception( + pht( + 'Response TTL is invalid: TTLs must be an epoch timestamp '. + 'coresponding to a future time (did you use a relative TTL by '. + 'mistake?).')); + } + + if (preg_match('/ /', $token->openEnvelope())) { + throw new Exception( + pht( + 'The response token for this challenge is invalid: response '. + 'tokens may not include spaces.')); + } + + $digest = PhabricatorHash::digestWithNamedKey( + $token->openEnvelope(), + self::TOKEN_DIGEST_KEY); + + if ($this->responseDigest !== null) { + if (!phutil_hashes_are_identical($digest, $this->responseDigest)) { + throw new Exception( + pht( + 'Invalid response token for this challenge: token digest does '. + 'not match stored digest.')); + } + } else { + $this->responseDigest = $digest; + } + + $this->responseToken = $token; + $this->responseTTL = $ttl; + + return $this; + } + + public function setResponseDigest($value) { + throw new Exception( + pht( + 'You can not set the response digest for a challenge directly. '. + 'Instead, set a response token. A response digest will be computed '. + 'automatically.')); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getProperty($key, $default = null) { + return $this->properties[$key]; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ From b63783c06718b4d757d4eacb1ed5f412415693a7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Dec 2018 10:57:23 -0800 Subject: [PATCH 54/62] Carry MFA responses which have been "answered" but not "completed" through the MFA workflow Summary: Depends on D19894. Ref T13222. See PHI873. When you provide a correct response to an MFA challenge, we mark it as "answered". Currently, we never let you reuse an "answered" token. That's usually fine, but if you have 2+ factors on your account and get one or more (but fewer than all of them) right when you submit the form, you need to answer them all again, possibly after waiting for a lockout period. This is needless. When you answer a challenge correctly, add a hidden input with a code proving you got it right so you don't need to provide another answer for a little while. Why not just put your response in a form input, e.g. ``? - We may allow the "answered" response to be valid for a different amount of time than the actual answer. For TOTP, we currently allow a response to remain valid for 60 seconds, but the actual code you entered might expire sooner. - In some cases, there's no response we can provide (with push + approve MFA, you don't enter a code, you just tap "yes, allow this" on your phone). Conceivably, we may not be able to re-verify a push+approve code if the remote implements one-shot answers. - The "responseToken" stuff may end up embedded in normal forms in some cases in the future, and this approach just generally reduces the amount of plaintext MFA we have floating around. Test Plan: - Added 2 MFA tokens to my account. - Hit the MFA prompt. - Provided one good response and one bad response. - Submitted the form. - Old behavior: good response gets locked out for ~120 seconds. - New behavior: good response is marked "answered", fixing the other response lets me submit the form. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19895 --- .../engine/PhabricatorAuthSessionEngine.php | 19 +++ .../auth/factor/PhabricatorAuthFactor.php | 34 +++++ .../auth/factor/PhabricatorTOTPAuthFactor.php | 18 +-- .../auth/storage/PhabricatorAuthChallenge.php | 118 +++++++++++++++--- 4 files changed, 164 insertions(+), 25 deletions(-) diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 74183f4ffa..c129d898f2 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -517,6 +517,11 @@ final class PhabricatorAuthSessionEngine extends Phobject { ->withUserPHIDs(array($viewer->getPHID())) ->withChallengeTTLBetween($now, null) ->execute(); + + PhabricatorAuthChallenge::newChallengeResponsesFromRequest( + $challenges, + $request); + $challenge_map = mgroup($challenges, 'getFactorPHID'); $validation_results = array(); @@ -710,6 +715,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { ->setUser($viewer) ->appendRemarkupInstructions(''); + $answered = array(); foreach ($factors as $factor) { $result = $validation_results[$factor->getPHID()]; @@ -718,10 +724,23 @@ final class PhabricatorAuthSessionEngine extends Phobject { $form, $viewer, $result); + + $answered_challenge = $result->getAnsweredChallenge(); + if ($answered_challenge) { + $answered[] = $answered_challenge; + } } $form->appendRemarkupInstructions(''); + if ($answered) { + $http_params = PhabricatorAuthChallenge::newHTTPParametersFromChallenges( + $answered); + foreach ($http_params as $key => $value) { + $form->addHiddenInput($key, $value); + } + } + return $form; } diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 8cd61f089d..a9483a9809 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -165,4 +165,38 @@ abstract class PhabricatorAuthFactor extends Phobject { AphrontRequest $request, array $challenges); + final protected function newAutomaticControl( + PhabricatorAuthFactorResult $result) { + + $is_answered = (bool)$result->getAnsweredChallenge(); + if ($is_answered) { + return $this->newAnsweredControl($result); + } + + $is_wait = $result->getIsWait(); + if ($is_wait) { + return $this->newWaitControl($result); + } + + return null; + } + + private function newWaitControl( + PhabricatorAuthFactorResult $result) { + + $error = $result->getErrorMessage(); + + return id(new AphrontFormMarkupControl()) + ->setValue($error) + ->setError(pht('Wait')); + } + + private function newAnsweredControl( + PhabricatorAuthFactorResult $result) { + + return id(new AphrontFormMarkupControl()) + ->setValue(pht('Answered!')); + } + + } diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 0984347197..2a8999f2e4 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -202,15 +202,11 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { PhabricatorUser $viewer, PhabricatorAuthFactorResult $result) { - $value = $result->getValue(); - $error = $result->getErrorMessage(); - $is_wait = $result->getIsWait(); + $control = $this->newAutomaticControl($result); + if (!$control) { + $value = $result->getValue(); + $error = $result->getErrorMessage(); - if ($is_wait) { - $control = id(new AphrontFormMarkupControl()) - ->setValue($error) - ->setError(pht('Wait')); - } else { $control = id(new PHUIFormNumberControl()) ->setName($this->getParameterName($config, 'totpcode')) ->setDisableAutocomplete(true) @@ -321,6 +317,12 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $challenge = head($challenges); + // If the client has already provided a valid answer to this challenge and + // submitted a token proving they answered it, we're all set. + if ($challenge->getIsAnsweredChallenge()) { + return $result->setAnsweredChallenge($challenge); + } + $challenge_timestep = (int)$challenge->getChallengeKey(); $current_timestep = $this->getCurrentTimestep(); diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php index 89ffd67ef7..9e49ee154a 100644 --- a/src/applications/auth/storage/PhabricatorAuthChallenge.php +++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php @@ -17,6 +17,7 @@ final class PhabricatorAuthChallenge private $responseToken; + const HTTPKEY = '__hisec.challenges__'; const TOKEN_DIGEST_KEY = 'auth.challenge.token'; public static function initializeNewChallenge() { @@ -24,6 +25,89 @@ final class PhabricatorAuthChallenge ->setIsCompleted(0); } + public static function newHTTPParametersFromChallenges(array $challenges) { + assert_instances_of($challenges, __CLASS__); + + $token_list = array(); + foreach ($challenges as $challenge) { + $token = $challenge->getResponseToken(); + if ($token) { + $token_list[] = sprintf( + '%s:%s', + $challenge->getPHID(), + $token->openEnvelope()); + } + } + + if (!$token_list) { + return array(); + } + + $token_list = implode(' ', $token_list); + + return array( + self::HTTPKEY => $token_list, + ); + } + + public static function newChallengeResponsesFromRequest( + array $challenges, + AphrontRequest $request) { + assert_instances_of($challenges, __CLASS__); + + $token_list = $request->getStr(self::HTTPKEY); + $token_list = explode(' ', $token_list); + + $token_map = array(); + foreach ($token_list as $token_element) { + $token_element = trim($token_element, ' '); + + if (!strlen($token_element)) { + continue; + } + + // NOTE: This error message is intentionally not printing the token to + // avoid disclosing it. As a result, it isn't terribly useful, but no + // normal user should ever end up here. + if (!preg_match('/^[^:]+:/', $token_element)) { + throw new Exception( + pht( + 'This request included an improperly formatted MFA challenge '. + 'token and can not be processed.')); + } + + list($phid, $token) = explode(':', $token_element, 2); + + if (isset($token_map[$phid])) { + throw new Exception( + pht( + 'This request improperly specifies an MFA challenge token ("%s") '. + 'multiple times and can not be processed.', + $phid)); + } + + $token_map[$phid] = new PhutilOpaqueEnvelope($token); + } + + $challenges = mpull($challenges, null, 'getPHID'); + + $now = PhabricatorTime::getNow(); + foreach ($challenges as $challenge_phid => $challenge) { + // If the response window has expired, don't attach the token. + if ($challenge->getResponseTTL() < $now) { + continue; + } + + $token = idx($token_map, $challenge_phid); + if (!$token) { + continue; + } + + $challenge->setResponseToken($token); + } + } + + protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( @@ -58,12 +142,17 @@ final class PhabricatorAuthChallenge return true; } - // TODO: A challenge is "reused" if it has been answered previously and - // the request doesn't include proof that the client provided the answer. - // Since we aren't tracking client responses yet, any answered challenge - // is always a reused challenge for now. + if (!$this->getIsAnsweredChallenge()) { + return false; + } - return $this->getIsAnsweredChallenge(); + // If the challenge has been answered but the client has provided a token + // proving that they answered it, this is still a valid response. + if ($this->getResponseToken()) { + return false; + } + + return true; } public function getIsAnsweredChallenge() { @@ -75,7 +164,8 @@ final class PhabricatorAuthChallenge $token = new PhutilOpaqueEnvelope($token); return $this - ->setResponseToken($token, $ttl) + ->setResponseToken($token) + ->setResponseTTL($ttl) ->save(); } @@ -85,7 +175,7 @@ final class PhabricatorAuthChallenge ->save(); } - public function setResponseToken(PhutilOpaqueEnvelope $token, $ttl) { + public function setResponseToken(PhutilOpaqueEnvelope $token) { if (!$this->getUserPHID()) { throw new PhutilInvalidStateException('setUserPHID'); } @@ -97,15 +187,6 @@ final class PhabricatorAuthChallenge 'set a new response token.')); } - $now = PhabricatorTime::getNow(); - if ($ttl < $now) { - throw new Exception( - pht( - 'Response TTL is invalid: TTLs must be an epoch timestamp '. - 'coresponding to a future time (did you use a relative TTL by '. - 'mistake?).')); - } - if (preg_match('/ /', $token->openEnvelope())) { throw new Exception( pht( @@ -129,11 +210,14 @@ final class PhabricatorAuthChallenge } $this->responseToken = $token; - $this->responseTTL = $ttl; return $this; } + public function getResponseToken() { + return $this->responseToken; + } + public function setResponseDigest($value) { throw new Exception( pht( From 961fd7e8491e7df29dabddd2c7cdeabbc11fae1b Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Dec 2018 11:17:03 -0800 Subject: [PATCH 55/62] In Legalpad, prompt for MFA at the end of the workflow instead of the beginning Summary: Depends on D19895. Ref T13222. This is a simple behavioral improvement for the current MFA implementation in Legalpad: don't MFA the user and //then// realize that they forgot to actually check the box. Test Plan: - Submitted form without the box checked, got an error saying "check the box" instead of MFA. - Submitted the form with the box checked, got an MFA prompt. - Passed the MFA gate, got a signed form. - Tried to sign another form, hit MFA timed lockout. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19896 --- .../LegalpadDocumentSignController.php | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php index 27769432c2..ab98c0bb78 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -151,21 +151,6 @@ final class LegalpadDocumentSignController extends LegalpadController { $errors = array(); $hisec_token = null; if ($request->isFormOrHisecPost() && !$has_signed) { - - // Require two-factor auth to sign legal documents. - if ($viewer->isLoggedIn()) { - $workflow_key = sprintf( - 'legalpad.sign(%s)', - $document->getPHID()); - - $hisec_token = id(new PhabricatorAuthSessionEngine()) - ->setWorkflowKey($workflow_key) - ->requireHighSecurityToken( - $viewer, - $request, - $document->getURI()); - } - list($form_data, $errors, $field_errors) = $this->readSignatureForm( $document, $request); @@ -192,6 +177,20 @@ final class LegalpadDocumentSignController extends LegalpadController { $signature->setVerified($verified); if (!$errors) { + // Require MFA to sign legal documents. + if ($viewer->isLoggedIn()) { + $workflow_key = sprintf( + 'legalpad.sign(%s)', + $document->getPHID()); + + $hisec_token = id(new PhabricatorAuthSessionEngine()) + ->setWorkflowKey($workflow_key) + ->requireHighSecurityToken( + $viewer, + $request, + $document->getURI()); + } + $signature->save(); // If the viewer is logged in, signing for themselves, send them to From 21f07bf6f73b46d7370175c3534b07c97a032478 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 19 Dec 2018 10:57:26 -0800 Subject: [PATCH 56/62] Make Images in Pholio refer to mocks by PHID instead of ID Summary: Ref T11351. In Pholio, we currently use a `mockID`, but a `mockPHID` is generally preferable / more modern / more flexible. In particular, we need PHIDs to load handles and prefer PHIDs when exposing information to the API, and using PHIDs internally makes a bunch of things easier/better/faster and ~nothing harder/worse/slower. I'll add some inlines about a few things. Test Plan: Ran migrations, spot-checked database for sanity. Loaded Pholio, saw data unchanged. Created and edited images. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19914 --- .../20181219.pholio.01.imagephid.sql | 2 + .../20181219.pholio.02.imagemigrate.php | 35 +++++++++++++++++ .../20181219.pholio.03.imageid.sql | 2 + .../pholio/editor/PholioMockEditor.php | 2 +- ...PhabricatorPholioMockTestDataGenerator.php | 2 +- .../pholio/phid/PholioImagePHIDType.php | 10 ++--- .../pholio/query/PholioImageQuery.php | 35 ++++++----------- .../pholio/query/PholioMockQuery.php | 8 ++-- .../pholio/storage/PholioImage.php | 39 +++++++++++++------ .../pholio/storage/PholioMock.php | 2 +- 10 files changed, 88 insertions(+), 49 deletions(-) create mode 100644 resources/sql/autopatches/20181219.pholio.01.imagephid.sql create mode 100644 resources/sql/autopatches/20181219.pholio.02.imagemigrate.php create mode 100644 resources/sql/autopatches/20181219.pholio.03.imageid.sql diff --git a/resources/sql/autopatches/20181219.pholio.01.imagephid.sql b/resources/sql/autopatches/20181219.pholio.01.imagephid.sql new file mode 100644 index 0000000000..870cddd950 --- /dev/null +++ b/resources/sql/autopatches/20181219.pholio.01.imagephid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_pholio.pholio_image + ADD mockPHID VARBINARY(64); diff --git a/resources/sql/autopatches/20181219.pholio.02.imagemigrate.php b/resources/sql/autopatches/20181219.pholio.02.imagemigrate.php new file mode 100644 index 0000000000..f1fc1b3c37 --- /dev/null +++ b/resources/sql/autopatches/20181219.pholio.02.imagemigrate.php @@ -0,0 +1,35 @@ +establishConnection('w'); +$iterator = new LiskRawMigrationIterator($conn, $image->getTableName()); + +foreach ($iterator as $image_row) { + if ($image_row['mockPHID']) { + continue; + } + + $mock_id = $image_row['mockID']; + + $mock_row = queryfx_one( + $conn, + 'SELECT phid FROM %R WHERE id = %d', + $mock, + $mock_id); + + if (!$mock_row) { + continue; + } + + queryfx( + $conn, + 'UPDATE %R SET mockPHID = %s WHERE id = %d', + $image, + $mock_row['phid'], + $image_row['id']); +} diff --git a/resources/sql/autopatches/20181219.pholio.03.imageid.sql b/resources/sql/autopatches/20181219.pholio.03.imageid.sql new file mode 100644 index 0000000000..3a3cb029ac --- /dev/null +++ b/resources/sql/autopatches/20181219.pholio.03.imageid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_pholio.pholio_image + DROP mockID; diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php index d1f7daf3a5..a653272fba 100644 --- a/src/applications/pholio/editor/PholioMockEditor.php +++ b/src/applications/pholio/editor/PholioMockEditor.php @@ -91,7 +91,7 @@ final class PholioMockEditor extends PhabricatorApplicationTransactionEditor { $images = $this->getNewImages(); foreach ($images as $image) { - $image->setMockID($object->getID()); + $image->setMockPHID($object->getPHID()); $image->save(); } diff --git a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php index 041e14fcbf..b97a5fc3c7 100644 --- a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php +++ b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php @@ -65,7 +65,7 @@ final class PhabricatorPholioMockTestDataGenerator ->setActor($author) ->applyTransactions($mock, $transactions); foreach ($images as $image) { - $image->setMockID($mock->getID()); + $image->setMockPHID($mock->getPHID()); $image->save(); } diff --git a/src/applications/pholio/phid/PholioImagePHIDType.php b/src/applications/pholio/phid/PholioImagePHIDType.php index b28dbd64d5..d7a9984fd5 100644 --- a/src/applications/pholio/phid/PholioImagePHIDType.php +++ b/src/applications/pholio/phid/PholioImagePHIDType.php @@ -32,13 +32,9 @@ final class PholioImagePHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $image = $objects[$phid]; - $id = $image->getID(); - $mock_id = $image->getMockID(); - $name = $image->getName(); - - $handle->setURI("/M{$mock_id}/{$id}/"); - $handle->setName($name); - $handle->setFullName($name); + $handle + ->setName($image->getName()) + ->setURI($image->getURI()); } } diff --git a/src/applications/pholio/query/PholioImageQuery.php b/src/applications/pholio/query/PholioImageQuery.php index 5f541f1768..0fa7bfb1a0 100644 --- a/src/applications/pholio/query/PholioImageQuery.php +++ b/src/applications/pholio/query/PholioImageQuery.php @@ -5,8 +5,7 @@ final class PholioImageQuery private $ids; private $phids; - private $mockIDs; - private $obsolete; + private $mockPHIDs; private $needInlineComments; private $mockCache = array(); @@ -21,13 +20,8 @@ final class PholioImageQuery return $this; } - public function withMockIDs(array $mock_ids) { - $this->mockIDs = $mock_ids; - return $this; - } - - public function withObsolete($obsolete) { - $this->obsolete = $obsolete; + public function withMockPHIDs(array $mock_phids) { + $this->mockPHIDs = $mock_phids; return $this; } @@ -69,18 +63,11 @@ final class PholioImageQuery $this->phids); } - if ($this->mockIDs !== null) { + if ($this->mockPHIDs !== null) { $where[] = qsprintf( $conn, - 'mockID IN (%Ld)', - $this->mockIDs); - } - - if ($this->obsolete !== null) { - $where[] = qsprintf( - $conn, - 'isObsolete = %d', - $this->obsolete); + 'mockPHID IN (%Ls)', + $this->mockPHIDs); } return $where; @@ -92,16 +79,18 @@ final class PholioImageQuery if ($this->getMockCache()) { $mocks = $this->getMockCache(); } else { - $mock_ids = mpull($images, 'getMockID'); + $mock_phids = mpull($images, 'getMockPHID'); + // DO NOT set needImages to true; recursion results! $mocks = id(new PholioMockQuery()) ->setViewer($this->getViewer()) - ->withIDs($mock_ids) + ->withPHIDs($mock_phids) ->execute(); - $mocks = mpull($mocks, null, 'getID'); + $mocks = mpull($mocks, null, 'getPHID'); } + foreach ($images as $index => $image) { - $mock = idx($mocks, $image->getMockID()); + $mock = idx($mocks, $image->getMockPHID()); if ($mock) { $image->attachMock($mock); } else { diff --git a/src/applications/pholio/query/PholioMockQuery.php b/src/applications/pholio/query/PholioMockQuery.php index 5f1711def6..9e3154ec5c 100644 --- a/src/applications/pholio/query/PholioMockQuery.php +++ b/src/applications/pholio/query/PholioMockQuery.php @@ -115,18 +115,18 @@ final class PholioMockQuery $need_inline_comments) { assert_instances_of($mocks, 'PholioMock'); - $mock_map = mpull($mocks, null, 'getID'); + $mock_map = mpull($mocks, null, 'getPHID'); $all_images = id(new PholioImageQuery()) ->setViewer($viewer) ->setMockCache($mock_map) - ->withMockIDs(array_keys($mock_map)) + ->withMockPHIDs(array_keys($mock_map)) ->needInlineComments($need_inline_comments) ->execute(); - $image_groups = mgroup($all_images, 'getMockID'); + $image_groups = mgroup($all_images, 'getMockPHID'); foreach ($mocks as $mock) { - $mock_images = idx($image_groups, $mock->getID(), array()); + $mock_images = idx($image_groups, $mock->getPHID(), array()); $mock->attachAllImages($mock_images); $active_images = mfilter($mock_images, 'getIsObsolete', true); $mock->attachImages(msort($active_images, 'getSequence')); diff --git a/src/applications/pholio/storage/PholioImage.php b/src/applications/pholio/storage/PholioImage.php index 0f800bbb0f..1440773722 100644 --- a/src/applications/pholio/storage/PholioImage.php +++ b/src/applications/pholio/storage/PholioImage.php @@ -6,7 +6,7 @@ final class PholioImage extends PholioDAO PhabricatorExtendedPolicyInterface { protected $authorPHID; - protected $mockID; + protected $mockPHID; protected $filePHID; protected $name; protected $description; @@ -29,7 +29,7 @@ final class PholioImage extends PholioDAO return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( - 'mockID' => 'id?', + 'mockPHID' => 'phid?', 'name' => 'text128', 'description' => 'text', 'sequence' => 'uint32', @@ -37,14 +37,9 @@ final class PholioImage extends PholioDAO 'replacesImagePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => null, - 'keyPHID' => array( - 'columns' => array('phid'), - 'unique' => true, - ), - 'mockID' => array( - 'columns' => array('mockID', 'isObsolete', 'sequence'), - ), + // TODO: There should be a key starting with "mockPHID" here at a + // minimum, but it's not entirely clear what other columns we should + // have as part of the key. ), ) + parent::getConfiguration(); } @@ -71,6 +66,10 @@ final class PholioImage extends PholioDAO return $this->assertAttached($this->mock); } + public function hasMock() { + return (bool)$this->getMockPHID(); + } + public function attachInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PholioTransactionComment'); $this->inlineComments = $inline_comments; @@ -82,6 +81,22 @@ final class PholioImage extends PholioDAO return $this->inlineComments; } + public function getURI() { + if ($this->hasMock()) { + $mock = $this->getMock(); + + $mock_uri = $mock->getURI(); + $image_id = $this->getID(); + + return "{$mock_uri}/{$image_id}/"; + } + + // For now, standalone images have no URI. We could provide one at some + // point, although it's not clear that there's any motivation to do so. + + return null; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -96,7 +111,7 @@ final class PholioImage extends PholioDAO public function getPolicy($capability) { // If the image is attached to a mock, we use an extended policy to match // the mock's permissions. - if ($this->getMockID()) { + if ($this->hasMock()) { return PhabricatorPolicies::getMostOpenPolicy(); } @@ -113,7 +128,7 @@ final class PholioImage extends PholioDAO public function getExtendedPolicy($capability, PhabricatorUser $viewer) { - if ($this->getMockID()) { + if ($this->hasMock()) { return array( array( $this->getMock(), diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index cf32a34131..62285d0a59 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -259,7 +259,7 @@ final class PholioMock extends PholioDAO $this->openTransaction(); $images = id(new PholioImageQuery()) ->setViewer($engine->getViewer()) - ->withMockIDs(array($this->getID())) + ->withMockIDs(array($this->getPHID())) ->execute(); foreach ($images as $image) { $image->delete(); From 6c43d1d52cbd7a9808cf84d6d730c0a46241c111 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 19 Dec 2018 11:52:53 -0800 Subject: [PATCH 57/62] Remove "willRenderTimeline()" from ApplicationTransactionInterface Summary: Depends on D19914. Ref T11351. Some of the Phoilo rabbit holes go very deep. `PhabricatorApplicationTransactionInterface` currently requires you to implement `willRenderTimeline()`. Almost every object just implements this as `return $timeline`; only Pholio, Diffusion, and Differential specialize it. In all cases, they are specializing it mostly to render inline comments. The actual implementations are a bit of a weird mess and the way the data is threaded through the call stack is weird and not very modern. Try to clean this up: - Stop requiring `willRenderTimeline()` to be implemented. - Stop requiring `getApplicationTransactionViewObject()` to be implemented (only the three above, plus Legalpad, implement this, and Legalpad's implementation is a no-op). These two methods are inherently pretty coupled for almost any reasonable thing you might want to do with the timeline. - Simplify the handling of "renderdata" and call it "View Data". This is additional information about the current view of the transaction timeline that is required to render it correctly. This is only used in Differential, to decide if we can link an inline comment to an anchor on the same page or should link it to another page. We could perhaps do this on the client instead, but having this data doesn't seem inherently bad to me. - If objects want to customize timeline rendering, they now implement `PhabricatorTimelineInterface` and provide a `TimelineEngine` which gets a nice formal stack. This leaves a lot of empty `willRenderTimeline()` implementations hanging around. I'll remove these in the next change, it's just going to be deleting a couple dozen copies of an identical empty method implementation. Test Plan: - Viewed audits, revisions, and mocks with inline comments. - Used "Show Older" to page a revision back in history (this is relevant for "View Data"). - Grepped for symbols: willRenderTimeline, getApplicationTransactionViewObject, Legalpad classes. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19918 --- resources/celerity/map.php | 18 ++-- src/__phutil_library_map__.php | 16 +++- .../storage/PhabricatorAuditTransaction.php | 4 - .../PhabricatorBadgesCommentController.php | 1 + .../base/controller/PhabricatorController.php | 20 ++-- .../PhabricatorConfigHistoryController.php | 8 +- .../DifferentialRevisionTimelineEngine.php | 78 +++++++++++++++ .../storage/DifferentialRevision.php | 77 ++------------- .../storage/DifferentialTransaction.php | 4 - .../controller/DiffusionCommitController.php | 2 - .../engine/DiffusionCommitTimelineEngine.php | 30 ++++++ .../legalpad/storage/LegalpadTransaction.php | 4 - .../legalpad/view/LegalpadTransactionView.php | 4 - .../PholioMockCommentController.php | 5 +- .../engine/PholioMockTimelineEngine.php | 19 ++++ .../pholio/storage/PholioMock.php | 21 ++-- .../pholio/storage/PholioTransaction.php | 4 - .../PonderAnswerCommentController.php | 1 + .../PonderQuestionCommentController.php | 1 + .../ReleephRequestCommentController.php | 1 + .../storage/PhabricatorRepositoryCommit.php | 36 ++----- .../PhabricatorSlowvoteCommentController.php | 1 + ...licationTransactionShowOlderController.php | 13 ++- .../editengine/PhabricatorEditEngine.php | 1 + .../PhabricatorStandardTimelineEngine.php | 4 + .../engine/PhabricatorTimelineEngine.php | 95 +++++++++++++++++++ ...ricatorApplicationTransactionInterface.php | 16 ---- .../PhabricatorTimelineInterface.php | 7 ++ ...bricatorApplicationTransactionResponse.php | 42 ++++---- .../PhabricatorApplicationTransaction.php | 4 - .../PhabricatorApplicationTransactionView.php | 41 ++++---- src/view/phui/PHUITimelineView.php | 12 ++- .../behavior-show-older-transactions.js | 6 +- 33 files changed, 366 insertions(+), 230 deletions(-) create mode 100644 src/applications/differential/engine/DifferentialRevisionTimelineEngine.php create mode 100644 src/applications/diffusion/engine/DiffusionCommitTimelineEngine.php delete mode 100644 src/applications/legalpad/view/LegalpadTransactionView.php create mode 100644 src/applications/pholio/engine/PholioMockTimelineEngine.php create mode 100644 src/applications/transactions/engine/PhabricatorStandardTimelineEngine.php create mode 100644 src/applications/transactions/engine/PhabricatorTimelineEngine.php create mode 100644 src/applications/transactions/interface/PhabricatorTimelineInterface.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 3b86ebeeb4..7044c5293a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', 'core.pkg.css' => '9d1148a4', - 'core.pkg.js' => '4bde473b', + 'core.pkg.js' => 'bd89cb1d', 'differential.pkg.css' => '06dc617c', 'differential.pkg.js' => 'ef0b989b', 'diffusion.pkg.css' => 'a2d17c7d', @@ -425,7 +425,7 @@ return array( 'rsrc/js/application/transactions/behavior-comment-actions.js' => '59e27e74', 'rsrc/js/application/transactions/behavior-reorder-configs.js' => 'd7a74243', 'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'b59e1e96', - 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '8f29b364', + 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '0e1eca96', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6', 'rsrc/js/application/transactions/behavior-transaction-list.js' => '1f6794f6', 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec', @@ -639,7 +639,7 @@ return array( 'javelin-behavior-phabricator-remarkup-assist' => 'acd29eee', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', 'javelin-behavior-phabricator-search-typeahead' => 'c3e917d9', - 'javelin-behavior-phabricator-show-older-transactions' => '8f29b364', + 'javelin-behavior-phabricator-show-older-transactions' => '0e1eca96', 'javelin-behavior-phabricator-tooltips' => 'c420b0b9', 'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6', 'javelin-behavior-phabricator-transaction-list' => '1f6794f6', @@ -950,6 +950,12 @@ return array( 'javelin-install', 'phuix-button-view', ), + '0e1eca96' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + 'phabricator-busy', + ), '0f764c35' => array( 'javelin-install', 'javelin-util', @@ -1581,12 +1587,6 @@ return array( '8e1baf68' => array( 'phui-button-css', ), - '8f29b364' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - 'phabricator-busy', - ), '8ff5e24c' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7767a93561..6320811c9a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -647,6 +647,7 @@ phutil_register_library_map(array( 'DifferentialRevisionSummaryTransaction' => 'applications/differential/xaction/DifferentialRevisionSummaryTransaction.php', 'DifferentialRevisionTestPlanHeraldField' => 'applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php', 'DifferentialRevisionTestPlanTransaction' => 'applications/differential/xaction/DifferentialRevisionTestPlanTransaction.php', + 'DifferentialRevisionTimelineEngine' => 'applications/differential/engine/DifferentialRevisionTimelineEngine.php', 'DifferentialRevisionTitleHeraldField' => 'applications/differential/herald/DifferentialRevisionTitleHeraldField.php', 'DifferentialRevisionTitleTransaction' => 'applications/differential/xaction/DifferentialRevisionTitleTransaction.php', 'DifferentialRevisionTransactionType' => 'applications/differential/xaction/DifferentialRevisionTransactionType.php', @@ -769,6 +770,7 @@ phutil_register_library_map(array( 'DiffusionCommitSearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitSearchConduitAPIMethod.php', 'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php', 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', + 'DiffusionCommitTimelineEngine' => 'applications/diffusion/engine/DiffusionCommitTimelineEngine.php', 'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php', 'DiffusionCommitVerifyTransaction' => 'applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php', 'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php', @@ -1630,7 +1632,6 @@ phutil_register_library_map(array( 'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php', 'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php', 'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php', - 'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php', 'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php', 'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php', 'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php', @@ -4433,6 +4434,7 @@ phutil_register_library_map(array( 'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php', 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php', + 'PhabricatorStandardTimelineEngine' => 'applications/transactions/engine/PhabricatorStandardTimelineEngine.php', 'PhabricatorStaticEditField' => 'applications/transactions/editfield/PhabricatorStaticEditField.php', 'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php', 'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php', @@ -4532,6 +4534,8 @@ phutil_register_library_map(array( 'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php', 'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php', 'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php', + 'PhabricatorTimelineEngine' => 'applications/transactions/engine/PhabricatorTimelineEngine.php', + 'PhabricatorTimelineInterface' => 'applications/transactions/interface/PhabricatorTimelineInterface.php', 'PhabricatorTimezoneIgnoreOffsetSetting' => 'applications/settings/setting/PhabricatorTimezoneIgnoreOffsetSetting.php', 'PhabricatorTimezoneSetting' => 'applications/settings/setting/PhabricatorTimezoneSetting.php', 'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php', @@ -4871,6 +4875,7 @@ phutil_register_library_map(array( 'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php', 'PholioMockStatusTransaction' => 'applications/pholio/xaction/PholioMockStatusTransaction.php', 'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php', + 'PholioMockTimelineEngine' => 'applications/pholio/engine/PholioMockTimelineEngine.php', 'PholioMockTransactionType' => 'applications/pholio/xaction/PholioMockTransactionType.php', 'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php', 'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php', @@ -5991,6 +5996,7 @@ phutil_register_library_map(array( 'PhabricatorSubscribableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', + 'PhabricatorTimelineInterface', 'PhabricatorMentionableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorProjectInterface', @@ -6072,6 +6078,7 @@ phutil_register_library_map(array( 'DifferentialRevisionSummaryTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionTestPlanHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionTestPlanTransaction' => 'DifferentialRevisionTransactionType', + 'DifferentialRevisionTimelineEngine' => 'PhabricatorTimelineEngine', 'DifferentialRevisionTitleHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionTitleTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType', @@ -6194,6 +6201,7 @@ phutil_register_library_map(array( 'DiffusionCommitSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitTagsController' => 'DiffusionController', + 'DiffusionCommitTimelineEngine' => 'PhabricatorTimelineEngine', 'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType', 'DiffusionCommitVerifyTransaction' => 'DiffusionCommitAuditTransaction', 'DiffusionCompareController' => 'DiffusionController', @@ -7201,7 +7209,6 @@ phutil_register_library_map(array( 'LegalpadTransaction' => 'PhabricatorModularTransaction', 'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment', 'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery', - 'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView', 'LiskChunkTestCase' => 'PhabricatorTestCase', 'LiskDAO' => array( 'Phobject', @@ -10079,6 +10086,7 @@ phutil_register_library_map(array( 'HarbormasterBuildkiteBuildableInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', + 'PhabricatorTimelineInterface', 'PhabricatorFulltextInterface', 'PhabricatorFerretInterface', 'PhabricatorConduitResultInterface', @@ -10469,6 +10477,7 @@ phutil_register_library_map(array( 'AphrontResponseProducerInterface', ), 'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorStandardTimelineEngine' => 'PhabricatorTimelineEngine', 'PhabricatorStaticEditField' => 'PhabricatorEditField', 'PhabricatorStatusController' => 'PhabricatorController', 'PhabricatorStatusUIExample' => 'PhabricatorUIExample', @@ -10567,6 +10576,7 @@ phutil_register_library_map(array( 'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting', 'PhabricatorTimeGuard' => 'Phobject', 'PhabricatorTimeTestCase' => 'PhabricatorTestCase', + 'PhabricatorTimelineEngine' => 'Phobject', 'PhabricatorTimezoneIgnoreOffsetSetting' => 'PhabricatorInternalSetting', 'PhabricatorTimezoneSetting' => 'PhabricatorOptionGroupSetting', 'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck', @@ -10967,6 +10977,7 @@ phutil_register_library_map(array( 'PhabricatorTokenReceiverInterface', 'PhabricatorFlaggableInterface', 'PhabricatorApplicationTransactionInterface', + 'PhabricatorTimelineInterface', 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', @@ -11001,6 +11012,7 @@ phutil_register_library_map(array( 'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PholioMockStatusTransaction' => 'PholioMockTransactionType', 'PholioMockThumbGridView' => 'AphrontView', + 'PholioMockTimelineEngine' => 'PhabricatorTimelineEngine', 'PholioMockTransactionType' => 'PholioTransactionType', 'PholioMockViewController' => 'PholioController', 'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule', diff --git a/src/applications/audit/storage/PhabricatorAuditTransaction.php b/src/applications/audit/storage/PhabricatorAuditTransaction.php index ad96edb3a4..da312e626a 100644 --- a/src/applications/audit/storage/PhabricatorAuditTransaction.php +++ b/src/applications/audit/storage/PhabricatorAuditTransaction.php @@ -32,10 +32,6 @@ final class PhabricatorAuditTransaction return new PhabricatorAuditTransactionComment(); } - public function getApplicationTransactionViewObject() { - return new PhabricatorAuditTransactionView(); - } - public function getRemarkupBlocks() { $blocks = parent::getRemarkupBlocks(); diff --git a/src/applications/badges/controller/PhabricatorBadgesCommentController.php b/src/applications/badges/controller/PhabricatorBadgesCommentController.php index a9108fd8dd..32e1775149 100644 --- a/src/applications/badges/controller/PhabricatorBadgesCommentController.php +++ b/src/applications/badges/controller/PhabricatorBadgesCommentController.php @@ -51,6 +51,7 @@ final class PhabricatorBadgesCommentController if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) + ->setObject($badge) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index df0c94c13d..e05a6016a6 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -482,14 +482,14 @@ abstract class PhabricatorController extends AphrontController { PhabricatorApplicationTransactionInterface $object, PhabricatorApplicationTransactionQuery $query, PhabricatorMarkupEngine $engine = null, - $render_data = array()) { + $view_data = array()) { - $viewer = $this->getRequest()->getUser(); + $request = $this->getRequest(); + $viewer = $this->getViewer(); $xaction = $object->getApplicationTransactionTemplate(); - $view = $xaction->getApplicationTransactionViewObject(); $pager = id(new AphrontCursorPagerView()) - ->readFromRequest($this->getRequest()) + ->readFromRequest($request) ->setURI(new PhutilURI( '/transactions/showolder/'.$object->getPHID().'/')); @@ -500,6 +500,13 @@ abstract class PhabricatorController extends AphrontController { ->executeWithCursorPager($pager); $xactions = array_reverse($xactions); + $timeline_engine = PhabricatorTimelineEngine::newForObject($object) + ->setViewer($viewer) + ->setTransactions($xactions) + ->setViewData($view_data); + + $view = $timeline_engine->buildTimelineView(); + if ($engine) { foreach ($xactions as $xaction) { if ($xaction->getComment()) { @@ -513,14 +520,9 @@ abstract class PhabricatorController extends AphrontController { } $timeline = $view - ->setUser($viewer) - ->setObjectPHID($object->getPHID()) - ->setTransactions($xactions) ->setPager($pager) - ->setRenderData($render_data) ->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID')) ->setQuoteRef($this->getRequest()->getStr('quoteRef')); - $object->willRenderTimeline($timeline, $this->getRequest()); return $timeline; } diff --git a/src/applications/config/controller/PhabricatorConfigHistoryController.php b/src/applications/config/controller/PhabricatorConfigHistoryController.php index 238d09234d..9157ecb8bb 100644 --- a/src/applications/config/controller/PhabricatorConfigHistoryController.php +++ b/src/applications/config/controller/PhabricatorConfigHistoryController.php @@ -16,18 +16,14 @@ final class PhabricatorConfigHistoryController $xaction = $object->getApplicationTransactionTemplate(); - $view = $xaction->getApplicationTransactionViewObject(); - - $timeline = $view - ->setUser($viewer) + $timeline = id(new PhabricatorApplicationTransactionView()) + ->setViewer($viewer) ->setTransactions($xactions) ->setRenderAsFeed(true) ->setObjectPHID(PhabricatorPHIDConstants::PHID_VOID); $timeline->setShouldTerminate(true); - $object->willRenderTimeline($timeline, $this->getRequest()); - $title = pht('Settings History'); $header = $this->buildHeaderView($title); diff --git a/src/applications/differential/engine/DifferentialRevisionTimelineEngine.php b/src/applications/differential/engine/DifferentialRevisionTimelineEngine.php new file mode 100644 index 0000000000..51d2c28fe0 --- /dev/null +++ b/src/applications/differential/engine/DifferentialRevisionTimelineEngine.php @@ -0,0 +1,78 @@ +getViewer(); + $xactions = $this->getTransactions(); + $revision = $this->getObject(); + + $view_data = $this->getViewData(); + if (!$view_data) { + $view_data = array(); + } + + $left = idx($view_data, 'left'); + $right = idx($view_data, 'right'); + + $diffs = id(new DifferentialDiffQuery()) + ->setViewer($viewer) + ->withIDs(array($left, $right)) + ->execute(); + $diffs = mpull($diffs, null, 'getID'); + $left_diff = $diffs[$left]; + $right_diff = $diffs[$right]; + + $old_ids = idx($view_data, 'old'); + $new_ids = idx($view_data, 'new'); + $old_ids = array_filter(explode(',', $old_ids)); + $new_ids = array_filter(explode(',', $new_ids)); + + $type_inline = DifferentialTransaction::TYPE_INLINE; + $changeset_ids = array_merge($old_ids, $new_ids); + $inlines = array(); + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() == $type_inline) { + $inlines[] = $xaction->getComment(); + $changeset_ids[] = $xaction->getComment()->getChangesetID(); + } + } + + if ($changeset_ids) { + $changesets = id(new DifferentialChangesetQuery()) + ->setViewer($viewer) + ->withIDs($changeset_ids) + ->execute(); + $changesets = mpull($changesets, null, 'getID'); + } else { + $changesets = array(); + } + + foreach ($inlines as $key => $inline) { + $inlines[$key] = DifferentialInlineComment::newFromModernComment( + $inline); + } + + $query = id(new DifferentialInlineCommentQuery()) + ->needHidden(true) + ->setViewer($viewer); + + // NOTE: This is a bit sketchy: this method adjusts the inlines as a + // side effect, which means it will ultimately adjust the transaction + // comments and affect timeline rendering. + $query->adjustInlinesForChangesets( + $inlines, + array_select_keys($changesets, $old_ids), + array_select_keys($changesets, $new_ids), + $revision); + + return id(new DifferentialTransactionView()) + ->setViewData($view_data) + ->setChangesets($changesets) + ->setRevision($revision) + ->setLeftDiff($left_diff) + ->setRightDiff($right_diff); + } + +} diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 9b169f3c67..ef94da7f67 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -11,6 +11,7 @@ final class DifferentialRevision extends DifferentialDAO PhabricatorSubscribableInterface, PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, + PhabricatorTimelineInterface, PhabricatorMentionableInterface, PhabricatorDestructibleInterface, PhabricatorProjectInterface, @@ -998,73 +999,6 @@ final class DifferentialRevision extends DifferentialDAO return new DifferentialTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - $viewer = $request->getViewer(); - - $render_data = $timeline->getRenderData(); - $left = $request->getInt('left', idx($render_data, 'left')); - $right = $request->getInt('right', idx($render_data, 'right')); - - $diffs = id(new DifferentialDiffQuery()) - ->setViewer($request->getUser()) - ->withIDs(array($left, $right)) - ->execute(); - $diffs = mpull($diffs, null, 'getID'); - $left_diff = $diffs[$left]; - $right_diff = $diffs[$right]; - - $old_ids = $request->getStr('old', idx($render_data, 'old')); - $new_ids = $request->getStr('new', idx($render_data, 'new')); - $old_ids = array_filter(explode(',', $old_ids)); - $new_ids = array_filter(explode(',', $new_ids)); - - $type_inline = DifferentialTransaction::TYPE_INLINE; - $changeset_ids = array_merge($old_ids, $new_ids); - $inlines = array(); - foreach ($timeline->getTransactions() as $xaction) { - if ($xaction->getTransactionType() == $type_inline) { - $inlines[] = $xaction->getComment(); - $changeset_ids[] = $xaction->getComment()->getChangesetID(); - } - } - - if ($changeset_ids) { - $changesets = id(new DifferentialChangesetQuery()) - ->setViewer($request->getUser()) - ->withIDs($changeset_ids) - ->execute(); - $changesets = mpull($changesets, null, 'getID'); - } else { - $changesets = array(); - } - - foreach ($inlines as $key => $inline) { - $inlines[$key] = DifferentialInlineComment::newFromModernComment( - $inline); - } - - $query = id(new DifferentialInlineCommentQuery()) - ->needHidden(true) - ->setViewer($viewer); - - // NOTE: This is a bit sketchy: this method adjusts the inlines as a - // side effect, which means it will ultimately adjust the transaction - // comments and affect timeline rendering. - $query->adjustInlinesForChangesets( - $inlines, - array_select_keys($changesets, $old_ids), - array_select_keys($changesets, $new_ids), - $this); - - return $timeline - ->setChangesets($changesets) - ->setRevision($this) - ->setLeftDiff($left_diff) - ->setRightDiff($right_diff); - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ @@ -1206,4 +1140,13 @@ final class DifferentialRevision extends DifferentialDAO return new DifferentialRevisionDraftEngine(); } + +/* -( PhabricatorTimelineInterface )--------------------------------------- */ + + + public function newTimelineEngine() { + return new DifferentialRevisionTimelineEngine(); + } + + } diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 53fdc71a15..c49e40f988 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -65,10 +65,6 @@ final class DifferentialTransaction return new DifferentialTransactionComment(); } - public function getApplicationTransactionViewObject() { - return new DifferentialTransactionView(); - } - public function shouldHide() { $old = $this->getOldValue(); $new = $this->getNewValue(); diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 5621b1fa12..fae215f381 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -740,8 +740,6 @@ final class DiffusionCommitController extends DiffusionController { $commit, new PhabricatorAuditTransactionQuery()); - $commit->willRenderTimeline($timeline, $this->getRequest()); - $timeline->setQuoteRef($commit->getMonogram()); return $timeline; diff --git a/src/applications/diffusion/engine/DiffusionCommitTimelineEngine.php b/src/applications/diffusion/engine/DiffusionCommitTimelineEngine.php new file mode 100644 index 0000000000..49914c4b4f --- /dev/null +++ b/src/applications/diffusion/engine/DiffusionCommitTimelineEngine.php @@ -0,0 +1,30 @@ +getTransactions(); + + $path_ids = array(); + foreach ($xactions as $xaction) { + if ($xaction->hasComment()) { + $path_id = $xaction->getComment()->getPathID(); + if ($path_id) { + $path_ids[] = $path_id; + } + } + } + + $path_map = array(); + if ($path_ids) { + $path_map = id(new DiffusionPathQuery()) + ->withPathIDs($path_ids) + ->execute(); + $path_map = ipull($path_map, 'path', 'id'); + } + + return id(new PhabricatorAuditTransactionView()) + ->setPathMap($path_map); + } +} diff --git a/src/applications/legalpad/storage/LegalpadTransaction.php b/src/applications/legalpad/storage/LegalpadTransaction.php index c43c86c5fc..dc4bbfe942 100644 --- a/src/applications/legalpad/storage/LegalpadTransaction.php +++ b/src/applications/legalpad/storage/LegalpadTransaction.php @@ -14,10 +14,6 @@ final class LegalpadTransaction extends PhabricatorModularTransaction { return new LegalpadTransactionComment(); } - public function getApplicationTransactionViewObject() { - return new LegalpadTransactionView(); - } - public function getBaseTransactionClass() { return 'LegalpadDocumentTransactionType'; } diff --git a/src/applications/legalpad/view/LegalpadTransactionView.php b/src/applications/legalpad/view/LegalpadTransactionView.php deleted file mode 100644 index a68619c000..0000000000 --- a/src/applications/legalpad/view/LegalpadTransactionView.php +++ /dev/null @@ -1,4 +0,0 @@ -isAjax() && $is_preview) { - $xaction_view = id(new PholioTransactionView()) - ->setMock($mock); - return id(new PhabricatorApplicationTransactionResponse()) + ->setObject($mock) ->setViewer($viewer) ->setTransactions($xactions) - ->setTransactionView($xaction_view) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse())->setURI($mock_uri); diff --git a/src/applications/pholio/engine/PholioMockTimelineEngine.php b/src/applications/pholio/engine/PholioMockTimelineEngine.php new file mode 100644 index 0000000000..80b64e73b7 --- /dev/null +++ b/src/applications/pholio/engine/PholioMockTimelineEngine.php @@ -0,0 +1,19 @@ +getViewer(); + $object = $this->getObject(); + + PholioMockQuery::loadImages( + $viewer, + array($object), + $need_inline_comments = true); + + return id(new PholioTransactionView()) + ->setMock($object); + } + +} diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 62285d0a59..51bdd1a196 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -7,6 +7,7 @@ final class PholioMock extends PholioDAO PhabricatorTokenReceiverInterface, PhabricatorFlaggableInterface, PhabricatorApplicationTransactionInterface, + PhabricatorTimelineInterface, PhabricatorProjectInterface, PhabricatorDestructibleInterface, PhabricatorSpacesInterface, @@ -228,17 +229,6 @@ final class PholioMock extends PholioDAO return new PholioTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - PholioMockQuery::loadImages( - $request->getUser(), - array($this), - $need_inline_comments = true); - $timeline->setMock($this); - return $timeline; - } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ @@ -288,9 +278,18 @@ final class PholioMock extends PholioDAO /* -( PhabricatorFerretInterface )----------------------------------------- */ + public function newFerretEngine() { return new PholioMockFerretEngine(); } +/* -( PhabricatorTimelineInterace )---------------------------------------- */ + + + public function newTimelineEngine() { + return new PholioMockTimelineEngine(); + } + + } diff --git a/src/applications/pholio/storage/PholioTransaction.php b/src/applications/pholio/storage/PholioTransaction.php index 240b9d93e6..e4656dc7f6 100644 --- a/src/applications/pholio/storage/PholioTransaction.php +++ b/src/applications/pholio/storage/PholioTransaction.php @@ -23,10 +23,6 @@ final class PholioTransaction extends PhabricatorModularTransaction { return new PholioTransactionComment(); } - public function getApplicationTransactionViewObject() { - return new PholioTransactionView(); - } - public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { diff --git a/src/applications/ponder/controller/PonderAnswerCommentController.php b/src/applications/ponder/controller/PonderAnswerCommentController.php index 4b60fb939a..3d3e3a1392 100644 --- a/src/applications/ponder/controller/PonderAnswerCommentController.php +++ b/src/applications/ponder/controller/PonderAnswerCommentController.php @@ -50,6 +50,7 @@ final class PonderAnswerCommentController extends PonderController { if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) + ->setObject($answer) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); diff --git a/src/applications/ponder/controller/PonderQuestionCommentController.php b/src/applications/ponder/controller/PonderQuestionCommentController.php index 84c276cd5d..d50cd637c4 100644 --- a/src/applications/ponder/controller/PonderQuestionCommentController.php +++ b/src/applications/ponder/controller/PonderQuestionCommentController.php @@ -46,6 +46,7 @@ final class PonderQuestionCommentController extends PonderController { if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) + ->setObject($question) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); diff --git a/src/applications/releeph/controller/request/ReleephRequestCommentController.php b/src/applications/releeph/controller/request/ReleephRequestCommentController.php index 0a31261a13..96500e8bc5 100644 --- a/src/applications/releeph/controller/request/ReleephRequestCommentController.php +++ b/src/applications/releeph/controller/request/ReleephRequestCommentController.php @@ -51,6 +51,7 @@ final class ReleephRequestCommentController if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) + ->setObject($pull) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index b5fe08b6e0..ddc4dbbf9f 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -14,6 +14,7 @@ final class PhabricatorRepositoryCommit HarbormasterBuildkiteBuildableInterface, PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, + PhabricatorTimelineInterface, PhabricatorFulltextInterface, PhabricatorFerretInterface, PhabricatorConduitResultInterface, @@ -738,33 +739,6 @@ final class PhabricatorRepositoryCommit return new PhabricatorAuditTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - $xactions = $timeline->getTransactions(); - - $path_ids = array(); - foreach ($xactions as $xaction) { - if ($xaction->hasComment()) { - $path_id = $xaction->getComment()->getPathID(); - if ($path_id) { - $path_ids[] = $path_id; - } - } - } - - $path_map = array(); - if ($path_ids) { - $path_map = id(new DiffusionPathQuery()) - ->withPathIDs($path_ids) - ->execute(); - $path_map = ipull($path_map, 'path', 'id'); - } - - return $timeline->setPathMap($path_map); - } - /* -( PhabricatorFulltextInterface )--------------------------------------- */ @@ -916,4 +890,12 @@ final class PhabricatorRepositoryCommit return $this; } + +/* -( PhabricatorTimelineInterface )--------------------------------------- */ + + + public function newTimelineEngine() { + return new DiffusionCommitTimelineEngine(); + } + } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php index 035eb577d8..3d48c31866 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php @@ -51,6 +51,7 @@ final class PhabricatorSlowvoteCommentController if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) + ->setObject($poll) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php index cdbdbf1ba9..741e1e6f93 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php @@ -27,7 +27,18 @@ final class PhabricatorApplicationTransactionShowOlderController return new Aphront404Response(); } - $timeline = $this->buildTransactionTimeline($object, $query); + $raw_view_data = $request->getStr('viewData'); + try { + $view_data = phutil_json_decode($raw_view_data); + } catch (Exception $ex) { + $view_data = array(); + } + + $timeline = $this->buildTransactionTimeline( + $object, + $query, + null, + $view_data); $phui_timeline = $timeline->buildPHUITimelineView($with_hiding = false); $phui_timeline->setShouldAddSpacers(false); diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 86cc014102..3b3e5abdd7 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1955,6 +1955,7 @@ abstract class PhabricatorEditEngine $preview_content = $this->newCommentPreviewContent($object, $xactions); return id(new PhabricatorApplicationTransactionResponse()) + ->setObject($object) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview) diff --git a/src/applications/transactions/engine/PhabricatorStandardTimelineEngine.php b/src/applications/transactions/engine/PhabricatorStandardTimelineEngine.php new file mode 100644 index 0000000000..a9f6ea4ba0 --- /dev/null +++ b/src/applications/transactions/engine/PhabricatorStandardTimelineEngine.php @@ -0,0 +1,4 @@ +newTimelineEngine(); + } else { + $engine = new PhabricatorStandardTimelineEngine(); + } + + $engine->setObject($object); + + return $engine; + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setObject($object) { + $this->object = $object; + return $this; + } + + final public function getObject() { + return $this->object; + } + + final public function setTransactions(array $xactions) { + assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); + $this->xactions = $xactions; + return $this; + } + + final public function getTransactions() { + return $this->xactions; + } + + final public function setRequest(AphrontRequest $request) { + $this->request = $request; + return $this; + } + + final public function getRequest() { + return $this->request; + } + + final public function setViewData(array $view_data) { + $this->viewData = $view_data; + return $this; + } + + final public function getViewData() { + return $this->viewData; + } + + final public function buildTimelineView() { + $view = $this->newTimelineView(); + + if (!($view instanceof PhabricatorApplicationTransactionView)) { + throw new Exception( + pht( + 'Expected "newTimelineView()" to return an object of class "%s" '. + '(in engine "%s").', + 'PhabricatorApplicationTransactionView', + get_class($this))); + } + + $viewer = $this->getViewer(); + $object = $this->getObject(); + $xactions = $this->getTransactions(); + + return $view + ->setViewer($viewer) + ->setObjectPHID($object->getPHID()) + ->setTransactions($xactions); + } + + protected function newTimelineView() { + return new PhabricatorApplicationTransactionView(); + } + +} diff --git a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php index fdccf7b775..1c6b4a0049 100644 --- a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php +++ b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php @@ -35,15 +35,6 @@ interface PhabricatorApplicationTransactionInterface { */ public function getApplicationTransactionTemplate(); - /** - * Hook to augment the $timeline with additional data for rendering. - * - * @return PhabricatorApplicationTransactionView - */ - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request); - } // TEMPLATE IMPLEMENTATION ///////////////////////////////////////////////////// @@ -64,11 +55,4 @@ interface PhabricatorApplicationTransactionInterface { return new <<>>Transaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - */ diff --git a/src/applications/transactions/interface/PhabricatorTimelineInterface.php b/src/applications/transactions/interface/PhabricatorTimelineInterface.php new file mode 100644 index 0000000000..2ec9fb2103 --- /dev/null +++ b/src/applications/transactions/interface/PhabricatorTimelineInterface.php @@ -0,0 +1,7 @@ +transactionView = $transaction_view; - return $this; - } - - public function getTransactionView() { - return $this->transactionView; - } + private $object; protected function buildProxy() { return new AphrontAjaxResponse(); @@ -33,6 +24,15 @@ final class PhabricatorApplicationTransactionResponse return $this->transactions; } + public function setObject($object) { + $this->object = $object; + return $this; + } + + public function getObject() { + return $this->object; + } + public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; @@ -57,19 +57,17 @@ final class PhabricatorApplicationTransactionResponse } public function reduceProxyResponse() { - if ($this->transactionView) { - $view = $this->transactionView; - } else if ($this->getTransactions()) { - $view = head($this->getTransactions()) - ->getApplicationTransactionViewObject(); - } else { - $view = new PhabricatorApplicationTransactionView(); - } + $object = $this->getObject(); + $viewer = $this->getViewer(); + $xactions = $this->getTransactions(); - $view - ->setUser($this->getViewer()) - ->setTransactions($this->getTransactions()) - ->setIsPreview($this->isPreview); + $timeline_engine = PhabricatorTimelineEngine::newForObject($object) + ->setViewer($viewer) + ->setTransactions($xactions); + + $view = $timeline_engine->buildTimelineView(); + + $view->setIsPreview($this->isPreview); if ($this->isPreview) { $xactions = mpull($view->buildEvents(), 'render'); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index f38c56acb3..00e723c0c6 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -79,10 +79,6 @@ abstract class PhabricatorApplicationTransaction throw new PhutilMethodNotImplementedException(); } - public function getApplicationTransactionViewObject() { - return new PhabricatorApplicationTransactionView(); - } - public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 9916628edf..c2b32aa190 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -15,8 +15,8 @@ class PhabricatorApplicationTransactionView extends AphrontView { private $quoteRef; private $pager; private $renderAsFeed; - private $renderData = array(); private $hideCommentOptions = false; + private $viewData = array(); public function setRenderAsFeed($feed) { $this->renderAsFeed = $feed; @@ -97,21 +97,6 @@ class PhabricatorApplicationTransactionView extends AphrontView { return $this->pager; } - /** - * This is additional data that may be necessary to render the next set - * of transactions. Objects that implement - * PhabricatorApplicationTransactionInterface use this data in - * willRenderTimeline. - */ - public function setRenderData(array $data) { - $this->renderData = $data; - return $this; - } - - public function getRenderData() { - return $this->renderData; - } - public function setHideCommentOptions($hide_comment_options) { $this->hideCommentOptions = $hide_comment_options; return $this; @@ -121,6 +106,15 @@ class PhabricatorApplicationTransactionView extends AphrontView { return $this->hideCommentOptions; } + public function setViewData(array $view_data) { + $this->viewData = $view_data; + return $this; + } + + public function getViewData() { + return $this->viewData; + } + public function buildEvents($with_hiding = false) { $user = $this->getUser(); @@ -216,10 +210,11 @@ class PhabricatorApplicationTransactionView extends AphrontView { } $view = id(new PHUITimelineView()) - ->setUser($this->getUser()) + ->setViewer($this->getViewer()) ->setShouldTerminate($this->shouldTerminate) ->setQuoteTargetID($this->getQuoteTargetID()) - ->setQuoteRef($this->getQuoteRef()); + ->setQuoteRef($this->getQuoteRef()) + ->setViewData($this->getViewData()); $events = $this->buildEvents($with_hiding); foreach ($events as $event) { @@ -230,10 +225,6 @@ class PhabricatorApplicationTransactionView extends AphrontView { $view->setPager($this->getPager()); } - if ($this->getRenderData()) { - $view->setRenderData($this->getRenderData()); - } - return $view; } @@ -246,7 +237,7 @@ class PhabricatorApplicationTransactionView extends AphrontView { $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; $engine = id(new PhabricatorMarkupEngine()) - ->setViewer($this->getUser()); + ->setViewer($this->getViewer()); foreach ($this->transactions as $xaction) { if (!$xaction->hasComment()) { continue; @@ -414,10 +405,10 @@ class PhabricatorApplicationTransactionView extends AphrontView { private function renderEvent( PhabricatorApplicationTransaction $xaction, array $group) { - $viewer = $this->getUser(); + $viewer = $this->getViewer(); $event = id(new PHUITimelineEventView()) - ->setUser($viewer) + ->setViewer($viewer) ->setAuthorPHID($xaction->getAuthorPHID()) ->setTransactionPHID($xaction->getPHID()) ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php index 3353a2e2bd..d0e942f461 100644 --- a/src/view/phui/PHUITimelineView.php +++ b/src/view/phui/PHUITimelineView.php @@ -7,7 +7,7 @@ final class PHUITimelineView extends AphrontView { private $shouldTerminate = false; private $shouldAddSpacers = true; private $pager; - private $renderData = array(); + private $viewData = array(); private $quoteTargetID; private $quoteRef; @@ -40,11 +40,15 @@ final class PHUITimelineView extends AphrontView { return $this; } - public function setRenderData(array $data) { - $this->renderData = $data; + public function setViewData(array $data) { + $this->viewData = $data; return $this; } + public function getViewData() { + return $this->viewData; + } + public function setQuoteTargetID($quote_target_id) { $this->quoteTargetID = $quote_target_id; return $this; @@ -72,7 +76,7 @@ final class PHUITimelineView extends AphrontView { 'phabricator-show-older-transactions', array( 'timelineID' => $this->id, - 'renderData' => $this->renderData, + 'viewData' => $this->getViewData(), )); } $events = $this->buildEvents(); diff --git a/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js b/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js index 28754ee3a2..74b17cd45c 100644 --- a/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js +++ b/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js @@ -83,7 +83,11 @@ JX.behavior('phabricator-show-older-transactions', function(config) { }; var fetch_older_workflow = function(href, callback, swap) { - return new JX.Workflow(href, config.renderData) + var params = { + viewData: JX.JSON.stringify(config.viewData) + }; + + return new JX.Workflow(href, params) .setHandler(JX.bind(null, callback, swap)); }; From 937e88c399ab6b77bf678acf24717b4008539659 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Dec 2018 10:22:25 -0800 Subject: [PATCH 58/62] Remove obsolete, no-op implementations of "willRenderTimeline()" Summary: Depends on D19918. Ref T11351. In D19918, I removed all calls to this method. Now, remove all implementations. All of these implementations just `return $timeline`, only the three sites in D19918 did anything interesting. Test Plan: Used `grep willRenderTimeline` to find callsites, found none. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19919 --- src/applications/almanac/storage/AlmanacBinding.php | 6 ------ src/applications/almanac/storage/AlmanacDevice.php | 7 ------- src/applications/almanac/storage/AlmanacInterface.php | 6 ------ src/applications/almanac/storage/AlmanacNamespace.php | 6 ------ src/applications/almanac/storage/AlmanacNetwork.php | 7 ------- src/applications/almanac/storage/AlmanacService.php | 7 ------- src/applications/auth/storage/PhabricatorAuthPassword.php | 8 -------- .../auth/storage/PhabricatorAuthProviderConfig.php | 7 ------- src/applications/auth/storage/PhabricatorAuthSSHKey.php | 6 ------ .../badges/storage/PhabricatorBadgesBadge.php | 7 ------- src/applications/base/PhabricatorApplication.php | 6 ------ .../calendar/storage/PhabricatorCalendarEvent.php | 6 ------ .../calendar/storage/PhabricatorCalendarExport.php | 7 ------- .../calendar/storage/PhabricatorCalendarImport.php | 6 ------ .../config/storage/PhabricatorConfigEntry.php | 7 ------- .../conpherence/storage/ConpherenceThread.php | 5 ----- .../countdown/storage/PhabricatorCountdown.php | 6 ------ .../dashboard/storage/PhabricatorDashboard.php | 7 ------- .../dashboard/storage/PhabricatorDashboardPanel.php | 7 ------- .../differential/storage/DifferentialDiff.php | 7 ------- src/applications/diviner/storage/DivinerLiveBook.php | 7 ------- src/applications/drydock/storage/DrydockBlueprint.php | 7 ------- src/applications/files/storage/PhabricatorFile.php | 7 ------- src/applications/fund/storage/FundBacker.php | 7 ------- src/applications/fund/storage/FundInitiative.php | 7 ------- .../harbormaster/storage/HarbormasterBuildable.php | 7 ------- .../harbormaster/storage/build/HarbormasterBuild.php | 7 ------- .../storage/configuration/HarbormasterBuildPlan.php | 8 -------- .../storage/configuration/HarbormasterBuildStep.php | 7 ------- src/applications/herald/storage/HeraldRule.php | 7 ------- src/applications/herald/storage/HeraldWebhook.php | 6 ------ src/applications/legalpad/storage/LegalpadDocument.php | 7 ------- .../macro/storage/PhabricatorFileImageMacro.php | 7 ------- src/applications/maniphest/storage/ManiphestTask.php | 7 ------- .../storage/PhabricatorMetaMTAApplicationEmail.php | 6 ------ src/applications/nuance/storage/NuanceItem.php | 6 ------ src/applications/nuance/storage/NuanceQueue.php | 6 ------ src/applications/nuance/storage/NuanceSource.php | 7 ------- .../oauthserver/storage/PhabricatorOAuthServerClient.php | 6 ------ .../owners/storage/PhabricatorOwnersPackage.php | 6 ------ .../packages/storage/PhabricatorPackagesPackage.php | 6 ------ .../packages/storage/PhabricatorPackagesPublisher.php | 6 ------ .../packages/storage/PhabricatorPackagesVersion.php | 6 ------ .../passphrase/storage/PassphraseCredential.php | 7 ------- src/applications/paste/storage/PhabricatorPaste.php | 7 ------- src/applications/people/storage/PhabricatorUser.php | 6 ------ src/applications/phame/storage/PhameBlog.php | 6 ------ src/applications/phame/storage/PhamePost.php | 7 ------- src/applications/phlux/storage/PhluxVariable.php | 7 ------- src/applications/phortune/storage/PhortuneAccount.php | 7 ------- src/applications/phortune/storage/PhortuneCart.php | 7 ------- src/applications/phortune/storage/PhortuneMerchant.php | 7 ------- .../phortune/storage/PhortunePaymentProviderConfig.php | 7 ------- src/applications/phriction/storage/PhrictionDocument.php | 7 ------- src/applications/phurl/storage/PhabricatorPhurlURL.php | 6 ------ src/applications/ponder/storage/PonderAnswer.php | 7 ------- src/applications/ponder/storage/PonderQuestion.php | 7 ------- src/applications/project/storage/PhabricatorProject.php | 7 ------- .../project/storage/PhabricatorProjectColumn.php | 7 ------- src/applications/releeph/storage/ReleephBranch.php | 7 ------- src/applications/releeph/storage/ReleephProject.php | 7 ------- src/applications/releeph/storage/ReleephRequest.php | 7 ------- .../repository/storage/PhabricatorRepository.php | 7 ------- .../repository/storage/PhabricatorRepositoryIdentity.php | 7 ------- .../repository/storage/PhabricatorRepositoryURI.php | 6 ------ .../storage/PhabricatorProfileMenuItemConfiguration.php | 7 ------- .../settings/storage/PhabricatorUserPreferences.php | 6 ------ .../slowvote/storage/PhabricatorSlowvotePoll.php | 7 ------- .../spaces/storage/PhabricatorSpacesNamespace.php | 6 ------ .../storage/PhabricatorEditEngineConfiguration.php | 6 ------ .../daemon/workers/storage/PhabricatorWorkerBulkJob.php | 5 ----- 71 files changed, 471 deletions(-) diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index c593e40fa7..5577ee5e7d 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -214,12 +214,6 @@ final class AlmanacBinding return new AlmanacBindingTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index a1ebdfffae..f25b2eb5a1 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -212,13 +212,6 @@ final class AlmanacDevice return new AlmanacDeviceTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 5c7f65ddd1..9a5023e751 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -176,12 +176,6 @@ final class AlmanacInterface return new AlmanacInterfaceTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorConduitResultInterface )---------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php index 238a7b628b..0c8f0c5ec7 100644 --- a/src/applications/almanac/storage/AlmanacNamespace.php +++ b/src/applications/almanac/storage/AlmanacNamespace.php @@ -199,12 +199,6 @@ final class AlmanacNamespace return new AlmanacNamespaceTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 6d2f23032e..3913d2f6e7 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -69,13 +69,6 @@ final class AlmanacNetwork return new AlmanacNetworkTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index ee40d83400..7348abd936 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -234,13 +234,6 @@ final class AlmanacService return new AlmanacServiceTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php index 3bcb95693e..930cbe61cb 100644 --- a/src/applications/auth/storage/PhabricatorAuthPassword.php +++ b/src/applications/auth/storage/PhabricatorAuthPassword.php @@ -225,12 +225,4 @@ final class PhabricatorAuthPassword return new PhabricatorAuthPasswordTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - - } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php index ba9b43a967..d4d1f5517c 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -99,13 +99,6 @@ final class PhabricatorAuthProviderConfig return new PhabricatorAuthProviderConfigTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKey.php b/src/applications/auth/storage/PhabricatorAuthSSHKey.php index 5bbb7de834..df4f09c11b 100644 --- a/src/applications/auth/storage/PhabricatorAuthSSHKey.php +++ b/src/applications/auth/storage/PhabricatorAuthSSHKey.php @@ -167,10 +167,4 @@ final class PhabricatorAuthSSHKey return new PhabricatorAuthSSHKeyTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - } diff --git a/src/applications/badges/storage/PhabricatorBadgesBadge.php b/src/applications/badges/storage/PhabricatorBadgesBadge.php index e2c63c1d0c..c6701a5b62 100644 --- a/src/applications/badges/storage/PhabricatorBadgesBadge.php +++ b/src/applications/badges/storage/PhabricatorBadgesBadge.php @@ -133,13 +133,6 @@ final class PhabricatorBadgesBadge extends PhabricatorBadgesDAO return new PhabricatorBadgesTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 1cabeb0709..e699a890d4 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -657,10 +657,4 @@ abstract class PhabricatorApplication return new PhabricatorApplicationApplicationTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 501d02efa9..51c9a8a973 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1319,12 +1319,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return new PhabricatorCalendarEventTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/calendar/storage/PhabricatorCalendarExport.php b/src/applications/calendar/storage/PhabricatorCalendarExport.php index 5a8ad84da4..93daaf6e5d 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarExport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarExport.php @@ -174,13 +174,6 @@ final class PhabricatorCalendarExport extends PhabricatorCalendarDAO return new PhabricatorCalendarExportTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index 1c7c1a5431..09e7ee138b 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -148,12 +148,6 @@ final class PhabricatorCalendarImport return new PhabricatorCalendarImportTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - public function newLogMessage($type, array $parameters) { $parameters = array( 'type' => $type, diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php index a8f00133a9..4e28180eed 100644 --- a/src/applications/config/storage/PhabricatorConfigEntry.php +++ b/src/applications/config/storage/PhabricatorConfigEntry.php @@ -69,13 +69,6 @@ final class PhabricatorConfigEntry return new PhabricatorConfigTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index bb355154a4..4a99fccdb9 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -319,11 +319,6 @@ final class ConpherenceThread extends ConpherenceDAO return new ConpherenceTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } /* -( PhabricatorNgramInterface )------------------------------------------ */ diff --git a/src/applications/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php index cfc6690085..0e275604da 100644 --- a/src/applications/countdown/storage/PhabricatorCountdown.php +++ b/src/applications/countdown/storage/PhabricatorCountdown.php @@ -103,12 +103,6 @@ final class PhabricatorCountdown extends PhabricatorCountdownDAO return new PhabricatorCountdownTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php index 2e673de19c..86181dbf9d 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -138,13 +138,6 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO return new PhabricatorDashboardTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php index 9f8875dc1b..ea71e04771 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php @@ -121,13 +121,6 @@ final class PhabricatorDashboardPanel return new PhabricatorDashboardPanelTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 5f229f39b3..0b882228c5 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -707,13 +707,6 @@ final class DifferentialDiff return new DifferentialDiffTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php index 07089264ae..d9dfc0107e 100644 --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -151,13 +151,6 @@ final class DivinerLiveBook extends DivinerDAO return new DivinerLiveBookTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorFulltextInterface )--------------------------------------- */ diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index 61e2dbfcfa..368dbe8f6a 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -303,13 +303,6 @@ final class DrydockBlueprint extends DrydockDAO return new DrydockBlueprintTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 5109202a33..21c9bd2fd3 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1552,13 +1552,6 @@ final class PhabricatorFile extends PhabricatorFileDAO return new PhabricatorFileTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php index d2a97cd32c..67ce3c2d8c 100644 --- a/src/applications/fund/storage/FundBacker.php +++ b/src/applications/fund/storage/FundBacker.php @@ -118,11 +118,4 @@ final class FundBacker extends FundDAO return new FundBackerTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index 2b9fbf1f71..077646fea0 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -168,13 +168,6 @@ final class FundInitiative extends FundDAO return new FundInitiativeTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index 98f374579b..0e2e93a0c9 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -291,13 +291,6 @@ final class HarbormasterBuildable return new HarbormasterBuildableTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 6acbac6468..04ba35ee1e 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -401,13 +401,6 @@ final class HarbormasterBuild extends HarbormasterDAO return new HarbormasterBuildTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index fc9ab5f573..a41dc89790 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -144,14 +144,6 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return new HarbormasterBuildPlanTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php index 54c069e97d..cee9a3f9c3 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php @@ -129,13 +129,6 @@ final class HarbormasterBuildStep extends HarbormasterDAO return new HarbormasterBuildStepTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index d2b8d36f05..6b078336aa 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -326,13 +326,6 @@ final class HeraldRule extends HeraldDAO return new HeraldRuleTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/herald/storage/HeraldWebhook.php b/src/applications/herald/storage/HeraldWebhook.php index 05ec69e194..aa992ec203 100644 --- a/src/applications/herald/storage/HeraldWebhook.php +++ b/src/applications/herald/storage/HeraldWebhook.php @@ -208,12 +208,6 @@ final class HeraldWebhook return new HeraldWebhookTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 004802de39..428a35b56c 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -217,13 +217,6 @@ final class LegalpadDocument extends LegalpadDAO return new LegalpadTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 0cd6726f5f..8c0631cf84 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -106,13 +106,6 @@ final class PhabricatorFileImageMacro extends PhabricatorFileDAO return new PhabricatorMacroTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index cdac563a14..7b9b7d2b91 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -460,13 +460,6 @@ final class ManiphestTask extends ManiphestDAO return new ManiphestTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorSpacesInterface )----------------------------------------- */ diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php index fb70b377a7..623fd88e91 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php @@ -131,12 +131,6 @@ final class PhabricatorMetaMTAApplicationEmail return new PhabricatorMetaMTAApplicationEmailTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 09a106ca7a..4f9ab750c1 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -201,10 +201,4 @@ final class NuanceItem return new NuanceItemTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - } diff --git a/src/applications/nuance/storage/NuanceQueue.php b/src/applications/nuance/storage/NuanceQueue.php index f0ba5bb45c..7cbb1e761c 100644 --- a/src/applications/nuance/storage/NuanceQueue.php +++ b/src/applications/nuance/storage/NuanceQueue.php @@ -87,10 +87,4 @@ final class NuanceQueue return new NuanceQueueTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - } diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php index c3f1d57c9b..0f0dd4d7de 100644 --- a/src/applications/nuance/storage/NuanceSource.php +++ b/src/applications/nuance/storage/NuanceSource.php @@ -107,13 +107,6 @@ final class NuanceSource extends NuanceDAO return new NuanceSourceTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php index 4154383c30..1ebecbc369 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php @@ -99,12 +99,6 @@ final class PhabricatorOAuthServerClient return new PhabricatorOAuthServerTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 7e2a348c89..17954ea80a 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -615,12 +615,6 @@ final class PhabricatorOwnersPackage return new PhabricatorOwnersPackageTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorCustomFieldInterface )------------------------------------ */ diff --git a/src/applications/packages/storage/PhabricatorPackagesPackage.php b/src/applications/packages/storage/PhabricatorPackagesPackage.php index 53e040e5f4..822988e8fb 100644 --- a/src/applications/packages/storage/PhabricatorPackagesPackage.php +++ b/src/applications/packages/storage/PhabricatorPackagesPackage.php @@ -197,12 +197,6 @@ final class PhabricatorPackagesPackage return new PhabricatorPackagesPackageTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorNgramsInterface )----------------------------------------- */ diff --git a/src/applications/packages/storage/PhabricatorPackagesPublisher.php b/src/applications/packages/storage/PhabricatorPackagesPublisher.php index 57737cdab9..b7610499d4 100644 --- a/src/applications/packages/storage/PhabricatorPackagesPublisher.php +++ b/src/applications/packages/storage/PhabricatorPackagesPublisher.php @@ -173,12 +173,6 @@ final class PhabricatorPackagesPublisher return new PhabricatorPackagesPublisherTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorNgramsInterface )----------------------------------------- */ diff --git a/src/applications/packages/storage/PhabricatorPackagesVersion.php b/src/applications/packages/storage/PhabricatorPackagesVersion.php index aba2473073..e1abf043a1 100644 --- a/src/applications/packages/storage/PhabricatorPackagesVersion.php +++ b/src/applications/packages/storage/PhabricatorPackagesVersion.php @@ -164,12 +164,6 @@ final class PhabricatorPackagesVersion return new PhabricatorPackagesVersionTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorNgramsInterface )----------------------------------------- */ diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index 737a20c1e9..6cb7cbf24e 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -125,13 +125,6 @@ final class PassphraseCredential extends PassphraseDAO return new PassphraseCredentialTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index 19aabcf8e0..ad3965ce46 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -227,13 +227,6 @@ final class PhabricatorPaste extends PhabricatorPasteDAO return new PhabricatorPasteTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorSpacesInterface )----------------------------------------- */ diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 5d310378e0..9b6f6cdbe4 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1379,12 +1379,6 @@ final class PhabricatorUser return new PhabricatorUserTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorFulltextInterface )--------------------------------------- */ diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index fa2ac00454..fde39da0fc 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -339,12 +339,6 @@ final class PhameBlog extends PhameDAO return new PhameBlogTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 8380a18f4d..c95d48054b 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -287,13 +287,6 @@ final class PhamePost extends PhameDAO return new PhamePostTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/phlux/storage/PhluxVariable.php b/src/applications/phlux/storage/PhluxVariable.php index 875b3dee92..d8c2d5315e 100644 --- a/src/applications/phlux/storage/PhluxVariable.php +++ b/src/applications/phlux/storage/PhluxVariable.php @@ -49,13 +49,6 @@ final class PhluxVariable extends PhluxDAO return new PhluxTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index ff3b0d8a84..8402ffa6d1 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -113,13 +113,6 @@ final class PhortuneAccount extends PhortuneDAO return new PhortuneAccountTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 07554ccb87..81f7099d0f 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -644,13 +644,6 @@ final class PhortuneCart extends PhortuneDAO return new PhortuneCartTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index 81d0391370..a4fff91c5e 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -86,13 +86,6 @@ final class PhortuneMerchant extends PhortuneDAO return new PhortuneMerchantTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php index b358b51d0b..65d5b3e65c 100644 --- a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php @@ -114,11 +114,4 @@ final class PhortunePaymentProviderConfig extends PhortuneDAO return new PhortunePaymentProviderConfigTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index b6dcd6d56d..24f216fe26 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -231,13 +231,6 @@ final class PhrictionDocument extends PhrictionDAO return new PhrictionTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/phurl/storage/PhabricatorPhurlURL.php b/src/applications/phurl/storage/PhabricatorPhurlURL.php index c36bf6e8cb..76a4fd045a 100644 --- a/src/applications/phurl/storage/PhabricatorPhurlURL.php +++ b/src/applications/phurl/storage/PhabricatorPhurlURL.php @@ -165,12 +165,6 @@ final class PhabricatorPhurlURL extends PhabricatorPhurlDAO return new PhabricatorPhurlURLTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index f9e3e8eb8d..0dcf44d617 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -125,13 +125,6 @@ final class PonderAnswer extends PonderDAO return new PonderAnswerTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - // Markup interface diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index 17f7ee3fdc..2ae6c76d68 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -153,13 +153,6 @@ final class PonderQuestion extends PonderDAO return new PonderQuestionTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - // Markup interface diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index f134b2c633..adbfb0dc36 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -703,13 +703,6 @@ final class PhabricatorProject extends PhabricatorProjectDAO return new PhabricatorProjectTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorSpacesInterface )----------------------------------------- */ diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 7ddfb4351d..b4c5df885e 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -242,13 +242,6 @@ final class PhabricatorProjectColumn return new PhabricatorProjectColumnTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/releeph/storage/ReleephBranch.php b/src/applications/releeph/storage/ReleephBranch.php index cef3f16ed0..5e23499a9d 100644 --- a/src/applications/releeph/storage/ReleephBranch.php +++ b/src/applications/releeph/storage/ReleephBranch.php @@ -166,13 +166,6 @@ final class ReleephBranch extends ReleephDAO return new ReleephBranchTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/releeph/storage/ReleephProject.php b/src/applications/releeph/storage/ReleephProject.php index bda0d0372d..318d687efd 100644 --- a/src/applications/releeph/storage/ReleephProject.php +++ b/src/applications/releeph/storage/ReleephProject.php @@ -127,13 +127,6 @@ final class ReleephProject extends ReleephDAO return new ReleephProductTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index a877192107..23956e4a3c 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -307,13 +307,6 @@ final class ReleephRequest extends ReleephDAO return new ReleephRequestTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 12c2b9e8a3..6967085644 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2621,13 +2621,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return new PhabricatorRepositoryTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index 416d1a3cd2..d361ccda40 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -142,11 +142,4 @@ final class PhabricatorRepositoryIdentity return new PhabricatorRepositoryIdentityTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - } diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php index c8d560705a..cc0f28b828 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryURI.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php @@ -613,12 +613,6 @@ final class PhabricatorRepositoryURI return new PhabricatorRepositoryURITransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index 37124393fb..ba6a5b2ef8 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -273,11 +273,4 @@ final class PhabricatorProfileMenuItemConfiguration return new PhabricatorProfileMenuItemConfigurationTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 63b2bd3a0a..9280608457 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -253,10 +253,4 @@ final class PhabricatorUserPreferences return new PhabricatorUserPreferencesTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index 3b642256d2..fcf0009448 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -139,13 +139,6 @@ final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO return new PhabricatorSlowvoteTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - - return $timeline; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php index 5f01376840..6b51d9c999 100644 --- a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php +++ b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php @@ -99,12 +99,6 @@ final class PhabricatorSpacesNamespace return new PhabricatorSpacesNamespaceTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index 3a1c8ec60b..707dbe9714 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -347,10 +347,4 @@ final class PhabricatorEditEngineConfiguration return new PhabricatorEditEngineConfigurationTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } - } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php index 312281617d..d6e8437091 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php @@ -241,11 +241,6 @@ final class PhabricatorWorkerBulkJob return new PhabricatorWorkerBulkJobTransaction(); } - public function willRenderTimeline( - PhabricatorApplicationTransactionView $timeline, - AphrontRequest $request) { - return $timeline; - } /* -( PhabricatorDestructibleInterface )----------------------------------- */ From 11cf8f05b176f4f911675d023285b0fb35e7f01f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Dec 2018 10:41:01 -0800 Subject: [PATCH 59/62] Remove "getApplicationTransactionObject()" from ApplicationTransactionInterface Summary: Depends on D19919. Ref T11351. This method appeared in D8802 (note that "get...Object" was renamed to "get...Transaction" there, so this method was actually "new" even though a method of the same name had existed before). The goal at the time was to let Harbormaster post build results to Diffs and have them end up on Revisions, but this eventually got a better implementation (see below) where the Harbormaster-specific code can just specify a "publishable object" where build results should go. The new `get...Object` semantics ultimately broke some stuff, and the actual implementation in Differential was removed in D10911, so this method hasn't really served a purpose since December 2014. I think that broke the Harbormaster thing by accident and we just lived with it for a bit, then Harbormaster got some more work and D17139 introduced "publishable" objects which was a better approach. This was later refined by D19281. So: the original problem (sending build results to the right place) has a good solution now, this method hasn't done anything for 4 years, and it was probably a bad idea in the first place since it's pretty weird/surprising/fragile. Note that `Comment` objects still have an unrelated method with the same name. In that case, the method ties the `Comment` storage object to the related `Transaction` storage object. Test Plan: Grepped for `getApplicationTransactionObject`, verified that all remaining callsites are related to `Comment` objects. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19920 --- .../almanac/storage/AlmanacBinding.php | 4 ---- .../almanac/storage/AlmanacDevice.php | 4 ---- .../almanac/storage/AlmanacInterface.php | 4 ---- .../almanac/storage/AlmanacNamespace.php | 4 ---- .../almanac/storage/AlmanacNetwork.php | 4 ---- .../almanac/storage/AlmanacService.php | 4 ---- .../auth/storage/PhabricatorAuthPassword.php | 4 ---- .../storage/PhabricatorAuthProviderConfig.php | 4 ---- .../auth/storage/PhabricatorAuthSSHKey.php | 4 ---- .../badges/storage/PhabricatorBadgesBadge.php | 4 ---- src/applications/base/PhabricatorApplication.php | 4 ---- .../calendar/storage/PhabricatorCalendarEvent.php | 4 ---- .../storage/PhabricatorCalendarExport.php | 4 ---- .../storage/PhabricatorCalendarImport.php | 4 ---- .../config/storage/PhabricatorConfigEntry.php | 4 ---- .../conpherence/storage/ConpherenceThread.php | 4 ---- .../countdown/storage/PhabricatorCountdown.php | 4 ---- .../dashboard/storage/PhabricatorDashboard.php | 4 ---- .../storage/PhabricatorDashboardPanel.php | 4 ---- .../differential/storage/DifferentialDiff.php | 4 ---- .../differential/storage/DifferentialRevision.php | 4 ---- .../diviner/storage/DivinerLiveBook.php | 4 ---- .../drydock/storage/DrydockBlueprint.php | 4 ---- .../files/storage/PhabricatorFile.php | 4 ---- src/applications/fund/storage/FundBacker.php | 4 ---- src/applications/fund/storage/FundInitiative.php | 4 ---- .../engine/HarbormasterBuildableEngine.php | 4 +--- .../storage/HarbormasterBuildable.php | 4 ---- .../storage/build/HarbormasterBuild.php | 4 ---- .../configuration/HarbormasterBuildPlan.php | 4 ---- .../configuration/HarbormasterBuildStep.php | 4 ---- src/applications/herald/storage/HeraldRule.php | 4 ---- src/applications/herald/storage/HeraldWebhook.php | 4 ---- .../legalpad/storage/LegalpadDocument.php | 4 ---- .../macro/storage/PhabricatorFileImageMacro.php | 4 ---- .../maniphest/storage/ManiphestTask.php | 4 ---- .../PhabricatorMetaMTAApplicationEmail.php | 4 ---- src/applications/nuance/storage/NuanceItem.php | 4 ---- src/applications/nuance/storage/NuanceQueue.php | 4 ---- src/applications/nuance/storage/NuanceSource.php | 4 ---- .../storage/PhabricatorOAuthServerClient.php | 4 ---- .../owners/storage/PhabricatorOwnersPackage.php | 4 ---- .../storage/PhabricatorPackagesPackage.php | 4 ---- .../storage/PhabricatorPackagesPublisher.php | 4 ---- .../storage/PhabricatorPackagesVersion.php | 4 ---- .../passphrase/storage/PassphraseCredential.php | 4 ---- .../paste/storage/PhabricatorPaste.php | 4 ---- .../people/storage/PhabricatorUser.php | 4 ---- src/applications/phame/storage/PhameBlog.php | 4 ---- src/applications/phame/storage/PhamePost.php | 4 ---- src/applications/phlux/storage/PhluxVariable.php | 4 ---- src/applications/pholio/storage/PholioMock.php | 4 ---- .../phortune/storage/PhortuneAccount.php | 4 ---- .../phortune/storage/PhortuneCart.php | 4 ---- .../phortune/storage/PhortuneMerchant.php | 4 ---- .../storage/PhortunePaymentProviderConfig.php | 4 ---- .../phriction/storage/PhrictionDocument.php | 4 ---- .../phurl/storage/PhabricatorPhurlURL.php | 4 ---- src/applications/ponder/storage/PonderAnswer.php | 4 ---- .../ponder/storage/PonderQuestion.php | 4 ---- .../project/storage/PhabricatorProject.php | 4 ---- .../project/storage/PhabricatorProjectColumn.php | 4 ---- .../releeph/storage/ReleephBranch.php | 4 ---- .../releeph/storage/ReleephProject.php | 4 ---- .../releeph/storage/ReleephRequest.php | 4 ---- .../repository/storage/PhabricatorRepository.php | 4 ---- .../storage/PhabricatorRepositoryCommit.php | 4 ---- .../storage/PhabricatorRepositoryIdentity.php | 4 ---- .../storage/PhabricatorRepositoryURI.php | 4 ---- .../PhabricatorProfileMenuItemConfiguration.php | 4 ---- .../storage/PhabricatorUserPreferences.php | 4 ---- .../slowvote/storage/PhabricatorSlowvotePoll.php | 4 ---- .../spaces/storage/PhabricatorSpacesNamespace.php | 4 ---- .../PhabricatorSubscriptionsEditController.php | 4 +--- .../PhabricatorSubscriptionsMuteController.php | 4 +--- .../tokens/editor/PhabricatorTokenGivenEditor.php | 4 +--- .../PhabricatorApplicationTransactionEditor.php | 3 +-- ...PhabricatorApplicationTransactionInterface.php | 15 --------------- ...bricatorApplicationTransactionReplyHandler.php | 4 +--- .../PhabricatorEditEngineConfiguration.php | 4 ---- .../workers/storage/PhabricatorWorkerBulkJob.php | 4 ---- 81 files changed, 6 insertions(+), 328 deletions(-) diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 5577ee5e7d..a7096fc51f 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -206,10 +206,6 @@ final class AlmanacBinding return new AlmanacBindingEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacBindingTransaction(); } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index f25b2eb5a1..1d1010733a 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -204,10 +204,6 @@ final class AlmanacDevice return new AlmanacDeviceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacDeviceTransaction(); } diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 9a5023e751..6cd318186f 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -168,10 +168,6 @@ final class AlmanacInterface return new AlmanacInterfaceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacInterfaceTransaction(); } diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php index 0c8f0c5ec7..128cfdb72e 100644 --- a/src/applications/almanac/storage/AlmanacNamespace.php +++ b/src/applications/almanac/storage/AlmanacNamespace.php @@ -191,10 +191,6 @@ final class AlmanacNamespace return new AlmanacNamespaceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacNamespaceTransaction(); } diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 3913d2f6e7..78313fad77 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -61,10 +61,6 @@ final class AlmanacNetwork return new AlmanacNetworkEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacNetworkTransaction(); } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 7348abd936..2979b74367 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -226,10 +226,6 @@ final class AlmanacService return new AlmanacServiceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new AlmanacServiceTransaction(); } diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php index 930cbe61cb..3196b58e60 100644 --- a/src/applications/auth/storage/PhabricatorAuthPassword.php +++ b/src/applications/auth/storage/PhabricatorAuthPassword.php @@ -217,10 +217,6 @@ final class PhabricatorAuthPassword return new PhabricatorAuthPasswordEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorAuthPasswordTransaction(); } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php index d4d1f5517c..1de34c4077 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -91,10 +91,6 @@ final class PhabricatorAuthProviderConfig return new PhabricatorAuthProviderConfigEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorAuthProviderConfigTransaction(); } diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKey.php b/src/applications/auth/storage/PhabricatorAuthSSHKey.php index df4f09c11b..7350af8cfd 100644 --- a/src/applications/auth/storage/PhabricatorAuthSSHKey.php +++ b/src/applications/auth/storage/PhabricatorAuthSSHKey.php @@ -159,10 +159,6 @@ final class PhabricatorAuthSSHKey return new PhabricatorAuthSSHKeyEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorAuthSSHKeyTransaction(); } diff --git a/src/applications/badges/storage/PhabricatorBadgesBadge.php b/src/applications/badges/storage/PhabricatorBadgesBadge.php index c6701a5b62..2fc787d9ee 100644 --- a/src/applications/badges/storage/PhabricatorBadgesBadge.php +++ b/src/applications/badges/storage/PhabricatorBadgesBadge.php @@ -125,10 +125,6 @@ final class PhabricatorBadgesBadge extends PhabricatorBadgesDAO return new PhabricatorBadgesEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorBadgesTransaction(); } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index e699a890d4..9526fb1442 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -649,10 +649,6 @@ abstract class PhabricatorApplication return new PhabricatorApplicationEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorApplicationApplicationTransaction(); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 51c9a8a973..a8092aaa88 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1311,10 +1311,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return new PhabricatorCalendarEventEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCalendarEventTransaction(); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarExport.php b/src/applications/calendar/storage/PhabricatorCalendarExport.php index 93daaf6e5d..4ad2b457f3 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarExport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarExport.php @@ -166,10 +166,6 @@ final class PhabricatorCalendarExport extends PhabricatorCalendarDAO return new PhabricatorCalendarExportEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCalendarExportTransaction(); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index 09e7ee138b..37ddac857c 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -140,10 +140,6 @@ final class PhabricatorCalendarImport return new PhabricatorCalendarImportEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCalendarImportTransaction(); } diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php index 4e28180eed..3462c4e7ba 100644 --- a/src/applications/config/storage/PhabricatorConfigEntry.php +++ b/src/applications/config/storage/PhabricatorConfigEntry.php @@ -61,10 +61,6 @@ final class PhabricatorConfigEntry return new PhabricatorConfigEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorConfigTransaction(); } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 4a99fccdb9..7a5f97ed41 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -311,10 +311,6 @@ final class ConpherenceThread extends ConpherenceDAO return new ConpherenceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ConpherenceTransaction(); } diff --git a/src/applications/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php index 0e275604da..1c61ae7ffc 100644 --- a/src/applications/countdown/storage/PhabricatorCountdown.php +++ b/src/applications/countdown/storage/PhabricatorCountdown.php @@ -95,10 +95,6 @@ final class PhabricatorCountdown extends PhabricatorCountdownDAO return new PhabricatorCountdownEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorCountdownTransaction(); } diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php index 86181dbf9d..53bc2f857d 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -130,10 +130,6 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO return new PhabricatorDashboardTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorDashboardTransaction(); } diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php index ea71e04771..89b577ab87 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php @@ -113,10 +113,6 @@ final class PhabricatorDashboardPanel return new PhabricatorDashboardPanelTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorDashboardPanelTransaction(); } diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 0b882228c5..e4c33dc766 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -699,10 +699,6 @@ final class DifferentialDiff return new DifferentialDiffEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DifferentialDiffTransaction(); } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index ef94da7f67..3397f9cb03 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -991,10 +991,6 @@ final class DifferentialRevision extends DifferentialDAO return new DifferentialTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php index d9dfc0107e..480bc50d05 100644 --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -143,10 +143,6 @@ final class DivinerLiveBook extends DivinerDAO return new DivinerLiveBookEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DivinerLiveBookTransaction(); } diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index 368dbe8f6a..ebe2f9f601 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -295,10 +295,6 @@ final class DrydockBlueprint extends DrydockDAO return new DrydockBlueprintEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new DrydockBlueprintTransaction(); } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 21c9bd2fd3..cc80d272b1 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1544,10 +1544,6 @@ final class PhabricatorFile extends PhabricatorFileDAO return new PhabricatorFileEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorFileTransaction(); } diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php index 67ce3c2d8c..87ab342e2a 100644 --- a/src/applications/fund/storage/FundBacker.php +++ b/src/applications/fund/storage/FundBacker.php @@ -110,10 +110,6 @@ final class FundBacker extends FundDAO return new FundBackerEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new FundBackerTransaction(); } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index 077646fea0..5e4dd48026 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -160,10 +160,6 @@ final class FundInitiative extends FundDAO return new FundInitiativeEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new FundInitiativeTransaction(); } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php index 25222975b7..7f743b3adf 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php @@ -96,9 +96,7 @@ abstract class HarbormasterBuildableEngine $publishable = $this->getPublishableObject(); $editor = $this->newEditor(); - $editor->applyTransactions( - $publishable->getApplicationTransactionObject(), - $xactions); + $editor->applyTransactions($publishable, $xactions); } public function getAuthorIdentity() { diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index 0e2e93a0c9..aabfd49eb9 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -283,10 +283,6 @@ final class HarbormasterBuildable return new HarbormasterBuildableTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildableTransaction(); } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 04ba35ee1e..602e388477 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -393,10 +393,6 @@ final class HarbormasterBuild extends HarbormasterDAO return new HarbormasterBuildTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index a41dc89790..2e379aab23 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -136,10 +136,6 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return new HarbormasterBuildPlanEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildPlanTransaction(); } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php index cee9a3f9c3..dd0ebdc507 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php @@ -121,10 +121,6 @@ final class HarbormasterBuildStep extends HarbormasterDAO return new HarbormasterBuildStepEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HarbormasterBuildStepTransaction(); } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 6b078336aa..1b005898cb 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -318,10 +318,6 @@ final class HeraldRule extends HeraldDAO return new HeraldRuleEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HeraldRuleTransaction(); } diff --git a/src/applications/herald/storage/HeraldWebhook.php b/src/applications/herald/storage/HeraldWebhook.php index aa992ec203..0101dbef52 100644 --- a/src/applications/herald/storage/HeraldWebhook.php +++ b/src/applications/herald/storage/HeraldWebhook.php @@ -200,10 +200,6 @@ final class HeraldWebhook return new HeraldWebhookEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new HeraldWebhookTransaction(); } diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 428a35b56c..efcd6b5f15 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -209,10 +209,6 @@ final class LegalpadDocument extends LegalpadDAO return new LegalpadDocumentEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new LegalpadTransaction(); } diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 8c0631cf84..656a4c9c57 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -98,10 +98,6 @@ final class PhabricatorFileImageMacro extends PhabricatorFileDAO return new PhabricatorMacroEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorMacroTransaction(); } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 7b9b7d2b91..1372e2cb88 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -452,10 +452,6 @@ final class ManiphestTask extends ManiphestDAO return new ManiphestTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ManiphestTransaction(); } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php index 623fd88e91..39eb4001cd 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php @@ -123,10 +123,6 @@ final class PhabricatorMetaMTAApplicationEmail return new PhabricatorMetaMTAApplicationEmailEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorMetaMTAApplicationEmailTransaction(); } diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 4f9ab750c1..f182e17580 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -193,10 +193,6 @@ final class NuanceItem return new NuanceItemEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new NuanceItemTransaction(); } diff --git a/src/applications/nuance/storage/NuanceQueue.php b/src/applications/nuance/storage/NuanceQueue.php index 7cbb1e761c..a19f1693b3 100644 --- a/src/applications/nuance/storage/NuanceQueue.php +++ b/src/applications/nuance/storage/NuanceQueue.php @@ -79,10 +79,6 @@ final class NuanceQueue return new NuanceQueueEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new NuanceQueueTransaction(); } diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php index 0f0dd4d7de..5e06a5dc0a 100644 --- a/src/applications/nuance/storage/NuanceSource.php +++ b/src/applications/nuance/storage/NuanceSource.php @@ -99,10 +99,6 @@ final class NuanceSource extends NuanceDAO return new NuanceSourceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new NuanceSourceTransaction(); } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php index 1ebecbc369..a951bf5781 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php @@ -91,10 +91,6 @@ final class PhabricatorOAuthServerClient return new PhabricatorOAuthServerEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorOAuthServerTransaction(); } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 17954ea80a..564fc8a28b 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -607,10 +607,6 @@ final class PhabricatorOwnersPackage return new PhabricatorOwnersPackageTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorOwnersPackageTransaction(); } diff --git a/src/applications/packages/storage/PhabricatorPackagesPackage.php b/src/applications/packages/storage/PhabricatorPackagesPackage.php index 822988e8fb..15748329e7 100644 --- a/src/applications/packages/storage/PhabricatorPackagesPackage.php +++ b/src/applications/packages/storage/PhabricatorPackagesPackage.php @@ -189,10 +189,6 @@ final class PhabricatorPackagesPackage return new PhabricatorPackagesPackageEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPackagesPackageTransaction(); } diff --git a/src/applications/packages/storage/PhabricatorPackagesPublisher.php b/src/applications/packages/storage/PhabricatorPackagesPublisher.php index b7610499d4..21b586dc1f 100644 --- a/src/applications/packages/storage/PhabricatorPackagesPublisher.php +++ b/src/applications/packages/storage/PhabricatorPackagesPublisher.php @@ -165,10 +165,6 @@ final class PhabricatorPackagesPublisher return new PhabricatorPackagesPublisherEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPackagesPublisherTransaction(); } diff --git a/src/applications/packages/storage/PhabricatorPackagesVersion.php b/src/applications/packages/storage/PhabricatorPackagesVersion.php index e1abf043a1..cd5e2648f8 100644 --- a/src/applications/packages/storage/PhabricatorPackagesVersion.php +++ b/src/applications/packages/storage/PhabricatorPackagesVersion.php @@ -156,10 +156,6 @@ final class PhabricatorPackagesVersion return new PhabricatorPackagesVersionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPackagesVersionTransaction(); } diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index 6cb7cbf24e..b10d392d36 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -117,10 +117,6 @@ final class PassphraseCredential extends PassphraseDAO return new PassphraseCredentialTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PassphraseCredentialTransaction(); } diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index ad3965ce46..79f1a953f6 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -219,10 +219,6 @@ final class PhabricatorPaste extends PhabricatorPasteDAO return new PhabricatorPasteEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPasteTransaction(); } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 9b6f6cdbe4..8b1c7cba5c 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1371,10 +1371,6 @@ final class PhabricatorUser return new PhabricatorUserTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorUserTransaction(); } diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index fde39da0fc..71a5186225 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -331,10 +331,6 @@ final class PhameBlog extends PhameDAO return new PhameBlogEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhameBlogTransaction(); } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index c95d48054b..300579b086 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -279,10 +279,6 @@ final class PhamePost extends PhameDAO return new PhamePostEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhamePostTransaction(); } diff --git a/src/applications/phlux/storage/PhluxVariable.php b/src/applications/phlux/storage/PhluxVariable.php index d8c2d5315e..fb0d5c2f80 100644 --- a/src/applications/phlux/storage/PhluxVariable.php +++ b/src/applications/phlux/storage/PhluxVariable.php @@ -41,10 +41,6 @@ final class PhluxVariable extends PhluxDAO return new PhluxVariableEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhluxTransaction(); } diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 51bdd1a196..a7e9a9b05d 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -221,10 +221,6 @@ final class PholioMock extends PholioDAO return new PholioMockEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PholioTransaction(); } diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 8402ffa6d1..b0b57645c3 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -105,10 +105,6 @@ final class PhortuneAccount extends PhortuneDAO return new PhortuneAccountEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhortuneAccountTransaction(); } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 81f7099d0f..2b121a3b0c 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -636,10 +636,6 @@ final class PhortuneCart extends PhortuneDAO return new PhortuneCartEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhortuneCartTransaction(); } diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index a4fff91c5e..4916cfede7 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -78,10 +78,6 @@ final class PhortuneMerchant extends PhortuneDAO return new PhortuneMerchantEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhortuneMerchantTransaction(); } diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php index 65d5b3e65c..1e151b3fbb 100644 --- a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php @@ -106,10 +106,6 @@ final class PhortunePaymentProviderConfig extends PhortuneDAO return new PhortunePaymentProviderConfigEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhortunePaymentProviderConfigTransaction(); } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index 24f216fe26..9f8a4a475b 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -223,10 +223,6 @@ final class PhrictionDocument extends PhrictionDAO return new PhrictionTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhrictionTransaction(); } diff --git a/src/applications/phurl/storage/PhabricatorPhurlURL.php b/src/applications/phurl/storage/PhabricatorPhurlURL.php index 76a4fd045a..4f3ee36dea 100644 --- a/src/applications/phurl/storage/PhabricatorPhurlURL.php +++ b/src/applications/phurl/storage/PhabricatorPhurlURL.php @@ -157,10 +157,6 @@ final class PhabricatorPhurlURL extends PhabricatorPhurlDAO return new PhabricatorPhurlURLEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorPhurlURLTransaction(); } diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 0dcf44d617..9179e22a44 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -117,10 +117,6 @@ final class PonderAnswer extends PonderDAO return new PonderAnswerEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PonderAnswerTransaction(); } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index 2ae6c76d68..40ec9fbee8 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -145,10 +145,6 @@ final class PonderQuestion extends PonderDAO return new PonderQuestionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PonderQuestionTransaction(); } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index adbfb0dc36..4dae13f3c6 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -695,10 +695,6 @@ final class PhabricatorProject extends PhabricatorProjectDAO return new PhabricatorProjectTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorProjectTransaction(); } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index b4c5df885e..756c356ee1 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -234,10 +234,6 @@ final class PhabricatorProjectColumn return new PhabricatorProjectColumnTransactionEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorProjectColumnTransaction(); } diff --git a/src/applications/releeph/storage/ReleephBranch.php b/src/applications/releeph/storage/ReleephBranch.php index 5e23499a9d..28323a9981 100644 --- a/src/applications/releeph/storage/ReleephBranch.php +++ b/src/applications/releeph/storage/ReleephBranch.php @@ -158,10 +158,6 @@ final class ReleephBranch extends ReleephDAO return new ReleephBranchEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ReleephBranchTransaction(); } diff --git a/src/applications/releeph/storage/ReleephProject.php b/src/applications/releeph/storage/ReleephProject.php index 318d687efd..db47ac5d39 100644 --- a/src/applications/releeph/storage/ReleephProject.php +++ b/src/applications/releeph/storage/ReleephProject.php @@ -119,10 +119,6 @@ final class ReleephProject extends ReleephDAO return new ReleephProductEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ReleephProductTransaction(); } diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index 23956e4a3c..c21f9b28c8 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -299,10 +299,6 @@ final class ReleephRequest extends ReleephDAO return new ReleephRequestTransactionalEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new ReleephRequestTransaction(); } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 6967085644..e1febe7ac8 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2613,10 +2613,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return new PhabricatorRepositoryEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryTransaction(); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index ddc4dbbf9f..fecb1762bd 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -731,10 +731,6 @@ final class PhabricatorRepositoryCommit return new PhabricatorAuditEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorAuditTransaction(); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index d361ccda40..76c6aed9e0 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -134,10 +134,6 @@ final class PhabricatorRepositoryIdentity return new DiffusionRepositoryIdentityEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryIdentityTransaction(); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php index cc0f28b828..8b1de62dd6 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryURI.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php @@ -605,10 +605,6 @@ final class PhabricatorRepositoryURI return new DiffusionURIEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryURITransaction(); } diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index ba6a5b2ef8..8520cac1bd 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -265,10 +265,6 @@ final class PhabricatorProfileMenuItemConfiguration return new PhabricatorProfileMenuEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorProfileMenuItemConfigurationTransaction(); } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 9280608457..0af6b4553c 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -245,10 +245,6 @@ final class PhabricatorUserPreferences return new PhabricatorUserPreferencesEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorUserPreferencesTransaction(); } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index fcf0009448..b8355c0586 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -131,10 +131,6 @@ final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO return new PhabricatorSlowvoteEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorSlowvoteTransaction(); } diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php index 6b51d9c999..e8b2afb435 100644 --- a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php +++ b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php @@ -91,10 +91,6 @@ final class PhabricatorSpacesNamespace return new PhabricatorSpacesNamespaceEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorSpacesNamespaceTransaction(); } diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php index 817e92ce39..13a0e14c23 100644 --- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php @@ -77,9 +77,7 @@ final class PhabricatorSubscriptionsEditController ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request); - $editor->applyTransactions( - $object->getApplicationTransactionObject(), - array($xaction)); + $editor->applyTransactions($object, array($xaction)); } else { // TODO: Eventually, get rid of this once everything implements diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php index e29d5e3802..929a4985dd 100644 --- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php @@ -55,9 +55,7 @@ final class PhabricatorSubscriptionsMuteController ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request); - $editor->applyTransactions( - $object->getApplicationTransactionObject(), - array($xaction)); + $editor->applyTransactions($object, array($xaction)); return id(new AphrontReloadResponse())->setURI($object_uri); } diff --git a/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php b/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php index ea4e8367c0..0d2f143635 100644 --- a/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php +++ b/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php @@ -166,9 +166,7 @@ final class PhabricatorTokenGivenEditor ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); - $editor->applyTransactions( - $object->getApplicationTransactionObject(), - $xactions); + $editor->applyTransactions($object, $xactions); } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 738fcf098b..697c4600a5 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3766,7 +3766,6 @@ abstract class PhabricatorApplicationTransactionEditor $editor = $node->getApplicationTransactionEditor(); $template = $node->getApplicationTransactionTemplate(); - $target = $node->getApplicationTransactionObject(); if (isset($add[$node->getPHID()])) { $edge_edit_type = '+'; @@ -3792,7 +3791,7 @@ abstract class PhabricatorApplicationTransactionEditor ->setActingAsPHID($this->getActingAsPHID()) ->setContentSource($this->getContentSource()); - $editor->applyTransactions($target, array($template)); + $editor->applyTransactions($node, array($template)); } } diff --git a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php index 1c6b4a0049..205253ba22 100644 --- a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php +++ b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php @@ -17,17 +17,6 @@ interface PhabricatorApplicationTransactionInterface { public function getApplicationTransactionEditor(); - /** - * Return the object to apply transactions to. Normally this is the current - * object (that is, `$this`), but in some cases transactions may apply to - * a different object: for example, @{class:DifferentialDiff} applies - * transactions to the associated @{class:DifferentialRevision}. - * - * @return PhabricatorLiskDAO Object to apply transactions to. - */ - public function getApplicationTransactionObject(); - - /** * Return a template transaction for this object. * @@ -47,10 +36,6 @@ interface PhabricatorApplicationTransactionInterface { return new <<>>Editor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new <<>>Transaction(); } diff --git a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php index 5457708953..9a369717f0 100644 --- a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php +++ b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php @@ -107,11 +107,9 @@ abstract class PhabricatorApplicationTransactionReplyHandler ->attachComment($comment); } - $target = $object->getApplicationTransactionObject(); - $this->newEditor($mail) ->setContinueOnNoEffect(true) - ->applyTransactions($target, $xactions); + ->applyTransactions($object, $xactions); } private function processMailCommands( diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index 707dbe9714..6c9f3a50a6 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -339,10 +339,6 @@ final class PhabricatorEditEngineConfiguration return new PhabricatorEditEngineConfigurationEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorEditEngineConfigurationTransaction(); } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php index d6e8437091..68263fccb7 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php @@ -233,10 +233,6 @@ final class PhabricatorWorkerBulkJob return new PhabricatorWorkerBulkJobEditor(); } - public function getApplicationTransactionObject() { - return $this; - } - public function getApplicationTransactionTemplate() { return new PhabricatorWorkerBulkJobTransaction(); } From 28989ac23145457fa49ab194244bdb86e886e9c9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Dec 2018 11:36:23 -0800 Subject: [PATCH 60/62] Make the Pholio Mock "getImages" / "getAllImages" API more clear Summary: Depends on D19920. Ref T11351. Currently, "images" and "all images" are attached to Mocks separately, and `getImages()` gets you only some images. Clean this up slightly: - One attach method; attach everything. - Two getters, one for "images" (returns all images); one for "active images" (returns active images). Test Plan: Browsed around Pholio without any apparent behavioral changes. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19921 --- .../PholioMockCommentController.php | 4 +-- .../controller/PholioMockEditController.php | 2 +- .../pholio/query/PholioMockQuery.php | 6 ++-- .../pholio/query/PholioMockSearchEngine.php | 2 +- .../pholio/storage/PholioMock.php | 32 ++++++++----------- .../pholio/view/PholioMockEmbedView.php | 4 +-- .../pholio/view/PholioMockImagesView.php | 6 ++-- .../pholio/view/PholioMockThumbGridView.php | 6 ++-- .../pholio/view/PholioTransactionView.php | 2 +- .../xaction/PholioImageFileTransaction.php | 4 +-- 10 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/applications/pholio/controller/PholioMockCommentController.php b/src/applications/pholio/controller/PholioMockCommentController.php index d54b4677e5..a4f9daf886 100644 --- a/src/applications/pholio/controller/PholioMockCommentController.php +++ b/src/applications/pholio/controller/PholioMockCommentController.php @@ -24,7 +24,7 @@ final class PholioMockCommentController extends PholioController { $draft = PhabricatorDraft::buildFromRequest($request); - $mock_uri = '/M'.$mock->getID(); + $mock_uri = $mock->getURI(); $comment = $request->getStr('comment'); @@ -33,7 +33,7 @@ final class PholioMockCommentController extends PholioController { $inline_comments = id(new PholioTransactionComment())->loadAllWhere( 'authorphid = %s AND transactionphid IS NULL AND imageid IN (%Ld)', $viewer->getPHID(), - mpull($mock->getImages(), 'getID')); + mpull($mock->getActiveImages(), 'getID')); if (!$inline_comments || strlen($comment)) { $xactions[] = id(new PholioTransaction()) diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index fd1df77a8e..6c0ea58d13 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -25,7 +25,7 @@ final class PholioMockEditController extends PholioController { $title = pht('Edit Mock: %s', $mock->getName()); $is_new = false; - $mock_images = $mock->getImages(); + $mock_images = $mock->getActiveImages(); $files = mpull($mock_images, 'getFile'); $mock_images = mpull($mock_images, null, 'getFilePHID'); } else { diff --git a/src/applications/pholio/query/PholioMockQuery.php b/src/applications/pholio/query/PholioMockQuery.php index 9e3154ec5c..465f23d34b 100644 --- a/src/applications/pholio/query/PholioMockQuery.php +++ b/src/applications/pholio/query/PholioMockQuery.php @@ -58,7 +58,7 @@ final class PholioMockQuery } protected function loadPage() { - $mocks = $this->loadStandardPage(new PholioMock()); + $mocks = $this->loadStandardPage($this->newResultObject()); if ($mocks && $this->needImages) { self::loadImages($this->getViewer(), $mocks, $this->needInlineComments); @@ -127,9 +127,7 @@ final class PholioMockQuery foreach ($mocks as $mock) { $mock_images = idx($image_groups, $mock->getPHID(), array()); - $mock->attachAllImages($mock_images); - $active_images = mfilter($mock_images, 'getIsObsolete', true); - $mock->attachImages(msort($active_images, 'getSequence')); + $mock->attachImages($mock_images); } } diff --git a/src/applications/pholio/query/PholioMockSearchEngine.php b/src/applications/pholio/query/PholioMockSearchEngine.php index 2433484d69..cbb175b2de 100644 --- a/src/applications/pholio/query/PholioMockSearchEngine.php +++ b/src/applications/pholio/query/PholioMockSearchEngine.php @@ -111,7 +111,7 @@ final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine { ->setImageURI($image_uri) ->setImageSize($x, $y) ->setDisabled($mock->isClosed()) - ->addIconCount('fa-picture-o', count($mock->getImages())) + ->addIconCount('fa-picture-o', count($mock->getActiveImages())) ->addIconCount('fa-trophy', $mock->getTokenCount()); if ($mock->getAuthorPHID()) { diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index a7e9a9b05d..0e61ffc2d1 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -30,7 +30,6 @@ final class PholioMock extends PholioDAO protected $spacePHID; private $images = self::ATTACHABLE; - private $allImages = self::ATTACHABLE; private $coverFile = self::ATTACHABLE; private $tokenCount = self::ATTACHABLE; @@ -93,33 +92,28 @@ final class PholioMock extends PholioDAO return parent::save(); } - /** - * These should be the images currently associated with the Mock. - */ public function attachImages(array $images) { assert_instances_of($images, 'PholioImage'); + $images = mpull($images, null, 'getPHID'); + $images = msort($images, 'getSequence'); $this->images = $images; return $this; } public function getImages() { - $this->assertAttached($this->images); - return $this->images; + return $this->assertAttached($this->images); } - /** - * These should be *all* images associated with the Mock. This includes - * images which have been removed and / or replaced from the Mock. - */ - public function attachAllImages(array $images) { - assert_instances_of($images, 'PholioImage'); - $this->allImages = $images; - return $this; - } + public function getActiveImages() { + $images = $this->getImages(); - public function getAllImages() { - $this->assertAttached($this->images); - return $this->allImages; + foreach ($images as $phid => $image) { + if ($image->getIsObsolete()) { + unset($images[$phid]); + } + } + + return $images; } public function attachCoverFile(PhabricatorFile $file) { @@ -143,7 +137,7 @@ final class PholioMock extends PholioDAO } public function getImageHistorySet($image_id) { - $images = $this->getAllImages(); + $images = $this->getImages(); $images = mpull($images, null, 'getID'); $selected_image = $images[$image_id]; diff --git a/src/applications/pholio/view/PholioMockEmbedView.php b/src/applications/pholio/view/PholioMockEmbedView.php index 88f2f2ac55..38b375b68b 100644 --- a/src/applications/pholio/view/PholioMockEmbedView.php +++ b/src/applications/pholio/view/PholioMockEmbedView.php @@ -25,7 +25,7 @@ final class PholioMockEmbedView extends AphrontView { $thumbnail = null; if (!empty($this->images)) { $images_to_show = array_intersect_key( - $this->mock->getImages(), array_flip($this->images)); + $this->mock->getActiveImages(), array_flip($this->images)); } $xform = PhabricatorFileTransform::getTransformByKey( @@ -54,7 +54,7 @@ final class PholioMockEmbedView extends AphrontView { ->setImageURI($thumbnail) ->setImageSize($x, $y) ->setDisabled($mock->isClosed()) - ->addIconCount('fa-picture-o', count($mock->getImages())) + ->addIconCount('fa-picture-o', count($mock->getActiveImages())) ->addIconCount('fa-trophy', $mock->getTokenCount()); return $item; diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index d5ac41cd03..70f5fe8bb3 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -74,7 +74,7 @@ final class PholioMockImagesView extends AphrontView { $images = array(); $current_set = 0; - foreach ($mock->getAllImages() as $image) { + foreach ($mock->getImages() as $image) { $file = $image->getFile(); $metadata = $file->getMetadata(); $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH); @@ -110,7 +110,7 @@ final class PholioMockImagesView extends AphrontView { ); } - $ids = mpull($mock->getImages(), 'getID'); + $ids = mpull($mock->getActiveImages(), 'getID'); if ($this->imageID && isset($ids[$this->imageID])) { $selected_id = $this->imageID; } else { @@ -118,7 +118,7 @@ final class PholioMockImagesView extends AphrontView { } $navsequence = array(); - foreach ($mock->getImages() as $image) { + foreach ($mock->getActiveImages() as $image) { $navsequence[] = $image->getID(); } diff --git a/src/applications/pholio/view/PholioMockThumbGridView.php b/src/applications/pholio/view/PholioMockThumbGridView.php index 457d700f1f..b859eaaa9a 100644 --- a/src/applications/pholio/view/PholioMockThumbGridView.php +++ b/src/applications/pholio/view/PholioMockThumbGridView.php @@ -12,7 +12,7 @@ final class PholioMockThumbGridView extends AphrontView { public function render() { $mock = $this->mock; - $all_images = $mock->getAllImages(); + $all_images = $mock->getImages(); $all_images = mpull($all_images, null, 'getPHID'); $history = mpull($all_images, 'getReplacesImagePHID', 'getPHID'); @@ -25,10 +25,10 @@ final class PholioMockThumbGridView extends AphrontView { } // Figure out the columns. Start with all the active images. - $images = mpull($mock->getImages(), null, 'getPHID'); + $images = mpull($mock->getActiveImages(), null, 'getPHID'); // Now, find deleted images: obsolete images which were not replaced. - foreach ($mock->getAllImages() as $image) { + foreach ($mock->getImages() as $image) { if (!$image->getIsObsolete()) { // Image is current. continue; diff --git a/src/applications/pholio/view/PholioTransactionView.php b/src/applications/pholio/view/PholioTransactionView.php index 69613c5428..1126db9c85 100644 --- a/src/applications/pholio/view/PholioTransactionView.php +++ b/src/applications/pholio/view/PholioTransactionView.php @@ -97,7 +97,7 @@ final class PholioTransactionView private function renderInlineContent(PholioTransaction $inline) { $comment = $inline->getComment(); $mock = $this->getMock(); - $images = $mock->getAllImages(); + $images = $mock->getImages(); $images = mpull($images, null, 'getID'); $image = idx($images, $comment->getImageID()); diff --git a/src/applications/pholio/xaction/PholioImageFileTransaction.php b/src/applications/pholio/xaction/PholioImageFileTransaction.php index 5f68dad9f1..6d257c313e 100644 --- a/src/applications/pholio/xaction/PholioImageFileTransaction.php +++ b/src/applications/pholio/xaction/PholioImageFileTransaction.php @@ -6,7 +6,7 @@ final class PholioImageFileTransaction const TRANSACTIONTYPE = 'image-file'; public function generateOldValue($object) { - $images = $object->getImages(); + $images = $object->getActiveImages(); return array_values(mpull($images, 'getPHID')); } @@ -24,7 +24,7 @@ final class PholioImageFileTransaction $new_map = array_fuse($this->getNewValue()); $obsolete_map = array_diff_key($old_map, $new_map); - $images = $object->getImages(); + $images = $object->getActiveImages(); foreach ($images as $seq => $image) { if (isset($obsolete_map[$image->getPHID()])) { $image->setIsObsolete(1); From 1e2bc7775bc45300bb383b2332bada6c9af21662 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Dec 2018 11:53:40 -0800 Subject: [PATCH 61/62] Remove the onboard "mailKey" from Pholio Mocks Summary: Depends on D19921. Ref T11351. Ref T13065. Update Pholio to use the shared mail infrastructure. See D19670 for a previous change in this vein. Test Plan: Ran upgrade, spot-checked that everything made it into the new table alive. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13065, T11351 Differential Revision: https://secure.phabricator.com/D19922 --- .../20181220.pholio.01.mailkey.php | 28 +++++++++++++++++++ .../20181220.pholio.02.dropmailkey.sql | 2 ++ .../pholio/storage/PholioMock.php | 18 ++---------- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 resources/sql/autopatches/20181220.pholio.01.mailkey.php create mode 100644 resources/sql/autopatches/20181220.pholio.02.dropmailkey.sql diff --git a/resources/sql/autopatches/20181220.pholio.01.mailkey.php b/resources/sql/autopatches/20181220.pholio.01.mailkey.php new file mode 100644 index 0000000000..37dcfd1434 --- /dev/null +++ b/resources/sql/autopatches/20181220.pholio.01.mailkey.php @@ -0,0 +1,28 @@ +establishConnection('w'); + +$properties_table = new PhabricatorMetaMTAMailProperties(); +$conn = $properties_table->establishConnection('w'); + +$iterator = new LiskRawMigrationIterator( + $mock_conn, + $mock_table->getTableName()); + +foreach ($iterator as $row) { + queryfx( + $conn, + 'INSERT IGNORE INTO %T + (objectPHID, mailProperties, dateCreated, dateModified) + VALUES + (%s, %s, %d, %d)', + $properties_table->getTableName(), + $row['phid'], + phutil_json_encode( + array( + 'mailKey' => $row['mailKey'], + )), + PhabricatorTime::getNow(), + PhabricatorTime::getNow()); +} diff --git a/resources/sql/autopatches/20181220.pholio.02.dropmailkey.sql b/resources/sql/autopatches/20181220.pholio.02.dropmailkey.sql new file mode 100644 index 0000000000..a71bc5dc69 --- /dev/null +++ b/resources/sql/autopatches/20181220.pholio.02.dropmailkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_pholio.pholio_mock + DROP mailKey; diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 0e61ffc2d1..25b216ba5d 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -25,7 +25,6 @@ final class PholioMock extends PholioDAO protected $name; protected $description; protected $coverPHID; - protected $mailKey; protected $status; protected $spacePHID; @@ -65,15 +64,9 @@ final class PholioMock extends PholioDAO self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'description' => 'text', - 'mailKey' => 'bytes20', 'status' => 'text12', ), self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => null, - 'phid' => array( - 'columns' => array('phid'), - 'unique' => true, - ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), @@ -81,15 +74,8 @@ final class PholioMock extends PholioDAO ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID('MOCK'); - } - - public function save() { - if (!$this->getMailKey()) { - $this->setMailKey(Filesystem::readRandomCharacters(20)); - } - return parent::save(); + public function getPHIDType() { + return PholioMockPHIDType::TYPECONST; } public function attachImages(array $images) { From b1e7e3a10e8c2a9e1e9df665addce909ed8847d1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 19 Dec 2018 11:47:45 -0800 Subject: [PATCH 62/62] Reduce the amount of weird "static" and "cache" behavior in Pholio query classes Summary: Depends on D19922. Ref T11351. These query classes have some slightly weird behavior, including `public static function loadImages(...)`. Convert all this stuff into more standard query patterns. Test Plan: Grepped for callsites, browsed around in Pholio. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T11351 Differential Revision: https://secure.phabricator.com/D19923 --- .../engine/PholioMockTimelineEngine.php | 11 +- .../pholio/query/PholioImageQuery.php | 66 ++++++----- .../pholio/query/PholioMockQuery.php | 104 ++++++++---------- .../pholio/view/PholioMockImagesView.php | 2 +- 4 files changed, 95 insertions(+), 88 deletions(-) diff --git a/src/applications/pholio/engine/PholioMockTimelineEngine.php b/src/applications/pholio/engine/PholioMockTimelineEngine.php index 80b64e73b7..8543649e68 100644 --- a/src/applications/pholio/engine/PholioMockTimelineEngine.php +++ b/src/applications/pholio/engine/PholioMockTimelineEngine.php @@ -7,10 +7,13 @@ final class PholioMockTimelineEngine $viewer = $this->getViewer(); $object = $this->getObject(); - PholioMockQuery::loadImages( - $viewer, - array($object), - $need_inline_comments = true); + $images = id(new PholioImageQuery()) + ->setViewer($viewer) + ->withMocks(array($object)) + ->needInlineComments(true) + ->execute(); + + $object->attachImages($images); return id(new PholioTransactionView()) ->setMock($object); diff --git a/src/applications/pholio/query/PholioImageQuery.php b/src/applications/pholio/query/PholioImageQuery.php index 0fa7bfb1a0..0d64540f91 100644 --- a/src/applications/pholio/query/PholioImageQuery.php +++ b/src/applications/pholio/query/PholioImageQuery.php @@ -6,9 +6,9 @@ final class PholioImageQuery private $ids; private $phids; private $mockPHIDs; + private $mocks; private $needInlineComments; - private $mockCache = array(); public function withIDs(array $ids) { $this->ids = $ids; @@ -20,6 +20,16 @@ final class PholioImageQuery return $this; } + public function withMocks(array $mocks) { + assert_instances_of($mocks, 'PholioMock'); + + $mocks = mpull($mocks, null, 'getPHID'); + $this->mocks = $mocks; + $this->mockPHIDs = array_keys($mocks); + + return $this; + } + public function withMockPHIDs(array $mock_phids) { $this->mockPHIDs = $mock_phids; return $this; @@ -30,14 +40,6 @@ final class PholioImageQuery return $this; } - public function setMockCache($mock_cache) { - $this->mockCache = $mock_cache; - return $this; - } - public function getMockCache() { - return $this->mockCache; - } - public function newResultObject() { return new PholioImage(); } @@ -76,26 +78,40 @@ final class PholioImageQuery protected function willFilterPage(array $images) { assert_instances_of($images, 'PholioImage'); - if ($this->getMockCache()) { - $mocks = $this->getMockCache(); - } else { - $mock_phids = mpull($images, 'getMockPHID'); + $mock_phids = array(); + foreach ($images as $image) { + if (!$image->hasMock()) { + continue; + } - // DO NOT set needImages to true; recursion results! - $mocks = id(new PholioMockQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($mock_phids) - ->execute(); - $mocks = mpull($mocks, null, 'getPHID'); + $mock_phids[] = $image->getMockPHID(); } - foreach ($images as $index => $image) { - $mock = idx($mocks, $image->getMockPHID()); - if ($mock) { - $image->attachMock($mock); + if ($mock_phids) { + if ($this->mocks) { + $mocks = $this->mocks; } else { - // mock is missing or we can't see it - unset($images[$index]); + $mocks = id(new PholioMockQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($mock_phids) + ->execute(); + } + + $mocks = mpull($mocks, null, 'getPHID'); + + foreach ($images as $key => $image) { + if (!$image->hasMock()) { + continue; + } + + $mock = idx($mocks, $image->getMockPHID()); + if (!$mock) { + unset($images[$key]); + $this->didRejectResult($image); + continue; + } + + $image->attachMock($mock); } } diff --git a/src/applications/pholio/query/PholioMockQuery.php b/src/applications/pholio/query/PholioMockQuery.php index 465f23d34b..4820ffd8eb 100644 --- a/src/applications/pholio/query/PholioMockQuery.php +++ b/src/applications/pholio/query/PholioMockQuery.php @@ -58,21 +58,14 @@ final class PholioMockQuery } protected function loadPage() { - $mocks = $this->loadStandardPage($this->newResultObject()); - - if ($mocks && $this->needImages) { - self::loadImages($this->getViewer(), $mocks, $this->needInlineComments); + if ($this->needInlineComments && !$this->needImages) { + throw new Exception( + pht( + 'You can not query for inline comments without also querying for '. + 'images.')); } - if ($mocks && $this->needCoverFiles) { - $this->loadCoverFiles($mocks); - } - - if ($mocks && $this->needTokenCounts) { - $this->loadTokenCounts($mocks); - } - - return $mocks; + return $this->loadStandardPage(new PholioMock()); } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { @@ -109,58 +102,53 @@ final class PholioMockQuery return $where; } - public static function loadImages( - PhabricatorUser $viewer, - array $mocks, - $need_inline_comments) { - assert_instances_of($mocks, 'PholioMock'); + protected function didFilterPage(array $mocks) { + $viewer = $this->getViewer(); - $mock_map = mpull($mocks, null, 'getPHID'); - $all_images = id(new PholioImageQuery()) - ->setViewer($viewer) - ->setMockCache($mock_map) - ->withMockPHIDs(array_keys($mock_map)) - ->needInlineComments($need_inline_comments) - ->execute(); + if ($this->needImages) { + $images = id(new PholioImageQuery()) + ->setViewer($viewer) + ->withMocks($mocks) + ->needInlineComments($this->needInlineComments) + ->execute(); - $image_groups = mgroup($all_images, 'getMockPHID'); - - foreach ($mocks as $mock) { - $mock_images = idx($image_groups, $mock->getPHID(), array()); - $mock->attachImages($mock_images); - } - } - - private function loadCoverFiles(array $mocks) { - assert_instances_of($mocks, 'PholioMock'); - $cover_file_phids = mpull($mocks, 'getCoverPHID'); - $cover_files = id(new PhabricatorFileQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($cover_file_phids) - ->execute(); - - $cover_files = mpull($cover_files, null, 'getPHID'); - - foreach ($mocks as $mock) { - $file = idx($cover_files, $mock->getCoverPHID()); - if (!$file) { - $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png'); + $image_groups = mgroup($images, 'getMockPHID'); + foreach ($mocks as $mock) { + $images = idx($image_groups, $mock->getPHID(), array()); + $mock->attachImages($images); } - $mock->attachCoverFile($file); } - } - private function loadTokenCounts(array $mocks) { - assert_instances_of($mocks, 'PholioMock'); + if ($this->needCoverFiles) { + $cover_files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(mpull($mocks, 'getCoverPHID')) + ->execute(); + $cover_files = mpull($cover_files, null, 'getPHID'); - $phids = mpull($mocks, 'getPHID'); - $counts = id(new PhabricatorTokenCountQuery()) - ->withObjectPHIDs($phids) - ->execute(); - - foreach ($mocks as $mock) { - $mock->attachTokenCount(idx($counts, $mock->getPHID(), 0)); + foreach ($mocks as $mock) { + $file = idx($cover_files, $mock->getCoverPHID()); + if (!$file) { + $file = PhabricatorFile::loadBuiltin( + $viewer, + 'missing.png'); + } + $mock->attachCoverFile($file); + } } + + if ($this->needTokenCounts) { + $counts = id(new PhabricatorTokenCountQuery()) + ->withObjectPHIDs(mpull($mocks, 'getPHID')) + ->execute(); + + foreach ($mocks as $mock) { + $token_count = idx($counts, $mock->getPHID(), 0); + $mock->attachTokenCount($token_count); + } + } + + return $mocks; } public function getQueryApplicationClass() { diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 70f5fe8bb3..99645c4a91 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -110,7 +110,7 @@ final class PholioMockImagesView extends AphrontView { ); } - $ids = mpull($mock->getActiveImages(), 'getID'); + $ids = mpull($mock->getActiveImages(), null, 'getID'); if ($this->imageID && isset($ids[$this->imageID])) { $selected_id = $this->imageID; } else {