@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<?php
2
3final class PhabricatorProjectTrigger
4 extends PhabricatorProjectDAO
5 implements
6 PhabricatorApplicationTransactionInterface,
7 PhabricatorPolicyInterface,
8 PhabricatorIndexableInterface,
9 PhabricatorDestructibleInterface {
10
11 protected $name;
12 protected $ruleset = array();
13 protected $editPolicy;
14
15 private $triggerRules;
16 private $viewer;
17 private $usage = self::ATTACHABLE;
18
19 public static function initializeNewTrigger() {
20 $default_edit = PhabricatorPolicies::POLICY_USER;
21
22 return id(new self())
23 ->setName('')
24 ->setEditPolicy($default_edit);
25 }
26
27 protected function getConfiguration() {
28 return array(
29 self::CONFIG_AUX_PHID => true,
30 self::CONFIG_SERIALIZATION => array(
31 'ruleset' => self::SERIALIZATION_JSON,
32 ),
33 self::CONFIG_COLUMN_SCHEMA => array(
34 'name' => 'text255',
35 ),
36 self::CONFIG_KEY_SCHEMA => array(
37 ),
38 ) + parent::getConfiguration();
39 }
40
41 public function getPHIDType() {
42 return PhabricatorProjectTriggerPHIDType::TYPECONST;
43 }
44
45 public function getViewer() {
46 return $this->viewer;
47 }
48
49 public function setViewer(PhabricatorUser $user) {
50 $this->viewer = $user;
51 return $this;
52 }
53
54 public function getDisplayName() {
55 $name = $this->getName();
56 if (strlen($name)) {
57 return $name;
58 }
59
60 return $this->getDefaultName();
61 }
62
63 public function getDefaultName() {
64 return pht('Custom Trigger');
65 }
66
67 public function getURI() {
68 return urisprintf(
69 '/project/trigger/%d/',
70 $this->getID());
71 }
72
73 public function getObjectName() {
74 return pht('Trigger %d', $this->getID());
75 }
76
77 public function setRuleset(array $ruleset) {
78 // Clear any cached trigger rules, since we're changing the ruleset
79 // for the trigger.
80 $this->triggerRules = null;
81
82 parent::setRuleset($ruleset);
83 }
84
85 public function getTriggerRules($viewer = null) {
86 if ($this->triggerRules === null) {
87 if (!$viewer) {
88 $viewer = $this->getViewer();
89 }
90
91 $trigger_rules = self::newTriggerRulesFromRuleSpecifications(
92 $this->getRuleset(),
93 $allow_invalid = true,
94 $viewer);
95
96 $this->triggerRules = $trigger_rules;
97 }
98
99 return $this->triggerRules;
100 }
101
102 public static function newTriggerRulesFromRuleSpecifications(
103 array $list,
104 $allow_invalid,
105 PhabricatorUser $viewer) {
106
107 // NOTE: With "$allow_invalid" set, we're trying to preserve the database
108 // state in the rule structure, even if it includes rule types we don't
109 // have implementations for, or rules with invalid rule values.
110
111 // If an administrator adds or removes extensions which add rules, or
112 // an upgrade affects rule validity, existing rules may become invalid.
113 // When they do, we still want the UI to reflect the ruleset state
114 // accurately and "Edit" + "Save" shouldn't destroy data unless the
115 // user explicitly modifies the ruleset.
116
117 // In this mode, when we run into rules which are structured correctly but
118 // which have types we don't know about, we replace them with "Unknown
119 // Rules". If we know about the type of a rule but the value doesn't
120 // validate, we replace it with "Invalid Rules". These two rule types don't
121 // take any actions when a card is dropped into the column, but they show
122 // the user what's wrong with the ruleset and can be saved without causing
123 // any collateral damage.
124
125 $rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules();
126
127 // If the stored rule data isn't a list of rules (or we encounter other
128 // fundamental structural problems, below), there isn't much we can do
129 // to try to represent the state.
130 if (!is_array($list)) {
131 throw new PhabricatorProjectTriggerCorruptionException(
132 pht(
133 'Trigger ruleset is corrupt: expected a list of rule '.
134 'specifications, found "%s".',
135 phutil_describe_type($list)));
136 }
137
138 $trigger_rules = array();
139 foreach ($list as $key => $rule) {
140 if (!is_array($rule)) {
141 throw new PhabricatorProjectTriggerCorruptionException(
142 pht(
143 'Trigger ruleset is corrupt: rule (at index "%s") should be a '.
144 'rule specification, but is actually "%s".',
145 $key,
146 phutil_describe_type($rule)));
147 }
148
149 try {
150 PhutilTypeSpec::checkMap(
151 $rule,
152 array(
153 'type' => 'string',
154 'value' => 'wild',
155 ));
156 } catch (PhutilTypeCheckException $ex) {
157 throw new PhabricatorProjectTriggerCorruptionException(
158 pht(
159 'Trigger ruleset is corrupt: rule (at index "%s") is not a '.
160 'valid rule specification: %s',
161 $key,
162 $ex->getMessage()));
163 }
164
165 $record = id(new PhabricatorProjectTriggerRuleRecord())
166 ->setType(idx($rule, 'type'))
167 ->setValue(idx($rule, 'value'));
168
169 if (!isset($rule_map[$record->getType()])) {
170 if (!$allow_invalid) {
171 throw new PhabricatorProjectTriggerCorruptionException(
172 pht(
173 'Trigger ruleset is corrupt: rule type "%s" is unknown.',
174 $record->getType()));
175 }
176
177 $rule = new PhabricatorProjectTriggerUnknownRule();
178 } else {
179 $rule = clone $rule_map[$record->getType()];
180 }
181
182 try {
183 $rule->setRecord($record);
184 } catch (Exception $ex) {
185 if (!$allow_invalid) {
186 throw new PhabricatorProjectTriggerCorruptionException(
187 pht(
188 'Trigger ruleset is corrupt, rule (of type "%s") does not '.
189 'validate: %s',
190 $record->getType(),
191 $ex->getMessage()));
192 }
193
194 $rule = id(new PhabricatorProjectTriggerInvalidRule())
195 ->setRecord($record)
196 ->setException($ex);
197 }
198 $rule->setViewer($viewer);
199
200 $trigger_rules[] = $rule;
201 }
202
203 return $trigger_rules;
204 }
205
206
207 public function getDropEffects() {
208 $effects = array();
209
210 $rules = $this->getTriggerRules();
211 foreach ($rules as $rule) {
212 foreach ($rule->getDropEffects() as $effect) {
213 $effects[] = $effect;
214 }
215 }
216
217 return $effects;
218 }
219
220 public function newDropTransactions(
221 PhabricatorUser $viewer,
222 PhabricatorProjectColumn $column,
223 $object) {
224
225 $trigger_xactions = array();
226 foreach ($this->getTriggerRules($viewer) as $rule) {
227 $rule
228 ->setTrigger($this)
229 ->setColumn($column)
230 ->setObject($object);
231
232 $xactions = $rule->getDropTransactions(
233 $object,
234 $rule->getRecord()->getValue());
235
236 if (!is_array($xactions)) {
237 throw new Exception(
238 pht(
239 'Expected trigger rule (of class "%s") to return a list of '.
240 'transactions from "newDropTransactions()", but got "%s".',
241 get_class($rule),
242 phutil_describe_type($xactions)));
243 }
244
245 $expect_type = get_class($object->getApplicationTransactionTemplate());
246 assert_instances_of($xactions, $expect_type);
247
248 foreach ($xactions as $xaction) {
249 $trigger_xactions[] = $xaction;
250 }
251 }
252
253 return $trigger_xactions;
254 }
255
256 public function getPreviewEffect() {
257 $header = pht('Trigger: %s', $this->getDisplayName());
258
259 return id(new PhabricatorProjectDropEffect())
260 ->setIcon('fa-cogs')
261 ->setColor('blue')
262 ->setIsHeader(true)
263 ->setContent($header);
264 }
265
266 public function getSoundEffects() {
267 $sounds = array();
268
269 foreach ($this->getTriggerRules() as $rule) {
270 foreach ($rule->getSoundEffects() as $effect) {
271 $sounds[] = $effect;
272 }
273 }
274
275 return $sounds;
276 }
277
278 public function getUsage() {
279 return $this->assertAttached($this->usage);
280 }
281
282 public function attachUsage(PhabricatorProjectTriggerUsage $usage) {
283 $this->usage = $usage;
284 return $this;
285 }
286
287
288/* -( PhabricatorApplicationTransactionInterface )------------------------- */
289
290
291 public function getApplicationTransactionEditor() {
292 return new PhabricatorProjectTriggerEditor();
293 }
294
295 public function getApplicationTransactionTemplate() {
296 return new PhabricatorProjectTriggerTransaction();
297 }
298
299
300/* -( PhabricatorPolicyInterface )----------------------------------------- */
301
302
303 public function getCapabilities() {
304 return array(
305 PhabricatorPolicyCapability::CAN_VIEW,
306 PhabricatorPolicyCapability::CAN_EDIT,
307 );
308 }
309
310 public function getPolicy($capability) {
311 switch ($capability) {
312 case PhabricatorPolicyCapability::CAN_VIEW:
313 return PhabricatorPolicies::getMostOpenPolicy();
314 case PhabricatorPolicyCapability::CAN_EDIT:
315 return $this->getEditPolicy();
316 }
317 }
318
319 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
320 return false;
321 }
322
323
324/* -( PhabricatorDestructibleInterface )----------------------------------- */
325
326
327 public function destroyObjectPermanently(
328 PhabricatorDestructionEngine $engine) {
329
330 $this->openTransaction();
331 $conn = $this->establishConnection('w');
332
333 // Remove the reference to this trigger from any columns which use it.
334 queryfx(
335 $conn,
336 'UPDATE %R SET triggerPHID = null WHERE triggerPHID = %s',
337 new PhabricatorProjectColumn(),
338 $this->getPHID());
339
340 // Remove the usage index row for this trigger, if one exists.
341 queryfx(
342 $conn,
343 'DELETE FROM %R WHERE triggerPHID = %s',
344 new PhabricatorProjectTriggerUsage(),
345 $this->getPHID());
346
347 $this->delete();
348
349 $this->saveTransaction();
350 }
351
352}