[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/herald/adapter/ -> HeraldAdapter.php (source)

   1  <?php
   2  
   3  /**
   4   * @task customfield Custom Field Integration
   5   */
   6  abstract class HeraldAdapter {
   7  
   8    const FIELD_TITLE                  = 'title';
   9    const FIELD_BODY                   = 'body';
  10    const FIELD_AUTHOR                 = 'author';
  11    const FIELD_ASSIGNEE               = 'assignee';
  12    const FIELD_REVIEWER               = 'reviewer';
  13    const FIELD_REVIEWERS              = 'reviewers';
  14    const FIELD_COMMITTER              = 'committer';
  15    const FIELD_CC                     = 'cc';
  16    const FIELD_TAGS                   = 'tags';
  17    const FIELD_DIFF_FILE              = 'diff-file';
  18    const FIELD_DIFF_CONTENT           = 'diff-content';
  19    const FIELD_DIFF_ADDED_CONTENT     = 'diff-added-content';
  20    const FIELD_DIFF_REMOVED_CONTENT   = 'diff-removed-content';
  21    const FIELD_DIFF_ENORMOUS          = 'diff-enormous';
  22    const FIELD_REPOSITORY             = 'repository';
  23    const FIELD_REPOSITORY_PROJECTS    = 'repository-projects';
  24    const FIELD_RULE                   = 'rule';
  25    const FIELD_AFFECTED_PACKAGE       = 'affected-package';
  26    const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
  27    const FIELD_CONTENT_SOURCE         = 'contentsource';
  28    const FIELD_ALWAYS                 = 'always';
  29    const FIELD_AUTHOR_PROJECTS        = 'authorprojects';
  30    const FIELD_PROJECTS               = 'projects';
  31    const FIELD_PUSHER                 = 'pusher';
  32    const FIELD_PUSHER_PROJECTS        = 'pusher-projects';
  33    const FIELD_DIFFERENTIAL_REVISION  = 'differential-revision';
  34    const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
  35    const FIELD_DIFFERENTIAL_CCS       = 'differential-ccs';
  36    const FIELD_DIFFERENTIAL_ACCEPTED  = 'differential-accepted';
  37    const FIELD_IS_MERGE_COMMIT        = 'is-merge-commit';
  38    const FIELD_BRANCHES               = 'branches';
  39    const FIELD_AUTHOR_RAW             = 'author-raw';
  40    const FIELD_COMMITTER_RAW          = 'committer-raw';
  41    const FIELD_IS_NEW_OBJECT          = 'new-object';
  42    const FIELD_TASK_PRIORITY          = 'taskpriority';
  43    const FIELD_TASK_STATUS            = 'taskstatus';
  44    const FIELD_ARCANIST_PROJECT       = 'arcanist-project';
  45    const FIELD_PUSHER_IS_COMMITTER    = 'pusher-is-committer';
  46    const FIELD_PATH                   = 'path';
  47  
  48    const CONDITION_CONTAINS        = 'contains';
  49    const CONDITION_NOT_CONTAINS    = '!contains';
  50    const CONDITION_IS              = 'is';
  51    const CONDITION_IS_NOT          = '!is';
  52    const CONDITION_IS_ANY          = 'isany';
  53    const CONDITION_IS_NOT_ANY      = '!isany';
  54    const CONDITION_INCLUDE_ALL     = 'all';
  55    const CONDITION_INCLUDE_ANY     = 'any';
  56    const CONDITION_INCLUDE_NONE    = 'none';
  57    const CONDITION_IS_ME           = 'me';
  58    const CONDITION_IS_NOT_ME       = '!me';
  59    const CONDITION_REGEXP          = 'regexp';
  60    const CONDITION_RULE            = 'conditions';
  61    const CONDITION_NOT_RULE        = '!conditions';
  62    const CONDITION_EXISTS          = 'exists';
  63    const CONDITION_NOT_EXISTS      = '!exists';
  64    const CONDITION_UNCONDITIONALLY = 'unconditionally';
  65    const CONDITION_NEVER           = 'never';
  66    const CONDITION_REGEXP_PAIR     = 'regexp-pair';
  67    const CONDITION_HAS_BIT         = 'bit';
  68    const CONDITION_NOT_BIT         = '!bit';
  69    const CONDITION_IS_TRUE         = 'true';
  70    const CONDITION_IS_FALSE        = 'false';
  71  
  72    const ACTION_ADD_CC       = 'addcc';
  73    const ACTION_REMOVE_CC    = 'remcc';
  74    const ACTION_EMAIL        = 'email';
  75    const ACTION_NOTHING      = 'nothing';
  76    const ACTION_AUDIT        = 'audit';
  77    const ACTION_FLAG         = 'flag';
  78    const ACTION_ASSIGN_TASK  = 'assigntask';
  79    const ACTION_ADD_PROJECTS = 'addprojects';
  80    const ACTION_ADD_REVIEWERS = 'addreviewers';
  81    const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
  82    const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
  83    const ACTION_BLOCK = 'block';
  84    const ACTION_REQUIRE_SIGNATURE = 'signature';
  85  
  86    const VALUE_TEXT            = 'text';
  87    const VALUE_NONE            = 'none';
  88    const VALUE_EMAIL           = 'email';
  89    const VALUE_USER            = 'user';
  90    const VALUE_TAG             = 'tag';
  91    const VALUE_RULE            = 'rule';
  92    const VALUE_REPOSITORY      = 'repository';
  93    const VALUE_OWNERS_PACKAGE  = 'package';
  94    const VALUE_PROJECT         = 'project';
  95    const VALUE_FLAG_COLOR      = 'flagcolor';
  96    const VALUE_CONTENT_SOURCE  = 'contentsource';
  97    const VALUE_USER_OR_PROJECT = 'userorproject';
  98    const VALUE_BUILD_PLAN      = 'buildplan';
  99    const VALUE_TASK_PRIORITY   = 'taskpriority';
 100    const VALUE_TASK_STATUS     = 'taskstatus';
 101    const VALUE_ARCANIST_PROJECT = 'arcanistprojects';
 102    const VALUE_LEGAL_DOCUMENTS = 'legaldocuments';
 103  
 104    private $contentSource;
 105    private $isNewObject;
 106    private $customFields = false;
 107    private $customActions = null;
 108    private $queuedTransactions = array();
 109  
 110    public function getCustomActions() {
 111      if ($this->customActions === null) {
 112        $custom_actions = id(new PhutilSymbolLoader())
 113          ->setAncestorClass('HeraldCustomAction')
 114          ->loadObjects();
 115  
 116        foreach ($custom_actions as $key => $object) {
 117          if (!$object->appliesToAdapter($this)) {
 118            unset($custom_actions[$key]);
 119          }
 120        }
 121  
 122        $this->customActions = array();
 123        foreach ($custom_actions as $action) {
 124          $key = $action->getActionKey();
 125  
 126          if (array_key_exists($key, $this->customActions)) {
 127            throw new Exception(
 128              'More than one Herald custom action implementation '.
 129              'handles the action key: \''.$key.'\'.');
 130          }
 131  
 132          $this->customActions[$key] = $action;
 133        }
 134      }
 135  
 136      return $this->customActions;
 137    }
 138  
 139    public function setContentSource(PhabricatorContentSource $content_source) {
 140      $this->contentSource = $content_source;
 141      return $this;
 142    }
 143    public function getContentSource() {
 144      return $this->contentSource;
 145    }
 146  
 147    public function getIsNewObject() {
 148      if (is_bool($this->isNewObject)) {
 149        return $this->isNewObject;
 150      }
 151  
 152      throw new Exception(pht('You must setIsNewObject to a boolean first!'));
 153    }
 154    public function setIsNewObject($new) {
 155      $this->isNewObject = (bool) $new;
 156      return $this;
 157    }
 158  
 159    abstract public function getPHID();
 160    abstract public function getHeraldName();
 161  
 162    public function getHeraldField($field_name) {
 163      switch ($field_name) {
 164        case self::FIELD_RULE:
 165          return null;
 166        case self::FIELD_CONTENT_SOURCE:
 167          return $this->getContentSource()->getSource();
 168        case self::FIELD_ALWAYS:
 169          return true;
 170        case self::FIELD_IS_NEW_OBJECT:
 171          return $this->getIsNewObject();
 172        default:
 173          if ($this->isHeraldCustomKey($field_name)) {
 174            return $this->getCustomFieldValue($field_name);
 175          }
 176  
 177          throw new Exception(
 178            "Unknown field '{$field_name}'!");
 179      }
 180    }
 181  
 182    abstract public function applyHeraldEffects(array $effects);
 183  
 184    protected function handleCustomHeraldEffect(HeraldEffect $effect) {
 185      $custom_action = idx($this->getCustomActions(), $effect->getAction());
 186  
 187      if ($custom_action !== null) {
 188        return $custom_action->applyEffect(
 189          $this,
 190          $this->getObject(),
 191          $effect);
 192      }
 193  
 194      return null;
 195    }
 196  
 197    public function isAvailableToUser(PhabricatorUser $viewer) {
 198      $applications = id(new PhabricatorApplicationQuery())
 199        ->setViewer($viewer)
 200        ->withInstalled(true)
 201        ->withClasses(array($this->getAdapterApplicationClass()))
 202        ->execute();
 203  
 204      return !empty($applications);
 205    }
 206  
 207    public function queueTransaction($transaction) {
 208      $this->queuedTransactions[] = $transaction;
 209    }
 210  
 211    public function getQueuedTransactions() {
 212      return $this->queuedTransactions;
 213    }
 214  
 215  
 216    /**
 217     * NOTE: You generally should not override this; it exists to support legacy
 218     * adapters which had hard-coded content types.
 219     */
 220    public function getAdapterContentType() {
 221      return get_class($this);
 222    }
 223  
 224    abstract public function getAdapterContentName();
 225    abstract public function getAdapterContentDescription();
 226    abstract public function getAdapterApplicationClass();
 227    abstract public function getObject();
 228  
 229    public function supportsRuleType($rule_type) {
 230      return false;
 231    }
 232  
 233    public function canTriggerOnObject($object) {
 234      return false;
 235    }
 236  
 237    public function explainValidTriggerObjects() {
 238      return pht('This adapter can not trigger on objects.');
 239    }
 240  
 241    public function getTriggerObjectPHIDs() {
 242      return array($this->getPHID());
 243    }
 244  
 245    public function getAdapterSortKey() {
 246      return sprintf(
 247        '%08d%s',
 248        $this->getAdapterSortOrder(),
 249        $this->getAdapterContentName());
 250    }
 251  
 252    public function getAdapterSortOrder() {
 253      return 1000;
 254    }
 255  
 256  
 257  /* -(  Fields  )------------------------------------------------------------- */
 258  
 259  
 260    public function getFields() {
 261      $fields = array();
 262  
 263      $fields[] = self::FIELD_ALWAYS;
 264      $fields[] = self::FIELD_RULE;
 265  
 266      $custom_fields = $this->getCustomFields();
 267      if ($custom_fields) {
 268        foreach ($custom_fields->getFields() as $custom_field) {
 269          $key = $custom_field->getFieldKey();
 270          $fields[] = $this->getHeraldKeyFromCustomKey($key);
 271        }
 272      }
 273  
 274      return $fields;
 275    }
 276  
 277    public function getFieldNameMap() {
 278      return array(
 279        self::FIELD_TITLE => pht('Title'),
 280        self::FIELD_BODY => pht('Body'),
 281        self::FIELD_AUTHOR => pht('Author'),
 282        self::FIELD_ASSIGNEE => pht('Assignee'),
 283        self::FIELD_COMMITTER => pht('Committer'),
 284        self::FIELD_REVIEWER => pht('Reviewer'),
 285        self::FIELD_REVIEWERS => pht('Reviewers'),
 286        self::FIELD_CC => pht('CCs'),
 287        self::FIELD_TAGS => pht('Tags'),
 288        self::FIELD_DIFF_FILE => pht('Any changed filename'),
 289        self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
 290        self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
 291        self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
 292        self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'),
 293        self::FIELD_REPOSITORY => pht('Repository'),
 294        self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
 295        self::FIELD_RULE => pht('Another Herald rule'),
 296        self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
 297        self::FIELD_AFFECTED_PACKAGE_OWNER =>
 298          pht("Any affected package's owner"),
 299        self::FIELD_CONTENT_SOURCE => pht('Content Source'),
 300        self::FIELD_ALWAYS => pht('Always'),
 301        self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
 302        self::FIELD_PROJECTS => pht('Projects'),
 303        self::FIELD_PUSHER => pht('Pusher'),
 304        self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
 305        self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
 306        self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
 307        self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
 308        self::FIELD_DIFFERENTIAL_ACCEPTED
 309          => pht('Accepted Differential revision'),
 310        self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
 311        self::FIELD_BRANCHES => pht('Commit\'s branches'),
 312        self::FIELD_AUTHOR_RAW => pht('Raw author name'),
 313        self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
 314        self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'),
 315        self::FIELD_TASK_PRIORITY => pht('Task priority'),
 316        self::FIELD_TASK_STATUS => pht('Task status'),
 317        self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'),
 318        self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
 319        self::FIELD_PATH => pht('Path'),
 320      ) + $this->getCustomFieldNameMap();
 321    }
 322  
 323  
 324  /* -(  Conditions  )--------------------------------------------------------- */
 325  
 326  
 327    public function getConditionNameMap() {
 328      return array(
 329        self::CONDITION_CONTAINS        => pht('contains'),
 330        self::CONDITION_NOT_CONTAINS    => pht('does not contain'),
 331        self::CONDITION_IS              => pht('is'),
 332        self::CONDITION_IS_NOT          => pht('is not'),
 333        self::CONDITION_IS_ANY          => pht('is any of'),
 334        self::CONDITION_IS_TRUE         => pht('is true'),
 335        self::CONDITION_IS_FALSE        => pht('is false'),
 336        self::CONDITION_IS_NOT_ANY      => pht('is not any of'),
 337        self::CONDITION_INCLUDE_ALL     => pht('include all of'),
 338        self::CONDITION_INCLUDE_ANY     => pht('include any of'),
 339        self::CONDITION_INCLUDE_NONE    => pht('do not include'),
 340        self::CONDITION_IS_ME           => pht('is myself'),
 341        self::CONDITION_IS_NOT_ME       => pht('is not myself'),
 342        self::CONDITION_REGEXP          => pht('matches regexp'),
 343        self::CONDITION_RULE            => pht('matches:'),
 344        self::CONDITION_NOT_RULE        => pht('does not match:'),
 345        self::CONDITION_EXISTS          => pht('exists'),
 346        self::CONDITION_NOT_EXISTS      => pht('does not exist'),
 347        self::CONDITION_UNCONDITIONALLY => '',  // don't show anything!
 348        self::CONDITION_NEVER           => '',  // don't show anything!
 349        self::CONDITION_REGEXP_PAIR     => pht('matches regexp pair'),
 350        self::CONDITION_HAS_BIT         => pht('has bit'),
 351        self::CONDITION_NOT_BIT         => pht('lacks bit'),
 352      );
 353    }
 354  
 355    public function getConditionsForField($field) {
 356      switch ($field) {
 357        case self::FIELD_TITLE:
 358        case self::FIELD_BODY:
 359        case self::FIELD_COMMITTER_RAW:
 360        case self::FIELD_AUTHOR_RAW:
 361        case self::FIELD_PATH:
 362          return array(
 363            self::CONDITION_CONTAINS,
 364            self::CONDITION_NOT_CONTAINS,
 365            self::CONDITION_IS,
 366            self::CONDITION_IS_NOT,
 367            self::CONDITION_REGEXP,
 368          );
 369        case self::FIELD_REVIEWER:
 370        case self::FIELD_PUSHER:
 371        case self::FIELD_TASK_PRIORITY:
 372        case self::FIELD_TASK_STATUS:
 373        case self::FIELD_ARCANIST_PROJECT:
 374          return array(
 375            self::CONDITION_IS_ANY,
 376            self::CONDITION_IS_NOT_ANY,
 377          );
 378        case self::FIELD_REPOSITORY:
 379        case self::FIELD_ASSIGNEE:
 380        case self::FIELD_AUTHOR:
 381        case self::FIELD_COMMITTER:
 382          return array(
 383            self::CONDITION_IS_ANY,
 384            self::CONDITION_IS_NOT_ANY,
 385            self::CONDITION_EXISTS,
 386            self::CONDITION_NOT_EXISTS,
 387          );
 388        case self::FIELD_TAGS:
 389        case self::FIELD_REVIEWERS:
 390        case self::FIELD_CC:
 391        case self::FIELD_AUTHOR_PROJECTS:
 392        case self::FIELD_PROJECTS:
 393        case self::FIELD_AFFECTED_PACKAGE:
 394        case self::FIELD_AFFECTED_PACKAGE_OWNER:
 395        case self::FIELD_PUSHER_PROJECTS:
 396        case self::FIELD_REPOSITORY_PROJECTS:
 397          return array(
 398            self::CONDITION_INCLUDE_ALL,
 399            self::CONDITION_INCLUDE_ANY,
 400            self::CONDITION_INCLUDE_NONE,
 401            self::CONDITION_EXISTS,
 402            self::CONDITION_NOT_EXISTS,
 403          );
 404        case self::FIELD_DIFF_FILE:
 405        case self::FIELD_BRANCHES:
 406          return array(
 407            self::CONDITION_CONTAINS,
 408            self::CONDITION_REGEXP,
 409          );
 410        case self::FIELD_DIFF_CONTENT:
 411        case self::FIELD_DIFF_ADDED_CONTENT:
 412        case self::FIELD_DIFF_REMOVED_CONTENT:
 413          return array(
 414            self::CONDITION_CONTAINS,
 415            self::CONDITION_REGEXP,
 416            self::CONDITION_REGEXP_PAIR,
 417          );
 418        case self::FIELD_RULE:
 419          return array(
 420            self::CONDITION_RULE,
 421            self::CONDITION_NOT_RULE,
 422          );
 423        case self::FIELD_CONTENT_SOURCE:
 424          return array(
 425            self::CONDITION_IS,
 426            self::CONDITION_IS_NOT,
 427          );
 428        case self::FIELD_ALWAYS:
 429          return array(
 430            self::CONDITION_UNCONDITIONALLY,
 431          );
 432        case self::FIELD_DIFFERENTIAL_REVIEWERS:
 433          return array(
 434            self::CONDITION_EXISTS,
 435            self::CONDITION_NOT_EXISTS,
 436            self::CONDITION_INCLUDE_ALL,
 437            self::CONDITION_INCLUDE_ANY,
 438            self::CONDITION_INCLUDE_NONE,
 439          );
 440        case self::FIELD_DIFFERENTIAL_CCS:
 441          return array(
 442            self::CONDITION_INCLUDE_ALL,
 443            self::CONDITION_INCLUDE_ANY,
 444            self::CONDITION_INCLUDE_NONE,
 445          );
 446        case self::FIELD_DIFFERENTIAL_REVISION:
 447        case self::FIELD_DIFFERENTIAL_ACCEPTED:
 448          return array(
 449            self::CONDITION_EXISTS,
 450            self::CONDITION_NOT_EXISTS,
 451          );
 452        case self::FIELD_IS_MERGE_COMMIT:
 453        case self::FIELD_DIFF_ENORMOUS:
 454        case self::FIELD_IS_NEW_OBJECT:
 455        case self::FIELD_PUSHER_IS_COMMITTER:
 456          return array(
 457            self::CONDITION_IS_TRUE,
 458            self::CONDITION_IS_FALSE,
 459          );
 460        default:
 461          if ($this->isHeraldCustomKey($field)) {
 462            return $this->getCustomFieldConditions($field);
 463          }
 464          throw new Exception(
 465            "This adapter does not define conditions for field '{$field}'!");
 466      }
 467    }
 468  
 469    public function doesConditionMatch(
 470      HeraldEngine $engine,
 471      HeraldRule $rule,
 472      HeraldCondition $condition,
 473      $field_value) {
 474  
 475      $condition_type = $condition->getFieldCondition();
 476      $condition_value = $condition->getValue();
 477  
 478      switch ($condition_type) {
 479        case self::CONDITION_CONTAINS:
 480          // "Contains" can take an array of strings, as in "Any changed
 481          // filename" for diffs.
 482          foreach ((array)$field_value as $value) {
 483            if (stripos($value, $condition_value) !== false) {
 484              return true;
 485            }
 486          }
 487          return false;
 488        case self::CONDITION_NOT_CONTAINS:
 489          return (stripos($field_value, $condition_value) === false);
 490        case self::CONDITION_IS:
 491          return ($field_value == $condition_value);
 492        case self::CONDITION_IS_NOT:
 493          return ($field_value != $condition_value);
 494        case self::CONDITION_IS_ME:
 495          return ($field_value == $rule->getAuthorPHID());
 496        case self::CONDITION_IS_NOT_ME:
 497          return ($field_value != $rule->getAuthorPHID());
 498        case self::CONDITION_IS_ANY:
 499          if (!is_array($condition_value)) {
 500            throw new HeraldInvalidConditionException(
 501              'Expected condition value to be an array.');
 502          }
 503          $condition_value = array_fuse($condition_value);
 504          return isset($condition_value[$field_value]);
 505        case self::CONDITION_IS_NOT_ANY:
 506          if (!is_array($condition_value)) {
 507            throw new HeraldInvalidConditionException(
 508              'Expected condition value to be an array.');
 509          }
 510          $condition_value = array_fuse($condition_value);
 511          return !isset($condition_value[$field_value]);
 512        case self::CONDITION_INCLUDE_ALL:
 513          if (!is_array($field_value)) {
 514            throw new HeraldInvalidConditionException(
 515              'Object produced non-array value!');
 516          }
 517          if (!is_array($condition_value)) {
 518            throw new HeraldInvalidConditionException(
 519              'Expected condition value to be an array.');
 520          }
 521  
 522          $have = array_select_keys(array_fuse($field_value), $condition_value);
 523          return (count($have) == count($condition_value));
 524        case self::CONDITION_INCLUDE_ANY:
 525          return (bool)array_select_keys(
 526            array_fuse($field_value),
 527            $condition_value);
 528        case self::CONDITION_INCLUDE_NONE:
 529          return !array_select_keys(
 530            array_fuse($field_value),
 531            $condition_value);
 532        case self::CONDITION_EXISTS:
 533        case self::CONDITION_IS_TRUE:
 534          return (bool)$field_value;
 535        case self::CONDITION_NOT_EXISTS:
 536        case self::CONDITION_IS_FALSE:
 537          return !$field_value;
 538        case self::CONDITION_UNCONDITIONALLY:
 539          return (bool)$field_value;
 540        case self::CONDITION_NEVER:
 541          return false;
 542        case self::CONDITION_REGEXP:
 543          foreach ((array)$field_value as $value) {
 544            // We add the 'S' flag because we use the regexp multiple times.
 545            // It shouldn't cause any troubles if the flag is already there
 546            // - /.*/S is evaluated same as /.*/SS.
 547            $result = @preg_match($condition_value.'S', $value);
 548            if ($result === false) {
 549              throw new HeraldInvalidConditionException(
 550                'Regular expression is not valid!');
 551            }
 552            if ($result) {
 553              return true;
 554            }
 555          }
 556          return false;
 557        case self::CONDITION_REGEXP_PAIR:
 558          // Match a JSON-encoded pair of regular expressions against a
 559          // dictionary. The first regexp must match the dictionary key, and the
 560          // second regexp must match the dictionary value. If any key/value pair
 561          // in the dictionary matches both regexps, the condition is satisfied.
 562          $regexp_pair = json_decode($condition_value, true);
 563          if (!is_array($regexp_pair)) {
 564            throw new HeraldInvalidConditionException(
 565              'Regular expression pair is not valid JSON!');
 566          }
 567          if (count($regexp_pair) != 2) {
 568            throw new HeraldInvalidConditionException(
 569              'Regular expression pair is not a pair!');
 570          }
 571  
 572          $key_regexp   = array_shift($regexp_pair);
 573          $value_regexp = array_shift($regexp_pair);
 574  
 575          foreach ((array)$field_value as $key => $value) {
 576            $key_matches = @preg_match($key_regexp, $key);
 577            if ($key_matches === false) {
 578              throw new HeraldInvalidConditionException(
 579                'First regular expression is invalid!');
 580            }
 581            if ($key_matches) {
 582              $value_matches = @preg_match($value_regexp, $value);
 583              if ($value_matches === false) {
 584                throw new HeraldInvalidConditionException(
 585                  'Second regular expression is invalid!');
 586              }
 587              if ($value_matches) {
 588                return true;
 589              }
 590            }
 591          }
 592          return false;
 593        case self::CONDITION_RULE:
 594        case self::CONDITION_NOT_RULE:
 595          $rule = $engine->getRule($condition_value);
 596          if (!$rule) {
 597            throw new HeraldInvalidConditionException(
 598              'Condition references a rule which does not exist!');
 599          }
 600  
 601          $is_not = ($condition_type == self::CONDITION_NOT_RULE);
 602          $result = $engine->doesRuleMatch($rule, $this);
 603          if ($is_not) {
 604            $result = !$result;
 605          }
 606          return $result;
 607        case self::CONDITION_HAS_BIT:
 608          return (($condition_value & $field_value) === (int) $condition_value);
 609        case self::CONDITION_NOT_BIT:
 610          return (($condition_value & $field_value) !== (int) $condition_value);
 611        default:
 612          throw new HeraldInvalidConditionException(
 613            "Unknown condition '{$condition_type}'.");
 614      }
 615    }
 616  
 617    public function willSaveCondition(HeraldCondition $condition) {
 618      $condition_type = $condition->getFieldCondition();
 619      $condition_value = $condition->getValue();
 620  
 621      switch ($condition_type) {
 622        case self::CONDITION_REGEXP:
 623          $ok = @preg_match($condition_value, '');
 624          if ($ok === false) {
 625            throw new HeraldInvalidConditionException(
 626              pht(
 627                'The regular expression "%s" is not valid. Regular expressions '.
 628                'must have enclosing characters (e.g. "@/path/to/file@", not '.
 629                '"/path/to/file") and be syntactically correct.',
 630                $condition_value));
 631          }
 632          break;
 633        case self::CONDITION_REGEXP_PAIR:
 634          $json = json_decode($condition_value, true);
 635          if (!is_array($json)) {
 636            throw new HeraldInvalidConditionException(
 637              pht(
 638                'The regular expression pair "%s" is not valid JSON. Enter a '.
 639                'valid JSON array with two elements.',
 640                $condition_value));
 641          }
 642  
 643          if (count($json) != 2) {
 644            throw new HeraldInvalidConditionException(
 645              pht(
 646                'The regular expression pair "%s" must have exactly two '.
 647                'elements.',
 648                $condition_value));
 649          }
 650  
 651          $key_regexp = array_shift($json);
 652          $val_regexp = array_shift($json);
 653  
 654          $key_ok = @preg_match($key_regexp, '');
 655          if ($key_ok === false) {
 656            throw new HeraldInvalidConditionException(
 657              pht(
 658                'The first regexp in the regexp pair, "%s", is not a valid '.
 659                'regexp.',
 660                $key_regexp));
 661          }
 662  
 663          $val_ok = @preg_match($val_regexp, '');
 664          if ($val_ok === false) {
 665            throw new HeraldInvalidConditionException(
 666              pht(
 667                'The second regexp in the regexp pair, "%s", is not a valid '.
 668                'regexp.',
 669                $val_regexp));
 670          }
 671          break;
 672        case self::CONDITION_CONTAINS:
 673        case self::CONDITION_NOT_CONTAINS:
 674        case self::CONDITION_IS:
 675        case self::CONDITION_IS_NOT:
 676        case self::CONDITION_IS_ANY:
 677        case self::CONDITION_IS_NOT_ANY:
 678        case self::CONDITION_INCLUDE_ALL:
 679        case self::CONDITION_INCLUDE_ANY:
 680        case self::CONDITION_INCLUDE_NONE:
 681        case self::CONDITION_IS_ME:
 682        case self::CONDITION_IS_NOT_ME:
 683        case self::CONDITION_RULE:
 684        case self::CONDITION_NOT_RULE:
 685        case self::CONDITION_EXISTS:
 686        case self::CONDITION_NOT_EXISTS:
 687        case self::CONDITION_UNCONDITIONALLY:
 688        case self::CONDITION_NEVER:
 689        case self::CONDITION_HAS_BIT:
 690        case self::CONDITION_NOT_BIT:
 691        case self::CONDITION_IS_TRUE:
 692        case self::CONDITION_IS_FALSE:
 693          // No explicit validation for these types, although there probably
 694          // should be in some cases.
 695          break;
 696        default:
 697          throw new HeraldInvalidConditionException(
 698            pht(
 699              'Unknown condition "%s"!',
 700              $condition_type));
 701      }
 702    }
 703  
 704  
 705  /* -(  Actions  )------------------------------------------------------------ */
 706  
 707    public function getCustomActionsForRuleType($rule_type) {
 708      $results = array();
 709      foreach ($this->getCustomActions() as $custom_action) {
 710        if ($custom_action->appliesToRuleType($rule_type)) {
 711          $results[] = $custom_action;
 712        }
 713      }
 714      return $results;
 715    }
 716  
 717    public function getActions($rule_type) {
 718      $custom_actions = $this->getCustomActionsForRuleType($rule_type);
 719      return mpull($custom_actions, 'getActionKey');
 720    }
 721  
 722    public function getActionNameMap($rule_type) {
 723      switch ($rule_type) {
 724        case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
 725        case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
 726          $standard = array(
 727            self::ACTION_NOTHING      => pht('Do nothing'),
 728            self::ACTION_ADD_CC       => pht('Add emails to CC'),
 729            self::ACTION_REMOVE_CC    => pht('Remove emails from CC'),
 730            self::ACTION_EMAIL        => pht('Send an email to'),
 731            self::ACTION_AUDIT        => pht('Trigger an Audit by'),
 732            self::ACTION_FLAG         => pht('Mark with flag'),
 733            self::ACTION_ASSIGN_TASK  => pht('Assign task to'),
 734            self::ACTION_ADD_PROJECTS => pht('Add projects'),
 735            self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
 736            self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
 737            self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
 738            self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'),
 739            self::ACTION_BLOCK => pht('Block change with message'),
 740          );
 741          break;
 742        case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
 743          $standard = array(
 744            self::ACTION_NOTHING      => pht('Do nothing'),
 745            self::ACTION_ADD_CC       => pht('Add me to CC'),
 746            self::ACTION_REMOVE_CC    => pht('Remove me from CC'),
 747            self::ACTION_EMAIL        => pht('Send me an email'),
 748            self::ACTION_AUDIT        => pht('Trigger an Audit by me'),
 749            self::ACTION_FLAG         => pht('Mark with flag'),
 750            self::ACTION_ASSIGN_TASK  => pht('Assign task to me'),
 751            self::ACTION_ADD_PROJECTS => pht('Add projects'),
 752            self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
 753            self::ACTION_ADD_BLOCKING_REVIEWERS =>
 754              pht('Add me as a blocking reviewer'),
 755          );
 756          break;
 757        default:
 758          throw new Exception("Unknown rule type '{$rule_type}'!");
 759      }
 760  
 761      $custom_actions = $this->getCustomActionsForRuleType($rule_type);
 762      $standard += mpull($custom_actions, 'getActionName', 'getActionKey');
 763  
 764      return $standard;
 765    }
 766  
 767    public function willSaveAction(
 768      HeraldRule $rule,
 769      HeraldAction $action) {
 770  
 771      $target = $action->getTarget();
 772      if (is_array($target)) {
 773        $target = array_keys($target);
 774      }
 775  
 776      $author_phid = $rule->getAuthorPHID();
 777  
 778      $rule_type = $rule->getRuleType();
 779      if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
 780        switch ($action->getAction()) {
 781          case self::ACTION_EMAIL:
 782          case self::ACTION_ADD_CC:
 783          case self::ACTION_REMOVE_CC:
 784          case self::ACTION_AUDIT:
 785          case self::ACTION_ASSIGN_TASK:
 786          case self::ACTION_ADD_REVIEWERS:
 787          case self::ACTION_ADD_BLOCKING_REVIEWERS:
 788            // For personal rules, force these actions to target the rule owner.
 789            $target = array($author_phid);
 790            break;
 791          case self::ACTION_FLAG:
 792            // Make sure flag color is valid; set to blue if not.
 793            $color_map = PhabricatorFlagColor::getColorNameMap();
 794            if (empty($color_map[$target])) {
 795              $target = PhabricatorFlagColor::COLOR_BLUE;
 796            }
 797            break;
 798          case self::ACTION_BLOCK:
 799          case self::ACTION_NOTHING:
 800            break;
 801          default:
 802            throw new HeraldInvalidActionException(
 803              pht(
 804                'Unrecognized action type "%s"!',
 805                $action->getAction()));
 806        }
 807      }
 808  
 809      $action->setTarget($target);
 810    }
 811  
 812  
 813  
 814  /* -(  Values  )------------------------------------------------------------- */
 815  
 816  
 817    public function getValueTypeForFieldAndCondition($field, $condition) {
 818  
 819      if ($this->isHeraldCustomKey($field)) {
 820        $value_type = $this->getCustomFieldValueTypeForFieldAndCondition(
 821          $field,
 822          $condition);
 823        if ($value_type !== null) {
 824          return $value_type;
 825        }
 826      }
 827  
 828      switch ($condition) {
 829        case self::CONDITION_CONTAINS:
 830        case self::CONDITION_NOT_CONTAINS:
 831        case self::CONDITION_REGEXP:
 832        case self::CONDITION_REGEXP_PAIR:
 833          return self::VALUE_TEXT;
 834        case self::CONDITION_IS:
 835        case self::CONDITION_IS_NOT:
 836          switch ($field) {
 837            case self::FIELD_CONTENT_SOURCE:
 838              return self::VALUE_CONTENT_SOURCE;
 839            default:
 840              return self::VALUE_TEXT;
 841          }
 842          break;
 843        case self::CONDITION_IS_ANY:
 844        case self::CONDITION_IS_NOT_ANY:
 845          switch ($field) {
 846            case self::FIELD_REPOSITORY:
 847              return self::VALUE_REPOSITORY;
 848            case self::FIELD_TASK_PRIORITY:
 849              return self::VALUE_TASK_PRIORITY;
 850            case self::FIELD_TASK_STATUS:
 851              return self::VALUE_TASK_STATUS;
 852            case self::FIELD_ARCANIST_PROJECT:
 853              return self::VALUE_ARCANIST_PROJECT;
 854            default:
 855              return self::VALUE_USER;
 856          }
 857          break;
 858        case self::CONDITION_INCLUDE_ALL:
 859        case self::CONDITION_INCLUDE_ANY:
 860        case self::CONDITION_INCLUDE_NONE:
 861          switch ($field) {
 862            case self::FIELD_REPOSITORY:
 863              return self::VALUE_REPOSITORY;
 864            case self::FIELD_CC:
 865              return self::VALUE_EMAIL;
 866            case self::FIELD_TAGS:
 867              return self::VALUE_TAG;
 868            case self::FIELD_AFFECTED_PACKAGE:
 869              return self::VALUE_OWNERS_PACKAGE;
 870            case self::FIELD_AUTHOR_PROJECTS:
 871            case self::FIELD_PUSHER_PROJECTS:
 872            case self::FIELD_PROJECTS:
 873            case self::FIELD_REPOSITORY_PROJECTS:
 874              return self::VALUE_PROJECT;
 875            case self::FIELD_REVIEWERS:
 876              return self::VALUE_USER_OR_PROJECT;
 877            default:
 878              return self::VALUE_USER;
 879          }
 880          break;
 881        case self::CONDITION_IS_ME:
 882        case self::CONDITION_IS_NOT_ME:
 883        case self::CONDITION_EXISTS:
 884        case self::CONDITION_NOT_EXISTS:
 885        case self::CONDITION_UNCONDITIONALLY:
 886        case self::CONDITION_NEVER:
 887        case self::CONDITION_IS_TRUE:
 888        case self::CONDITION_IS_FALSE:
 889          return self::VALUE_NONE;
 890        case self::CONDITION_RULE:
 891        case self::CONDITION_NOT_RULE:
 892          return self::VALUE_RULE;
 893        default:
 894          throw new Exception("Unknown condition '{$condition}'.");
 895      }
 896    }
 897  
 898    public function getValueTypeForAction($action, $rule_type) {
 899      $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
 900  
 901      if ($is_personal) {
 902        switch ($action) {
 903          case self::ACTION_ADD_CC:
 904          case self::ACTION_REMOVE_CC:
 905          case self::ACTION_EMAIL:
 906          case self::ACTION_NOTHING:
 907          case self::ACTION_AUDIT:
 908          case self::ACTION_ASSIGN_TASK:
 909          case self::ACTION_ADD_REVIEWERS:
 910          case self::ACTION_ADD_BLOCKING_REVIEWERS:
 911            return self::VALUE_NONE;
 912          case self::ACTION_FLAG:
 913            return self::VALUE_FLAG_COLOR;
 914          case self::ACTION_ADD_PROJECTS:
 915            return self::VALUE_PROJECT;
 916        }
 917      } else {
 918        switch ($action) {
 919          case self::ACTION_ADD_CC:
 920          case self::ACTION_REMOVE_CC:
 921          case self::ACTION_EMAIL:
 922            return self::VALUE_EMAIL;
 923          case self::ACTION_NOTHING:
 924            return self::VALUE_NONE;
 925          case self::ACTION_ADD_PROJECTS:
 926            return self::VALUE_PROJECT;
 927          case self::ACTION_FLAG:
 928            return self::VALUE_FLAG_COLOR;
 929          case self::ACTION_ASSIGN_TASK:
 930            return self::VALUE_USER;
 931          case self::ACTION_AUDIT:
 932          case self::ACTION_ADD_REVIEWERS:
 933          case self::ACTION_ADD_BLOCKING_REVIEWERS:
 934            return self::VALUE_USER_OR_PROJECT;
 935          case self::ACTION_APPLY_BUILD_PLANS:
 936            return self::VALUE_BUILD_PLAN;
 937          case self::ACTION_REQUIRE_SIGNATURE:
 938            return self::VALUE_LEGAL_DOCUMENTS;
 939          case self::ACTION_BLOCK:
 940            return self::VALUE_TEXT;
 941        }
 942      }
 943  
 944      $custom_action = idx($this->getCustomActions(), $action);
 945      if ($custom_action !== null) {
 946        return $custom_action->getActionType();
 947      }
 948  
 949      throw new Exception("Unknown or invalid action '".$action."'.");
 950    }
 951  
 952  
 953  /* -(  Repetition  )--------------------------------------------------------- */
 954  
 955  
 956    public function getRepetitionOptions() {
 957      return array(
 958        HeraldRepetitionPolicyConfig::EVERY,
 959      );
 960    }
 961  
 962  
 963    public static function applyFlagEffect(HeraldEffect $effect, $phid) {
 964      $color = $effect->getTarget();
 965  
 966      // TODO: Silly that we need to load this again here.
 967      $rule = id(new HeraldRule())->load($effect->getRuleID());
 968      $user = id(new PhabricatorUser())->loadOneWhere(
 969        'phid = %s',
 970        $rule->getAuthorPHID());
 971  
 972      $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
 973      if ($flag) {
 974        return new HeraldApplyTranscript(
 975          $effect,
 976          false,
 977          pht('Object already flagged.'));
 978      }
 979  
 980      $handle = id(new PhabricatorHandleQuery())
 981        ->setViewer($user)
 982        ->withPHIDs(array($phid))
 983        ->executeOne();
 984  
 985      $flag = new PhabricatorFlag();
 986      $flag->setOwnerPHID($user->getPHID());
 987      $flag->setType($handle->getType());
 988      $flag->setObjectPHID($handle->getPHID());
 989  
 990      // TOOD: Should really be transcript PHID, but it doesn't exist yet.
 991      $flag->setReasonPHID($user->getPHID());
 992  
 993      $flag->setColor($color);
 994      $flag->setNote(
 995        pht('Flagged by Herald Rule "%s".', $rule->getName()));
 996      $flag->save();
 997  
 998      return new HeraldApplyTranscript(
 999        $effect,
1000        true,
1001        pht('Added flag.'));
1002    }
1003  
1004    public static function getAllAdapters() {
1005      static $adapters;
1006      if (!$adapters) {
1007        $adapters = id(new PhutilSymbolLoader())
1008          ->setAncestorClass(__CLASS__)
1009          ->loadObjects();
1010        $adapters = msort($adapters, 'getAdapterSortKey');
1011      }
1012      return $adapters;
1013    }
1014  
1015    public static function getAdapterForContentType($content_type) {
1016      $adapters = self::getAllAdapters();
1017  
1018      foreach ($adapters as $adapter) {
1019        if ($adapter->getAdapterContentType() == $content_type) {
1020          return $adapter;
1021        }
1022      }
1023  
1024      throw new Exception(
1025        pht(
1026          'No adapter exists for Herald content type "%s".',
1027          $content_type));
1028    }
1029  
1030    public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
1031      $map = array();
1032  
1033      $adapters = HeraldAdapter::getAllAdapters();
1034      foreach ($adapters as $adapter) {
1035        if (!$adapter->isAvailableToUser($viewer)) {
1036          continue;
1037        }
1038        $type = $adapter->getAdapterContentType();
1039        $name = $adapter->getAdapterContentName();
1040        $map[$type] = $name;
1041      }
1042  
1043      return $map;
1044    }
1045  
1046    public function renderRuleAsText(HeraldRule $rule, array $handles) {
1047      assert_instances_of($handles, 'PhabricatorObjectHandle');
1048  
1049      require_celerity_resource('herald-css');
1050  
1051      $icon = id(new PHUIIconView())
1052        ->setIconFont('fa-chevron-circle-right lightgreytext')
1053        ->addClass('herald-list-icon');
1054  
1055      if ($rule->getMustMatchAll()) {
1056        $match_text = pht('When all of these conditions are met:');
1057      } else {
1058        $match_text = pht('When any of these conditions are met:');
1059      }
1060  
1061      $match_title = phutil_tag(
1062        'p',
1063        array(
1064          'class' => 'herald-list-description',
1065        ),
1066        $match_text);
1067  
1068      $match_list = array();
1069      foreach ($rule->getConditions() as $condition) {
1070        $match_list[] = phutil_tag(
1071          'div',
1072          array(
1073            'class' => 'herald-list-item',
1074          ),
1075          array(
1076            $icon,
1077            $this->renderConditionAsText($condition, $handles),
1078          ));
1079      }
1080  
1081      $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
1082        HeraldRepetitionPolicyConfig::EVERY);
1083  
1084      if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
1085        $action_text =
1086          pht('Take these actions every time this rule matches:');
1087      } else {
1088        $action_text =
1089          pht('Take these actions the first time this rule matches:');
1090      }
1091  
1092      $action_title = phutil_tag(
1093        'p',
1094        array(
1095          'class' => 'herald-list-description',
1096        ),
1097        $action_text);
1098  
1099      $action_list = array();
1100      foreach ($rule->getActions() as $action) {
1101        $action_list[] = phutil_tag(
1102          'div',
1103          array(
1104            'class' => 'herald-list-item',
1105          ),
1106          array(
1107            $icon,
1108            $this->renderActionAsText($action, $handles),
1109          ));
1110      }
1111  
1112      return array(
1113        $match_title,
1114        $match_list,
1115        $action_title,
1116        $action_list,
1117      );
1118    }
1119  
1120    private function renderConditionAsText(
1121      HeraldCondition $condition,
1122      array $handles) {
1123  
1124      $field_type = $condition->getFieldName();
1125  
1126      $default = $this->isHeraldCustomKey($field_type)
1127        ? pht('(Unknown Custom Field "%s")', $field_type)
1128        : pht('(Unknown Field "%s")', $field_type);
1129  
1130      $field_name = idx($this->getFieldNameMap(), $field_type, $default);
1131  
1132      $condition_type = $condition->getFieldCondition();
1133      $condition_name = idx($this->getConditionNameMap(), $condition_type);
1134  
1135      $value = $this->renderConditionValueAsText($condition, $handles);
1136  
1137      return hsprintf('    %s %s %s', $field_name, $condition_name, $value);
1138    }
1139  
1140    private function renderActionAsText(
1141      HeraldAction $action,
1142      array $handles) {
1143      $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
1144  
1145      $action_type = $action->getAction();
1146      $action_name = idx($this->getActionNameMap($rule_global), $action_type);
1147  
1148      $target = $this->renderActionTargetAsText($action, $handles);
1149  
1150      return hsprintf('    %s %s', $action_name, $target);
1151    }
1152  
1153    private function renderConditionValueAsText(
1154      HeraldCondition $condition,
1155      array $handles) {
1156  
1157      $value = $condition->getValue();
1158      if (!is_array($value)) {
1159        $value = array($value);
1160      }
1161      switch ($condition->getFieldName()) {
1162        case self::FIELD_TASK_PRIORITY:
1163          $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
1164          foreach ($value as $index => $val) {
1165            $name = idx($priority_map, $val);
1166            if ($name) {
1167              $value[$index] = $name;
1168            }
1169          }
1170          break;
1171        case self::FIELD_TASK_STATUS:
1172          $status_map = ManiphestTaskStatus::getTaskStatusMap();
1173          foreach ($value as $index => $val) {
1174            $name = idx($status_map, $val);
1175            if ($name) {
1176              $value[$index] = $name;
1177            }
1178          }
1179          break;
1180        case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE:
1181          $change_map =
1182            PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
1183          foreach ($value as $index => $val) {
1184            $name = idx($change_map, $val);
1185            if ($name) {
1186              $value[$index] = $name;
1187            }
1188          }
1189          break;
1190        default:
1191          foreach ($value as $index => $val) {
1192            $handle = idx($handles, $val);
1193            if ($handle) {
1194              $value[$index] = $handle->renderLink();
1195            }
1196          }
1197          break;
1198      }
1199      $value = phutil_implode_html(', ', $value);
1200      return $value;
1201    }
1202  
1203    private function renderActionTargetAsText(
1204      HeraldAction $action,
1205      array $handles) {
1206  
1207      $target = $action->getTarget();
1208      if (!is_array($target)) {
1209        $target = array($target);
1210      }
1211      foreach ($target as $index => $val) {
1212        switch ($action->getAction()) {
1213          case self::ACTION_FLAG:
1214            $target[$index] = PhabricatorFlagColor::getColorName($val);
1215            break;
1216          default:
1217            $handle = idx($handles, $val);
1218            if ($handle) {
1219              $target[$index] = $handle->renderLink();
1220            }
1221            break;
1222        }
1223      }
1224      $target = phutil_implode_html(', ', $target);
1225      return $target;
1226    }
1227  
1228    /**
1229     * Given a @{class:HeraldRule}, this function extracts all the phids that
1230     * we'll want to load as handles later.
1231     *
1232     * This function performs a somewhat hacky approach to figuring out what
1233     * is and is not a phid - try to get the phid type and if the type is
1234     * *not* unknown assume its a valid phid.
1235     *
1236     * Don't try this at home. Use more strongly typed data at home.
1237     *
1238     * Think of the children.
1239     */
1240    public static function getHandlePHIDs(HeraldRule $rule) {
1241      $phids = array($rule->getAuthorPHID());
1242      foreach ($rule->getConditions() as $condition) {
1243        $value = $condition->getValue();
1244        if (!is_array($value)) {
1245          $value = array($value);
1246        }
1247        foreach ($value as $val) {
1248          if (phid_get_type($val) !=
1249              PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
1250            $phids[] = $val;
1251          }
1252        }
1253      }
1254  
1255      foreach ($rule->getActions() as $action) {
1256        $target = $action->getTarget();
1257        if (!is_array($target)) {
1258          $target = array($target);
1259        }
1260        foreach ($target as $val) {
1261          if (phid_get_type($val) !=
1262              PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
1263            $phids[] = $val;
1264          }
1265        }
1266      }
1267  
1268      if ($rule->isObjectRule()) {
1269        $phids[] = $rule->getTriggerObjectPHID();
1270      }
1271  
1272      return $phids;
1273    }
1274  
1275  /* -(  Custom Field Integration  )------------------------------------------- */
1276  
1277  
1278    /**
1279     * Return an object which custom fields can be generated from while editing
1280     * rules. Adapters must return an object from this method to enable custom
1281     * field rules.
1282     *
1283     * Normally, you'll return an empty version of the adapted object, assuming
1284     * it implements @{interface:PhabricatorCustomFieldInterface}:
1285     *
1286     *   return new ApplicationObject();
1287     *
1288     * This is normally the only adapter method you need to override to enable
1289     * Herald rules to run against custom fields.
1290     *
1291     * @return null|PhabricatorCustomFieldInterface Template object.
1292     * @task customfield
1293     */
1294    protected function getCustomFieldTemplateObject() {
1295      return null;
1296    }
1297  
1298  
1299    /**
1300     * Returns the prefix used to namespace Herald fields which are based on
1301     * custom fields.
1302     *
1303     * @return string Key prefix.
1304     * @task customfield
1305     */
1306    private function getCustomKeyPrefix() {
1307      return 'herald.custom/';
1308    }
1309  
1310  
1311    /**
1312     * Determine if a field key is based on a custom field or a regular internal
1313     * field.
1314     *
1315     * @param string Field key.
1316     * @return bool True if the field key is based on a custom field.
1317     * @task customfield
1318     */
1319    private function isHeraldCustomKey($key) {
1320      $prefix = $this->getCustomKeyPrefix();
1321      return (strncmp($key, $prefix, strlen($prefix)) == 0);
1322    }
1323  
1324  
1325    /**
1326     * Convert a custom field key into a Herald field key.
1327     *
1328     * @param string Custom field key.
1329     * @return string Herald field key.
1330     * @task customfield
1331     */
1332    private function getHeraldKeyFromCustomKey($key) {
1333      return $this->getCustomKeyPrefix().$key;
1334    }
1335  
1336  
1337    /**
1338     * Get custom fields for this adapter, if appliable. This will either return
1339     * a field list or `null` if the adapted object does not implement custom
1340     * fields or the adapter does not support them.
1341     *
1342     * @return PhabricatorCustomFieldList|null List of fields, or `null`.
1343     * @task customfield
1344     */
1345    private function getCustomFields() {
1346      if ($this->customFields === false) {
1347        $this->customFields = null;
1348  
1349  
1350        $template_object = $this->getCustomFieldTemplateObject();
1351        if ($template_object) {
1352          $object = $this->getObject();
1353          if (!$object) {
1354            $object = $template_object;
1355          }
1356  
1357          $fields = PhabricatorCustomField::getObjectFields(
1358            $object,
1359            PhabricatorCustomField::ROLE_HERALD);
1360          $fields->setViewer(PhabricatorUser::getOmnipotentUser());
1361          $fields->readFieldsFromStorage($object);
1362  
1363          $this->customFields = $fields;
1364        }
1365      }
1366  
1367      return $this->customFields;
1368    }
1369  
1370  
1371    /**
1372     * Get a custom field by Herald field key, or `null` if it does not exist
1373     * or custom fields are not supported.
1374     *
1375     * @param string Herald field key.
1376     * @return PhabricatorCustomField|null Matching field, if it exists.
1377     * @task customfield
1378     */
1379    private function getCustomField($herald_field_key) {
1380      $fields = $this->getCustomFields();
1381      if (!$fields) {
1382        return null;
1383      }
1384  
1385      foreach ($fields->getFields() as $custom_field) {
1386        $key = $custom_field->getFieldKey();
1387        if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) {
1388          return $custom_field;
1389        }
1390      }
1391  
1392      return null;
1393    }
1394  
1395  
1396    /**
1397     * Get the field map for custom fields.
1398     *
1399     * @return map<string, string> Map of Herald field keys to field names.
1400     * @task customfield
1401     */
1402    private function getCustomFieldNameMap() {
1403      $fields = $this->getCustomFields();
1404      if (!$fields) {
1405        return array();
1406      }
1407  
1408      $map = array();
1409      foreach ($fields->getFields() as $field) {
1410        $key = $field->getFieldKey();
1411        $name = $field->getHeraldFieldName();
1412        $map[$this->getHeraldKeyFromCustomKey($key)] = $name;
1413      }
1414  
1415      return $map;
1416    }
1417  
1418  
1419    /**
1420     * Get the value for a custom field.
1421     *
1422     * @param string Herald field key.
1423     * @return wild Custom field value.
1424     * @task customfield
1425     */
1426    private function getCustomFieldValue($field_key) {
1427      $field = $this->getCustomField($field_key);
1428      if (!$field) {
1429        return null;
1430      }
1431  
1432      return $field->getHeraldFieldValue();
1433    }
1434  
1435  
1436    /**
1437     * Get the Herald conditions for a custom field.
1438     *
1439     * @param string Herald field key.
1440     * @return list<const> List of Herald conditions.
1441     * @task customfield
1442     */
1443    private function getCustomFieldConditions($field_key) {
1444      $field = $this->getCustomField($field_key);
1445      if (!$field) {
1446        return array(
1447          self::CONDITION_NEVER,
1448        );
1449      }
1450  
1451      return $field->getHeraldFieldConditions();
1452    }
1453  
1454  
1455    /**
1456     * Get the Herald value type for a custom field and condition.
1457     *
1458     * @param string Herald field key.
1459     * @param const Herald condition constant.
1460     * @return const|null Herald value type constant, or null to use the default.
1461     * @task customfield
1462     */
1463    private function getCustomFieldValueTypeForFieldAndCondition(
1464      $field_key,
1465      $condition) {
1466  
1467      $field = $this->getCustomField($field_key);
1468      if (!$field) {
1469        return self::VALUE_NONE;
1470      }
1471  
1472      return $field->getHeraldFieldValueType($condition);
1473    }
1474  
1475  
1476  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1