Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/herald/engine/HeraldEngine.php
12256 views
1
<?php
2
3
final class HeraldEngine extends Phobject {
4
5
protected $rules = array();
6
protected $activeRule;
7
protected $transcript;
8
9
private $fieldCache = array();
10
private $fieldExceptions = array();
11
protected $object;
12
private $dryRun;
13
14
private $forbiddenFields = array();
15
private $forbiddenActions = array();
16
private $skipEffects = array();
17
18
private $profilerStack = array();
19
private $profilerFrames = array();
20
21
private $ruleResults;
22
private $ruleStack;
23
24
public function setDryRun($dry_run) {
25
$this->dryRun = $dry_run;
26
return $this;
27
}
28
29
public function getDryRun() {
30
return $this->dryRun;
31
}
32
33
public function getRule($phid) {
34
return idx($this->rules, $phid);
35
}
36
37
public function loadRulesForAdapter(HeraldAdapter $adapter) {
38
return id(new HeraldRuleQuery())
39
->setViewer(PhabricatorUser::getOmnipotentUser())
40
->withDisabled(false)
41
->withContentTypes(array($adapter->getAdapterContentType()))
42
->needConditionsAndActions(true)
43
->needAppliedToPHIDs(array($adapter->getPHID()))
44
->needValidateAuthors(true)
45
->execute();
46
}
47
48
public static function loadAndApplyRules(HeraldAdapter $adapter) {
49
$engine = new HeraldEngine();
50
51
$rules = $engine->loadRulesForAdapter($adapter);
52
$effects = $engine->applyRules($rules, $adapter);
53
$engine->applyEffects($effects, $adapter, $rules);
54
55
return $engine->getTranscript();
56
}
57
58
/* -( Rule Stack )--------------------------------------------------------- */
59
60
private function resetRuleStack() {
61
$this->ruleStack = array();
62
return $this;
63
}
64
65
private function hasRuleOnStack(HeraldRule $rule) {
66
$phid = $rule->getPHID();
67
return isset($this->ruleStack[$phid]);
68
}
69
70
private function pushRuleStack(HeraldRule $rule) {
71
$phid = $rule->getPHID();
72
$this->ruleStack[$phid] = $rule;
73
return $this;
74
}
75
76
private function getRuleStack() {
77
return array_values($this->ruleStack);
78
}
79
80
/* -( Rule Results )------------------------------------------------------- */
81
82
private function resetRuleResults() {
83
$this->ruleResults = array();
84
return $this;
85
}
86
87
private function setRuleResult(
88
HeraldRule $rule,
89
HeraldRuleResult $result) {
90
91
$phid = $rule->getPHID();
92
93
if ($this->hasRuleResult($rule)) {
94
throw new Exception(
95
pht(
96
'Herald rule "%s" already has an evaluation result.',
97
$phid));
98
}
99
100
$this->ruleResults[$phid] = $result;
101
102
$this->newRuleTranscript($rule)
103
->setRuleResult($result);
104
105
return $this;
106
}
107
108
private function hasRuleResult(HeraldRule $rule) {
109
$phid = $rule->getPHID();
110
return isset($this->ruleResults[$phid]);
111
}
112
113
private function getRuleResult(HeraldRule $rule) {
114
$phid = $rule->getPHID();
115
116
if (!$this->hasRuleResult($rule)) {
117
throw new Exception(
118
pht(
119
'Herald rule "%s" does not have an evaluation result.',
120
$phid));
121
}
122
123
return $this->ruleResults[$phid];
124
}
125
126
public function applyRules(array $rules, HeraldAdapter $object) {
127
assert_instances_of($rules, 'HeraldRule');
128
$t_start = microtime(true);
129
130
// Rules execute in a well-defined order: sort them into execution order.
131
$rules = msort($rules, 'getRuleExecutionOrderSortKey');
132
$rules = mpull($rules, null, 'getPHID');
133
134
$this->transcript = new HeraldTranscript();
135
$this->transcript->setObjectPHID((string)$object->getPHID());
136
$this->fieldCache = array();
137
$this->fieldExceptions = array();
138
$this->rules = $rules;
139
$this->object = $object;
140
141
$this->resetRuleResults();
142
143
$effects = array();
144
foreach ($rules as $phid => $rule) {
145
$this->resetRuleStack();
146
147
$caught = null;
148
$result = null;
149
try {
150
$is_first_only = $rule->isRepeatFirst();
151
152
if (!$this->getDryRun() &&
153
$is_first_only &&
154
$rule->getRuleApplied($object->getPHID())) {
155
156
// This is not a dry run, and this rule is only supposed to be
157
// applied a single time, and it has already been applied.
158
// That means automatic failure.
159
160
$result_code = HeraldRuleResult::RESULT_ALREADY_APPLIED;
161
$result = HeraldRuleResult::newFromResultCode($result_code);
162
} else if ($this->isForbidden($rule, $object)) {
163
$result_code = HeraldRuleResult::RESULT_OBJECT_STATE;
164
$result = HeraldRuleResult::newFromResultCode($result_code);
165
} else {
166
$result = $this->getRuleMatchResult($rule, $object);
167
}
168
} catch (HeraldRecursiveConditionsException $ex) {
169
$cycle_phids = array();
170
171
$stack = $this->getRuleStack();
172
foreach ($stack as $stack_rule) {
173
$cycle_phids[] = $stack_rule->getPHID();
174
}
175
// Add the rule which actually cycled to the list to make the
176
// result more clear when we show it to the user.
177
$cycle_phids[] = $phid;
178
179
foreach ($stack as $stack_rule) {
180
if ($this->hasRuleResult($stack_rule)) {
181
continue;
182
}
183
184
$result_code = HeraldRuleResult::RESULT_RECURSION;
185
$result_data = array(
186
'cyclePHIDs' => $cycle_phids,
187
);
188
189
$result = HeraldRuleResult::newFromResultCode($result_code)
190
->setResultData($result_data);
191
$this->setRuleResult($stack_rule, $result);
192
}
193
194
$result = $this->getRuleResult($rule);
195
} catch (HeraldRuleEvaluationException $ex) {
196
// When we encounter an evaluation exception, the condition which
197
// failed to evaluate is responsible for logging the details of the
198
// error.
199
200
$result_code = HeraldRuleResult::RESULT_EVALUATION_EXCEPTION;
201
$result = HeraldRuleResult::newFromResultCode($result_code);
202
} catch (Exception $ex) {
203
$caught = $ex;
204
} catch (Throwable $ex) {
205
$caught = $ex;
206
}
207
208
if ($caught) {
209
// These exceptions are unexpected, and did not arise during rule
210
// evaluation, so we're responsible for handling the details.
211
212
$result_code = HeraldRuleResult::RESULT_EXCEPTION;
213
214
$result_data = array(
215
'exception.class' => get_class($caught),
216
'exception.message' => $ex->getMessage(),
217
);
218
219
$result = HeraldRuleResult::newFromResultCode($result_code)
220
->setResultData($result_data);
221
}
222
223
if (!$this->hasRuleResult($rule)) {
224
$this->setRuleResult($rule, $result);
225
}
226
$result = $this->getRuleResult($rule);
227
228
if ($result->getShouldApplyActions()) {
229
foreach ($this->getRuleEffects($rule, $object) as $effect) {
230
$effects[] = $effect;
231
}
232
}
233
}
234
235
$xaction_phids = null;
236
$xactions = $object->getAppliedTransactions();
237
if ($xactions !== null) {
238
$xaction_phids = mpull($xactions, 'getPHID');
239
}
240
241
$object_transcript = id(new HeraldObjectTranscript())
242
->setPHID($object->getPHID())
243
->setName($object->getHeraldName())
244
->setType($object->getAdapterContentType())
245
->setFields($this->fieldCache)
246
->setAppliedTransactionPHIDs($xaction_phids)
247
->setProfile($this->getProfile());
248
249
$this->transcript->setObjectTranscript($object_transcript);
250
251
$t_end = microtime(true);
252
253
$this->transcript->setDuration($t_end - $t_start);
254
255
return $effects;
256
}
257
258
public function applyEffects(
259
array $effects,
260
HeraldAdapter $adapter,
261
array $rules) {
262
assert_instances_of($effects, 'HeraldEffect');
263
assert_instances_of($rules, 'HeraldRule');
264
265
$this->transcript->setDryRun((int)$this->getDryRun());
266
267
if ($this->getDryRun()) {
268
$xscripts = array();
269
foreach ($effects as $effect) {
270
$xscripts[] = new HeraldApplyTranscript(
271
$effect,
272
false,
273
pht('This was a dry run, so no actions were actually taken.'));
274
}
275
} else {
276
$xscripts = $adapter->applyHeraldEffects($effects);
277
}
278
279
assert_instances_of($xscripts, 'HeraldApplyTranscript');
280
foreach ($xscripts as $apply_xscript) {
281
$this->transcript->addApplyTranscript($apply_xscript);
282
}
283
284
// For dry runs, don't mark the rule as having applied to the object.
285
if ($this->getDryRun()) {
286
return;
287
}
288
289
// Update the "applied" state table. How this table works depends on the
290
// repetition policy for the rule.
291
//
292
// REPEAT_EVERY: We delete existing rows for the rule, then write nothing.
293
// This policy doesn't use any state.
294
//
295
// REPEAT_FIRST: We keep existing rows, then write additional rows for
296
// rules which fired. This policy accumulates state over the life of the
297
// object.
298
//
299
// REPEAT_CHANGE: We delete existing rows, then write all the rows which
300
// matched. This policy only uses the state from the previous run.
301
302
$rules = mpull($rules, null, 'getID');
303
$rule_ids = mpull($xscripts, 'getRuleID');
304
305
$delete_ids = array();
306
foreach ($rules as $rule_id => $rule) {
307
if ($rule->isRepeatFirst()) {
308
continue;
309
}
310
$delete_ids[] = $rule_id;
311
}
312
313
$applied_ids = array();
314
foreach ($rule_ids as $rule_id) {
315
if (!$rule_id) {
316
// Some apply transcripts are purely informational and not associated
317
// with a rule, e.g. carryover emails from earlier revisions.
318
continue;
319
}
320
321
$rule = idx($rules, $rule_id);
322
if (!$rule) {
323
continue;
324
}
325
326
if ($rule->isRepeatFirst() || $rule->isRepeatOnChange()) {
327
$applied_ids[] = $rule_id;
328
}
329
}
330
331
// Also include "only if this rule did not match the last time" rules
332
// which matched but were skipped in the "applied" list.
333
foreach ($this->skipEffects as $rule_id => $ignored) {
334
$applied_ids[] = $rule_id;
335
}
336
337
if ($delete_ids || $applied_ids) {
338
$conn_w = id(new HeraldRule())->establishConnection('w');
339
340
if ($delete_ids) {
341
queryfx(
342
$conn_w,
343
'DELETE FROM %T WHERE phid = %s AND ruleID IN (%Ld)',
344
HeraldRule::TABLE_RULE_APPLIED,
345
$adapter->getPHID(),
346
$delete_ids);
347
}
348
349
if ($applied_ids) {
350
$sql = array();
351
foreach ($applied_ids as $id) {
352
$sql[] = qsprintf(
353
$conn_w,
354
'(%s, %d)',
355
$adapter->getPHID(),
356
$id);
357
}
358
queryfx(
359
$conn_w,
360
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %LQ',
361
HeraldRule::TABLE_RULE_APPLIED,
362
$sql);
363
}
364
}
365
}
366
367
public function getTranscript() {
368
$this->transcript->save();
369
return $this->transcript;
370
}
371
372
public function doesRuleMatch(
373
HeraldRule $rule,
374
HeraldAdapter $object) {
375
$result = $this->getRuleMatchResult($rule, $object);
376
return $result->getShouldApplyActions();
377
}
378
379
private function getRuleMatchResult(
380
HeraldRule $rule,
381
HeraldAdapter $object) {
382
383
if ($this->hasRuleResult($rule)) {
384
// If we've already evaluated this rule because another rule depends
385
// on it, we don't need to reevaluate it.
386
return $this->getRuleResult($rule);
387
}
388
389
if ($this->hasRuleOnStack($rule)) {
390
// We've recursed, fail all of the rules on the stack. This happens when
391
// there's a dependency cycle with "Rule conditions match for rule ..."
392
// conditions.
393
throw new HeraldRecursiveConditionsException();
394
}
395
$this->pushRuleStack($rule);
396
397
$all = $rule->getMustMatchAll();
398
399
$conditions = $rule->getConditions();
400
401
$result_code = null;
402
$result_data = array();
403
404
$local_version = id(new HeraldRule())->getConfigVersion();
405
if ($rule->getConfigVersion() > $local_version) {
406
$result_code = HeraldRuleResult::RESULT_VERSION;
407
} else if (!$conditions) {
408
$result_code = HeraldRuleResult::RESULT_EMPTY;
409
} else if (!$rule->hasValidAuthor()) {
410
$result_code = HeraldRuleResult::RESULT_OWNER;
411
} else if (!$this->canAuthorViewObject($rule, $object)) {
412
$result_code = HeraldRuleResult::RESULT_VIEW_POLICY;
413
} else if (!$this->canRuleApplyToObject($rule, $object)) {
414
$result_code = HeraldRuleResult::RESULT_OBJECT_RULE;
415
} else {
416
foreach ($conditions as $condition) {
417
$caught = null;
418
419
try {
420
$match = $this->doesConditionMatch(
421
$rule,
422
$condition,
423
$object);
424
} catch (HeraldRuleEvaluationException $ex) {
425
throw $ex;
426
} catch (HeraldRecursiveConditionsException $ex) {
427
throw $ex;
428
} catch (Exception $ex) {
429
$caught = $ex;
430
} catch (Throwable $ex) {
431
$caught = $ex;
432
}
433
434
if ($caught) {
435
throw new HeraldRuleEvaluationException();
436
}
437
438
if (!$all && $match) {
439
$result_code = HeraldRuleResult::RESULT_ANY_MATCHED;
440
break;
441
}
442
443
if ($all && !$match) {
444
$result_code = HeraldRuleResult::RESULT_ANY_FAILED;
445
break;
446
}
447
}
448
449
if ($result_code === null) {
450
if ($all) {
451
$result_code = HeraldRuleResult::RESULT_ALL_MATCHED;
452
} else {
453
$result_code = HeraldRuleResult::RESULT_ALL_FAILED;
454
}
455
}
456
}
457
458
// If this rule matched, and is set to run "if it did not match the last
459
// time", and we matched the last time, we're going to return a special
460
// result code which records a match but doesn't actually apply effects.
461
462
// We need the rule to match so that storage gets updated properly. If we
463
// just pretend the rule didn't match it won't cause any effects (which
464
// is correct), but it also won't set the "it matched" flag in storage,
465
// so the next run after this one would incorrectly trigger again.
466
467
$result = HeraldRuleResult::newFromResultCode($result_code)
468
->setResultData($result_data);
469
470
$should_apply = $result->getShouldApplyActions();
471
472
$is_dry_run = $this->getDryRun();
473
if ($should_apply && !$is_dry_run) {
474
$is_on_change = $rule->isRepeatOnChange();
475
if ($is_on_change) {
476
$did_apply = $rule->getRuleApplied($object->getPHID());
477
if ($did_apply) {
478
// Replace the result with our modified result.
479
$result_code = HeraldRuleResult::RESULT_LAST_MATCHED;
480
$result = HeraldRuleResult::newFromResultCode($result_code);
481
482
$this->skipEffects[$rule->getID()] = true;
483
}
484
}
485
}
486
487
$this->setRuleResult($rule, $result);
488
489
return $result;
490
}
491
492
private function doesConditionMatch(
493
HeraldRule $rule,
494
HeraldCondition $condition,
495
HeraldAdapter $adapter) {
496
497
$transcript = $this->newConditionTranscript($rule, $condition);
498
499
$caught = null;
500
$result_data = array();
501
502
try {
503
$field_key = $condition->getFieldName();
504
505
$field_value = $this->getProfiledObjectFieldValue(
506
$adapter,
507
$field_key);
508
509
$is_match = $this->getProfiledConditionMatch(
510
$adapter,
511
$rule,
512
$condition,
513
$field_value);
514
if ($is_match) {
515
$result_code = HeraldConditionResult::RESULT_MATCHED;
516
} else {
517
$result_code = HeraldConditionResult::RESULT_FAILED;
518
}
519
} catch (HeraldRecursiveConditionsException $ex) {
520
$result_code = HeraldConditionResult::RESULT_RECURSION;
521
$caught = $ex;
522
} catch (HeraldInvalidConditionException $ex) {
523
$result_code = HeraldConditionResult::RESULT_INVALID;
524
$caught = $ex;
525
} catch (Exception $ex) {
526
$result_code = HeraldConditionResult::RESULT_EXCEPTION;
527
$caught = $ex;
528
} catch (Throwable $ex) {
529
$result_code = HeraldConditionResult::RESULT_EXCEPTION;
530
$caught = $ex;
531
}
532
533
if ($caught) {
534
$result_data = array(
535
'exception.class' => get_class($caught),
536
'exception.message' => $ex->getMessage(),
537
);
538
}
539
540
$result = HeraldConditionResult::newFromResultCode($result_code)
541
->setResultData($result_data);
542
543
$transcript->setResult($result);
544
545
if ($caught) {
546
throw $caught;
547
}
548
549
return $result->getIsMatch();
550
}
551
552
private function getProfiledConditionMatch(
553
HeraldAdapter $adapter,
554
HeraldRule $rule,
555
HeraldCondition $condition,
556
$field_value) {
557
558
// Here, we're profiling the cost to match the condition value against
559
// whatever test is configured. Normally, this cost should be very
560
// small (<<1ms) since it amounts to a single comparison:
561
//
562
// [ Task author ][ is any of ][ alice ]
563
//
564
// However, it may be expensive in some cases, particularly if you
565
// write a rule with a very creative regular expression that backtracks
566
// explosively.
567
//
568
// At time of writing, the "Another Herald Rule" field is also
569
// evaluated inside the matching function. This may be arbitrarily
570
// expensive (it can prompt us to execute any finite number of other
571
// Herald rules), although we'll push the profiler stack appropriately
572
// so we don't count the evaluation time against this rule in the final
573
// profile.
574
575
$this->pushProfilerRule($rule);
576
577
$caught = null;
578
try {
579
$is_match = $adapter->doesConditionMatch(
580
$this,
581
$rule,
582
$condition,
583
$field_value);
584
} catch (Exception $ex) {
585
$caught = $ex;
586
} catch (Throwable $ex) {
587
$caught = $ex;
588
}
589
590
$this->popProfilerRule($rule);
591
592
if ($caught) {
593
throw $caught;
594
}
595
596
return $is_match;
597
}
598
599
private function getProfiledObjectFieldValue(
600
HeraldAdapter $adapter,
601
$field_key) {
602
603
// Before engaging the profiler, make sure the field class is loaded.
604
605
$adapter->willGetHeraldField($field_key);
606
607
// The first time we read a field value, we'll actually generate it, which
608
// may be slow.
609
610
// After it is generated for the first time, this will just read it from a
611
// cache, which should be very fast.
612
613
// We still want to profile the request even if it goes to cache so we can
614
// get an accurate count of how many times we access the field value: when
615
// trying to improve the performance of Herald rules, it's helpful to know
616
// how many rules rely on the value of a field which is slow to generate.
617
618
$caught = null;
619
620
$this->pushProfilerField($field_key);
621
try {
622
$value = $this->getObjectFieldValue($field_key);
623
} catch (Exception $ex) {
624
$caught = $ex;
625
} catch (Throwable $ex) {
626
$caught = $ex;
627
}
628
$this->popProfilerField($field_key);
629
630
if ($caught) {
631
throw $caught;
632
}
633
634
return $value;
635
}
636
637
private function getObjectFieldValue($field_key) {
638
if (array_key_exists($field_key, $this->fieldExceptions)) {
639
throw $this->fieldExceptions[$field_key];
640
}
641
642
if (array_key_exists($field_key, $this->fieldCache)) {
643
return $this->fieldCache[$field_key];
644
}
645
646
$adapter = $this->object;
647
648
$caught = null;
649
try {
650
$value = $adapter->getHeraldField($field_key);
651
} catch (Exception $ex) {
652
$caught = $ex;
653
} catch (Throwable $ex) {
654
$caught = $ex;
655
}
656
657
if ($caught) {
658
$this->fieldExceptions[$field_key] = $caught;
659
throw $caught;
660
}
661
662
$this->fieldCache[$field_key] = $value;
663
664
return $value;
665
}
666
667
protected function getRuleEffects(
668
HeraldRule $rule,
669
HeraldAdapter $object) {
670
671
$rule_id = $rule->getID();
672
if (isset($this->skipEffects[$rule_id])) {
673
return array();
674
}
675
676
$effects = array();
677
foreach ($rule->getActions() as $action) {
678
$effect = id(new HeraldEffect())
679
->setObjectPHID($object->getPHID())
680
->setAction($action->getAction())
681
->setTarget($action->getTarget())
682
->setRule($rule);
683
684
$name = $rule->getName();
685
$id = $rule->getID();
686
$effect->setReason(
687
pht(
688
'Conditions were met for %s',
689
"H{$id} {$name}"));
690
691
$effects[] = $effect;
692
}
693
return $effects;
694
}
695
696
private function canAuthorViewObject(
697
HeraldRule $rule,
698
HeraldAdapter $adapter) {
699
700
// Authorship is irrelevant for global rules and object rules.
701
if ($rule->isGlobalRule() || $rule->isObjectRule()) {
702
return true;
703
}
704
705
// The author must be able to create rules for the adapter's content type.
706
// In particular, this means that the application must be installed and
707
// accessible to the user. For example, if a user writes a Differential
708
// rule and then loses access to Differential, this disables the rule.
709
$enabled = HeraldAdapter::getEnabledAdapterMap($rule->getAuthor());
710
if (empty($enabled[$adapter->getAdapterContentType()])) {
711
return false;
712
}
713
714
// Finally, the author must be able to see the object itself. You can't
715
// write a personal rule that CC's you on revisions you wouldn't otherwise
716
// be able to see, for example.
717
$object = $adapter->getObject();
718
return PhabricatorPolicyFilter::hasCapability(
719
$rule->getAuthor(),
720
$object,
721
PhabricatorPolicyCapability::CAN_VIEW);
722
}
723
724
private function canRuleApplyToObject(
725
HeraldRule $rule,
726
HeraldAdapter $adapter) {
727
728
// Rules which are not object rules can apply to anything.
729
if (!$rule->isObjectRule()) {
730
return true;
731
}
732
733
$trigger_phid = $rule->getTriggerObjectPHID();
734
$object_phids = $adapter->getTriggerObjectPHIDs();
735
736
if ($object_phids) {
737
if (in_array($trigger_phid, $object_phids)) {
738
return true;
739
}
740
}
741
742
return false;
743
}
744
745
private function newRuleTranscript(HeraldRule $rule) {
746
$xscript = id(new HeraldRuleTranscript())
747
->setRuleID($rule->getID())
748
->setRuleName($rule->getName())
749
->setRuleOwner($rule->getAuthorPHID());
750
751
$this->transcript->addRuleTranscript($xscript);
752
753
return $xscript;
754
}
755
756
private function newConditionTranscript(
757
HeraldRule $rule,
758
HeraldCondition $condition) {
759
760
$xscript = id(new HeraldConditionTranscript())
761
->setRuleID($rule->getID())
762
->setConditionID($condition->getID())
763
->setFieldName($condition->getFieldName())
764
->setCondition($condition->getFieldCondition())
765
->setTestValue($condition->getValue());
766
767
$this->transcript->addConditionTranscript($xscript);
768
769
return $xscript;
770
}
771
772
private function newApplyTranscript(
773
HeraldAdapter $adapter,
774
HeraldRule $rule,
775
HeraldActionRecord $action) {
776
777
$effect = id(new HeraldEffect())
778
->setObjectPHID($adapter->getPHID())
779
->setAction($action->getAction())
780
->setTarget($action->getTarget())
781
->setRule($rule);
782
783
$xscript = new HeraldApplyTranscript($effect, false);
784
785
$this->transcript->addApplyTranscript($xscript);
786
787
return $xscript;
788
}
789
790
private function isForbidden(
791
HeraldRule $rule,
792
HeraldAdapter $adapter) {
793
794
$forbidden = $adapter->getForbiddenActions();
795
if (!$forbidden) {
796
return false;
797
}
798
799
$forbidden = array_fuse($forbidden);
800
801
$is_forbidden = false;
802
803
foreach ($rule->getConditions() as $condition) {
804
$field_key = $condition->getFieldName();
805
806
if (!isset($this->forbiddenFields[$field_key])) {
807
$reason = null;
808
809
try {
810
$states = $adapter->getRequiredFieldStates($field_key);
811
} catch (Exception $ex) {
812
$states = array();
813
}
814
815
foreach ($states as $state) {
816
if (!isset($forbidden[$state])) {
817
continue;
818
}
819
$reason = $adapter->getForbiddenReason($state);
820
break;
821
}
822
823
$this->forbiddenFields[$field_key] = $reason;
824
}
825
826
$forbidden_reason = $this->forbiddenFields[$field_key];
827
if ($forbidden_reason !== null) {
828
$result_code = HeraldConditionResult::RESULT_OBJECT_STATE;
829
$result_data = array(
830
'reason' => $forbidden_reason,
831
);
832
833
$result = HeraldConditionResult::newFromResultCode($result_code)
834
->setResultData($result_data);
835
836
$this->newConditionTranscript($rule, $condition)
837
->setResult($result);
838
839
$is_forbidden = true;
840
}
841
}
842
843
foreach ($rule->getActions() as $action_record) {
844
$action_key = $action_record->getAction();
845
846
if (!isset($this->forbiddenActions[$action_key])) {
847
$reason = null;
848
849
try {
850
$states = $adapter->getRequiredActionStates($action_key);
851
} catch (Exception $ex) {
852
$states = array();
853
}
854
855
foreach ($states as $state) {
856
if (!isset($forbidden[$state])) {
857
continue;
858
}
859
$reason = $adapter->getForbiddenReason($state);
860
break;
861
}
862
863
$this->forbiddenActions[$action_key] = $reason;
864
}
865
866
$forbidden_reason = $this->forbiddenActions[$action_key];
867
if ($forbidden_reason !== null) {
868
$this->newApplyTranscript($adapter, $rule, $action_record)
869
->setAppliedReason(
870
array(
871
array(
872
'type' => HeraldAction::DO_STANDARD_FORBIDDEN,
873
'data' => $forbidden_reason,
874
),
875
));
876
877
$is_forbidden = true;
878
}
879
}
880
881
return $is_forbidden;
882
}
883
884
/* -( Profiler )----------------------------------------------------------- */
885
886
private function pushProfilerField($field_key) {
887
return $this->pushProfilerStack('field', $field_key);
888
}
889
890
private function popProfilerField($field_key) {
891
return $this->popProfilerStack('field', $field_key);
892
}
893
894
private function pushProfilerRule(HeraldRule $rule) {
895
return $this->pushProfilerStack('rule', $rule->getPHID());
896
}
897
898
private function popProfilerRule(HeraldRule $rule) {
899
return $this->popProfilerStack('rule', $rule->getPHID());
900
}
901
902
private function pushProfilerStack($type, $key) {
903
$this->profilerStack[] = array(
904
'type' => $type,
905
'key' => $key,
906
'start' => microtime(true),
907
);
908
909
return $this;
910
}
911
912
private function popProfilerStack($type, $key) {
913
if (!$this->profilerStack) {
914
throw new Exception(
915
pht(
916
'Unable to pop profiler stack: profiler stack is empty.'));
917
}
918
919
$frame = last($this->profilerStack);
920
if (($frame['type'] !== $type) || ($frame['key'] !== $key)) {
921
throw new Exception(
922
pht(
923
'Unable to pop profiler stack: expected frame of type "%s" with '.
924
'key "%s", but found frame of type "%s" with key "%s".',
925
$type,
926
$key,
927
$frame['type'],
928
$frame['key']));
929
}
930
931
// Accumulate the new timing information into the existing profile. If this
932
// is the first time we've seen this particular rule or field, we'll
933
// create a new empty frame first.
934
935
$elapsed = microtime(true) - $frame['start'];
936
$frame_key = sprintf('%s/%s', $type, $key);
937
938
if (!isset($this->profilerFrames[$frame_key])) {
939
$current = array(
940
'type' => $type,
941
'key' => $key,
942
'elapsed' => 0,
943
'count' => 0,
944
);
945
} else {
946
$current = $this->profilerFrames[$frame_key];
947
}
948
949
$current['elapsed'] += $elapsed;
950
$current['count']++;
951
952
$this->profilerFrames[$frame_key] = $current;
953
954
array_pop($this->profilerStack);
955
}
956
957
private function getProfile() {
958
if ($this->profilerStack) {
959
$frame = last($this->profilerStack);
960
$frame_type = $frame['type'];
961
$frame_key = $frame['key'];
962
$frame_count = count($this->profilerStack);
963
964
throw new Exception(
965
pht(
966
'Unable to retrieve profile: profiler stack is not empty. The '.
967
'stack has %s frame(s); the final frame has type "%s" and key '.
968
'"%s".',
969
new PhutilNumber($frame_count),
970
$frame_type,
971
$frame_key));
972
}
973
974
return array_values($this->profilerFrames);
975
}
976
977
978
}
979
980