[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/transactions/editor/ -> PhabricatorApplicationTransactionEditor.php (source)

   1  <?php
   2  
   3  /**
   4   * @task mail   Sending Mail
   5   * @task feed   Publishing Feed Stories
   6   * @task search Search Index
   7   * @task files  Integration with Files
   8   */
   9  abstract class PhabricatorApplicationTransactionEditor
  10    extends PhabricatorEditor {
  11  
  12    private $contentSource;
  13    private $object;
  14    private $xactions;
  15  
  16    private $isNewObject;
  17    private $mentionedPHIDs;
  18    private $continueOnNoEffect;
  19    private $continueOnMissingFields;
  20    private $parentMessageID;
  21    private $heraldAdapter;
  22    private $heraldTranscript;
  23    private $subscribers;
  24    private $unmentionablePHIDMap = array();
  25  
  26    private $isPreview;
  27    private $isHeraldEditor;
  28    private $isInverseEdgeEditor;
  29    private $actingAsPHID;
  30    private $disableEmail;
  31  
  32  
  33    /**
  34     * Get the class name for the application this editor is a part of.
  35     *
  36     * Uninstalling the application will disable the editor.
  37     *
  38     * @return string Editor's application class name.
  39     */
  40    abstract public function getEditorApplicationClass();
  41  
  42  
  43    /**
  44     * Get a description of the objects this editor edits, like "Differential
  45     * Revisions".
  46     *
  47     * @return string Human readable description of edited objects.
  48     */
  49    abstract public function getEditorObjectsDescription();
  50  
  51  
  52    public function setActingAsPHID($acting_as_phid) {
  53      $this->actingAsPHID = $acting_as_phid;
  54      return $this;
  55    }
  56  
  57    public function getActingAsPHID() {
  58      if ($this->actingAsPHID) {
  59        return $this->actingAsPHID;
  60      }
  61      return $this->getActor()->getPHID();
  62    }
  63  
  64  
  65    /**
  66     * When the editor tries to apply transactions that have no effect, should
  67     * it raise an exception (default) or drop them and continue?
  68     *
  69     * Generally, you will set this flag for edits coming from "Edit" interfaces,
  70     * and leave it cleared for edits coming from "Comment" interfaces, so the
  71     * user will get a useful error if they try to submit a comment that does
  72     * nothing (e.g., empty comment with a status change that has already been
  73     * performed by another user).
  74     *
  75     * @param bool  True to drop transactions without effect and continue.
  76     * @return this
  77     */
  78    public function setContinueOnNoEffect($continue) {
  79      $this->continueOnNoEffect = $continue;
  80      return $this;
  81    }
  82  
  83    public function getContinueOnNoEffect() {
  84      return $this->continueOnNoEffect;
  85    }
  86  
  87  
  88    /**
  89     * When the editor tries to apply transactions which don't populate all of
  90     * an object's required fields, should it raise an exception (default) or
  91     * drop them and continue?
  92     *
  93     * For example, if a user adds a new required custom field (like "Severity")
  94     * to a task, all existing tasks won't have it populated. When users
  95     * manually edit existing tasks, it's usually desirable to have them provide
  96     * a severity. However, other operations (like batch editing just the
  97     * owner of a task) will fail by default.
  98     *
  99     * By setting this flag for edit operations which apply to specific fields
 100     * (like the priority, batch, and merge editors in Maniphest), these
 101     * operations can continue to function even if an object is outdated.
 102     *
 103     * @param bool  True to continue when transactions don't completely satisfy
 104     *              all required fields.
 105     * @return this
 106     */
 107    public function setContinueOnMissingFields($continue_on_missing_fields) {
 108      $this->continueOnMissingFields = $continue_on_missing_fields;
 109      return $this;
 110    }
 111  
 112    public function getContinueOnMissingFields() {
 113      return $this->continueOnMissingFields;
 114    }
 115  
 116  
 117    /**
 118     * Not strictly necessary, but reply handlers ideally set this value to
 119     * make email threading work better.
 120     */
 121    public function setParentMessageID($parent_message_id) {
 122      $this->parentMessageID = $parent_message_id;
 123      return $this;
 124    }
 125    public function getParentMessageID() {
 126      return $this->parentMessageID;
 127    }
 128  
 129    public function getIsNewObject() {
 130      return $this->isNewObject;
 131    }
 132  
 133    protected function getMentionedPHIDs() {
 134      return $this->mentionedPHIDs;
 135    }
 136  
 137    public function setIsPreview($is_preview) {
 138      $this->isPreview = $is_preview;
 139      return $this;
 140    }
 141  
 142    public function getIsPreview() {
 143      return $this->isPreview;
 144    }
 145  
 146    public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
 147      $this->isInverseEdgeEditor = $is_inverse_edge_editor;
 148      return $this;
 149    }
 150  
 151    public function getIsInverseEdgeEditor() {
 152      return $this->isInverseEdgeEditor;
 153    }
 154  
 155    public function setIsHeraldEditor($is_herald_editor) {
 156      $this->isHeraldEditor = $is_herald_editor;
 157      return $this;
 158    }
 159  
 160    public function getIsHeraldEditor() {
 161      return $this->isHeraldEditor;
 162    }
 163  
 164    /**
 165     * Prevent this editor from generating email when applying transactions.
 166     *
 167     * @param bool  True to disable email.
 168     * @return this
 169     */
 170    public function setDisableEmail($disable_email) {
 171      $this->disableEmail = $disable_email;
 172      return $this;
 173    }
 174  
 175    public function getDisableEmail() {
 176      return $this->disableEmail;
 177    }
 178  
 179    public function setUnmentionablePHIDMap(array $map) {
 180      $this->unmentionablePHIDMap = $map;
 181      return $this;
 182    }
 183  
 184    public function getUnmentionablePHIDMap() {
 185      return $this->unmentionablePHIDMap;
 186    }
 187  
 188    public function getTransactionTypes() {
 189      $types = array();
 190  
 191      if ($this->object instanceof PhabricatorSubscribableInterface) {
 192        $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
 193      }
 194  
 195      if ($this->object instanceof PhabricatorCustomFieldInterface) {
 196        $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
 197      }
 198  
 199      if ($this->object instanceof HarbormasterBuildableInterface) {
 200        $types[] = PhabricatorTransactions::TYPE_BUILDABLE;
 201      }
 202  
 203      if ($this->object instanceof PhabricatorTokenReceiverInterface) {
 204        $types[] = PhabricatorTransactions::TYPE_TOKEN;
 205      }
 206  
 207      if ($this->object instanceof PhabricatorProjectInterface) {
 208        $types[] = PhabricatorTransactions::TYPE_EDGE;
 209      }
 210  
 211      return $types;
 212    }
 213  
 214    private function adjustTransactionValues(
 215      PhabricatorLiskDAO $object,
 216      PhabricatorApplicationTransaction $xaction) {
 217  
 218      if ($xaction->shouldGenerateOldValue()) {
 219        $old = $this->getTransactionOldValue($object, $xaction);
 220        $xaction->setOldValue($old);
 221      }
 222  
 223      $new = $this->getTransactionNewValue($object, $xaction);
 224      $xaction->setNewValue($new);
 225    }
 226  
 227    private function getTransactionOldValue(
 228      PhabricatorLiskDAO $object,
 229      PhabricatorApplicationTransaction $xaction) {
 230      switch ($xaction->getTransactionType()) {
 231        case PhabricatorTransactions::TYPE_SUBSCRIBERS:
 232          return array_values($this->subscribers);
 233        case PhabricatorTransactions::TYPE_VIEW_POLICY:
 234          return $object->getViewPolicy();
 235        case PhabricatorTransactions::TYPE_EDIT_POLICY:
 236          return $object->getEditPolicy();
 237        case PhabricatorTransactions::TYPE_JOIN_POLICY:
 238          return $object->getJoinPolicy();
 239        case PhabricatorTransactions::TYPE_EDGE:
 240          $edge_type = $xaction->getMetadataValue('edge:type');
 241          if (!$edge_type) {
 242            throw new Exception("Edge transaction has no 'edge:type'!");
 243          }
 244  
 245          $old_edges = array();
 246          if ($object->getPHID()) {
 247            $edge_src = $object->getPHID();
 248  
 249            $old_edges = id(new PhabricatorEdgeQuery())
 250              ->withSourcePHIDs(array($edge_src))
 251              ->withEdgeTypes(array($edge_type))
 252              ->needEdgeData(true)
 253              ->execute();
 254  
 255            $old_edges = $old_edges[$edge_src][$edge_type];
 256          }
 257          return $old_edges;
 258        case PhabricatorTransactions::TYPE_CUSTOMFIELD:
 259          // NOTE: Custom fields have their old value pre-populated when they are
 260          // built by PhabricatorCustomFieldList.
 261          return $xaction->getOldValue();
 262        case PhabricatorTransactions::TYPE_COMMENT:
 263          return null;
 264        default:
 265          return $this->getCustomTransactionOldValue($object, $xaction);
 266      }
 267    }
 268  
 269    private function getTransactionNewValue(
 270      PhabricatorLiskDAO $object,
 271      PhabricatorApplicationTransaction $xaction) {
 272      switch ($xaction->getTransactionType()) {
 273        case PhabricatorTransactions::TYPE_SUBSCRIBERS:
 274          return $this->getPHIDTransactionNewValue($xaction);
 275        case PhabricatorTransactions::TYPE_VIEW_POLICY:
 276        case PhabricatorTransactions::TYPE_EDIT_POLICY:
 277        case PhabricatorTransactions::TYPE_JOIN_POLICY:
 278        case PhabricatorTransactions::TYPE_BUILDABLE:
 279        case PhabricatorTransactions::TYPE_TOKEN:
 280          return $xaction->getNewValue();
 281        case PhabricatorTransactions::TYPE_EDGE:
 282          return $this->getEdgeTransactionNewValue($xaction);
 283        case PhabricatorTransactions::TYPE_CUSTOMFIELD:
 284          $field = $this->getCustomFieldForTransaction($object, $xaction);
 285          return $field->getNewValueFromApplicationTransactions($xaction);
 286        case PhabricatorTransactions::TYPE_COMMENT:
 287          return null;
 288        default:
 289          return $this->getCustomTransactionNewValue($object, $xaction);
 290      }
 291    }
 292  
 293    protected function getCustomTransactionOldValue(
 294      PhabricatorLiskDAO $object,
 295      PhabricatorApplicationTransaction $xaction) {
 296      throw new Exception('Capability not supported!');
 297    }
 298  
 299    protected function getCustomTransactionNewValue(
 300      PhabricatorLiskDAO $object,
 301      PhabricatorApplicationTransaction $xaction) {
 302      throw new Exception('Capability not supported!');
 303    }
 304  
 305    protected function transactionHasEffect(
 306      PhabricatorLiskDAO $object,
 307      PhabricatorApplicationTransaction $xaction) {
 308  
 309      switch ($xaction->getTransactionType()) {
 310        case PhabricatorTransactions::TYPE_COMMENT:
 311          return $xaction->hasComment();
 312        case PhabricatorTransactions::TYPE_CUSTOMFIELD:
 313          $field = $this->getCustomFieldForTransaction($object, $xaction);
 314          return $field->getApplicationTransactionHasEffect($xaction);
 315        case PhabricatorTransactions::TYPE_EDGE:
 316          // A straight value comparison here doesn't always get the right
 317          // result, because newly added edges aren't fully populated. Instead,
 318          // compare the changes in a more granular way.
 319          $old = $xaction->getOldValue();
 320          $new = $xaction->getNewValue();
 321  
 322          $old_dst = array_keys($old);
 323          $new_dst = array_keys($new);
 324  
 325          // NOTE: For now, we don't consider edge reordering to be a change.
 326          // We have very few order-dependent edges and effectively no order
 327          // oriented UI. This might change in the future.
 328          sort($old_dst);
 329          sort($new_dst);
 330  
 331          if ($old_dst !== $new_dst) {
 332            // We've added or removed edges, so this transaction definitely
 333            // has an effect.
 334            return true;
 335          }
 336  
 337          // We haven't added or removed edges, but we might have changed
 338          // edge data.
 339          foreach ($old as $key => $old_value) {
 340            $new_value = $new[$key];
 341            if ($old_value['data'] !== $new_value['data']) {
 342              return true;
 343            }
 344          }
 345  
 346          return false;
 347      }
 348  
 349      return ($xaction->getOldValue() !== $xaction->getNewValue());
 350    }
 351  
 352    protected function shouldApplyInitialEffects(
 353      PhabricatorLiskDAO $object,
 354      array $xactions) {
 355      return false;
 356    }
 357  
 358    protected function applyInitialEffects(
 359      PhabricatorLiskDAO $object,
 360      array $xactions) {
 361      throw new PhutilMethodNotImplementedException();
 362    }
 363  
 364    private function applyInternalEffects(
 365      PhabricatorLiskDAO $object,
 366      PhabricatorApplicationTransaction $xaction) {
 367  
 368      switch ($xaction->getTransactionType()) {
 369        case PhabricatorTransactions::TYPE_BUILDABLE:
 370        case PhabricatorTransactions::TYPE_TOKEN:
 371          return;
 372        case PhabricatorTransactions::TYPE_VIEW_POLICY:
 373          $object->setViewPolicy($xaction->getNewValue());
 374          break;
 375        case PhabricatorTransactions::TYPE_EDIT_POLICY:
 376          $object->setEditPolicy($xaction->getNewValue());
 377          break;
 378        case PhabricatorTransactions::TYPE_CUSTOMFIELD:
 379          $field = $this->getCustomFieldForTransaction($object, $xaction);
 380          return $field->applyApplicationTransactionInternalEffects($xaction);
 381      }
 382  
 383      return $this->applyCustomInternalTransaction($object, $xaction);
 384    }
 385  
 386    private function applyExternalEffects(
 387      PhabricatorLiskDAO $object,
 388      PhabricatorApplicationTransaction $xaction) {
 389      switch ($xaction->getTransactionType()) {
 390        case PhabricatorTransactions::TYPE_BUILDABLE:
 391        case PhabricatorTransactions::TYPE_TOKEN:
 392          return;
 393        case PhabricatorTransactions::TYPE_SUBSCRIBERS:
 394          $subeditor = id(new PhabricatorSubscriptionsEditor())
 395            ->setObject($object)
 396            ->setActor($this->requireActor());
 397  
 398          $old_map = array_fuse($xaction->getOldValue());
 399          $new_map = array_fuse($xaction->getNewValue());
 400  
 401          $subeditor->unsubscribe(
 402            array_keys(
 403              array_diff_key($old_map, $new_map)));
 404  
 405          $subeditor->subscribeExplicit(
 406            array_keys(
 407              array_diff_key($new_map, $old_map)));
 408  
 409          $subeditor->save();
 410  
 411          // for the rest of these edits, subscribers should include those just
 412          // added as well as those just removed.
 413          $subscribers = array_unique(array_merge(
 414            $this->subscribers,
 415            $xaction->getOldValue(),
 416            $xaction->getNewValue()));
 417          $this->subscribers = $subscribers;
 418  
 419          break;
 420        case PhabricatorTransactions::TYPE_EDGE:
 421          if ($this->getIsInverseEdgeEditor()) {
 422            // If we're writing an inverse edge transaction, don't actually
 423            // do anything. The initiating editor on the other side of the
 424            // transaction will take care of the edge writes.
 425            break;
 426          }
 427  
 428          $old = $xaction->getOldValue();
 429          $new = $xaction->getNewValue();
 430          $src = $object->getPHID();
 431          $const = $xaction->getMetadataValue('edge:type');
 432  
 433          $type = PhabricatorEdgeType::getByConstant($const);
 434          if ($type->shouldWriteInverseTransactions()) {
 435            $this->applyInverseEdgeTransactions(
 436              $object,
 437              $xaction,
 438              $type->getInverseEdgeConstant());
 439          }
 440  
 441          foreach ($new as $dst_phid => $edge) {
 442            $new[$dst_phid]['src'] = $src;
 443          }
 444  
 445          $editor = new PhabricatorEdgeEditor();
 446  
 447          foreach ($old as $dst_phid => $edge) {
 448            if (!empty($new[$dst_phid])) {
 449              if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
 450                continue;
 451              }
 452            }
 453            $editor->removeEdge($src, $const, $dst_phid);
 454          }
 455  
 456          foreach ($new as $dst_phid => $edge) {
 457            if (!empty($old[$dst_phid])) {
 458              if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
 459                continue;
 460              }
 461            }
 462  
 463            $data = array(
 464              'data' => $edge['data'],
 465            );
 466  
 467            $editor->addEdge($src, $const, $dst_phid, $data);
 468          }
 469  
 470          $editor->save();
 471          break;
 472        case PhabricatorTransactions::TYPE_CUSTOMFIELD:
 473          $field = $this->getCustomFieldForTransaction($object, $xaction);
 474          return $field->applyApplicationTransactionExternalEffects($xaction);
 475      }
 476  
 477      return $this->applyCustomExternalTransaction($object, $xaction);
 478    }
 479  
 480    protected function applyCustomInternalTransaction(
 481      PhabricatorLiskDAO $object,
 482      PhabricatorApplicationTransaction $xaction) {
 483      $type = $xaction->getTransactionType();
 484      throw new Exception(
 485        "Transaction type '{$type}' is missing an internal apply ".
 486        "implementation!");
 487    }
 488  
 489    protected function applyCustomExternalTransaction(
 490      PhabricatorLiskDAO $object,
 491      PhabricatorApplicationTransaction $xaction) {
 492      $type = $xaction->getTransactionType();
 493      throw new Exception(
 494        "Transaction type '{$type}' is missing an external apply ".
 495        "implementation!");
 496    }
 497  
 498    /**
 499     * Fill in a transaction's common values, like author and content source.
 500     */
 501    protected function populateTransaction(
 502      PhabricatorLiskDAO $object,
 503      PhabricatorApplicationTransaction $xaction) {
 504  
 505      $actor = $this->getActor();
 506  
 507      // TODO: This needs to be more sophisticated once we have meta-policies.
 508      $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
 509  
 510      if ($actor->isOmnipotent()) {
 511        $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
 512      } else {
 513        $xaction->setEditPolicy($this->getActingAsPHID());
 514      }
 515  
 516      $xaction->setAuthorPHID($this->getActingAsPHID());
 517      $xaction->setContentSource($this->getContentSource());
 518      $xaction->attachViewer($actor);
 519      $xaction->attachObject($object);
 520  
 521      if ($object->getPHID()) {
 522        $xaction->setObjectPHID($object->getPHID());
 523      }
 524  
 525      return $xaction;
 526    }
 527  
 528  
 529    protected function applyFinalEffects(
 530      PhabricatorLiskDAO $object,
 531      array $xactions) {
 532      return $xactions;
 533    }
 534  
 535    public function setContentSource(PhabricatorContentSource $content_source) {
 536      $this->contentSource = $content_source;
 537      return $this;
 538    }
 539  
 540    public function setContentSourceFromRequest(AphrontRequest $request) {
 541      return $this->setContentSource(
 542        PhabricatorContentSource::newFromRequest($request));
 543    }
 544  
 545    public function setContentSourceFromConduitRequest(
 546      ConduitAPIRequest $request) {
 547  
 548      $content_source = PhabricatorContentSource::newForSource(
 549        PhabricatorContentSource::SOURCE_CONDUIT,
 550        array());
 551  
 552      return $this->setContentSource($content_source);
 553    }
 554  
 555    public function getContentSource() {
 556      return $this->contentSource;
 557    }
 558  
 559    final public function applyTransactions(
 560      PhabricatorLiskDAO $object,
 561      array $xactions) {
 562  
 563      $this->object = $object;
 564      $this->xactions = $xactions;
 565      $this->isNewObject = ($object->getPHID() === null);
 566  
 567      $this->validateEditParameters($object, $xactions);
 568  
 569      $actor = $this->requireActor();
 570  
 571      // NOTE: Some transaction expansion requires that the edited object be
 572      // attached.
 573      foreach ($xactions as $xaction) {
 574        $xaction->attachObject($object);
 575        $xaction->attachViewer($actor);
 576      }
 577  
 578      $xactions = $this->expandTransactions($object, $xactions);
 579      $xactions = $this->expandSupportTransactions($object, $xactions);
 580      $xactions = $this->combineTransactions($xactions);
 581  
 582      foreach ($xactions as $xaction) {
 583        $xaction = $this->populateTransaction($object, $xaction);
 584      }
 585  
 586      $is_preview = $this->getIsPreview();
 587      $read_locking = false;
 588      $transaction_open = false;
 589  
 590      if (!$is_preview) {
 591        $errors = array();
 592        $type_map = mgroup($xactions, 'getTransactionType');
 593        foreach ($this->getTransactionTypes() as $type) {
 594          $type_xactions = idx($type_map, $type, array());
 595          $errors[] = $this->validateTransaction($object, $type, $type_xactions);
 596        }
 597  
 598        $errors = array_mergev($errors);
 599  
 600        $continue_on_missing = $this->getContinueOnMissingFields();
 601        foreach ($errors as $key => $error) {
 602          if ($continue_on_missing && $error->getIsMissingFieldError()) {
 603            unset($errors[$key]);
 604          }
 605        }
 606  
 607        if ($errors) {
 608          throw new PhabricatorApplicationTransactionValidationException($errors);
 609        }
 610  
 611        $file_phids = $this->extractFilePHIDs($object, $xactions);
 612  
 613        if ($object->getID()) {
 614          foreach ($xactions as $xaction) {
 615  
 616            // If any of the transactions require a read lock, hold one and
 617            // reload the object. We need to do this fairly early so that the
 618            // call to `adjustTransactionValues()` (which populates old values)
 619            // is based on the synchronized state of the object, which may differ
 620            // from the state when it was originally loaded.
 621  
 622            if ($this->shouldReadLock($object, $xaction)) {
 623              $object->openTransaction();
 624              $object->beginReadLocking();
 625              $transaction_open = true;
 626              $read_locking = true;
 627              $object->reload();
 628              break;
 629            }
 630          }
 631        }
 632  
 633        if ($this->shouldApplyInitialEffects($object, $xactions)) {
 634          if (!$transaction_open) {
 635            $object->openTransaction();
 636            $transaction_open = true;
 637          }
 638        }
 639      }
 640  
 641      if ($this->shouldApplyInitialEffects($object, $xactions)) {
 642        $this->applyInitialEffects($object, $xactions);
 643      }
 644  
 645      foreach ($xactions as $xaction) {
 646        $this->adjustTransactionValues($object, $xaction);
 647      }
 648  
 649      $xactions = $this->filterTransactions($object, $xactions);
 650  
 651      if (!$xactions) {
 652        if ($read_locking) {
 653          $object->endReadLocking();
 654          $read_locking = false;
 655        }
 656        if ($transaction_open) {
 657          $object->killTransaction();
 658          $transaction_open = false;
 659        }
 660        return array();
 661      }
 662  
 663      // Now that we've merged, filtered, and combined transactions, check for
 664      // required capabilities.
 665      foreach ($xactions as $xaction) {
 666        $this->requireCapabilities($object, $xaction);
 667      }
 668  
 669      $xactions = $this->sortTransactions($xactions);
 670  
 671      if ($is_preview) {
 672        $this->loadHandles($xactions);
 673        return $xactions;
 674      }
 675  
 676      $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
 677        ->setActor($actor)
 678        ->setActingAsPHID($this->getActingAsPHID())
 679        ->setContentSource($this->getContentSource());
 680  
 681      if (!$transaction_open) {
 682        $object->openTransaction();
 683      }
 684  
 685        foreach ($xactions as $xaction) {
 686          $this->applyInternalEffects($object, $xaction);
 687        }
 688  
 689        $object->save();
 690  
 691        foreach ($xactions as $xaction) {
 692          $xaction->setObjectPHID($object->getPHID());
 693          if ($xaction->getComment()) {
 694            $xaction->setPHID($xaction->generatePHID());
 695            $comment_editor->applyEdit($xaction, $xaction->getComment());
 696          } else {
 697            $xaction->save();
 698          }
 699        }
 700  
 701        if ($file_phids) {
 702          $this->attachFiles($object, $file_phids);
 703        }
 704  
 705        foreach ($xactions as $xaction) {
 706          $this->applyExternalEffects($object, $xaction);
 707        }
 708  
 709        $xactions = $this->applyFinalEffects($object, $xactions);
 710  
 711        if ($read_locking) {
 712          $object->endReadLocking();
 713          $read_locking = false;
 714        }
 715  
 716      $object->saveTransaction();
 717  
 718      // Now that we've completely applied the core transaction set, try to apply
 719      // Herald rules. Herald rules are allowed to either take direct actions on
 720      // the database (like writing flags), or take indirect actions (like saving
 721      // some targets for CC when we generate mail a little later), or return
 722      // transactions which we'll apply normally using another Editor.
 723  
 724      // First, check if *this* is a sub-editor which is itself applying Herald
 725      // rules: if it is, stop working and return so we don't descend into
 726      // madness.
 727  
 728      // Otherwise, we're not a Herald editor, so process Herald rules (possibly
 729      // using a Herald editor to apply resulting transactions) and then send out
 730      // mail, notifications, and feed updates about everything.
 731  
 732      if ($this->getIsHeraldEditor()) {
 733        // We are the Herald editor, so stop work here and return the updated
 734        // transactions.
 735        return $xactions;
 736      } else if ($this->shouldApplyHeraldRules($object, $xactions)) {
 737        // We are not the Herald editor, so try to apply Herald rules.
 738        $herald_xactions = $this->applyHeraldRules($object, $xactions);
 739  
 740        if ($herald_xactions) {
 741          $xscript_id = $this->getHeraldTranscript()->getID();
 742          foreach ($herald_xactions as $herald_xaction) {
 743            $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
 744          }
 745  
 746          // NOTE: We're acting as the omnipotent user because rules deal with
 747          // their own policy issues. We use a synthetic author PHID (the
 748          // Herald application) as the author of record, so that transactions
 749          // will render in a reasonable way ("Herald assigned this task ...").
 750          $herald_actor = PhabricatorUser::getOmnipotentUser();
 751          $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
 752  
 753          // TODO: It would be nice to give transactions a more specific source
 754          // which points at the rule which generated them. You can figure this
 755          // out from transcripts, but it would be cleaner if you didn't have to.
 756  
 757          $herald_source = PhabricatorContentSource::newForSource(
 758            PhabricatorContentSource::SOURCE_HERALD,
 759            array());
 760  
 761          $herald_editor = newv(get_class($this), array())
 762            ->setContinueOnNoEffect(true)
 763            ->setContinueOnMissingFields(true)
 764            ->setParentMessageID($this->getParentMessageID())
 765            ->setIsHeraldEditor(true)
 766            ->setActor($herald_actor)
 767            ->setActingAsPHID($herald_phid)
 768            ->setContentSource($herald_source);
 769  
 770          $herald_xactions = $herald_editor->applyTransactions(
 771            $object,
 772            $herald_xactions);
 773  
 774          // Merge the new transactions into the transaction list: we want to
 775          // send email and publish feed stories about them, too.
 776          $xactions = array_merge($xactions, $herald_xactions);
 777        }
 778      }
 779  
 780      // Before sending mail or publishing feed stories, reload the object
 781      // subscribers to pick up changes caused by Herald (or by other side effects
 782      // in various transaction phases).
 783      $this->loadSubscribers($object);
 784  
 785      $this->loadHandles($xactions);
 786  
 787      $mail = null;
 788      if (!$this->getDisableEmail()) {
 789        if ($this->shouldSendMail($object, $xactions)) {
 790          $mail = $this->sendMail($object, $xactions);
 791        }
 792      }
 793  
 794      if ($this->supportsSearch()) {
 795        id(new PhabricatorSearchIndexer())
 796          ->queueDocumentForIndexing($object->getPHID());
 797      }
 798  
 799      if ($this->shouldPublishFeedStory($object, $xactions)) {
 800        $mailed = array();
 801        if ($mail) {
 802          $mailed = $mail->buildRecipientList();
 803        }
 804        $this->publishFeedStory(
 805          $object,
 806          $xactions,
 807          $mailed);
 808      }
 809  
 810      $this->didApplyTransactions($xactions);
 811  
 812      if ($object instanceof PhabricatorCustomFieldInterface) {
 813        // Maybe this makes more sense to move into the search index itself? For
 814        // now I'm putting it here since I think we might end up with things that
 815        // need it to be up to date once the next page loads, but if we don't go
 816        // there we we could move it into search once search moves to the daemons.
 817  
 818        // It now happens in the search indexer as well, but the search indexer is
 819        // always daemonized, so the logic above still potentially holds. We could
 820        // possibly get rid of this. The major motivation for putting it in the
 821        // indexer was to enable reindexing to work.
 822  
 823        $fields = PhabricatorCustomField::getObjectFields(
 824          $object,
 825          PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
 826        $fields->readFieldsFromStorage($object);
 827        $fields->rebuildIndexes($object);
 828      }
 829  
 830      return $xactions;
 831    }
 832  
 833    protected function didApplyTransactions(array $xactions) {
 834      // Hook for subclasses.
 835      return;
 836    }
 837  
 838  
 839    /**
 840     * Determine if the editor should hold a read lock on the object while
 841     * applying a transaction.
 842     *
 843     * If the editor does not hold a lock, two editors may read an object at the
 844     * same time, then apply their changes without any synchronization. For most
 845     * transactions, this does not matter much. However, it is important for some
 846     * transactions. For example, if an object has a transaction count on it, both
 847     * editors may read the object with `count = 23`, then independently update it
 848     * and save the object with `count = 24` twice. This will produce the wrong
 849     * state: the object really has 25 transactions, but the count is only 24.
 850     *
 851     * Generally, transactions fall into one of four buckets:
 852     *
 853     *   - Append operations: Actions like adding a comment to an object purely
 854     *     add information to its state, and do not depend on the current object
 855     *     state in any way. These transactions never need to hold locks.
 856     *   - Overwrite operations: Actions like changing the title or description
 857     *     of an object replace the current value with a new value, so the end
 858     *     state is consistent without a lock. We currently do not lock these
 859     *     transactions, although we may in the future.
 860     *   - Edge operations: Edge and subscription operations have internal
 861     *     synchronization which limits the damage race conditions can cause.
 862     *     We do not currently lock these transactions, although we may in the
 863     *     future.
 864     *   - Update operations: Actions like incrementing a count on an object.
 865     *     These operations generally should use locks, unless it is not
 866     *     important that the state remain consistent in the presence of races.
 867     *
 868     * @param   PhabricatorLiskDAO  Object being updated.
 869     * @param   PhabricatorApplicationTransaction Transaction being applied.
 870     * @return  bool                True to synchronize the edit with a lock.
 871     */
 872    protected function shouldReadLock(
 873      PhabricatorLiskDAO $object,
 874      PhabricatorApplicationTransaction $xaction) {
 875      return false;
 876    }
 877  
 878    private function loadHandles(array $xactions) {
 879      $phids = array();
 880      foreach ($xactions as $key => $xaction) {
 881        $phids[$key] = $xaction->getRequiredHandlePHIDs();
 882      }
 883      $handles = array();
 884      $merged = array_mergev($phids);
 885      if ($merged) {
 886        $handles = id(new PhabricatorHandleQuery())
 887          ->setViewer($this->requireActor())
 888          ->withPHIDs($merged)
 889          ->execute();
 890      }
 891      foreach ($xactions as $key => $xaction) {
 892        $xaction->setHandles(array_select_keys($handles, $phids[$key]));
 893      }
 894    }
 895  
 896    private function loadSubscribers(PhabricatorLiskDAO $object) {
 897      if ($object->getPHID() &&
 898          ($object instanceof PhabricatorSubscribableInterface)) {
 899        $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
 900          $object->getPHID());
 901        $this->subscribers = array_fuse($subs);
 902      } else {
 903        $this->subscribers = array();
 904      }
 905    }
 906  
 907    private function validateEditParameters(
 908      PhabricatorLiskDAO $object,
 909      array $xactions) {
 910  
 911      if (!$this->getContentSource()) {
 912        throw new Exception(
 913          'Call setContentSource() before applyTransactions()!');
 914      }
 915  
 916      // Do a bunch of sanity checks that the incoming transactions are fresh.
 917      // They should be unsaved and have only "transactionType" and "newValue"
 918      // set.
 919  
 920      $types = array_fill_keys($this->getTransactionTypes(), true);
 921  
 922      assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
 923      foreach ($xactions as $xaction) {
 924        if ($xaction->getPHID() || $xaction->getID()) {
 925          throw new PhabricatorApplicationTransactionStructureException(
 926            $xaction,
 927            pht(
 928              'You can not apply transactions which already have IDs/PHIDs!'));
 929        }
 930        if ($xaction->getObjectPHID()) {
 931          throw new PhabricatorApplicationTransactionStructureException(
 932            $xaction,
 933            pht(
 934              'You can not apply transactions which already have objectPHIDs!'));
 935        }
 936        if ($xaction->getAuthorPHID()) {
 937          throw new PhabricatorApplicationTransactionStructureException(
 938            $xaction,
 939            pht(
 940              'You can not apply transactions which already have authorPHIDs!'));
 941        }
 942        if ($xaction->getCommentPHID()) {
 943          throw new PhabricatorApplicationTransactionStructureException(
 944            $xaction,
 945            pht(
 946              'You can not apply transactions which already have '.
 947              'commentPHIDs!'));
 948        }
 949        if ($xaction->getCommentVersion() !== 0) {
 950          throw new PhabricatorApplicationTransactionStructureException(
 951            $xaction,
 952            pht(
 953              'You can not apply transactions which already have '.
 954              'commentVersions!'));
 955        }
 956  
 957        $expect_value = !$xaction->shouldGenerateOldValue();
 958        $has_value = $xaction->hasOldValue();
 959  
 960        if ($expect_value && !$has_value) {
 961          throw new PhabricatorApplicationTransactionStructureException(
 962            $xaction,
 963            pht(
 964              'This transaction is supposed to have an oldValue set, but '.
 965              'it does not!'));
 966        }
 967  
 968        if ($has_value && !$expect_value) {
 969          throw new PhabricatorApplicationTransactionStructureException(
 970            $xaction,
 971            pht(
 972              'This transaction should generate its oldValue automatically, '.
 973              'but has already had one set!'));
 974        }
 975  
 976        $type = $xaction->getTransactionType();
 977        if (empty($types[$type])) {
 978          throw new PhabricatorApplicationTransactionStructureException(
 979            $xaction,
 980            pht(
 981              'Transaction has type "%s", but that transaction type is not '.
 982              'supported by this editor (%s).',
 983              $type,
 984              get_class($this)));
 985        }
 986      }
 987    }
 988  
 989    protected function requireCapabilities(
 990      PhabricatorLiskDAO $object,
 991      PhabricatorApplicationTransaction $xaction) {
 992  
 993      if ($this->getIsNewObject()) {
 994        return;
 995      }
 996  
 997      $actor = $this->requireActor();
 998      switch ($xaction->getTransactionType()) {
 999        case PhabricatorTransactions::TYPE_COMMENT:
1000          PhabricatorPolicyFilter::requireCapability(
1001            $actor,
1002            $object,
1003            PhabricatorPolicyCapability::CAN_VIEW);
1004          break;
1005        case PhabricatorTransactions::TYPE_VIEW_POLICY:
1006          PhabricatorPolicyFilter::requireCapability(
1007            $actor,
1008            $object,
1009            PhabricatorPolicyCapability::CAN_EDIT);
1010          break;
1011        case PhabricatorTransactions::TYPE_EDIT_POLICY:
1012          PhabricatorPolicyFilter::requireCapability(
1013            $actor,
1014            $object,
1015            PhabricatorPolicyCapability::CAN_EDIT);
1016          break;
1017        case PhabricatorTransactions::TYPE_JOIN_POLICY:
1018          PhabricatorPolicyFilter::requireCapability(
1019            $actor,
1020            $object,
1021            PhabricatorPolicyCapability::CAN_EDIT);
1022          break;
1023      }
1024    }
1025  
1026    private function buildSubscribeTransaction(
1027      PhabricatorLiskDAO $object,
1028      array $xactions,
1029      array $blocks) {
1030  
1031      if (!($object instanceof PhabricatorSubscribableInterface)) {
1032        return null;
1033      }
1034  
1035      $texts = array_mergev($blocks);
1036      $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
1037        $this->getActor(),
1038        $texts);
1039  
1040      $this->mentionedPHIDs = $phids;
1041  
1042      if ($object->getPHID()) {
1043        // Don't try to subscribe already-subscribed mentions: we want to generate
1044        // a dialog about an action having no effect if the user explicitly adds
1045        // existing CCs, but not if they merely mention existing subscribers.
1046        $phids = array_diff($phids, $this->subscribers);
1047      }
1048  
1049      foreach ($phids as $key => $phid) {
1050        if ($object->isAutomaticallySubscribed($phid)) {
1051          unset($phids[$key]);
1052        }
1053      }
1054      $phids = array_values($phids);
1055  
1056      if (!$phids) {
1057        return null;
1058      }
1059  
1060      $xaction = newv(get_class(head($xactions)), array());
1061      $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
1062      $xaction->setNewValue(array('+' => $phids));
1063  
1064      return $xaction;
1065    }
1066  
1067    protected function getRemarkupBlocksFromTransaction(
1068      PhabricatorApplicationTransaction $transaction) {
1069      return $transaction->getRemarkupBlocks();
1070    }
1071  
1072    protected function mergeTransactions(
1073      PhabricatorApplicationTransaction $u,
1074      PhabricatorApplicationTransaction $v) {
1075  
1076      $type = $u->getTransactionType();
1077  
1078      switch ($type) {
1079        case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1080          return $this->mergePHIDOrEdgeTransactions($u, $v);
1081        case PhabricatorTransactions::TYPE_EDGE:
1082          $u_type = $u->getMetadataValue('edge:type');
1083          $v_type = $v->getMetadataValue('edge:type');
1084          if ($u_type == $v_type) {
1085            return $this->mergePHIDOrEdgeTransactions($u, $v);
1086          }
1087          return null;
1088      }
1089  
1090      // By default, do not merge the transactions.
1091      return null;
1092    }
1093  
1094    /**
1095     * Optionally expand transactions which imply other effects. For example,
1096     * resigning from a revision in Differential implies removing yourself as
1097     * a reviewer.
1098     */
1099    private function expandTransactions(
1100      PhabricatorLiskDAO $object,
1101      array $xactions) {
1102  
1103      $results = array();
1104      foreach ($xactions as $xaction) {
1105        foreach ($this->expandTransaction($object, $xaction) as $expanded) {
1106          $results[] = $expanded;
1107        }
1108      }
1109  
1110      return $results;
1111    }
1112  
1113    protected function expandTransaction(
1114      PhabricatorLiskDAO $object,
1115      PhabricatorApplicationTransaction $xaction) {
1116      return array($xaction);
1117    }
1118  
1119  
1120    private function expandSupportTransactions(
1121      PhabricatorLiskDAO $object,
1122      array $xactions) {
1123      $this->loadSubscribers($object);
1124  
1125      $xactions = $this->applyImplicitCC($object, $xactions);
1126  
1127      $blocks = array();
1128      foreach ($xactions as $key => $xaction) {
1129        $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction);
1130      }
1131  
1132      $subscribe_xaction = $this->buildSubscribeTransaction(
1133        $object,
1134        $xactions,
1135        $blocks);
1136      if ($subscribe_xaction) {
1137        $xactions[] = $subscribe_xaction;
1138      }
1139  
1140      // TODO: For now, this is just a placeholder.
1141      $engine = PhabricatorMarkupEngine::getEngine('extract');
1142      $engine->setConfig('viewer', $this->requireActor());
1143  
1144      $block_xactions = $this->expandRemarkupBlockTransactions(
1145        $object,
1146        $xactions,
1147        $blocks,
1148        $engine);
1149  
1150      foreach ($block_xactions as $xaction) {
1151        $xactions[] = $xaction;
1152      }
1153  
1154      return $xactions;
1155    }
1156  
1157    private function expandRemarkupBlockTransactions(
1158      PhabricatorLiskDAO $object,
1159      array $xactions,
1160      $blocks,
1161      PhutilMarkupEngine $engine) {
1162  
1163      $block_xactions = $this->expandCustomRemarkupBlockTransactions(
1164        $object,
1165        $xactions,
1166        $blocks,
1167        $engine);
1168  
1169      $mentioned_phids = array();
1170      foreach ($blocks as $key => $xaction_blocks) {
1171        foreach ($xaction_blocks as $block) {
1172          $engine->markupText($block);
1173          $mentioned_phids += $engine->getTextMetadata(
1174            PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
1175            array());
1176        }
1177      }
1178  
1179      if (!$mentioned_phids) {
1180        return $block_xactions;
1181      }
1182  
1183      if ($object instanceof PhabricatorProjectInterface) {
1184        $phids = $mentioned_phids;
1185        $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
1186        foreach ($phids as $key => $phid) {
1187          if (phid_get_type($phid) != $project_type) {
1188            unset($phids[$key]);
1189          }
1190        }
1191  
1192        if ($phids) {
1193          $edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
1194          $block_xactions[] = newv(get_class(head($xactions)), array())
1195            ->setIgnoreOnNoEffect(true)
1196            ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
1197            ->setMetadataValue('edge:type', $edge_type)
1198            ->setNewValue(array('+' => $phids));
1199        }
1200      }
1201  
1202      $mentioned_objects = id(new PhabricatorObjectQuery())
1203        ->setViewer($this->getActor())
1204        ->withPHIDs($mentioned_phids)
1205        ->execute();
1206  
1207      $mentionable_phids = array();
1208      foreach ($mentioned_objects as $mentioned_object) {
1209        if ($mentioned_object instanceof PhabricatorMentionableInterface) {
1210          $mentioned_phid = $mentioned_object->getPHID();
1211          if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
1212            continue;
1213          }
1214          // don't let objects mention themselves
1215          if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
1216            continue;
1217          }
1218          $mentionable_phids[$mentioned_phid] = $mentioned_phid;
1219        }
1220      }
1221      if ($mentionable_phids) {
1222        $edge_type = PhabricatorObjectMentionsObject::EDGECONST;
1223        $block_xactions[] = newv(get_class(head($xactions)), array())
1224          ->setIgnoreOnNoEffect(true)
1225          ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
1226          ->setMetadataValue('edge:type', $edge_type)
1227          ->setNewValue(array('+' => $mentionable_phids));
1228      }
1229  
1230      return $block_xactions;
1231    }
1232  
1233    protected function expandCustomRemarkupBlockTransactions(
1234      PhabricatorLiskDAO $object,
1235      array $xactions,
1236      $blocks,
1237      PhutilMarkupEngine $engine) {
1238      return array();
1239    }
1240  
1241  
1242    /**
1243     * Attempt to combine similar transactions into a smaller number of total
1244     * transactions. For example, two transactions which edit the title of an
1245     * object can be merged into a single edit.
1246     */
1247    private function combineTransactions(array $xactions) {
1248      $stray_comments = array();
1249  
1250      $result = array();
1251      $types = array();
1252      foreach ($xactions as $key => $xaction) {
1253        $type = $xaction->getTransactionType();
1254        if (isset($types[$type])) {
1255          foreach ($types[$type] as $other_key) {
1256            $merged = $this->mergeTransactions($result[$other_key], $xaction);
1257            if ($merged) {
1258              $result[$other_key] = $merged;
1259  
1260              if ($xaction->getComment() &&
1261                  ($xaction->getComment() !== $merged->getComment())) {
1262                $stray_comments[] = $xaction->getComment();
1263              }
1264  
1265              if ($result[$other_key]->getComment() &&
1266                  ($result[$other_key]->getComment() !== $merged->getComment())) {
1267                $stray_comments[] = $result[$other_key]->getComment();
1268              }
1269  
1270              // Move on to the next transaction.
1271              continue 2;
1272            }
1273          }
1274        }
1275        $result[$key] = $xaction;
1276        $types[$type][] = $key;
1277      }
1278  
1279      // If we merged any comments away, restore them.
1280      foreach ($stray_comments as $comment) {
1281        $xaction = newv(get_class(head($result)), array());
1282        $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
1283        $xaction->setComment($comment);
1284        $result[] = $xaction;
1285      }
1286  
1287      return array_values($result);
1288    }
1289  
1290    protected function mergePHIDOrEdgeTransactions(
1291      PhabricatorApplicationTransaction $u,
1292      PhabricatorApplicationTransaction $v) {
1293  
1294      $result = $u->getNewValue();
1295      foreach ($v->getNewValue() as $key => $value) {
1296        if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
1297          if (empty($result[$key])) {
1298            $result[$key] = $value;
1299          } else {
1300            // We're merging two lists of edge adds, sets, or removes. Merge
1301            // them by merging individual PHIDs within them.
1302            $merged = $result[$key];
1303  
1304            foreach ($value as $dst => $v_spec) {
1305              if (empty($merged[$dst])) {
1306                $merged[$dst] = $v_spec;
1307              } else {
1308                // Two transactions are trying to perform the same operation on
1309                // the same edge. Normalize the edge data and then merge it. This
1310                // allows transactions to specify how data merges execute in a
1311                // precise way.
1312  
1313                $u_spec = $merged[$dst];
1314  
1315                if (!is_array($u_spec)) {
1316                  $u_spec = array('dst' => $u_spec);
1317                }
1318                if (!is_array($v_spec)) {
1319                  $v_spec = array('dst' => $v_spec);
1320                }
1321  
1322                $ux_data = idx($u_spec, 'data', array());
1323                $vx_data = idx($v_spec, 'data', array());
1324  
1325                $merged_data = $this->mergeEdgeData(
1326                  $u->getMetadataValue('edge:type'),
1327                  $ux_data,
1328                  $vx_data);
1329  
1330                $u_spec['data'] = $merged_data;
1331                $merged[$dst] = $u_spec;
1332              }
1333            }
1334  
1335            $result[$key] = $merged;
1336          }
1337        } else {
1338          $result[$key] = array_merge($value, idx($result, $key, array()));
1339        }
1340      }
1341      $u->setNewValue($result);
1342  
1343      // When combining an "ignore" transaction with a normal transaction, make
1344      // sure we don't propagate the "ignore" flag.
1345      if (!$v->getIgnoreOnNoEffect()) {
1346        $u->setIgnoreOnNoEffect(false);
1347      }
1348  
1349      return $u;
1350    }
1351  
1352    protected function mergeEdgeData($type, array $u, array $v) {
1353      return $v + $u;
1354    }
1355  
1356    protected function getPHIDTransactionNewValue(
1357      PhabricatorApplicationTransaction $xaction) {
1358  
1359      $old = array_fuse($xaction->getOldValue());
1360  
1361      $new = $xaction->getNewValue();
1362      $new_add = idx($new, '+', array());
1363      unset($new['+']);
1364      $new_rem = idx($new, '-', array());
1365      unset($new['-']);
1366      $new_set = idx($new, '=', null);
1367      if ($new_set !== null) {
1368        $new_set = array_fuse($new_set);
1369      }
1370      unset($new['=']);
1371  
1372      if ($new) {
1373        throw new Exception(
1374          "Invalid 'new' value for PHID transaction. Value should contain only ".
1375          "keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS).");
1376      }
1377  
1378      $result = array();
1379  
1380      foreach ($old as $phid) {
1381        if ($new_set !== null && empty($new_set[$phid])) {
1382          continue;
1383        }
1384        $result[$phid] = $phid;
1385      }
1386  
1387      if ($new_set !== null) {
1388        foreach ($new_set as $phid) {
1389          $result[$phid] = $phid;
1390        }
1391      }
1392  
1393      foreach ($new_add as $phid) {
1394        $result[$phid] = $phid;
1395      }
1396  
1397      foreach ($new_rem as $phid) {
1398        unset($result[$phid]);
1399      }
1400  
1401      return array_values($result);
1402    }
1403  
1404    protected function getEdgeTransactionNewValue(
1405      PhabricatorApplicationTransaction $xaction) {
1406  
1407      $new = $xaction->getNewValue();
1408      $new_add = idx($new, '+', array());
1409      unset($new['+']);
1410      $new_rem = idx($new, '-', array());
1411      unset($new['-']);
1412      $new_set = idx($new, '=', null);
1413      unset($new['=']);
1414  
1415      if ($new) {
1416        throw new Exception(
1417          "Invalid 'new' value for Edge transaction. Value should contain only ".
1418          "keys '+' (add edges), '-' (remove edges) and '=' (set edges).");
1419      }
1420  
1421      $old = $xaction->getOldValue();
1422  
1423      $lists = array($new_set, $new_add, $new_rem);
1424      foreach ($lists as $list) {
1425        $this->checkEdgeList($list);
1426      }
1427  
1428      $result = array();
1429      foreach ($old as $dst_phid => $edge) {
1430        if ($new_set !== null && empty($new_set[$dst_phid])) {
1431          continue;
1432        }
1433        $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
1434          $xaction,
1435          $edge,
1436          $dst_phid);
1437      }
1438  
1439      if ($new_set !== null) {
1440        foreach ($new_set as $dst_phid => $edge) {
1441          $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
1442            $xaction,
1443            $edge,
1444            $dst_phid);
1445        }
1446      }
1447  
1448      foreach ($new_add as $dst_phid => $edge) {
1449        $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
1450          $xaction,
1451          $edge,
1452          $dst_phid);
1453      }
1454  
1455      foreach ($new_rem as $dst_phid => $edge) {
1456        unset($result[$dst_phid]);
1457      }
1458  
1459      return $result;
1460    }
1461  
1462    private function checkEdgeList($list) {
1463      if (!$list) {
1464        return;
1465      }
1466      foreach ($list as $key => $item) {
1467        if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
1468          throw new Exception(
1469            "Edge transactions must have destination PHIDs as in edge ".
1470            "lists (found key '{$key}').");
1471        }
1472        if (!is_array($item) && $item !== $key) {
1473          throw new Exception(
1474            "Edge transactions must have PHIDs or edge specs as values ".
1475            "(found value '{$item}').");
1476        }
1477      }
1478    }
1479  
1480    private function normalizeEdgeTransactionValue(
1481      PhabricatorApplicationTransaction $xaction,
1482      $edge,
1483      $dst_phid) {
1484  
1485      if (!is_array($edge)) {
1486        if ($edge != $dst_phid) {
1487          throw new Exception(
1488            pht(
1489              'Transaction edge data must either be the edge PHID or an edge '.
1490              'specification dictionary.'));
1491        }
1492        $edge = array();
1493      } else {
1494        foreach ($edge as $key => $value) {
1495          switch ($key) {
1496            case 'src':
1497            case 'dst':
1498            case 'type':
1499            case 'data':
1500            case 'dateCreated':
1501            case 'dateModified':
1502            case 'seq':
1503            case 'dataID':
1504              break;
1505            default:
1506              throw new Exception(
1507                pht(
1508                  'Transaction edge specification contains unexpected key '.
1509                  '"%s".',
1510                  $key));
1511          }
1512        }
1513      }
1514  
1515      $edge['dst'] = $dst_phid;
1516  
1517      $edge_type = $xaction->getMetadataValue('edge:type');
1518      if (empty($edge['type'])) {
1519        $edge['type'] = $edge_type;
1520      } else {
1521        if ($edge['type'] != $edge_type) {
1522          $this_type = $edge['type'];
1523          throw new Exception(
1524            "Edge transaction includes edge of type '{$this_type}', but ".
1525            "transaction is of type '{$edge_type}'. Each edge transaction must ".
1526            "alter edges of only one type.");
1527        }
1528      }
1529  
1530      if (!isset($edge['data'])) {
1531        $edge['data'] = array();
1532      }
1533  
1534      return $edge;
1535    }
1536  
1537    protected function sortTransactions(array $xactions) {
1538      $head = array();
1539      $tail = array();
1540  
1541      // Move bare comments to the end, so the actions precede them.
1542      foreach ($xactions as $xaction) {
1543        $type = $xaction->getTransactionType();
1544        if ($type == PhabricatorTransactions::TYPE_COMMENT) {
1545          $tail[] = $xaction;
1546        } else {
1547          $head[] = $xaction;
1548        }
1549      }
1550  
1551      return array_values(array_merge($head, $tail));
1552    }
1553  
1554  
1555    protected function filterTransactions(
1556      PhabricatorLiskDAO $object,
1557      array $xactions) {
1558  
1559      $type_comment = PhabricatorTransactions::TYPE_COMMENT;
1560  
1561      $no_effect = array();
1562      $has_comment = false;
1563      $any_effect = false;
1564      foreach ($xactions as $key => $xaction) {
1565        if ($this->transactionHasEffect($object, $xaction)) {
1566          if ($xaction->getTransactionType() != $type_comment) {
1567            $any_effect = true;
1568          }
1569        } else if ($xaction->getIgnoreOnNoEffect()) {
1570          unset($xactions[$key]);
1571        } else {
1572          $no_effect[$key] = $xaction;
1573        }
1574        if ($xaction->hasComment()) {
1575          $has_comment = true;
1576        }
1577      }
1578  
1579      if (!$no_effect) {
1580        return $xactions;
1581      }
1582  
1583      if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
1584        throw new PhabricatorApplicationTransactionNoEffectException(
1585          $no_effect,
1586          $any_effect,
1587          $has_comment);
1588      }
1589  
1590      if (!$any_effect && !$has_comment) {
1591        // If we only have empty comment transactions, just drop them all.
1592        return array();
1593      }
1594  
1595      foreach ($no_effect as $key => $xaction) {
1596        if ($xaction->getComment()) {
1597          $xaction->setTransactionType($type_comment);
1598          $xaction->setOldValue(null);
1599          $xaction->setNewValue(null);
1600        } else {
1601          unset($xactions[$key]);
1602        }
1603      }
1604  
1605      return $xactions;
1606    }
1607  
1608  
1609    /**
1610     * Hook for validating transactions. This callback will be invoked for each
1611     * available transaction type, even if an edit does not apply any transactions
1612     * of that type. This allows you to raise exceptions when required fields are
1613     * missing, by detecting that the object has no field value and there is no
1614     * transaction which sets one.
1615     *
1616     * @param PhabricatorLiskDAO Object being edited.
1617     * @param string Transaction type to validate.
1618     * @param list<PhabricatorApplicationTransaction> Transactions of given type,
1619     *   which may be empty if the edit does not apply any transactions of the
1620     *   given type.
1621     * @return list<PhabricatorApplicationTransactionValidationError> List of
1622     *   validation errors.
1623     */
1624    protected function validateTransaction(
1625      PhabricatorLiskDAO $object,
1626      $type,
1627      array $xactions) {
1628  
1629      $errors = array();
1630      switch ($type) {
1631        case PhabricatorTransactions::TYPE_VIEW_POLICY:
1632          $errors[] = $this->validatePolicyTransaction(
1633            $object,
1634            $xactions,
1635            $type,
1636            PhabricatorPolicyCapability::CAN_VIEW);
1637          break;
1638        case PhabricatorTransactions::TYPE_EDIT_POLICY:
1639          $errors[] = $this->validatePolicyTransaction(
1640            $object,
1641            $xactions,
1642            $type,
1643            PhabricatorPolicyCapability::CAN_EDIT);
1644          break;
1645        case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1646          $groups = array();
1647          foreach ($xactions as $xaction) {
1648            $groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
1649          }
1650  
1651          $field_list = PhabricatorCustomField::getObjectFields(
1652            $object,
1653            PhabricatorCustomField::ROLE_EDIT);
1654          $field_list->setViewer($this->getActor());
1655  
1656          $role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
1657          foreach ($field_list->getFields() as $field) {
1658            if (!$field->shouldEnableForRole($role_xactions)) {
1659              continue;
1660            }
1661            $errors[] = $field->validateApplicationTransactions(
1662              $this,
1663              $type,
1664              idx($groups, $field->getFieldKey(), array()));
1665          }
1666          break;
1667      }
1668  
1669      return array_mergev($errors);
1670    }
1671  
1672    private function validatePolicyTransaction(
1673      PhabricatorLiskDAO $object,
1674      array $xactions,
1675      $transaction_type,
1676      $capability) {
1677  
1678      $actor = $this->requireActor();
1679      $errors = array();
1680      // Note $this->xactions is necessary; $xactions is $this->xactions of
1681      // $transaction_type
1682      $policy_object = $this->adjustObjectForPolicyChecks(
1683        $object,
1684        $this->xactions);
1685  
1686      // Make sure the user isn't editing away their ability to $capability this
1687      // object.
1688      foreach ($xactions as $xaction) {
1689        try {
1690          PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
1691            $actor,
1692            $policy_object,
1693            $capability,
1694            $xaction->getNewValue());
1695        } catch (PhabricatorPolicyException $ex) {
1696          $errors[] = new PhabricatorApplicationTransactionValidationError(
1697            $transaction_type,
1698            pht('Invalid'),
1699            pht(
1700              'You can not select this %s policy, because you would no longer '.
1701              'be able to %s the object.',
1702              $capability,
1703              $capability),
1704            $xaction);
1705        }
1706      }
1707  
1708      if ($this->getIsNewObject()) {
1709        if (!$xactions) {
1710          $has_capability = PhabricatorPolicyFilter::hasCapability(
1711            $actor,
1712            $policy_object,
1713            $capability);
1714          if (!$has_capability) {
1715            $errors[] = new PhabricatorApplicationTransactionValidationError(
1716              $transaction_type,
1717              pht('Invalid'),
1718              pht('The selected %s policy excludes you. Choose a %s policy '.
1719                  'which allows you to %s the object.',
1720              $capability,
1721              $capability,
1722              $capability));
1723          }
1724        }
1725      }
1726  
1727      return $errors;
1728    }
1729  
1730    protected function adjustObjectForPolicyChecks(
1731      PhabricatorLiskDAO $object,
1732      array $xactions) {
1733  
1734      return clone $object;
1735    }
1736  
1737    /**
1738     * Check for a missing text field.
1739     *
1740     * A text field is missing if the object has no value and there are no
1741     * transactions which set a value, or if the transactions remove the value.
1742     * This method is intended to make implementing @{method:validateTransaction}
1743     * more convenient:
1744     *
1745     *   $missing = $this->validateIsEmptyTextField(
1746     *     $object->getName(),
1747     *     $xactions);
1748     *
1749     * This will return `true` if the net effect of the object and transactions
1750     * is an empty field.
1751     *
1752     * @param wild Current field value.
1753     * @param list<PhabricatorApplicationTransaction> Transactions editing the
1754     *          field.
1755     * @return bool True if the field will be an empty text field after edits.
1756     */
1757    protected function validateIsEmptyTextField($field_value, array $xactions) {
1758      if (strlen($field_value) && empty($xactions)) {
1759        return false;
1760      }
1761  
1762      if ($xactions && strlen(last($xactions)->getNewValue())) {
1763        return false;
1764      }
1765  
1766      return true;
1767    }
1768  
1769  
1770  /* -(  Implicit CCs  )------------------------------------------------------- */
1771  
1772  
1773    /**
1774     * When a user interacts with an object, we might want to add them to CC.
1775     */
1776    final public function applyImplicitCC(
1777      PhabricatorLiskDAO $object,
1778      array $xactions) {
1779  
1780      if (!($object instanceof PhabricatorSubscribableInterface)) {
1781        // If the object isn't subscribable, we can't CC them.
1782        return $xactions;
1783      }
1784  
1785      $actor_phid = $this->getActingAsPHID();
1786  
1787      $type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
1788      if (phid_get_type($actor_phid) != $type_user) {
1789        // Transactions by application actors like Herald, Harbormaster and
1790        // Diffusion should not CC the applications.
1791        return $xactions;
1792      }
1793  
1794      if ($object->isAutomaticallySubscribed($actor_phid)) {
1795        // If they're auto-subscribed, don't CC them.
1796        return $xactions;
1797      }
1798  
1799      $should_cc = false;
1800      foreach ($xactions as $xaction) {
1801        if ($this->shouldImplyCC($object, $xaction)) {
1802          $should_cc = true;
1803          break;
1804        }
1805      }
1806  
1807      if (!$should_cc) {
1808        // Only some types of actions imply a CC (like adding a comment).
1809        return $xactions;
1810      }
1811  
1812      if ($object->getPHID()) {
1813        if (isset($this->subscribers[$actor_phid])) {
1814          // If the user is already subscribed, don't implicitly CC them.
1815          return $xactions;
1816        }
1817  
1818        $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
1819          $object->getPHID(),
1820          PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
1821        $unsub = array_fuse($unsub);
1822        if (isset($unsub[$actor_phid])) {
1823          // If the user has previously unsubscribed from this object explicitly,
1824          // don't implicitly CC them.
1825          return $xactions;
1826        }
1827      }
1828  
1829      $xaction = newv(get_class(head($xactions)), array());
1830      $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
1831      $xaction->setNewValue(array('+' => array($actor_phid)));
1832  
1833      array_unshift($xactions, $xaction);
1834  
1835      return $xactions;
1836    }
1837  
1838    protected function shouldImplyCC(
1839      PhabricatorLiskDAO $object,
1840      PhabricatorApplicationTransaction $xaction) {
1841  
1842      return $xaction->isCommentTransaction();
1843    }
1844  
1845  
1846  /* -(  Sending Mail  )------------------------------------------------------- */
1847  
1848  
1849    /**
1850     * @task mail
1851     */
1852    protected function shouldSendMail(
1853      PhabricatorLiskDAO $object,
1854      array $xactions) {
1855      return false;
1856    }
1857  
1858  
1859    /**
1860     * @task mail
1861     */
1862    protected function sendMail(
1863      PhabricatorLiskDAO $object,
1864      array $xactions) {
1865  
1866      // Check if any of the transactions are visible. If we don't have any
1867      // visible transactions, don't send the mail.
1868  
1869      $any_visible = false;
1870      foreach ($xactions as $xaction) {
1871        if (!$xaction->shouldHideForMail($xactions)) {
1872          $any_visible = true;
1873          break;
1874        }
1875      }
1876  
1877      if (!$any_visible) {
1878        return;
1879      }
1880  
1881      $email_to = array_filter(array_unique($this->getMailTo($object)));
1882      $email_cc = array_filter(array_unique($this->getMailCC($object)));
1883  
1884      $phids = array_merge($email_to, $email_cc);
1885      $handles = id(new PhabricatorHandleQuery())
1886        ->setViewer($this->requireActor())
1887        ->withPHIDs($phids)
1888        ->execute();
1889  
1890      $template = $this->buildMailTemplate($object);
1891      $body = $this->buildMailBody($object, $xactions);
1892  
1893      $mail_tags = $this->getMailTags($object, $xactions);
1894      $action = $this->getMailAction($object, $xactions);
1895  
1896      $reply_handler = $this->buildReplyHandler($object);
1897      $reply_section = $reply_handler->getReplyHandlerInstructions();
1898      if ($reply_section !== null) {
1899        $body->addReplySection($reply_section);
1900      }
1901  
1902      $body->addEmailPreferenceSection();
1903  
1904      $template
1905        ->setFrom($this->getActingAsPHID())
1906        ->setSubjectPrefix($this->getMailSubjectPrefix())
1907        ->setVarySubjectPrefix('['.$action.']')
1908        ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
1909        ->setRelatedPHID($object->getPHID())
1910        ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
1911        ->setMailTags($mail_tags)
1912        ->setIsBulk(true)
1913        ->setBody($body->render())
1914        ->setHTMLBody($body->renderHTML());
1915  
1916      foreach ($body->getAttachments() as $attachment) {
1917        $template->addAttachment($attachment);
1918      }
1919  
1920      $herald_xscript = $this->getHeraldTranscript();
1921      if ($herald_xscript) {
1922        $herald_header = $herald_xscript->getXHeraldRulesHeader();
1923        $herald_header = HeraldTranscript::saveXHeraldRulesHeader(
1924          $object->getPHID(),
1925          $herald_header);
1926      } else {
1927        $herald_header = HeraldTranscript::loadXHeraldRulesHeader(
1928          $object->getPHID());
1929      }
1930  
1931      if ($herald_header) {
1932        $template->addHeader('X-Herald-Rules', $herald_header);
1933      }
1934  
1935      if ($object instanceof PhabricatorProjectInterface) {
1936        $this->addMailProjectMetadata($object, $template);
1937      }
1938  
1939      if ($this->getParentMessageID()) {
1940        $template->setParentMessageID($this->getParentMessageID());
1941      }
1942  
1943      $mails = $reply_handler->multiplexMail(
1944        $template,
1945        array_select_keys($handles, $email_to),
1946        array_select_keys($handles, $email_cc));
1947  
1948      foreach ($mails as $mail) {
1949        $mail->saveAndSend();
1950      }
1951  
1952      $template->addTos($email_to);
1953      $template->addCCs($email_cc);
1954  
1955      return $template;
1956    }
1957  
1958    private function addMailProjectMetadata(
1959      PhabricatorLiskDAO $object,
1960      PhabricatorMetaMTAMail $template) {
1961  
1962      $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
1963        $object->getPHID(),
1964        PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
1965  
1966      if (!$project_phids) {
1967        return;
1968      }
1969  
1970      // TODO: This viewer isn't quite right. It would be slightly better to use
1971      // the mail recipient, but that's not very easy given the way rendering
1972      // works today.
1973  
1974      $handles = id(new PhabricatorHandleQuery())
1975        ->setViewer($this->requireActor())
1976        ->withPHIDs($project_phids)
1977        ->execute();
1978  
1979      $project_tags = array();
1980      foreach ($handles as $handle) {
1981        if (!$handle->isComplete()) {
1982          continue;
1983        }
1984        $project_tags[] = '<'.$handle->getObjectName().'>';
1985      }
1986  
1987      if (!$project_tags) {
1988        return;
1989      }
1990  
1991      $project_tags = implode(', ', $project_tags);
1992      $template->addHeader('X-Phabricator-Projects', $project_tags);
1993    }
1994  
1995  
1996    protected function getMailThreadID(PhabricatorLiskDAO $object) {
1997      return $object->getPHID();
1998    }
1999  
2000  
2001    /**
2002     * @task mail
2003     */
2004    protected function getStrongestAction(
2005      PhabricatorLiskDAO $object,
2006      array $xactions) {
2007      return last(msort($xactions, 'getActionStrength'));
2008    }
2009  
2010  
2011    /**
2012     * @task mail
2013     */
2014    protected function buildReplyHandler(PhabricatorLiskDAO $object) {
2015      throw new Exception('Capability not supported.');
2016    }
2017  
2018    /**
2019     * @task mail
2020     */
2021    protected function getMailSubjectPrefix() {
2022      throw new Exception('Capability not supported.');
2023    }
2024  
2025  
2026    /**
2027     * @task mail
2028     */
2029    protected function getMailTags(
2030      PhabricatorLiskDAO $object,
2031      array $xactions) {
2032      $tags = array();
2033  
2034      foreach ($xactions as $xaction) {
2035        $tags[] = $xaction->getMailTags();
2036      }
2037  
2038      return array_mergev($tags);
2039    }
2040  
2041    /**
2042     * @task mail
2043     */
2044    public function getMailTagsMap() {
2045      // TODO: We should move shared mail tags, like "comment", here.
2046      return array();
2047    }
2048  
2049  
2050    /**
2051     * @task mail
2052     */
2053    protected function getMailAction(
2054      PhabricatorLiskDAO $object,
2055      array $xactions) {
2056      return $this->getStrongestAction($object, $xactions)->getActionName();
2057    }
2058  
2059  
2060    /**
2061     * @task mail
2062     */
2063    protected function buildMailTemplate(PhabricatorLiskDAO $object) {
2064      throw new Exception('Capability not supported.');
2065    }
2066  
2067  
2068    /**
2069     * @task mail
2070     */
2071    protected function getMailTo(PhabricatorLiskDAO $object) {
2072      throw new Exception('Capability not supported.');
2073    }
2074  
2075  
2076    /**
2077     * @task mail
2078     */
2079    protected function getMailCC(PhabricatorLiskDAO $object) {
2080      $phids = array();
2081      $has_support = false;
2082  
2083      if ($object instanceof PhabricatorSubscribableInterface) {
2084        $phids[] = $this->subscribers;
2085        $has_support = true;
2086      }
2087  
2088      if ($object instanceof PhabricatorProjectInterface) {
2089        $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
2090          $object->getPHID(),
2091          PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
2092  
2093        if ($project_phids) {
2094          $watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
2095  
2096          $query = id(new PhabricatorEdgeQuery())
2097            ->withSourcePHIDs($project_phids)
2098            ->withEdgeTypes(array($watcher_type));
2099          $query->execute();
2100  
2101          $watcher_phids = $query->getDestinationPHIDs();
2102          if ($watcher_phids) {
2103            // We need to do a visibility check for all the watchers, as
2104            // watching a project is not a guarantee that you can see objects
2105            // associated with it.
2106            $users = id(new PhabricatorPeopleQuery())
2107              ->setViewer($this->requireActor())
2108              ->withPHIDs($watcher_phids)
2109              ->execute();
2110  
2111            $watchers = array();
2112            foreach ($users as $user) {
2113              $can_see = PhabricatorPolicyFilter::hasCapability(
2114                $user,
2115                $object,
2116                PhabricatorPolicyCapability::CAN_VIEW);
2117              if ($can_see) {
2118                $watchers[] = $user->getPHID();
2119              }
2120            }
2121            $phids[] = $watchers;
2122          }
2123        }
2124  
2125        $has_support = true;
2126      }
2127  
2128      if (!$has_support) {
2129        throw new Exception('Capability not supported.');
2130      }
2131  
2132      return array_mergev($phids);
2133    }
2134  
2135  
2136    /**
2137     * @task mail
2138     */
2139    protected function buildMailBody(
2140      PhabricatorLiskDAO $object,
2141      array $xactions) {
2142  
2143      $headers = array();
2144      $comments = array();
2145  
2146      foreach ($xactions as $xaction) {
2147        if ($xaction->shouldHideForMail($xactions)) {
2148          continue;
2149        }
2150  
2151        $header = $xaction->getTitleForMail();
2152        if ($header !== null) {
2153          $headers[] = $header;
2154        }
2155  
2156        $comment = $xaction->getBodyForMail();
2157        if ($comment !== null) {
2158          $comments[] = $comment;
2159        }
2160      }
2161  
2162      $body = new PhabricatorMetaMTAMailBody();
2163      $body->setViewer($this->requireActor());
2164      $body->addRawSection(implode("\n", $headers));
2165  
2166      foreach ($comments as $comment) {
2167        $body->addRemarkupSection($comment);
2168      }
2169  
2170      if ($object instanceof PhabricatorCustomFieldInterface) {
2171        $field_list = PhabricatorCustomField::getObjectFields(
2172          $object,
2173          PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
2174        $field_list->setViewer($this->getActor());
2175        $field_list->readFieldsFromStorage($object);
2176  
2177        foreach ($field_list->getFields() as $field) {
2178          $field->updateTransactionMailBody(
2179            $body,
2180            $this,
2181            $xactions);
2182        }
2183      }
2184  
2185      return $body;
2186    }
2187  
2188  
2189  /* -(  Publishing Feed Stories  )-------------------------------------------- */
2190  
2191  
2192    /**
2193     * @task feed
2194     */
2195    protected function shouldPublishFeedStory(
2196      PhabricatorLiskDAO $object,
2197      array $xactions) {
2198      return false;
2199    }
2200  
2201  
2202    /**
2203     * @task feed
2204     */
2205    protected function getFeedStoryType() {
2206      return 'PhabricatorApplicationTransactionFeedStory';
2207    }
2208  
2209  
2210    /**
2211     * @task feed
2212     */
2213    protected function getFeedRelatedPHIDs(
2214      PhabricatorLiskDAO $object,
2215      array $xactions) {
2216  
2217      $phids = array(
2218        $object->getPHID(),
2219        $this->getActingAsPHID(),
2220      );
2221  
2222      if ($object instanceof PhabricatorProjectInterface) {
2223        $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
2224          $object->getPHID(),
2225          PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
2226        foreach ($project_phids as $project_phid) {
2227          $phids[] = $project_phid;
2228        }
2229      }
2230  
2231      return $phids;
2232    }
2233  
2234  
2235    /**
2236     * @task feed
2237     */
2238    protected function getFeedNotifyPHIDs(
2239      PhabricatorLiskDAO $object,
2240      array $xactions) {
2241  
2242      return array_unique(array_merge(
2243        $this->getMailTo($object),
2244        $this->getMailCC($object)));
2245    }
2246  
2247  
2248    /**
2249     * @task feed
2250     */
2251    protected function getFeedStoryData(
2252      PhabricatorLiskDAO $object,
2253      array $xactions) {
2254  
2255      $xactions = msort($xactions, 'getActionStrength');
2256      $xactions = array_reverse($xactions);
2257  
2258      return array(
2259        'objectPHID'        => $object->getPHID(),
2260        'transactionPHIDs'  => mpull($xactions, 'getPHID'),
2261      );
2262    }
2263  
2264  
2265    /**
2266     * @task feed
2267     */
2268    protected function publishFeedStory(
2269      PhabricatorLiskDAO $object,
2270      array $xactions,
2271      array $mailed_phids) {
2272  
2273      $xactions = mfilter($xactions, 'shouldHideForFeed', true);
2274  
2275      if (!$xactions) {
2276        return;
2277      }
2278  
2279      $related_phids = $this->getFeedRelatedPHIDs($object, $xactions);
2280      $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions);
2281  
2282      $story_type = $this->getFeedStoryType();
2283      $story_data = $this->getFeedStoryData($object, $xactions);
2284  
2285      id(new PhabricatorFeedStoryPublisher())
2286        ->setStoryType($story_type)
2287        ->setStoryData($story_data)
2288        ->setStoryTime(time())
2289        ->setStoryAuthorPHID($this->getActingAsPHID())
2290        ->setRelatedPHIDs($related_phids)
2291        ->setPrimaryObjectPHID($object->getPHID())
2292        ->setSubscribedPHIDs($subscribed_phids)
2293        ->setMailRecipientPHIDs($mailed_phids)
2294        ->setMailTags($this->getMailTags($object, $xactions))
2295        ->publish();
2296    }
2297  
2298  
2299  /* -(  Search Index  )------------------------------------------------------- */
2300  
2301  
2302    /**
2303     * @task search
2304     */
2305    protected function supportsSearch() {
2306      return false;
2307    }
2308  
2309  
2310  /* -(  Herald Integration )-------------------------------------------------- */
2311  
2312  
2313    protected function shouldApplyHeraldRules(
2314      PhabricatorLiskDAO $object,
2315      array $xactions) {
2316      return false;
2317    }
2318  
2319    protected function buildHeraldAdapter(
2320      PhabricatorLiskDAO $object,
2321      array $xactions) {
2322      throw new Exception('No herald adapter specified.');
2323    }
2324  
2325    private function setHeraldAdapter(HeraldAdapter $adapter) {
2326      $this->heraldAdapter = $adapter;
2327      return $this;
2328    }
2329  
2330    protected function getHeraldAdapter() {
2331      return $this->heraldAdapter;
2332    }
2333  
2334    private function setHeraldTranscript(HeraldTranscript $transcript) {
2335      $this->heraldTranscript = $transcript;
2336      return $this;
2337    }
2338  
2339    protected function getHeraldTranscript() {
2340      return $this->heraldTranscript;
2341    }
2342  
2343    private function applyHeraldRules(
2344      PhabricatorLiskDAO $object,
2345      array $xactions) {
2346  
2347      $adapter = $this->buildHeraldAdapter($object, $xactions);
2348      $adapter->setContentSource($this->getContentSource());
2349      $adapter->setIsNewObject($this->getIsNewObject());
2350      $xscript = HeraldEngine::loadAndApplyRules($adapter);
2351  
2352      $this->setHeraldAdapter($adapter);
2353      $this->setHeraldTranscript($xscript);
2354  
2355      return array_merge(
2356        $this->didApplyHeraldRules($object, $adapter, $xscript),
2357        $adapter->getQueuedTransactions());
2358    }
2359  
2360    protected function didApplyHeraldRules(
2361      PhabricatorLiskDAO $object,
2362      HeraldAdapter $adapter,
2363      HeraldTranscript $transcript) {
2364      return array();
2365    }
2366  
2367  
2368  /* -(  Custom Fields  )------------------------------------------------------ */
2369  
2370  
2371    /**
2372     * @task customfield
2373     */
2374    private function getCustomFieldForTransaction(
2375      PhabricatorLiskDAO $object,
2376      PhabricatorApplicationTransaction $xaction) {
2377  
2378      $field_key = $xaction->getMetadataValue('customfield:key');
2379      if (!$field_key) {
2380        throw new Exception(
2381          "Custom field transaction has no 'customfield:key'!");
2382      }
2383  
2384      $field = PhabricatorCustomField::getObjectField(
2385        $object,
2386        PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
2387        $field_key);
2388  
2389      if (!$field) {
2390        throw new Exception(
2391          "Custom field transaction has invalid 'customfield:key'; field ".
2392          "'{$field_key}' is disabled or does not exist.");
2393      }
2394  
2395      if (!$field->shouldAppearInApplicationTransactions()) {
2396        throw new Exception(
2397          "Custom field transaction '{$field_key}' does not implement ".
2398          "integration for ApplicationTransactions.");
2399      }
2400  
2401      $field->setViewer($this->getActor());
2402  
2403      return $field;
2404    }
2405  
2406  
2407  /* -(  Files  )-------------------------------------------------------------- */
2408  
2409  
2410    /**
2411     * Extract the PHIDs of any files which these transactions attach.
2412     *
2413     * @task files
2414     */
2415    private function extractFilePHIDs(
2416      PhabricatorLiskDAO $object,
2417      array $xactions) {
2418  
2419      $blocks = array();
2420      foreach ($xactions as $xaction) {
2421        $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction);
2422      }
2423      $blocks = array_mergev($blocks);
2424  
2425      $phids = array();
2426      if ($blocks) {
2427        $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
2428          $this->getActor(),
2429          $blocks);
2430      }
2431  
2432      foreach ($xactions as $xaction) {
2433        $phids[] = $this->extractFilePHIDsFromCustomTransaction(
2434          $object,
2435          $xaction);
2436      }
2437  
2438      $phids = array_unique(array_filter(array_mergev($phids)));
2439      if (!$phids) {
2440        return array();
2441      }
2442  
2443      // Only let a user attach files they can actually see, since this would
2444      // otherwise let you access any file by attaching it to an object you have
2445      // view permission on.
2446  
2447      $files = id(new PhabricatorFileQuery())
2448        ->setViewer($this->getActor())
2449        ->withPHIDs($phids)
2450        ->execute();
2451  
2452      return mpull($files, 'getPHID');
2453    }
2454  
2455    /**
2456     * @task files
2457     */
2458    protected function extractFilePHIDsFromCustomTransaction(
2459      PhabricatorLiskDAO $object,
2460      PhabricatorApplicationTransaction $xaction) {
2461      return array();
2462    }
2463  
2464  
2465    /**
2466     * @task files
2467     */
2468    private function attachFiles(
2469      PhabricatorLiskDAO $object,
2470      array $file_phids) {
2471  
2472      if (!$file_phids) {
2473        return;
2474      }
2475  
2476      $editor = new PhabricatorEdgeEditor();
2477  
2478      $src = $object->getPHID();
2479      $type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
2480      foreach ($file_phids as $dst) {
2481        $editor->addEdge($src, $type, $dst);
2482      }
2483  
2484      $editor->save();
2485    }
2486  
2487    private function applyInverseEdgeTransactions(
2488      PhabricatorLiskDAO $object,
2489      PhabricatorApplicationTransaction $xaction,
2490      $inverse_type) {
2491  
2492      $old = $xaction->getOldValue();
2493      $new = $xaction->getNewValue();
2494  
2495      $add = array_keys(array_diff_key($new, $old));
2496      $rem = array_keys(array_diff_key($old, $new));
2497  
2498      $add = array_fuse($add);
2499      $rem = array_fuse($rem);
2500      $all = $add + $rem;
2501  
2502      $nodes = id(new PhabricatorObjectQuery())
2503        ->setViewer($this->requireActor())
2504        ->withPHIDs($all)
2505        ->execute();
2506  
2507      foreach ($nodes as $node) {
2508        if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
2509          continue;
2510        }
2511  
2512        $editor = $node->getApplicationTransactionEditor();
2513        $template = $node->getApplicationTransactionTemplate();
2514        $target = $node->getApplicationTransactionObject();
2515  
2516        if (isset($add[$node->getPHID()])) {
2517          $edge_edit_type = '+';
2518        } else {
2519          $edge_edit_type = '-';
2520        }
2521  
2522        $template
2523          ->setTransactionType($xaction->getTransactionType())
2524          ->setMetadataValue('edge:type', $inverse_type)
2525          ->setNewValue(
2526            array(
2527              $edge_edit_type => array($object->getPHID() => $object->getPHID()),
2528            ));
2529  
2530        $editor
2531          ->setContinueOnNoEffect(true)
2532          ->setContinueOnMissingFields(true)
2533          ->setParentMessageID($this->getParentMessageID())
2534          ->setIsInverseEdgeEditor(true)
2535          ->setActor($this->requireActor())
2536          ->setActingAsPHID($this->getActingAsPHID())
2537          ->setContentSource($this->getContentSource());
2538  
2539        $editor->applyTransactions($target, array($template));
2540      }
2541    }
2542  
2543  }


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