@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
1
fork

Configure Feed

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

Add a "Subscribers" object policy

Summary:
Ref T5681. Getting this to work correctly is a bit tricky, mostly because of the policy checks we do prior to applying an edit.

I think I came up with a mostly-reasonable approach, although it's a little bit gross. It uses `spl_object_hash()` so it shouldn't be able to do anything bad/dangerous (the hints are strictly bound to the hinted object, which is a clone that we destroy moments later).

Test Plan:
- Added + ran a unit test.
- Created a task with a "Subscribers" policy with me as a subscriber (without the hint stuff, this isn't possible: since you aren't a subscriber *yet*, you get a "you won't be able to see it" error).
- Unsubscribed from a task with a "Subscribers" policy, was immediately unable to see it.
- Created a task with a "subscribers" policy and a project subscriber with/without me as a member (error / success, respectively).

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5681

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

+213
+2
src/__phutil_library_map__.php
··· 2645 2645 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', 2646 2646 'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php', 2647 2647 'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php', 2648 + 'PhabricatorSubscriptionsSubscribersPolicyRule' => 'applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php', 2648 2649 'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php', 2649 2650 'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php', 2650 2651 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php', ··· 6166 6167 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', 6167 6168 'PhabricatorSubscriptionsListController' => 'PhabricatorController', 6168 6169 'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand', 6170 + 'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule', 6169 6171 'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController', 6170 6172 'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener', 6171 6173 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
+27
src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php
··· 208 208 PhabricatorPolicyCapability::CAN_VIEW)); 209 209 } 210 210 211 + public function testObjectPolicyRuleSubscribers() { 212 + $author = $this->generateNewTestUser(); 213 + 214 + $rule = new PhabricatorSubscriptionsSubscribersPolicyRule(); 215 + 216 + $task = ManiphestTask::initializeNewTask($author); 217 + $task->setViewPolicy($rule->getObjectPolicyFullKey()); 218 + $task->save(); 219 + 220 + $this->assertFalse( 221 + PhabricatorPolicyFilter::hasCapability( 222 + $author, 223 + $task, 224 + PhabricatorPolicyCapability::CAN_VIEW)); 225 + 226 + id(new PhabricatorSubscriptionsEditor()) 227 + ->setActor($author) 228 + ->setObject($task) 229 + ->subscribeExplicit(array($author->getPHID())) 230 + ->save(); 231 + 232 + $this->assertTrue( 233 + PhabricatorPolicyFilter::hasCapability( 234 + $author, 235 + $task, 236 + PhabricatorPolicyCapability::CAN_VIEW)); 237 + } 211 238 212 239 }
+50
src/applications/policy/rule/PhabricatorPolicyRule.php
··· 97 97 } 98 98 99 99 100 + /* -( Transaction Hints )-------------------------------------------------- */ 101 + 102 + 103 + /** 104 + * Tell policy rules about upcoming transaction effects. 105 + * 106 + * Before transaction effects are applied, we try to stop users from making 107 + * edits which will lock them out of objects. We can't do this perfectly, 108 + * since they can set a policy to "the moon is full" moments before it wanes, 109 + * but we try to prevent as many mistakes as possible. 110 + * 111 + * Some policy rules depend on complex checks against object state which 112 + * we can't set up ahead of time. For example, subscriptions require database 113 + * writes. 114 + * 115 + * In cases like this, instead of doing writes, you can pass a hint about an 116 + * object to a policy rule. The rule can then look for hints and use them in 117 + * rendering a verdict about whether the user will be able to see the object 118 + * or not after applying the policy change. 119 + * 120 + * @param PhabricatorPolicyInterface Object to pass a hint about. 121 + * @param PhabricatorPolicyRule Rule to pass hint to. 122 + * @param wild Hint. 123 + * @return void 124 + */ 125 + public static function passTransactionHintToRule( 126 + PhabricatorPolicyInterface $object, 127 + PhabricatorPolicyRule $rule, 128 + $hint) { 129 + 130 + $cache = PhabricatorCaches::getRequestCache(); 131 + $cache->setKey(self::getObjectPolicyCacheKey($object, $rule), $hint); 132 + } 133 + 134 + protected function getTransactionHint( 135 + PhabricatorPolicyInterface $object) { 136 + 137 + $cache = PhabricatorCaches::getRequestCache(); 138 + return $cache->getKey(self::getObjectPolicyCacheKey($object, $this)); 139 + } 140 + 141 + private static function getObjectPolicyCacheKey( 142 + PhabricatorPolicyInterface $object, 143 + PhabricatorPolicyRule $rule) { 144 + $hash = spl_object_hash($object); 145 + $rule = get_class($rule); 146 + return 'policycache.'.$hash.'.'.$rule; 147 + } 148 + 149 + 100 150 /* -( Implementing Object Policies )--------------------------------------- */ 101 151 102 152
+121
src/applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php
··· 1 + <?php 2 + 3 + final class PhabricatorSubscriptionsSubscribersPolicyRule 4 + extends PhabricatorPolicyRule { 5 + 6 + private $subscribed = array(); 7 + private $sourcePHIDs = array(); 8 + 9 + public function getObjectPolicyKey() { 10 + return 'subscriptions.subscribers'; 11 + } 12 + 13 + public function getObjectPolicyName() { 14 + return pht('Subscribers'); 15 + } 16 + 17 + public function getPolicyExplanation() { 18 + return pht('Subscribers can take this action.'); 19 + } 20 + 21 + public function getRuleDescription() { 22 + return pht('subscribers'); 23 + } 24 + 25 + public function canApplyToObject(PhabricatorPolicyInterface $object) { 26 + return ($object instanceof PhabricatorSubscribableInterface); 27 + } 28 + 29 + public function willApplyRules( 30 + PhabricatorUser $viewer, 31 + array $values, 32 + array $objects) { 33 + 34 + // We want to let the user see the object if they're a subscriber or 35 + // a member of any project which is a subscriber. Additionally, because 36 + // subscriber state is complex, we need to read hints passed from 37 + // the TransactionEditor to predict policy state after transactions apply. 38 + 39 + $viewer_phid = $viewer->getPHID(); 40 + if (!$viewer_phid) { 41 + return; 42 + } 43 + 44 + if (empty($this->subscribed[$viewer_phid])) { 45 + $this->subscribed[$viewer_phid] = array(); 46 + } 47 + 48 + // Load the project PHIDs the user is a member of. 49 + if (!isset($this->sourcePHIDs[$viewer_phid])) { 50 + $source_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 51 + $viewer_phid, 52 + PhabricatorProjectMemberOfProjectEdgeType::EDGECONST); 53 + $source_phids[] = $viewer_phid; 54 + $this->sourcePHIDs[$viewer_phid] = $source_phids; 55 + } 56 + 57 + // Look for transaction hints. 58 + foreach ($objects as $key => $object) { 59 + $cache = $this->getTransactionHint($object); 60 + if ($cache === null) { 61 + // We don't have a hint for this object, so we'll deal with it below. 62 + continue; 63 + } 64 + 65 + // We have a hint, so use that as the source of truth. 66 + unset($objects[$key]); 67 + 68 + foreach ($this->sourcePHIDs[$viewer_phid] as $source_phid) { 69 + if (isset($cache[$source_phid])) { 70 + $this->subscribed[$viewer_phid][$object->getPHID()] = true; 71 + break; 72 + } 73 + } 74 + } 75 + 76 + $phids = mpull($objects, 'getPHID'); 77 + if (!$phids) { 78 + return; 79 + } 80 + 81 + $edge_query = id(new PhabricatorEdgeQuery()) 82 + ->withSourcePHIDs($this->sourcePHIDs[$viewer_phid]) 83 + ->withEdgeTypes( 84 + array( 85 + PhabricatorSubscribedToObjectEdgeType::EDGECONST, 86 + )) 87 + ->withDestinationPHIDs($phids); 88 + 89 + $edge_query->execute(); 90 + 91 + $subscribed = $edge_query->getDestinationPHIDs(); 92 + if (!$subscribed) { 93 + return; 94 + } 95 + 96 + $this->subscribed[$viewer_phid] += array_fill_keys($subscribed, true); 97 + } 98 + 99 + public function applyRule( 100 + PhabricatorUser $viewer, 101 + $value, 102 + PhabricatorPolicyInterface $object) { 103 + 104 + $viewer_phid = $viewer->getPHID(); 105 + if (!$viewer_phid) { 106 + return false; 107 + } 108 + 109 + if ($object->isAutomaticallySubscribed($viewer_phid)) { 110 + return true; 111 + } 112 + 113 + $subscribed = idx($this->subscribed, $viewer_phid); 114 + return isset($subscribed[$object->getPHID()]); 115 + } 116 + 117 + public function getValueControlType() { 118 + return self::CONTROL_TYPE_NONE; 119 + } 120 + 121 + }
+13
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 2087 2087 2088 2088 foreach ($xactions as $xaction) { 2089 2089 switch ($xaction->getTransactionType()) { 2090 + case PhabricatorTransactions::TYPE_SUBSCRIBERS: 2091 + $clone_xaction = clone $xaction; 2092 + $clone_xaction->setOldValue(array_values($this->subscribers)); 2093 + $clone_xaction->setNewValue( 2094 + $this->getPHIDTransactionNewValue( 2095 + $clone_xaction)); 2096 + 2097 + PhabricatorPolicyRule::passTransactionHintToRule( 2098 + $copy, 2099 + new PhabricatorSubscriptionsSubscribersPolicyRule(), 2100 + array_fuse($clone_xaction->getNewValue())); 2101 + 2102 + break; 2090 2103 case PhabricatorTransactions::TYPE_SPACE: 2091 2104 $space_phid = $this->getTransactionNewValue($object, $xaction); 2092 2105 $copy->setSpacePHID($space_phid);