@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow task statuses to "lock" them, preventing additional comments and interactions

Summary:
Ref T12335. See that task for discussion. Here are the behavioral changes:

- Statuses can be flagged with `locked`, which means that tasks in that status are locked to further discussion and interaction.
- A new "CAN_INTERACT" permission facilitates this. For most objects, "CAN_INTERACT" is just the same as "CAN_VIEW".
- For tasks, "CAN_INTERACT" is everyone if the status is a normal status, and no one if the status is a locked status.
- If a user doesn't have "Interact" permission:
- They can not submit the comment form.
- The comment form is replaced with text indicating "This thing is locked.".
- The "Edit" workflow prompts them.

This is a mixture of advisory and hard policy checks but sholuld represent a reasonable starting point.

Test Plan: Created a new "Locked" status, locked a task. Couldn't comment, saw lock warning, saw lock prompt on edit. Unlocked a task.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12335

Differential Revision: https://secure.phabricator.com/D17453

+272 -17
+8
src/__phutil_library_map__.php
··· 1487 1487 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', 1488 1488 'ManiphestTaskEditBulkJobType' => 'applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php', 1489 1489 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 1490 + 'ManiphestTaskEditEngineLock' => 'applications/maniphest/editor/ManiphestTaskEditEngineLock.php', 1490 1491 'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php', 1491 1492 'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php', 1492 1493 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', ··· 2617 2618 'PhabricatorEditEngineConfigurationViewController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php', 2618 2619 'PhabricatorEditEngineController' => 'applications/transactions/controller/PhabricatorEditEngineController.php', 2619 2620 'PhabricatorEditEngineDatasource' => 'applications/transactions/typeahead/PhabricatorEditEngineDatasource.php', 2621 + 'PhabricatorEditEngineDefaultLock' => 'applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php', 2620 2622 'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php', 2621 2623 'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php', 2622 2624 'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php', 2625 + 'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php', 2626 + 'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php', 2623 2627 'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php', 2624 2628 'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php', 2625 2629 'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php', ··· 6372 6376 'PhabricatorFulltextInterface', 6373 6377 'DoorkeeperBridgedObjectInterface', 6374 6378 'PhabricatorEditEngineSubtypeInterface', 6379 + 'PhabricatorEditEngineLockableInterface', 6375 6380 ), 6376 6381 'ManiphestTaskAssignHeraldAction' => 'HeraldAction', 6377 6382 'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction', ··· 6387 6392 'ManiphestTaskDetailController' => 'ManiphestController', 6388 6393 'ManiphestTaskEditBulkJobType' => 'PhabricatorWorkerBulkJobType', 6389 6394 'ManiphestTaskEditController' => 'ManiphestController', 6395 + 'ManiphestTaskEditEngineLock' => 'PhabricatorEditEngineLock', 6390 6396 'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine', 6391 6397 'ManiphestTaskGraph' => 'PhabricatorObjectGraph', 6392 6398 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', ··· 7679 7685 'PhabricatorEditEngineConfigurationViewController' => 'PhabricatorEditEngineController', 7680 7686 'PhabricatorEditEngineController' => 'PhabricatorApplicationTransactionController', 7681 7687 'PhabricatorEditEngineDatasource' => 'PhabricatorTypeaheadDatasource', 7688 + 'PhabricatorEditEngineDefaultLock' => 'PhabricatorEditEngineLock', 7682 7689 'PhabricatorEditEngineExtension' => 'Phobject', 7683 7690 'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule', 7684 7691 'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController', 7692 + 'PhabricatorEditEngineLock' => 'Phobject', 7685 7693 'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction', 7686 7694 'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem', 7687 7695 'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+2
src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
··· 210 210 - `claim` //Optional bool.// By default, closing an unassigned task claims 211 211 it. You can set this to `false` to disable this behavior for a particular 212 212 status. 213 + - `locked` //Optional bool.// Lock tasks in this status, preventing users 214 + from commenting. 213 215 214 216 Statuses will appear in the UI in the order specified. Note the status marked 215 217 `special` as `duplicate` is not settable directly and will not appear in UI
+5
src/applications/maniphest/constants/ManiphestTaskStatus.php
··· 156 156 return !self::isOpenStatus($status); 157 157 } 158 158 159 + public static function isLockedStatus($status) { 160 + return self::getStatusAttribute($status, 'locked', false); 161 + } 162 + 159 163 public static function getStatusActionName($status) { 160 164 return self::getStatusAttribute($status, 'name.action'); 161 165 } ··· 277 281 'keywords' => 'optional list<string>', 278 282 'disabled' => 'optional bool', 279 283 'claim' => 'optional bool', 284 + 'locked' => 'optional bool', 280 285 )); 281 286 } 282 287
+7 -1
src/applications/maniphest/controller/ManiphestTaskDetailController.php
··· 257 257 $task, 258 258 PhabricatorPolicyCapability::CAN_EDIT); 259 259 260 + $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task); 261 + 262 + // We expect a policy dialog if you can't edit the task, and expect a 263 + // lock override dialog if you can't interact with it. 264 + $workflow_edit = (!$can_edit || !$can_interact); 265 + 260 266 $curtain = $this->newCurtainView($task); 261 267 262 268 $curtain->addAction( ··· 265 271 ->setIcon('fa-pencil') 266 272 ->setHref($this->getApplicationURI("/task/edit/{$id}/")) 267 273 ->setDisabled(!$can_edit) 268 - ->setWorkflow(!$can_edit)); 274 + ->setWorkflow($workflow_edit)); 269 275 270 276 $edit_config = $edit_engine->loadDefaultEditConfiguration($task); 271 277 $can_create = (bool)$edit_config;
+28
src/applications/maniphest/editor/ManiphestTaskEditEngineLock.php
··· 1 + <?php 2 + 3 + final class ManiphestTaskEditEngineLock 4 + extends PhabricatorEditEngineLock { 5 + 6 + public function willPromptUserForLockOverrideWithDialog( 7 + AphrontDialogView $dialog) { 8 + 9 + return $dialog 10 + ->setTitle(pht('Edit Locked Task')) 11 + ->appendParagraph(pht('This task is locked. Edit it anyway?')) 12 + ->addSubmitButton(pht('Override Task Lock')); 13 + } 14 + 15 + public function willBlockUserInteractionWithDialog( 16 + AphrontDialogView $dialog) { 17 + 18 + return $dialog 19 + ->setTitle(pht('Task Locked')) 20 + ->appendParagraph( 21 + pht('You can not interact with this task because it is locked.')); 22 + } 23 + 24 + public function getLockedObjectDisplayText() { 25 + return pht('This task has been locked.'); 26 + } 27 + 28 + }
+21 -1
src/applications/maniphest/storage/ManiphestTask.php
··· 17 17 PhabricatorConduitResultInterface, 18 18 PhabricatorFulltextInterface, 19 19 DoorkeeperBridgedObjectInterface, 20 - PhabricatorEditEngineSubtypeInterface { 20 + PhabricatorEditEngineSubtypeInterface, 21 + PhabricatorEditEngineLockableInterface { 21 22 22 23 const MARKUP_FIELD_DESCRIPTION = 'markup:desc'; 23 24 ··· 213 214 return ManiphestTaskStatus::isClosedStatus($this->getStatus()); 214 215 } 215 216 217 + public function isLocked() { 218 + return ManiphestTaskStatus::isLockedStatus($this->getStatus()); 219 + } 220 + 216 221 public function setProperty($key, $value) { 217 222 $this->properties[$key] = $value; 218 223 return $this; ··· 343 348 public function getCapabilities() { 344 349 return array( 345 350 PhabricatorPolicyCapability::CAN_VIEW, 351 + PhabricatorPolicyCapability::CAN_INTERACT, 346 352 PhabricatorPolicyCapability::CAN_EDIT, 347 353 ); 348 354 } ··· 351 357 switch ($capability) { 352 358 case PhabricatorPolicyCapability::CAN_VIEW: 353 359 return $this->getViewPolicy(); 360 + case PhabricatorPolicyCapability::CAN_INTERACT: 361 + if ($this->isLocked()) { 362 + return PhabricatorPolicies::POLICY_NOONE; 363 + } else { 364 + return PhabricatorPolicies::POLICY_USER; 365 + } 354 366 case PhabricatorPolicyCapability::CAN_EDIT: 355 367 return $this->getEditPolicy(); 356 368 } ··· 560 572 public function newEditEngineSubtypeMap() { 561 573 $config = PhabricatorEnv::getEnvConfig('maniphest.subtypes'); 562 574 return PhabricatorEditEngineSubtype::newSubtypeMap($config); 575 + } 576 + 577 + 578 + /* -( PhabricatorEditEngineLockableInterface )----------------------------- */ 579 + 580 + 581 + public function newEditEngineLock() { 582 + return new ManiphestTaskEditEngineLock(); 563 583 } 564 584 565 585 }
+1
src/applications/policy/capability/PhabricatorPolicyCapability.php
··· 5 5 const CAN_VIEW = 'view'; 6 6 const CAN_EDIT = 'edit'; 7 7 const CAN_JOIN = 'join'; 8 + const CAN_INTERACT = 'interact'; 8 9 9 10 /** 10 11 * Get the unique key identifying this capability. This key must be globally
+30
src/applications/policy/filter/PhabricatorPolicyFilter.php
··· 86 86 return (count($result) == 1); 87 87 } 88 88 89 + public static function canInteract( 90 + PhabricatorUser $user, 91 + PhabricatorPolicyInterface $object) { 92 + 93 + $capabilities = $object->getCapabilities(); 94 + $capabilities = array_fuse($capabilities); 95 + 96 + $can_interact = PhabricatorPolicyCapability::CAN_INTERACT; 97 + $can_view = PhabricatorPolicyCapability::CAN_VIEW; 98 + 99 + $require = array(); 100 + 101 + // If the object doesn't support a separate "Interact" capability, we 102 + // only use the "View" capability: for most objects, you can interact 103 + // with them if you can see them. 104 + $require[] = $can_view; 105 + 106 + if (isset($capabilities[$can_interact])) { 107 + $require[] = $can_interact; 108 + } 109 + 110 + foreach ($require as $capability) { 111 + if (!self::hasCapability($user, $object, $capability)) { 112 + return false; 113 + } 114 + } 115 + 116 + return true; 117 + } 118 + 89 119 public function setViewer(PhabricatorUser $user) { 90 120 $this->viewer = $user; 91 121 return $this;
+9
src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
··· 47 47 $handle->getURI()); 48 48 } 49 49 50 + if (!PhabricatorPolicyFilter::canInteract($viewer, $object)) { 51 + $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); 52 + 53 + $dialog = $this->newDialog() 54 + ->addCancelButton($handle->getURI()); 55 + 56 + return $lock->willBlockUserInteractionWithDialog($dialog); 57 + } 58 + 50 59 if ($object instanceof PhabricatorApplicationTransactionInterface) { 51 60 if ($is_add) { 52 61 $xaction_value = array(
+6 -2
src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
··· 56 56 $subscribed = isset($edges[$src_phid][$edge_type][$user_phid]); 57 57 } 58 58 59 + $can_interact = PhabricatorPolicyFilter::canInteract($user, $object); 60 + 59 61 if ($subscribed) { 60 62 $sub_action = id(new PhabricatorActionView()) 61 63 ->setWorkflow(true) 62 64 ->setRenderAsForm(true) 63 65 ->setHref('/subscriptions/delete/'.$object->getPHID().'/') 64 66 ->setName(pht('Unsubscribe')) 65 - ->setIcon('fa-minus-circle'); 67 + ->setIcon('fa-minus-circle') 68 + ->setDisabled(!$can_interact); 66 69 } else { 67 70 $sub_action = id(new PhabricatorActionView()) 68 71 ->setWorkflow(true) 69 72 ->setRenderAsForm(true) 70 73 ->setHref('/subscriptions/add/'.$object->getPHID().'/') 71 74 ->setName(pht('Subscribe')) 72 - ->setIcon('fa-plus-circle'); 75 + ->setIcon('fa-plus-circle') 76 + ->setDisabled(!$can_interact); 73 77 } 74 78 75 79 if (!$user->isLoggedIn()) {
+57 -12
src/applications/transactions/editengine/PhabricatorEditEngine.php
··· 985 985 $fields = $this->buildEditFields($object); 986 986 $template = $object->getApplicationTransactionTemplate(); 987 987 988 + if ($this->getIsCreate()) { 989 + $cancel_uri = $this->getObjectCreateCancelURI($object); 990 + $submit_button = $this->getObjectCreateButtonText($object); 991 + } else { 992 + $cancel_uri = $this->getEffectiveObjectEditCancelURI($object); 993 + $submit_button = $this->getObjectEditButtonText($object); 994 + } 995 + 988 996 $config = $this->getEditEngineConfiguration() 989 997 ->attachEngine($this); 998 + 999 + $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object); 1000 + if (!$can_interact && 1001 + !$request->getBool('editEngine') && 1002 + !$request->getBool('overrideLock')) { 1003 + 1004 + $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); 1005 + 1006 + $dialog = $this->getController() 1007 + ->newDialog() 1008 + ->addHiddenInput('overrideLock', true) 1009 + ->setDisableWorkflowOnSubmit(true) 1010 + ->addCancelButton($cancel_uri); 1011 + 1012 + return $lock->willPromptUserForLockOverrideWithDialog($dialog); 1013 + } 990 1014 991 1015 $validation_exception = null; 992 1016 if ($request->isFormPost() && $request->getBool('editEngine')) { ··· 1154 1178 $form = $this->buildEditForm($object, $fields); 1155 1179 1156 1180 if ($request->isAjax()) { 1157 - if ($this->getIsCreate()) { 1158 - $cancel_uri = $this->getObjectCreateCancelURI($object); 1159 - $submit_button = $this->getObjectCreateButtonText($object); 1160 - } else { 1161 - $cancel_uri = $this->getEffectiveObjectEditCancelURI($object); 1162 - $submit_button = $this->getObjectEditButtonText($object); 1163 - } 1164 - 1165 1181 return $this->getController() 1166 1182 ->newDialog() 1167 1183 ->setWidth(AphrontDialogView::WIDTH_FULL) ··· 1554 1570 } 1555 1571 1556 1572 $viewer = $this->getViewer(); 1573 + 1574 + $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object); 1575 + if (!$can_interact) { 1576 + $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); 1577 + 1578 + return id(new PhabricatorApplicationTransactionCommentView()) 1579 + ->setEditEngineLock($lock); 1580 + } 1581 + 1557 1582 $object_phid = $object->getPHID(); 1558 - 1559 1583 $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); 1560 1584 1561 1585 if ($is_serious) { ··· 1700 1724 private function buildError($object, $title, $body) { 1701 1725 $cancel_uri = $this->getObjectCreateCancelURI($object); 1702 1726 1703 - return $this->getController() 1727 + $dialog = $this->getController() 1704 1728 ->newDialog() 1705 - ->setTitle($title) 1706 - ->appendParagraph($body) 1707 1729 ->addCancelButton($cancel_uri); 1730 + 1731 + if ($title !== null) { 1732 + $dialog->setTitle($title); 1733 + } 1734 + 1735 + if ($body !== null) { 1736 + $dialog->appendParagraph($body); 1737 + } 1738 + 1739 + return $dialog; 1708 1740 } 1709 1741 1710 1742 ··· 1761 1793 $config->getName())); 1762 1794 } 1763 1795 1796 + private function buildLockedObjectResponse($object) { 1797 + $dialog = $this->buildError($object, null, null); 1798 + $viewer = $this->getViewer(); 1799 + 1800 + $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); 1801 + return $lock->willBlockUserInteractionWithDialog($dialog); 1802 + } 1803 + 1764 1804 private function buildCommentResponse($object) { 1765 1805 $viewer = $this->getViewer(); 1766 1806 ··· 1773 1813 1774 1814 if (!$request->isFormPost()) { 1775 1815 return new Aphront400Response(); 1816 + } 1817 + 1818 + $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object); 1819 + if (!$can_interact) { 1820 + return $this->buildLockedObjectResponse($object); 1776 1821 } 1777 1822 1778 1823 $config = $this->loadDefaultEditConfiguration($object);
+4
src/applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php
··· 1 + <?php 2 + 3 + final class PhabricatorEditEngineDefaultLock 4 + extends PhabricatorEditEngineLock {}
+66
src/applications/transactions/editengine/PhabricatorEditEngineLock.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorEditEngineLock 4 + extends Phobject { 5 + 6 + private $viewer; 7 + private $object; 8 + 9 + final public function setViewer(PhabricatorUser $viewer) { 10 + $this->viewer = $viewer; 11 + return $this; 12 + } 13 + 14 + final public function getViewer() { 15 + return $this->viewer; 16 + } 17 + 18 + final public function setObject($object) { 19 + $this->object = $object; 20 + return $this; 21 + } 22 + 23 + final public function getObject() { 24 + return $this->object; 25 + } 26 + 27 + public function willPromptUserForLockOverrideWithDialog( 28 + AphrontDialogView $dialog) { 29 + 30 + return $dialog 31 + ->setTitle(pht('Edit Locked Object')) 32 + ->appendParagraph(pht('This object is locked. Edit it anyway?')) 33 + ->addSubmitButton(pht('Override Lock')); 34 + } 35 + 36 + public function willBlockUserInteractionWithDialog( 37 + AphrontDialogView $dialog) { 38 + 39 + return $dialog 40 + ->setTitle(pht('Object Locked')) 41 + ->appendParagraph( 42 + pht('You can not interact with this object because it is locked.')); 43 + } 44 + 45 + public function getLockedObjectDisplayText() { 46 + return pht('This object has been locked.'); 47 + } 48 + 49 + public static function newForObject( 50 + PhabricatorUser $viewer, 51 + $object) { 52 + 53 + if ($object instanceof PhabricatorEditEngineLockableInterface) { 54 + $lock = $object->newEditEngineLock(); 55 + } else { 56 + $lock = new PhabricatorEditEngineDefaultLock(); 57 + } 58 + 59 + return $lock 60 + ->setViewer($viewer) 61 + ->setObject($object); 62 + } 63 + 64 + 65 + 66 + }
+7
src/applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php
··· 1 + <?php 2 + 3 + interface PhabricatorEditEngineLockableInterface { 4 + 5 + public function newEditEngineLock(); 6 + 7 + }
+21 -1
src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
··· 22 22 private $noPermission; 23 23 private $fullWidth; 24 24 private $infoView; 25 + private $editEngineLock; 25 26 26 27 private $currentVersion; 27 28 private $versionedDraft; ··· 149 150 return $this->noPermission; 150 151 } 151 152 153 + public function setEditEngineLock(PhabricatorEditEngineLock $lock) { 154 + $this->editEngineLock = $lock; 155 + return $this; 156 + } 157 + 158 + public function getEditEngineLock() { 159 + return $this->editEngineLock; 160 + } 161 + 152 162 public function setTransactionTimeline( 153 163 PhabricatorApplicationTransactionView $timeline) { 154 164 155 165 $timeline->setQuoteTargetID($this->getCommentID()); 156 - if ($this->getNoPermission()) { 166 + if ($this->getNoPermission() || $this->getEditEngineLock()) { 157 167 $timeline->setShouldTerminate(true); 158 168 } 159 169 ··· 164 174 public function render() { 165 175 if ($this->getNoPermission()) { 166 176 return null; 177 + } 178 + 179 + $lock = $this->getEditEngineLock(); 180 + if ($lock) { 181 + return id(new PHUIInfoView()) 182 + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 183 + ->setErrors( 184 + array( 185 + $lock->getLockedObjectDisplayText(), 186 + )); 167 187 } 168 188 169 189 $user = $this->getUser();