diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2c0b26cad4..70e3eddf13 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -177,6 +177,27 @@ phutil_register_library_map(array( 'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/svn', 'DiffusionSvnHistoryQuery' => 'applications/diffusion/query/history/svn', 'DiffusionView' => 'applications/diffusion/view/base', + 'DryRunHeraldable' => 'applications/herald/heraldable/dryrun', + 'HeraldAction' => 'applications/herald/storage/action', + 'HeraldActionConfig' => 'applications/herald/config/action', + 'HeraldApplyTranscript' => 'applications/herald/storage/transcript/apply', + 'HeraldCondition' => 'applications/herald/storage/condition', + 'HeraldConditionConfig' => 'applications/herald/config/condition', + 'HeraldConditionTranscript' => 'applications/herald/storage/transcript/condition', + 'HeraldContentTypeConfig' => 'applications/herald/config/contenttype', + 'HeraldDAO' => 'applications/herald/storage/base', + 'HeraldEffect' => 'applications/herald/engine/effect', + 'HeraldEngine' => 'applications/herald/engine/engine', + 'HeraldFieldConfig' => 'applications/herald/config/field', + 'HeraldInvalidConditionException' => 'applications/herald/engine/engine/exception', + 'HeraldInvalidFieldException' => 'applications/herald/engine/engine/exception', + 'HeraldObjectTranscript' => 'applications/herald/storage/transcript/object', + 'HeraldRecursiveConditionsException' => 'applications/herald/engine/engine/exception', + 'HeraldRule' => 'applications/herald/storage/rule', + 'HeraldRuleTranscript' => 'applications/herald/storage/transcript/rule', + 'HeraldTranscript' => 'applications/herald/storage/transcript/base', + 'HeraldValueTypeConfig' => 'applications/herald/config/valuetype', + 'IHeraldable' => 'applications/herald/heraldable/base', 'Javelin' => 'infrastructure/javelin/api', 'LiskDAO' => 'storage/lisk/dao', 'ManiphestController' => 'applications/maniphest/controller/base', @@ -504,6 +525,12 @@ phutil_register_library_map(array( 'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionSvnHistoryQuery' => 'DiffusionHistoryQuery', 'DiffusionView' => 'AphrontView', + 'HeraldAction' => 'HeraldDAO', + 'HeraldApplyTranscript' => 'HeraldDAO', + 'HeraldCondition' => 'HeraldDAO', + 'HeraldDAO' => 'PhabricatorLiskDAO', + 'HeraldRule' => 'HeraldDAO', + 'HeraldTranscript' => 'HeraldDAO', 'ManiphestController' => 'PhabricatorController', 'ManiphestDAO' => 'PhabricatorLiskDAO', 'ManiphestTask' => 'ManiphestDAO', @@ -663,5 +690,9 @@ phutil_register_library_map(array( ), 'requires_interface' => array( + 'DryRunHeraldable' => + array( + 0 => 'IHeraldable', + ), ), )); diff --git a/src/applications/herald/config/action/HeraldActionConfig.php b/src/applications/herald/config/action/HeraldActionConfig.php new file mode 100644 index 0000000000..6f806822cc --- /dev/null +++ b/src/applications/herald/config/action/HeraldActionConfig.php @@ -0,0 +1,72 @@ + 'Add emails to CC', + self::ACTION_REMOVE_CC => 'Remove emails from CC', + self::ACTION_EMAIL => 'Send an email to', + self::ACTION_NOTHING => 'Do nothing', + ); + } + + public static function getActionMapForContentType($type) { + $map = self::getActionMap(); + switch ($type) { + case HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL: + return array_select_keys( + $map, + array( + self::ACTION_ADD_CC, + self::ACTION_REMOVE_CC, + self::ACTION_NOTHING, + )); + case HeraldContentTypeConfig::CONTENT_TYPE_COMMIT: + return array_select_keys( + $map, + array( + self::ACTION_EMAIL, + self::ACTION_NOTHING, + )); + case HeraldContentTypeConfig::CONTENT_TYPE_MERGE: + return array_select_keys( + $map, + array( + self::ACTION_EMAIL, + self::ACTION_NOTHING, + )); + case HeraldContentTypeConfig::CONTENT_TYPE_OWNERS: + return array_select_keys( + $map, + array( + self::ACTION_EMAIL, + self::ACTION_NOTHING, + )); + default: + throw new Exception("Unknown content type '{$type}'."); + } + } + +} diff --git a/src/applications/herald/config/action/__init__.php b/src/applications/herald/config/action/__init__.php new file mode 100644 index 0000000000..67737f7071 --- /dev/null +++ b/src/applications/herald/config/action/__init__.php @@ -0,0 +1,14 @@ + 'contains', + self::CONDITION_NOT_CONTAINS => 'does not contain', + self::CONDITION_IS => 'is', + self::CONDITION_IS_NOT => 'is not', + self::CONDITION_IS_ANY => 'is any of', + self::CONDITION_IS_NOT_ANY => 'is not any of', + self::CONDITION_INCLUDE_ALL => 'include all of', + self::CONDITION_INCLUDE_ANY => 'include any of', + self::CONDITION_INCLUDE_NONE => 'include none of', + self::CONDITION_IS_ME => 'is myself', + self::CONDITION_IS_NOT_ME => 'is not myself', + self::CONDITION_REGEXP => 'matches regexp', + self::CONDITION_RULE => 'matches:', + self::CONDITION_NOT_RULE => 'does not match:', + self::CONDITION_EXISTS => 'exists', + self::CONDITION_NOT_EXISTS => 'does not exist', + self::CONDITION_REGEXP_PAIR => 'matches regexp pair', + ); + + return $map; + } + + public static function getConditionMapForField($field) { + $map = self::getConditionMap(); + switch ($field) { + case HeraldFieldConfig::FIELD_TITLE: + case HeraldFieldConfig::FIELD_BODY: + return array_select_keys( + $map, + array( + self::CONDITION_CONTAINS, + self::CONDITION_NOT_CONTAINS, + self::CONDITION_IS, + self::CONDITION_IS_NOT, + self::CONDITION_REGEXP, + )); + case HeraldFieldConfig::FIELD_AUTHOR: + case HeraldFieldConfig::FIELD_REPOSITORY: + case HeraldFieldConfig::FIELD_REVIEWER: + case HeraldFieldConfig::FIELD_MERGE_REQUESTER: + return array_select_keys( + $map, + array( + self::CONDITION_IS_ANY, + self::CONDITION_IS_NOT_ANY, + )); + case HeraldFieldConfig::FIELD_TAGS: + case HeraldFieldConfig::FIELD_REVIEWERS: + case HeraldFieldConfig::FIELD_CC: + case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVIEWERS: + case HeraldFieldConfig::FIELD_DIFFERENTIAL_CCS: + return array_select_keys( + $map, + array( + self::CONDITION_INCLUDE_ALL, + self::CONDITION_INCLUDE_ANY, + self::CONDITION_INCLUDE_NONE, + )); + case HeraldFieldConfig::FIELD_DIFF_FILE: + return array_select_keys( + $map, + array( + self::CONDITION_CONTAINS, + self::CONDITION_REGEXP, + )); + case HeraldFieldConfig::FIELD_DIFF_CONTENT: + return array_select_keys( + $map, + array( + self::CONDITION_CONTAINS, + self::CONDITION_REGEXP, + self::CONDITION_REGEXP_PAIR, + )); + case HeraldFieldConfig::FIELD_RULE: + return array_select_keys( + $map, + array( + self::CONDITION_RULE, + self::CONDITION_NOT_RULE, + )); + case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE: + case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER: + return array_select_keys( + $map, + array( + self::CONDITION_INCLUDE_ANY, + self::CONDITION_INCLUDE_NONE, + )); + case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVISION: + return array_select_keys( + $map, + array( + self::CONDITION_EXISTS, + self::CONDITION_NOT_EXISTS, + )); + default: + throw new Exception("Unknown field type '{$field}'."); + } + } + +} diff --git a/src/applications/herald/config/condition/__init__.php b/src/applications/herald/config/condition/__init__.php new file mode 100644 index 0000000000..68fbcc4e6d --- /dev/null +++ b/src/applications/herald/config/condition/__init__.php @@ -0,0 +1,14 @@ + 'Differential Revisions', + self::CONTENT_TYPE_COMMIT => 'Commits', + self::CONTENT_TYPE_MERGE => 'Merge Requests', + self::CONTENT_TYPE_OWNERS => 'Owners Changes', + ); + return $map; + } +} diff --git a/src/applications/herald/config/contenttype/__init__.php b/src/applications/herald/config/contenttype/__init__.php new file mode 100644 index 0000000000..0224687e6d --- /dev/null +++ b/src/applications/herald/config/contenttype/__init__.php @@ -0,0 +1,10 @@ + 'Title', + self::FIELD_BODY => 'Body', + self::FIELD_AUTHOR => 'Author', + self::FIELD_REVIEWER => 'Reviewer', + self::FIELD_REVIEWERS => 'Reviewers', + self::FIELD_CC => 'CCs', + self::FIELD_TAGS => 'Tags', + self::FIELD_DIFF_FILE => 'Any changed filename', + self::FIELD_DIFF_CONTENT => 'Any changed file content', + self::FIELD_REPOSITORY => 'Repository', + self::FIELD_RULE => 'Another Herald rule', + self::FIELD_AFFECTED_PACKAGE => 'Any affected package', + self::FIELD_AFFECTED_PACKAGE_OWNER => "Any affected package's owner", + self::FIELD_DIFFERENTIAL_REVISION => 'Differential revision', + self::FIELD_DIFFERENTIAL_REVIEWERS => 'Differential reviewers', + self::FIELD_DIFFERENTIAL_CCS => 'Differential CCs', + self::FIELD_MERGE_REQUESTER => 'Merge requester' + ); + + return $map; + } + + public static function getFieldMapForContentType($type) { + $map = self::getFieldMap(); + + switch ($type) { + case HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL: + return array_select_keys( + $map, + array( + self::FIELD_TITLE, + self::FIELD_BODY, + self::FIELD_AUTHOR, + self::FIELD_REVIEWERS, + self::FIELD_CC, + self::FIELD_REPOSITORY, + self::FIELD_DIFF_FILE, + self::FIELD_DIFF_CONTENT, + self::FIELD_RULE, + self::FIELD_AFFECTED_PACKAGE, + self::FIELD_AFFECTED_PACKAGE_OWNER, + )); + case HeraldContentTypeConfig::CONTENT_TYPE_COMMIT: + return array_select_keys( + $map, + array( + self::FIELD_BODY, + self::FIELD_AUTHOR, + self::FIELD_REVIEWER, + self::FIELD_REPOSITORY, + self::FIELD_DIFF_FILE, + self::FIELD_DIFF_CONTENT, + self::FIELD_RULE, + self::FIELD_AFFECTED_PACKAGE, + self::FIELD_AFFECTED_PACKAGE_OWNER, + self::FIELD_DIFFERENTIAL_REVISION, + self::FIELD_DIFFERENTIAL_REVIEWERS, + self::FIELD_DIFFERENTIAL_CCS, + )); + case HeraldContentTypeConfig::CONTENT_TYPE_MERGE: + return array_select_keys( + $map, + array( + self::FIELD_BODY, + self::FIELD_AUTHOR, + self::FIELD_REVIEWER, + self::FIELD_REPOSITORY, + self::FIELD_DIFF_FILE, + self::FIELD_DIFF_CONTENT, + self::FIELD_RULE, + self::FIELD_AFFECTED_PACKAGE, + self::FIELD_AFFECTED_PACKAGE_OWNER, + self::FIELD_DIFFERENTIAL_REVISION, + self::FIELD_DIFFERENTIAL_REVIEWERS, + self::FIELD_DIFFERENTIAL_CCS, + self::FIELD_MERGE_REQUESTER, + )); + case HeraldContentTypeConfig::CONTENT_TYPE_OWNERS: + return array_select_keys( + $map, + array( + self::FIELD_AFFECTED_PACKAGE, + self::FIELD_AFFECTED_PACKAGE_OWNER, + )); + default: + throw new Exception("Unknown content type."); + } + } + +} diff --git a/src/applications/herald/config/field/__init__.php b/src/applications/herald/config/field/__init__.php new file mode 100644 index 0000000000..b8ff30f885 --- /dev/null +++ b/src/applications/herald/config/field/__init__.php @@ -0,0 +1,14 @@ +objectID = $object_id; + return $this; + } + + public function getObjectID() { + return $this->objectID; + } + + public function setAction($action) { + $this->action = $action; + return $this; + } + + public function getAction() { + return $this->action; + } + + public function setTarget($target) { + $this->target = $target; + return $this; + } + + public function getTarget() { + return $this->target; + } + + public function setRuleID($rule_id) { + $this->ruleID = $rule_id; + return $this; + } + + public function getRuleID() { + return $this->ruleID; + } + + public function setEffector($effector) { + $this->effector = $effector; + return $this; + } + + public function getEffector() { + return $this->effector; + } + + public function setReason($reason) { + $this->reason = $reason; + return $this; + } + + public function getReason() { + return $this->reason; + } + +} + diff --git a/src/applications/herald/engine/effect/__init__.php b/src/applications/herald/engine/effect/__init__.php new file mode 100644 index 0000000000..e3782e8021 --- /dev/null +++ b/src/applications/herald/engine/effect/__init__.php @@ -0,0 +1,10 @@ +getHeraldTypeName(); + $rules = HeraldRule::loadAllByContentTypeWithFullData($content_type); + + $engine = new HeraldEngine(); + $effects = $engine->applyRules($rules, $object); + $engine->applyEffects($effects, $object); + + return $engine->getTranscript(); + } + + public function applyRules(array $rules, IHeraldable $object) { + $t_start = microtime(true); + + $rules = mpull($rules, null, 'getID'); + + $this->transcript = new HeraldTranscript(); + $this->transcript->setObjectID((string)$object->getFBID()); + $this->fieldCache = array(); + $this->results = array(); + $this->rules = $rules; + $this->object = $object; + + $effects = array(); + foreach ($rules as $id => $rule) { + + + $this->stack = array(); + try { + $rule_matches = $this->doesRuleMatch($rule, $object); + } catch (HeraldRecursiveConditionsException $ex) { + $names = array(); + foreach ($this->stack as $rule_id => $ignored) { + $names[] = '"'.$rules[$rule_id]->getName().'"'; + } + $names = implode(', ', $names); + foreach ($this->stack as $rule_id => $ignored) { + $xscript = new HeraldRuleTranscript(); + $xscript->setRuleID($rule_id); + $xscript->setResult(false); + $xscript->setReason( + "Rules {$names} are recursively dependent upon one another! ". + "Don't do this! You have formed an unresolvable cycle in the ". + "dependency graph!"); + $xscript->setRuleName($rules[$rule_id]->getName()); + $xscript->setRuleOwner($rules[$rule_id]->getOwnerID()); + $this->transcript->addRuleTranscript($xscript); + } + $rule_matches = false; + } + $this->results[$id] = $rule_matches; + + if ($rule_matches) { + foreach ($this->getRuleEffects($rule, $object) as $effect) { + $effects[] = $effect; + } + } + } + + $object_transcript = new HeraldObjectTranscript(); + $object_transcript->setFBID($object->getFBID()); + $object_transcript->setName($object->getHeraldName()); + $object_transcript->setType($object->getHeraldTypeName()); + $object_transcript->setFields($this->fieldCache); + + $this->transcript->setObjectTranscript($object_transcript); + + $t_end = microtime(true); + + $this->transcript->setDuration($t_end - $t_start); + + return $effects; + } + + public function applyEffects(array $effects, IHeraldable $object) { + if ($object instanceof DryRunHeraldable) { + $this->transcript->setDryRun(true); + } else { + $this->transcript->setDryRun(false); + } + foreach ($object->applyHeraldEffects($effects) as $apply_xscript) { + if (!($apply_xscript instanceof HeraldApplyTranscript)) { + throw new Exception( + "Heraldable must return HeraldApplyTranscripts from ". + "applyHeraldEffect()."); + } + $this->transcript->addApplyTranscript($apply_xscript); + } + } + + public function getTranscript() { + $this->transcript->save(); + return $this->transcript; + } + + protected function doesRuleMatch(HeraldRule $rule, IHeraldable $object) { + $id = $rule->getID(); + + if (isset($this->results[$id])) { + // If we've already evaluated this rule because another rule depends + // on it, we don't need to reevaluate it. + return $this->results[$id]; + } + + if (isset($this->stack[$id])) { + // We've recursed, fail all of the rules on the stack. This happens when + // there's a dependency cycle with "Rule conditions match for rule ..." + // conditions. + foreach ($this->stack as $rule_id => $ignored) { + $this->results[$rule_id] = false; + } + throw new HeraldRecursiveConditionsException(); + } + + $this->stack[$id] = true; + + $all = $rule->getMustMatchAll(); + + $conditions = $rule->getConditions(); + + $result = null; + + $local_version = id(new HeraldRule())->getConfigVersion(); + if ($rule->getConfigVersion() > $local_version) { + $reason = "Rule could not be processed, it was created with a newer ". + "version of Herald."; + $result = false; + } else if (!$conditions) { + $reason = "Rule failed automatically because it has no conditions."; + $result = false; +/* TOOD: Restore this in some form? + } else if (!is_fb_employee($rule->getOwnerID())) { + $reason = "Rule failed automatically because its owner is not an ". + "active employee."; + $result = false; +*/ + } else { + foreach ($conditions as $condition) { + $match = $this->doesConditionMatch($rule, $condition, $object); + + if (!$all && $match) { + $reason = "Any condition matched."; + $result = true; + break; + } + + if ($all && !$match) { + $reason = "Not all conditions matched."; + $result = false; + break; + } + } + + if ($result === null) { + if ($all) { + $reason = "All conditions matched."; + $result = true; + } else { + $reason = "No conditions matched."; + $result = false; + } + } + } + + $rule_transcript = new HeraldRuleTranscript(); + $rule_transcript->setRuleID($rule->getID()); + $rule_transcript->setResult($result); + $rule_transcript->setReason($reason); + $rule_transcript->setRuleName($rule->getName()); + $rule_transcript->setRuleOwner($rule->getOwnerID()); + + $this->transcript->addRuleTranscript($rule_transcript); + + return $result; + } + + protected function doesConditionMatch( + HeraldRule $rule, + HeraldCondition $condition, + IHeraldable $object) { + + $object_value = $this->getConditionObjectValue($condition, $object); + $test_value = $condition->getValue(); + + $cond = $condition->getCondition(); + + $transcript = new HeraldConditionTranscript(); + $transcript->setRuleID($rule->getID()); + $transcript->setConditionID($condition->getID()); + $transcript->setFieldName($condition->getFieldName()); + $transcript->setCondition($cond); + $transcript->setTestValue($test_value); + + $result = null; + switch ($cond) { + case HeraldConditionConfig::CONDITION_CONTAINS: + // "Contains" can take an array of strings, as in "Any changed + // filename" for diffs. + foreach ((array)$object_value as $value) { + $result = (stripos($value, $test_value) !== false); + if ($result) { + break; + } + } + break; + case HeraldConditionConfig::CONDITION_NOT_CONTAINS: + $result = (stripos($object_value, $test_value) === false); + break; + case HeraldConditionConfig::CONDITION_IS: + $result = ($object_value == $test_value); + break; + case HeraldConditionConfig::CONDITION_IS_NOT: + $result = ($object_value != $test_value); + break; + case HeraldConditionConfig::CONDITION_IS_ME: + $result = ($object_value == $rule->getOwnerID()); + break; + case HeraldConditionConfig::CONDITION_IS_NOT_ME: + $result = ($object_value != $rule->getOwnerID()); + break; + case HeraldConditionConfig::CONDITION_IS_ANY: + $test_value = array_flip($test_value); + $result = isset($test_value[$object_value]); + break; + case HeraldConditionConfig::CONDITION_IS_NOT_ANY: + $test_value = array_flip($test_value); + $result = !isset($test_value[$object_value]); + break; + case HeraldConditionConfig::CONDITION_INCLUDE_ALL: + if (!is_array($object_value)) { + $transcript->setNote('Object produced bad value!'); + $result = false; + } else { + $have = array_select_keys(array_flip($object_value), + $test_value); + $result = (count($have) == count($test_value)); + } + break; + case HeraldConditionConfig::CONDITION_INCLUDE_ANY: + $result = (bool)array_select_keys(array_flip($object_value), + $test_value); + break; + case HeraldConditionConfig::CONDITION_INCLUDE_NONE: + $result = !array_select_keys(array_flip($object_value), + $test_value); + break; + case HeraldConditionConfig::CONDITION_EXISTS: + $result = (bool)$object_value; + break; + case HeraldConditionConfig::CONDITION_NOT_EXISTS: + $result = !$object_value; + break; + case HeraldConditionConfig::CONDITION_REGEXP: + foreach ((array)$object_value as $value) { + $result = @preg_match($test_value, $value); + if ($result === false) { + $transcript->setNote( + "Regular expression is not valid!"); + break; + } + if ($result) { + break; + } + } + $result = (bool)$result; + break; + case HeraldConditionConfig::CONDITION_REGEXP_PAIR: + // Match a JSON-encoded pair of regular expressions against a + // dictionary. The first regexp must match the dictionary key, and the + // second regexp must match the dictionary value. If any key/value pair + // in the dictionary matches both regexps, the condition is satisfied. + $regexp_pair = json_decode($test_value, true); + if (!is_array($regexp_pair)) { + $result = false; + $transcript->setNote("Regular expression pair is not valid JSON!"); + break; + } + if (count($regexp_pair) != 2) { + $result = false; + $transcript->setNote("Regular expression pair is not a pair!"); + break; + } + + $key_regexp = array_shift($regexp_pair); + $value_regexp = array_shift($regexp_pair); + + foreach ((array)$object_value as $key => $value) { + $key_matches = @preg_match($key_regexp, $key); + if ($key_matches === false) { + $result = false; + $transcript->setNote("First regular expression is invalid!"); + break 2; + } + if ($key_matches) { + $value_matches = @preg_match($value_regexp, $value); + if ($value_matches === false) { + $result = false; + $transcript->setNote("Second regular expression is invalid!"); + break 2; + } + if ($value_matches) { + $result = true; + break 2; + } + } + } + $result = false; + break; + case HeraldConditionConfig::CONDITION_RULE: + case HeraldConditionConfig::CONDITION_NOT_RULE: + + $rule = idx($this->rules, $test_value); + if (!$rule) { + $transcript->setNote( + "Condition references a rule which does not exist!"); + $result = false; + } else { + $is_not = ($cond == HeraldConditionConfig::CONDITION_NOT_RULE); + $result = $this->doesRuleMatch($rule, $object); + if ($is_not) { + $result = !$result; + } + } + break; + default: + throw new HeraldInvalidConditionException( + "Unknown condition '{$cond}'."); + } + + $transcript->setResult($result); + + $this->transcript->addConditionTranscript($transcript); + + return $result; + } + + protected function getConditionObjectValue( + HeraldCondition $condition, + IHeraldable $object) { + + $field = $condition->getFieldName(); + + return $this->getObjectFieldValue($field); + } + + public function getObjectFieldValue($field) { + if (isset($this->fieldCache[$field])) { + return $this->fieldCache[$field]; + } + + $result = null; + switch ($field) { + case HeraldFieldConfig::FIELD_RULE: + $result = null; + break; + case HeraldFieldConfig::FIELD_TITLE: + case HeraldFieldConfig::FIELD_BODY: + case HeraldFieldConfig::FIELD_DIFF_FILE: + case HeraldFieldConfig::FIELD_DIFF_CONTENT: + // TODO: Type should be string. + $result = $this->object->getHeraldField($field); + break; + case HeraldFieldConfig::FIELD_AUTHOR: + case HeraldFieldConfig::FIELD_REPOSITORY: + case HeraldFieldConfig::FIELD_MERGE_REQUESTER: + // TODO: Type should be fbid. + $result = $this->object->getHeraldField($field); + break; + case HeraldFieldConfig::FIELD_TAGS: + case HeraldFieldConfig::FIELD_REVIEWER: + case HeraldFieldConfig::FIELD_REVIEWERS: + case HeraldFieldConfig::FIELD_CC: + case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVIEWERS: + case HeraldFieldConfig::FIELD_DIFFERENTIAL_CCS: + // TODO: Type should be list. + $result = $this->object->getHeraldField($field); + break; + case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE: + case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER: + $result = $this->object->getHeraldField($field); + if (!is_array($result)) { + throw new HeraldInvalidFieldException( + "Value of field type {$field} is not an array!"); + } + break; + case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVISION: + // TODO: Type should be boolean I guess. + $result = $this->object->getHeraldField($field); + break; + default: + throw new HeraldInvalidConditionException( + "Unknown field type '{$field}'!"); + } + + $this->fieldCache[$field] = $result; + return $result; + } + + protected function getRuleEffects(HeraldRule $rule, IHeraldable $object) { + $effects = array(); + foreach ($rule->getActions() as $action) { + $effect = new HeraldEffect(); + $effect->setObjectID($object->getFBID()); + $effect->setAction($action->getAction()); + $effect->setTarget($action->getTarget()); + + $effect->setRuleID($rule->getID()); + + $name = $rule->getName(); + $id = $rule->getID(); + $effect->setReason( + 'Conditions were met for Herald rule "'.$name.'" (#'.$id.').'); + + $effects[] = $effect; + } + return $effects; + } + +} diff --git a/src/applications/herald/engine/engine/__init__.php b/src/applications/herald/engine/engine/__init__.php new file mode 100644 index 0000000000..5b7eab228c --- /dev/null +++ b/src/applications/herald/engine/engine/__init__.php @@ -0,0 +1,22 @@ + array( + 'target' => self::SERIALIZATION_JSON, + ) + ) + parent::getConfiguration(); + } + + +} diff --git a/src/applications/herald/storage/action/__init__.php b/src/applications/herald/storage/action/__init__.php new file mode 100644 index 0000000000..e1803d6a7a --- /dev/null +++ b/src/applications/herald/storage/action/__init__.php @@ -0,0 +1,12 @@ + array( + 'value' => self::SERIALIZATION_JSON, + ) + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/herald/storage/condition/__init__.php b/src/applications/herald/storage/condition/__init__.php new file mode 100644 index 0000000000..b32ed32bea --- /dev/null +++ b/src/applications/herald/storage/condition/__init__.php @@ -0,0 +1,12 @@ +loadAllWhere( + 'contentType = %s', + $content_type); + $rule_ids = mpull($rules, 'getID'); + + $conditions = id(new HeraldCondition())->loadAllWhere( + 'ruleID in (%Ld)', + $rule_ids); + + $actions = id(new HeraldAction())->loadAllWhere( + 'ruleID in (%Ld)', + $rule_ids); + + $conditions = mgroup($conditions, 'getRuleID'); + $actions = mgroup($actions, 'getRuleID'); + + foreach ($rules as $rule) { + $rule->attachConditions(idx($conditions, $rule->getID(), array())); + $rule->attachActions(idx($actions, $rule->getID(), array())); + } + + return $rules; + } + + public function loadConditions() { + if (!$this->getID()) { + return array(); + } + return id(new HeraldCondition())->loadAllWhere( + 'ruleID = %d', + $this->getID()); + } + + public function attachConditions(array $conditions) { + $this->conditions = $conditions; + return $this; + } + + public function getConditions() { + // TODO: validate conditions have been attached. + return $this->conditions; + } + + public function loadActions() { + if (!$this->getID()) { + return array(); + } + return id(new HeraldAction())->loadAllWhere( + 'ruleID = %d', + $this->getID()); + } + + public function attachActions(array $actions) { + // TODO: validate actions have been attached. + $this->actions = $actions; + return $this; + } + + public function getActions() { + return $this->actions; + } + + public function saveConditions(array $conditions) { + return $this->saveChildren( + id(new HeraldCondition())->getTableName(), + $conditions); + } + + public function saveActions(array $actions) { + return $this->saveChildren( + id(new HeraldAction())->getTableName(), + $actions); + } + + protected function saveChildren($table_name, array $children) { + if (!$this->getID()) { + throw new Exception("Save rule before saving children."); + } + + foreach ($children as $child) { + $child->setRuleID($this->getID()); + } + +// TODO: +// $this->openTransaction(); + $this->getLink('w')->query( + 'DELETE FROM %T WHERE ruleID = %d', + $table_name, + $this->getID()); + foreach ($children as $child) { + $child->save(); + } +// $this->saveTransaction(); + } + + public function delete() { + +// TODO: +// $this->openTransaction(); + $this->getLink('w')->query( + 'DELETE FROM %T WHERE ruleID = %d', + id(new HeraldCondition())->getTableName(), + $this->getID()); + $this->getLink('w')->query( + 'DELETE FROM %T WHERE ruleID = %d', + id(new HeraldAction())->getTableName(), + $this->getID()); + parent::delete(); +// $this->saveTransaction(); + } + +} diff --git a/src/applications/herald/storage/rule/__init__.php b/src/applications/herald/storage/rule/__init__.php new file mode 100644 index 0000000000..d40a9ab37c --- /dev/null +++ b/src/applications/herald/storage/rule/__init__.php @@ -0,0 +1,16 @@ +setAction($effect->getAction()); + $this->setTarget($effect->getTarget()); + $this->setRuleID($effect->getRuleID()); + $this->setEffector($effect->getEffector()); + $this->setReason($effect->getReason()); + $this->setApplied($applied); + $this->setAppliedReason($reason); + + } + + public function getAction() { + return $this->action; + } + + public function getTarget() { + return $this->target; + } + + public function getRuleID() { + return $this->ruleID; + } + + public function getEffector() { + return $this->effector; + } + + public function getReason() { + return $this->reason; + } + + public function getApplied() { + return $this->applied; + } + + public function setAppliedReason($applied_reason) { + $this->appliedReason = $applied_reason; + return $this; + } + + public function getAppliedReason() { + return $this->appliedReason; + } + +} diff --git a/src/applications/herald/storage/transcript/apply/__init__.php b/src/applications/herald/storage/transcript/apply/__init__.php new file mode 100644 index 0000000000..3f4fbdec95 --- /dev/null +++ b/src/applications/herald/storage/transcript/apply/__init__.php @@ -0,0 +1,12 @@ +applyTranscripts as $xscript) { + if ($xscript->getApplied()) { + if ($xscript->getRuleID()) { + $ids[] = $xscript->getRuleID(); + } + } + } + if (!$ids) { + return 'none'; + } + + // A rule may have multiple effects, which will cause it to be listed + // multiple times. + $ids = array_unique($ids); + + foreach ($ids as $k => $id) { + $ids[$k] = '<'.$id.'>'; + } + + return implode(', ', $ids); + } + + protected function getConfiguration() { + // Ugh. Too much of a mess to deal with. + return array( + self::CONFIG_AUX_FBID => 'HERALD_TRANSCRIPT', + self::CONFIG_SERIALIZATION => array( + 'objectTranscript' => self::SERIALIZATION_PHP, + 'ruleTranscripts' => self::SERIALIZATION_PHP, + 'conditionTranscripts' => self::SERIALIZATION_PHP, + 'applyTranscripts' => self::SERIALIZATION_PHP, + ), + ) + parent::getConfiguration(); + } + + public function __construct() { + $this->time = time(); + $this->host = php_uname('n'); + $this->path = realpath($_SERVER['PHP_ROOT']); + } + + public function addApplyTranscript(HeraldApplyTranscript $transcript) { + $this->applyTranscripts[] = $transcript; + return $this; + } + + public function getApplyTranscripts() { + return nonempty($this->applyTranscripts, array()); + } + + public function setDuration($duration) { + $this->duration = $duration; + return $this; + } + + public function setObjectTranscript(HeraldObjectTranscript $transcript) { + $this->objectTranscript = $transcript; + return $this; + } + + public function getObjectTranscript() { + return $this->objectTranscript; + } + + public function addRuleTranscript(HeraldRuleTranscript $transcript) { + $this->ruleTranscripts[$transcript->getRuleID()] = $transcript; + return $this; + } + + public function discardDetails() { + $this->applyTranscripts = null; + $this->ruleTranscripts = null; + $this->objectTranscript = null; + $this->conditionTranscripts = null; + } + + public function getRuleTranscripts() { + return nonempty($this->ruleTranscripts, array()); + } + + public function addConditionTranscript( + HeraldConditionTranscript $transcript) { + $rule_id = $transcript->getRuleID(); + $cond_id = $transcript->getConditionID(); + + $this->conditionTranscripts[$rule_id][$cond_id] = $transcript; + return $this; + } + + public function getConditionTranscriptsForRule($rule_id) { + return idx($this->conditionTranscripts, $rule_id, array()); + } + + public function getMetadataMap() { + return array( + 'Run At Epoch' => date('F jS, g:i A', $this->time), + 'Run On Host' => $this->host.':'.$this->path, + 'Run Duration' => (int)(1000 * $this->duration).' ms', + ); + } + + public function getURI() { + return 'http://tools.facebook.com/herald/transcript/'.$this->getID().'/'; + } + +} diff --git a/src/applications/herald/storage/transcript/base/__init__.php b/src/applications/herald/storage/transcript/base/__init__.php new file mode 100644 index 0000000000..dd3d4f4993 --- /dev/null +++ b/src/applications/herald/storage/transcript/base/__init__.php @@ -0,0 +1,14 @@ +ruleID = $rule_id; + return $this; + } + + public function getRuleID() { + return $this->ruleID; + } + + public function setConditionID($condition_id) { + $this->conditionID = $condition_id; + return $this; + } + + public function getConditionID() { + return $this->conditionID; + } + + public function setFieldName($field_name) { + $this->fieldName = $field_name; + return $this; + } + + public function getFieldName() { + return $this->fieldName; + } + + public function setCondition($condition) { + $this->condition = $condition; + return $this; + } + + public function getCondition() { + return $this->condition; + } + + public function setTestValue($test_value) { + $this->testValue = $test_value; + return $this; + } + + public function getTestValue() { + return $this->testValue; + } + + public function setNote($note) { + $this->note = $note; + return $this; + } + + public function getNote() { + return $this->note; + } + + public function setResult($result) { + $this->result = $result; + return $this; + } + + public function getResult() { + return $this->result; + } +} diff --git a/src/applications/herald/storage/transcript/condition/__init__.php b/src/applications/herald/storage/transcript/condition/__init__.php new file mode 100644 index 0000000000..16c51962d2 --- /dev/null +++ b/src/applications/herald/storage/transcript/condition/__init__.php @@ -0,0 +1,10 @@ +fbid = $fbid; + return $this; + } + + public function getFBID() { + return $this->fbid; + } + + public function setType($type) { + $this->type = $type; + return $this; + } + + public function getType() { + return $this->type; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setFields(array $fields) { + $this->fields = $fields; + return $this; + } + + public function getFields() { + return $this->fields; + } +} diff --git a/src/applications/herald/storage/transcript/object/__init__.php b/src/applications/herald/storage/transcript/object/__init__.php new file mode 100644 index 0000000000..e3e9604f63 --- /dev/null +++ b/src/applications/herald/storage/transcript/object/__init__.php @@ -0,0 +1,10 @@ +result = $result; + return $this; + } + + public function getResult() { + return $this->result; + } + + public function setReason($reason) { + $this->reason = $reason; + return $this; + } + + public function getReason() { + return $this->reason; + } + + public function setRuleID($rule_id) { + $this->ruleID = $rule_id; + return $this; + } + + public function getRuleID() { + return $this->ruleID; + } + + public function setRuleName($rule_name) { + $this->ruleName = $rule_name; + return $this; + } + + public function getRuleName() { + return $this->ruleName; + } + + public function setRuleOwner($rule_owner) { + $this->ruleOwner = $rule_owner; + return $this; + } + + public function getRuleOwner() { + return $this->ruleOwner; + } +} diff --git a/src/applications/herald/storage/transcript/rule/__init__.php b/src/applications/herald/storage/transcript/rule/__init__.php new file mode 100644 index 0000000000..6d346629fb --- /dev/null +++ b/src/applications/herald/storage/transcript/rule/__init__.php @@ -0,0 +1,10 @@ +