@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.

Provide a more structured result log for Herald conditions

Summary:
Ref T13586. Currently, Herald condition logs encode "pass" or "fail" robustly, "forbidden" through a sort of awkward side channel, and can not properly encode "invalid" or "exception" outcomes.

Structure the condition log so results are represented unambiguously and all possible outcomes (pass, fail, forbidden, invalid, exception) are clearly encoded.

Test Plan:
{F8446102}

{F8446103}

Maniphest Tasks: T13586

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

+427 -119
+2 -2
resources/celerity/map.php
··· 78 78 'rsrc/css/application/files/global-drag-and-drop.css' => '1d2713a4', 79 79 'rsrc/css/application/flag/flag.css' => '2b77be8d', 80 80 'rsrc/css/application/harbormaster/harbormaster.css' => '8dfe16b2', 81 - 'rsrc/css/application/herald/herald-test.css' => 'e004176f', 81 + 'rsrc/css/application/herald/herald-test.css' => '7e7bbdae', 82 82 'rsrc/css/application/herald/herald.css' => '648d39e2', 83 83 'rsrc/css/application/maniphest/report.css' => '3d53188b', 84 84 'rsrc/css/application/maniphest/task-edit.css' => '272daa84', ··· 587 587 'harbormaster-css' => '8dfe16b2', 588 588 'herald-css' => '648d39e2', 589 589 'herald-rule-editor' => '2633bef7', 590 - 'herald-test-css' => 'e004176f', 590 + 'herald-test-css' => '7e7bbdae', 591 591 'inline-comment-summary-css' => '81eb368d', 592 592 'javelin-aphlict' => '022516b4', 593 593 'javelin-behavior' => '1b6acc2a',
+2
src/__phutil_library_map__.php
··· 1566 1566 'HeraldCommentContentField' => 'applications/herald/field/HeraldCommentContentField.php', 1567 1567 'HeraldCommitAdapter' => 'applications/diffusion/herald/HeraldCommitAdapter.php', 1568 1568 'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php', 1569 + 'HeraldConditionResult' => 'applications/herald/storage/transcript/HeraldConditionResult.php', 1569 1570 'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php', 1570 1571 'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php', 1571 1572 'HeraldController' => 'applications/herald/controller/HeraldController.php', ··· 7793 7794 'HarbormasterBuildableAdapterInterface', 7794 7795 ), 7795 7796 'HeraldCondition' => 'HeraldDAO', 7797 + 'HeraldConditionResult' => 'Phobject', 7796 7798 'HeraldConditionTranscript' => 'Phobject', 7797 7799 'HeraldContentSourceField' => 'HeraldField', 7798 7800 'HeraldController' => 'PhabricatorController',
+21 -13
src/applications/herald/adapter/HeraldAdapter.php
··· 520 520 case self::CONDITION_NOT_REGEXP: 521 521 $result_if_match = ($condition_type == self::CONDITION_REGEXP); 522 522 523 + // We add the 'S' flag because we use the regexp multiple times. 524 + // It shouldn't cause any troubles if the flag is already there 525 + // - /.*/S is evaluated same as /.*/SS. 526 + $condition_pattern = $condition_value.'S'; 527 + 523 528 foreach ((array)$field_value as $value) { 524 - // We add the 'S' flag because we use the regexp multiple times. 525 - // It shouldn't cause any troubles if the flag is already there 526 - // - /.*/S is evaluated same as /.*/SS. 527 - $result = @preg_match($condition_value.'S', $value); 528 - if ($result === false) { 529 - throw new HeraldInvalidConditionException( 530 - pht( 531 - 'Regular expression "%s" in Herald rule "%s" is not valid, '. 532 - 'or exceeded backtracking or recursion limits while '. 533 - 'executing. Verify the expression and correct it or rewrite '. 534 - 'it with less backtracking.', 535 - $condition_value, 536 - $rule->getMonogram())); 529 + try { 530 + $result = phutil_preg_match($condition_pattern, $value); 531 + } catch (PhutilRegexException $ex) { 532 + $message = array(); 533 + $message[] = pht( 534 + 'Regular expression "%s" in Herald rule "%s" is not valid, '. 535 + 'or exceeded backtracking or recursion limits while '. 536 + 'executing. Verify the expression and correct it or rewrite '. 537 + 'it with less backtracking.', 538 + $condition_value, 539 + $rule->getMonogram()); 540 + $message[] = $ex->getMessage(); 541 + $message = implode("\n\n", $message); 542 + 543 + throw new HeraldInvalidConditionException($message); 537 544 } 545 + 538 546 if ($result) { 539 547 return $result_if_match; 540 548 }
+10 -24
src/applications/herald/controller/HeraldTranscriptController.php
··· 269 269 ->setTarget(phutil_tag('strong', array(), pht('Conditions')))); 270 270 271 271 foreach ($cond_xscripts as $cond_xscript) { 272 - if ($cond_xscript->isForbidden()) { 273 - $icon = 'fa-ban'; 274 - $color = 'indigo'; 275 - $result = pht('Forbidden'); 276 - } else if ($cond_xscript->getResult()) { 277 - $icon = 'fa-check'; 278 - $color = 'green'; 279 - $result = pht('Passed'); 280 - } else { 281 - $icon = 'fa-times'; 282 - $color = 'red'; 283 - $result = pht('Failed'); 284 - } 272 + $result = $cond_xscript->getResult(); 285 273 286 - if ($cond_xscript->getNote()) { 287 - $note_text = $cond_xscript->getNote(); 288 - if ($cond_xscript->isForbidden()) { 289 - $note_text = HeraldStateReasons::getExplanation($note_text); 290 - } 274 + $icon = $result->getIconIcon(); 275 + $color = $result->getIconColor(); 276 + $name = $result->getName(); 291 277 292 - $note = phutil_tag( 278 + $result_details = $result->newDetailsView(); 279 + if ($result_details !== null) { 280 + $result_details = phutil_tag( 293 281 'div', 294 282 array( 295 283 'class' => 'herald-condition-note', 296 284 ), 297 - $note_text); 298 - } else { 299 - $note = null; 285 + $result_details); 300 286 } 301 287 302 288 // TODO: This is not really translatable and should be driven through ··· 309 295 310 296 $cond_item = id(new PHUIStatusItemView()) 311 297 ->setIcon($icon, $color) 312 - ->setTarget($result) 313 - ->setNote(array($explanation, $note)); 298 + ->setTarget($name) 299 + ->setNote(array($explanation, $result_details)); 314 300 315 301 $cond_list->addItem($cond_item); 316 302 }
+176 -63
src/applications/herald/engine/HeraldEngine.php
··· 8 8 protected $activeRule; 9 9 protected $transcript; 10 10 11 - protected $fieldCache = array(); 11 + private $fieldCache = array(); 12 + private $fieldExceptions = array(); 12 13 protected $object; 13 14 private $dryRun; 14 15 ··· 64 65 $this->transcript = new HeraldTranscript(); 65 66 $this->transcript->setObjectPHID((string)$object->getPHID()); 66 67 $this->fieldCache = array(); 68 + $this->fieldExceptions = array(); 67 69 $this->results = array(); 68 70 $this->rules = $rules; 69 71 $this->object = $object; ··· 74 76 75 77 $is_first_only = $rule->isRepeatFirst(); 76 78 79 + $caught = null; 77 80 try { 78 81 if (!$this->getDryRun() && 79 82 $is_first_only && ··· 119 122 $names)); 120 123 } 121 124 $rule_matches = false; 125 + } catch (Exception $ex) { 126 + $caught = $ex; 127 + } catch (Throwable $ex) { 128 + $caught = $ex; 122 129 } 130 + 131 + if ($caught) { 132 + $this->newRuleTranscript($rules[$phid]) 133 + ->setResult(false) 134 + ->setReason( 135 + pht( 136 + 'Rule encountered an exception while evaluting.')); 137 + $rule_matches = false; 138 + } 139 + 123 140 $this->results[$phid] = $rule_matches; 124 141 125 142 if ($rule_matches) { ··· 323 340 $result = false; 324 341 } else { 325 342 foreach ($conditions as $condition) { 326 - try { 327 - $this->getConditionObjectValue($condition, $object); 328 - } catch (Exception $ex) { 329 - $reason = pht( 330 - 'Field "%s" does not exist!', 331 - $condition->getFieldName()); 332 - $result = false; 333 - break; 334 - } 335 - 336 - // Here, we're profiling the cost to match the condition value against 337 - // whatever test is configured. Normally, this cost should be very 338 - // small (<<1ms) since it amounts to a single comparison: 339 - // 340 - // [ Task author ][ is any of ][ alice ] 341 - // 342 - // However, it may be expensive in some cases, particularly if you 343 - // write a rule with a very creative regular expression that backtracks 344 - // explosively. 345 - // 346 - // At time of writing, the "Another Herald Rule" field is also 347 - // evaluated inside the matching function. This may be arbitrarily 348 - // expensive (it can prompt us to execute any finite number of other 349 - // Herald rules), although we'll push the profiler stack appropriately 350 - // so we don't count the evaluation time against this rule in the final 351 - // profile. 352 - 353 343 $caught = null; 354 344 355 - $this->pushProfilerRule($rule); 356 345 try { 357 - $match = $this->doesConditionMatch($rule, $condition, $object); 346 + $match = $this->doesConditionMatch( 347 + $rule, 348 + $condition, 349 + $object); 358 350 } catch (Exception $ex) { 359 351 $caught = $ex; 352 + } catch (Throwable $ex) { 353 + $caught = $ex; 360 354 } 361 - $this->popProfilerRule($rule); 362 355 363 356 if ($caught) { 364 357 throw $ex; ··· 419 412 return $result; 420 413 } 421 414 422 - protected function doesConditionMatch( 415 + private function doesConditionMatch( 423 416 HeraldRule $rule, 424 417 HeraldCondition $condition, 425 - HeraldAdapter $object) { 418 + HeraldAdapter $adapter) { 426 419 427 - $object_value = $this->getConditionObjectValue($condition, $object); 428 420 $transcript = $this->newConditionTranscript($rule, $condition); 429 421 422 + $caught = null; 423 + $result_data = array(); 424 + 430 425 try { 431 - $result = $object->doesConditionMatch( 432 - $this, 426 + $field_key = $condition->getFieldName(); 427 + 428 + $field_value = $this->getProfiledObjectFieldValue( 429 + $adapter, 430 + $field_key); 431 + 432 + $is_match = $this->getProfiledConditionMatch( 433 + $adapter, 433 434 $rule, 434 435 $condition, 435 - $object_value); 436 + $field_value); 437 + if ($is_match) { 438 + $result_code = HeraldConditionResult::RESULT_MATCHED; 439 + } else { 440 + $result_code = HeraldConditionResult::RESULT_FAILED; 441 + } 436 442 } catch (HeraldInvalidConditionException $ex) { 437 - $result = false; 438 - $transcript->setNote($ex->getMessage()); 443 + $result_code = HeraldConditionResult::RESULT_INVALID; 444 + $caught = $ex; 445 + } catch (Exception $ex) { 446 + $result_code = HeraldConditionResult::RESULT_EXCEPTION; 447 + $caught = $ex; 448 + } catch (Throwable $ex) { 449 + $result_code = HeraldConditionResult::RESULT_EXCEPTION; 450 + $caught = $ex; 439 451 } 440 452 453 + if ($caught) { 454 + $result_data = array( 455 + 'exception.class' => get_class($caught), 456 + 'exception.message' => $ex->getMessage(), 457 + ); 458 + } 459 + 460 + $result = HeraldConditionResult::newFromResultCode($result_code) 461 + ->setResultData($result_data); 462 + 441 463 $transcript->setResult($result); 442 464 443 - return $result; 465 + if ($caught) { 466 + throw $caught; 467 + } 468 + 469 + return $result->getIsMatch(); 444 470 } 445 471 446 - protected function getConditionObjectValue( 472 + private function getProfiledConditionMatch( 473 + HeraldAdapter $adapter, 474 + HeraldRule $rule, 447 475 HeraldCondition $condition, 448 - HeraldAdapter $object) { 476 + $field_value) { 477 + 478 + // Here, we're profiling the cost to match the condition value against 479 + // whatever test is configured. Normally, this cost should be very 480 + // small (<<1ms) since it amounts to a single comparison: 481 + // 482 + // [ Task author ][ is any of ][ alice ] 483 + // 484 + // However, it may be expensive in some cases, particularly if you 485 + // write a rule with a very creative regular expression that backtracks 486 + // explosively. 487 + // 488 + // At time of writing, the "Another Herald Rule" field is also 489 + // evaluated inside the matching function. This may be arbitrarily 490 + // expensive (it can prompt us to execute any finite number of other 491 + // Herald rules), although we'll push the profiler stack appropriately 492 + // so we don't count the evaluation time against this rule in the final 493 + // profile. 494 + 495 + $this->pushProfilerRule($rule); 496 + 497 + $caught = null; 498 + try { 499 + $is_match = $adapter->doesConditionMatch( 500 + $this, 501 + $rule, 502 + $condition, 503 + $field_value); 504 + } catch (Exception $ex) { 505 + $caught = $ex; 506 + } catch (Throwable $ex) { 507 + $caught = $ex; 508 + } 449 509 450 - $field = $condition->getFieldName(); 510 + $this->popProfilerRule($rule); 451 511 452 - return $this->getObjectFieldValue($field); 512 + if ($caught) { 513 + throw $caught; 514 + } 515 + 516 + return $is_match; 453 517 } 454 518 455 - public function getObjectFieldValue($field) { 456 - if (!array_key_exists($field, $this->fieldCache)) { 457 - $adapter = $this->object; 519 + private function getProfiledObjectFieldValue( 520 + HeraldAdapter $adapter, 521 + $field_key) { 458 522 459 - $adapter->willGetHeraldField($field); 523 + // Before engaging the profiler, make sure the field class is loaded. 460 524 461 - $caught = null; 525 + $adapter->willGetHeraldField($field_key); 462 526 463 - $this->pushProfilerField($field); 464 - try { 465 - $value = $adapter->getHeraldField($field); 466 - } catch (Exception $ex) { 467 - $caught = $ex; 468 - } 469 - $this->popProfilerField($field); 527 + // The first time we read a field value, we'll actually generate it, which 528 + // may be slow. 470 529 471 - if ($caught) { 472 - throw $caught; 473 - } 530 + // After it is generated for the first time, this will just read it from a 531 + // cache, which should be very fast. 474 532 475 - $this->fieldCache[$field] = $value; 533 + // We still want to profile the request even if it goes to cache so we can 534 + // get an accurate count of how many times we access the field value: when 535 + // trying to improve the performance of Herald rules, it's helpful to know 536 + // how many rules rely on the value of a field which is slow to generate. 537 + 538 + $caught = null; 539 + 540 + $this->pushProfilerField($field_key); 541 + try { 542 + $value = $this->getObjectFieldValue($field_key); 543 + } catch (Exception $ex) { 544 + $caught = $ex; 545 + } catch (Throwable $ex) { 546 + $caught = $ex; 547 + } 548 + $this->popProfilerField($field_key); 549 + 550 + if ($caught) { 551 + throw $caught; 476 552 } 477 553 478 - return $this->fieldCache[$field]; 554 + return $value; 555 + } 556 + 557 + private function getObjectFieldValue($field_key) { 558 + if (array_key_exists($field_key, $this->fieldExceptions)) { 559 + throw $this->fieldExceptions[$field_key]; 560 + } 561 + 562 + if (array_key_exists($field_key, $this->fieldCache)) { 563 + return $this->fieldCache[$field_key]; 564 + } 565 + 566 + $adapter = $this->object; 567 + 568 + $caught = null; 569 + try { 570 + $value = $adapter->getHeraldField($field_key); 571 + } catch (Exception $ex) { 572 + $caught = $ex; 573 + } catch (Throwable $ex) { 574 + $caught = $ex; 575 + } 576 + 577 + if ($caught) { 578 + $this->fieldExceptions[$field_key] = $caught; 579 + throw $caught; 580 + } 581 + 582 + $this->fieldCache[$field_key] = $value; 583 + 584 + return $value; 479 585 } 480 586 481 587 protected function getRuleEffects( ··· 639 745 640 746 $forbidden_reason = $this->forbiddenFields[$field_key]; 641 747 if ($forbidden_reason !== null) { 748 + $result_code = HeraldConditionResult::RESULT_OBJECT_STATE; 749 + $result_data = array( 750 + 'reason' => $forbidden_reason, 751 + ); 752 + 753 + $result = HeraldConditionResult::newFromResultCode($result_code) 754 + ->setResultData($result_data); 755 + 642 756 $this->newConditionTranscript($rule, $condition) 643 - ->setResult(HeraldConditionTranscript::RESULT_FORBIDDEN) 644 - ->setNote($forbidden_reason); 757 + ->setResult($result); 645 758 646 759 $is_forbidden = true; 647 760 }
+177
src/applications/herald/storage/transcript/HeraldConditionResult.php
··· 1 + <?php 2 + 3 + final class HeraldConditionResult 4 + extends Phobject { 5 + 6 + const RESULT_MATCHED = 'matched'; 7 + const RESULT_FAILED = 'failed'; 8 + const RESULT_OBJECT_STATE = 'object-state'; 9 + const RESULT_INVALID = 'invalid'; 10 + const RESULT_EXCEPTION = 'exception'; 11 + const RESULT_UNKNOWN = 'unknown'; 12 + 13 + private $resultCode; 14 + private $resultData = array(); 15 + 16 + public function toMap() { 17 + return array( 18 + 'code' => $this->getResultCode(), 19 + 'data' => $this->getResultData(), 20 + ); 21 + } 22 + 23 + public static function newFromMap(array $map) { 24 + $result_code = idx($map, 'code'); 25 + $result = self::newFromResultCode($result_code); 26 + 27 + $result_data = idx($map, 'data', array()); 28 + $result->setResultData($result_data); 29 + 30 + return $result; 31 + } 32 + 33 + public static function newFromResultCode($result_code) { 34 + $map = self::getResultSpecification($result_code); 35 + 36 + $result = new self(); 37 + $result->resultCode = $result_code; 38 + 39 + return $result; 40 + } 41 + 42 + public function getResultCode() { 43 + return $this->resultCode; 44 + } 45 + 46 + private function getResultData() { 47 + return $this->resultData; 48 + } 49 + 50 + public function getIconIcon() { 51 + return $this->getSpecificationProperty('icon'); 52 + } 53 + 54 + public function getIconColor() { 55 + return $this->getSpecificationProperty('color.icon'); 56 + } 57 + 58 + public function getIsMatch() { 59 + return ($this->getSpecificationProperty('match') === true); 60 + } 61 + 62 + public function getName() { 63 + return $this->getSpecificationProperty('name'); 64 + } 65 + 66 + public function newDetailsView() { 67 + switch ($this->resultCode) { 68 + case self::RESULT_OBJECT_STATE: 69 + $reason = $this->getDataProperty('reason'); 70 + $details = HeraldStateReasons::getExplanation($reason); 71 + break; 72 + case self::RESULT_INVALID: 73 + case self::RESULT_EXCEPTION: 74 + $error_class = $this->getDataProperty('exception.class'); 75 + $error_message = $this->getDataProperty('exception.message'); 76 + 77 + if (!strlen($error_class)) { 78 + $error_class = pht('Unknown Error'); 79 + } 80 + 81 + switch ($error_class) { 82 + case 'HeraldInvalidConditionException': 83 + $error_class = pht('Invalid Condition'); 84 + break; 85 + } 86 + 87 + if (!strlen($error_message)) { 88 + $error_message = pht( 89 + 'An unknown error occurred while evaluating this condition. No '. 90 + 'additional information is available.'); 91 + } 92 + 93 + $details = pht( 94 + '%s: %s', 95 + phutil_tag('strong', array(), $error_class), 96 + phutil_escape_html_newlines($error_message)); 97 + break; 98 + $details = 'exception'; 99 + break; 100 + default: 101 + $details = null; 102 + break; 103 + } 104 + 105 + return $details; 106 + } 107 + 108 + public function setResultData(array $result_data) { 109 + $this->resultData = $result_data; 110 + return $this; 111 + } 112 + 113 + private function getDataProperty($key) { 114 + $data = $this->getResultData(); 115 + return idx($data, $key); 116 + } 117 + 118 + private function getSpecificationProperty($key) { 119 + $map = self::getResultSpecification($this->resultCode); 120 + return $map[$key]; 121 + } 122 + 123 + private static function getResultSpecification($result_code) { 124 + $map = self::getResultSpecificationMap(); 125 + 126 + if (!isset($map[$result_code])) { 127 + throw new Exception( 128 + pht( 129 + 'Condition result "%s" is unknown.', 130 + $result_code)); 131 + } 132 + 133 + return $map[$result_code]; 134 + } 135 + 136 + private static function getResultSpecificationMap() { 137 + return array( 138 + self::RESULT_MATCHED => array( 139 + 'match' => true, 140 + 'icon' => 'fa-check', 141 + 'color.icon' => 'green', 142 + 'name' => pht('Passed'), 143 + ), 144 + self::RESULT_FAILED => array( 145 + 'match' => false, 146 + 'icon' => 'fa-times', 147 + 'color.icon' => 'red', 148 + 'name' => pht('Failed'), 149 + ), 150 + self::RESULT_OBJECT_STATE => array( 151 + 'match' => null, 152 + 'icon' => 'fa-ban', 153 + 'color.icon' => 'indigo', 154 + 'name' => pht('Forbidden'), 155 + ), 156 + self::RESULT_INVALID => array( 157 + 'match' => null, 158 + 'icon' => 'fa-exclamation-triangle', 159 + 'color.icon' => 'yellow', 160 + 'name' => pht('Invalid'), 161 + ), 162 + self::RESULT_EXCEPTION => array( 163 + 'match' => null, 164 + 'icon' => 'fa-exclamation-triangle', 165 + 'color.icon' => 'red', 166 + 'name' => pht('Exception'), 167 + ), 168 + self::RESULT_UNKNOWN => array( 169 + 'match' => null, 170 + 'icon' => 'fa-question', 171 + 'color.icon' => 'grey', 172 + 'name' => pht('Unknown'), 173 + ), 174 + ); 175 + } 176 + 177 + }
+37 -17
src/applications/herald/storage/transcript/HeraldConditionTranscript.php
··· 7 7 protected $fieldName; 8 8 protected $condition; 9 9 protected $testValue; 10 - protected $note; 11 - protected $result; 10 + protected $resultMap; 11 + 12 + // See T13586. Older versions of this record stored a boolean true, boolean 13 + // false, or the string "forbidden" in the "$result" field. They stored a 14 + // human-readable English-language message or a state code in the "$note" 15 + // field. 16 + 17 + // The modern record does not use either field. 12 18 13 - const RESULT_FORBIDDEN = 'forbidden'; 19 + protected $result; 20 + protected $note; 14 21 15 22 public function setRuleID($rule_id) { 16 23 $this->ruleID = $rule_id; ··· 57 64 return $this->testValue; 58 65 } 59 66 60 - public function setNote($note) { 61 - $this->note = $note; 67 + public function setResult(HeraldConditionResult $result) { 68 + $this->resultMap = $result->toMap(); 62 69 return $this; 63 70 } 64 71 65 - public function getNote() { 66 - return $this->note; 67 - } 72 + public function getResult() { 73 + $map = $this->resultMap; 74 + 75 + if (is_array($map)) { 76 + $result = HeraldConditionResult::newFromMap($map); 77 + } else { 78 + $legacy_result = $this->result; 79 + 80 + $result_data = array(); 68 81 69 - public function setResult($result) { 70 - $this->result = $result; 71 - return $this; 72 - } 82 + if ($legacy_result === 'forbidden') { 83 + $result_code = HeraldConditionResult::RESULT_OBJECT_STATE; 84 + $result_data = array( 85 + 'reason' => $this->note, 86 + ); 87 + } else if ($legacy_result === true) { 88 + $result_code = HeraldConditionResult::RESULT_MATCHED; 89 + } else if ($legacy_result === false) { 90 + $result_code = HeraldConditionResult::RESULT_FAILED; 91 + } else { 92 + $result_code = HeraldConditionResult::RESULT_UNKNOWN; 93 + } 73 94 74 - public function getResult() { 75 - return $this->result; 76 - } 95 + $result = HeraldConditionResult::newFromResultCode($result_code) 96 + ->setResultData($result_data); 97 + } 77 98 78 - public function isForbidden() { 79 - return ($this->getResult() === self::RESULT_FORBIDDEN); 99 + return $result; 80 100 } 81 101 82 102 }
+2
webroot/rsrc/css/application/herald/herald-test.css
··· 4 4 5 5 .herald-condition-note { 6 6 color: {$red}; 7 + padding: 4px 0; 8 + margin: 4px 0 8px; 7 9 } 8 10 9 11 textarea.herald-field-value-transcript {