PhabricatorApplicationTransactionEditor.php 158 KB
Newer Older
epriestley's avatar
epriestley committed
1 2
<?php

3
/**
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
 *
 * Publishing and Managing State
 * ======
 *
 * After applying changes, the Editor queues a worker to publish mail, feed,
 * and notifications, and to perform other background work like updating search
 * indexes. This allows it to do this work without impacting performance for
 * users.
 *
 * When work is moved to the daemons, the Editor state is serialized by
 * @{method:getWorkerState}, then reloaded in a daemon process by
 * @{method:loadWorkerState}. **This is fragile.**
 *
 * State is not persisted into the daemons by default, because we can not send
 * arbitrary objects into the queue. This means the default behavior of any
 * state properties is to reset to their defaults without warning prior to
 * publishing.
 *
 * The easiest way to avoid this is to keep Editors stateless: the overwhelming
 * majority of Editors can be written statelessly. If you need to maintain
 * state, you can either:
 *
 *   - not require state to exist during publishing; or
 *   - pass state to the daemons by implementing @{method:getCustomWorkerState}
 *     and @{method:loadCustomWorkerState}.
 *
 * This architecture isn't ideal, and we may eventually split this class into
 * "Editor" and "Publisher" parts to make it more robust. See T6367 for some
 * discussion and context.
 *
34 35
 * @task mail Sending Mail
 * @task feed Publishing Feed Stories
36
 * @task search Search Index
37 38
 * @task files Integration with Files
 * @task workers Managing Workers
39
 */
epriestley's avatar
epriestley committed
40 41 42 43 44 45 46 47 48
abstract class PhabricatorApplicationTransactionEditor
  extends PhabricatorEditor {

  private $contentSource;
  private $object;
  private $xactions;

  private $isNewObject;
  private $mentionedPHIDs;
49
  private $continueOnNoEffect;
50
  private $continueOnMissingFields;
51
  private $raiseWarnings;
52
  private $parentMessageID;
Bob Trahan's avatar
Bob Trahan committed
53 54
  private $heraldAdapter;
  private $heraldTranscript;
55
  private $subscribers;
56
  private $unmentionablePHIDMap = array();
57
  private $transactionGroupID;
58
  private $applicationEmail;
59

60
  private $isPreview;
61
  private $isHeraldEditor;
62
  private $isInverseEdgeEditor;
63 64
  private $actingAsPHID;

65 66 67
  private $heraldEmailPHIDs = array();
  private $heraldForcedEmailPHIDs = array();
  private $heraldHeader;
68 69 70 71
  private $mailToPHIDs = array();
  private $mailCCPHIDs = array();
  private $feedNotifyPHIDs = array();
  private $feedRelatedPHIDs = array();
72
  private $feedShouldPublish = false;
73
  private $mailShouldSend = false;
74
  private $modularTypes;
75
  private $silent;
76
  private $mustEncrypt = array();
77 78
  private $stampTemplates = array();
  private $mailStamps = array();
79 80 81
  private $oldTo = array();
  private $oldCC = array();
  private $mailRemovedPHIDs = array();
82
  private $mailUnexpandablePHIDs = array();
83
  private $mailMutedPHIDs = array();
84
  private $webhookMap = array();
epriestley's avatar
epriestley committed
85

86
  private $transactionQueue = array();
87
  private $sendHistory = false;
88 89 90 91
  private $shouldRequireMFA = false;
  private $hasRequiredMFA = false;
  private $request;
  private $cancelURI;
92
  private $extensions;
93

94 95 96 97 98
  private $parentEditor;
  private $subEditors = array();
  private $publishableObject;
  private $publishableTransactions;

99 100
  const STORAGE_ENCODING_BINARY = 'binary';

epriestley's avatar
epriestley committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
  /**
   * Get the class name for the application this editor is a part of.
   *
   * Uninstalling the application will disable the editor.
   *
   * @return string Editor's application class name.
   */
  abstract public function getEditorApplicationClass();


  /**
   * Get a description of the objects this editor edits, like "Differential
   * Revisions".
   *
   * @return string Human readable description of edited objects.
   */
  abstract public function getEditorObjectsDescription();


120 121 122 123 124 125 126 127 128 129 130
  public function setActingAsPHID($acting_as_phid) {
    $this->actingAsPHID = $acting_as_phid;
    return $this;
  }

  public function getActingAsPHID() {
    if ($this->actingAsPHID) {
      return $this->actingAsPHID;
    }
    return $this->getActor()->getPHID();
  }
131

epriestley's avatar
epriestley committed
132

133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
  /**
   * When the editor tries to apply transactions that have no effect, should
   * it raise an exception (default) or drop them and continue?
   *
   * Generally, you will set this flag for edits coming from "Edit" interfaces,
   * and leave it cleared for edits coming from "Comment" interfaces, so the
   * user will get a useful error if they try to submit a comment that does
   * nothing (e.g., empty comment with a status change that has already been
   * performed by another user).
   *
   * @param bool  True to drop transactions without effect and continue.
   * @return this
   */
  public function setContinueOnNoEffect($continue) {
    $this->continueOnNoEffect = $continue;
    return $this;
  }

  public function getContinueOnNoEffect() {
    return $this->continueOnNoEffect;
  }
epriestley's avatar
epriestley committed
154

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

  /**
   * When the editor tries to apply transactions which don't populate all of
   * an object's required fields, should it raise an exception (default) or
   * drop them and continue?
   *
   * For example, if a user adds a new required custom field (like "Severity")
   * to a task, all existing tasks won't have it populated. When users
   * manually edit existing tasks, it's usually desirable to have them provide
   * a severity. However, other operations (like batch editing just the
   * owner of a task) will fail by default.
   *
   * By setting this flag for edit operations which apply to specific fields
   * (like the priority, batch, and merge editors in Maniphest), these
   * operations can continue to function even if an object is outdated.
   *
   * @param bool  True to continue when transactions don't completely satisfy
   *              all required fields.
   * @return this
   */
  public function setContinueOnMissingFields($continue_on_missing_fields) {
    $this->continueOnMissingFields = $continue_on_missing_fields;
    return $this;
  }

  public function getContinueOnMissingFields() {
    return $this->continueOnMissingFields;
  }


185 186 187 188 189 190 191 192 193 194 195 196
  /**
   * Not strictly necessary, but reply handlers ideally set this value to
   * make email threading work better.
   */
  public function setParentMessageID($parent_message_id) {
    $this->parentMessageID = $parent_message_id;
    return $this;
  }
  public function getParentMessageID() {
    return $this->parentMessageID;
  }

197
  public function getIsNewObject() {
epriestley's avatar
epriestley committed
198 199 200
    return $this->isNewObject;
  }

201
  public function getMentionedPHIDs() {
epriestley's avatar
epriestley committed
202 203 204
    return $this->mentionedPHIDs;
  }

205 206 207 208 209 210 211 212 213
  public function setIsPreview($is_preview) {
    $this->isPreview = $is_preview;
    return $this;
  }

  public function getIsPreview() {
    return $this->isPreview;
  }

214 215 216 217 218 219 220 221 222
  public function setIsSilent($silent) {
    $this->silent = $silent;
    return $this;
  }

  public function getIsSilent() {
    return $this->silent;
  }

223 224 225 226
  public function getMustEncrypt() {
    return $this->mustEncrypt;
  }

227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
  public function getHeraldRuleMonograms() {
    // Convert the stored "<123>, <456>" string into a list: "H123", "H456".
    $list = $this->heraldHeader;
    $list = preg_split('/[, ]+/', $list);

    foreach ($list as $key => $item) {
      $item = trim($item, '<>');

      if (!is_numeric($item)) {
        unset($list[$key]);
        continue;
      }

      $list[$key] = 'H'.$item;
    }

    return $list;
  }

246 247 248 249 250 251 252 253 254
  public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
    $this->isInverseEdgeEditor = $is_inverse_edge_editor;
    return $this;
  }

  public function getIsInverseEdgeEditor() {
    return $this->isInverseEdgeEditor;
  }

255 256 257 258 259 260 261 262 263
  public function setIsHeraldEditor($is_herald_editor) {
    $this->isHeraldEditor = $is_herald_editor;
    return $this;
  }

  public function getIsHeraldEditor() {
    return $this->isHeraldEditor;
  }

264 265 266 267
  public function addUnmentionablePHIDs(array $phids) {
    foreach ($phids as $phid) {
      $this->unmentionablePHIDMap[$phid] = true;
    }
268 269 270
    return $this;
  }

271
  private function getUnmentionablePHIDMap() {
272 273 274
    return $this->unmentionablePHIDMap;
  }

epriestley's avatar
epriestley committed
275 276 277
  protected function shouldEnableMentions(
    PhabricatorLiskDAO $object,
    array $xactions) {
278 279 280
    return true;
  }

281 282 283 284 285 286 287 288 289 290
  public function setApplicationEmail(
    PhabricatorMetaMTAApplicationEmail $email) {
    $this->applicationEmail = $email;
    return $this;
  }

  public function getApplicationEmail() {
    return $this->applicationEmail;
  }

291 292 293 294 295 296 297 298 299
  public function setRaiseWarnings($raise_warnings) {
    $this->raiseWarnings = $raise_warnings;
    return $this;
  }

  public function getRaiseWarnings() {
    return $this->raiseWarnings;
  }

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
  public function setShouldRequireMFA($should_require_mfa) {
    if ($this->hasRequiredMFA) {
      throw new Exception(
        pht(
          'Call to setShouldRequireMFA() is too late: this Editor has already '.
          'checked for MFA requirements.'));
    }

    $this->shouldRequireMFA = $should_require_mfa;
    return $this;
  }

  public function getShouldRequireMFA() {
    return $this->shouldRequireMFA;
  }

316 317 318 319 320 321 322 323 324 325 326 327 328
  public function getTransactionTypesForObject($object) {
    $old = $this->object;
    try {
      $this->object = $object;
      $result = $this->getTransactionTypes();
      $this->object = $old;
    } catch (Exception $ex) {
      $this->object = $old;
      throw $ex;
    }
    return $result;
  }

epriestley's avatar
epriestley committed
329
  public function getTransactionTypes() {
330
    $types = array();
epriestley's avatar
epriestley committed
331

epriestley's avatar
epriestley committed
332
    $types[] = PhabricatorTransactions::TYPE_CREATE;
333
    $types[] = PhabricatorTransactions::TYPE_HISTORY;
epriestley's avatar
epriestley committed
334

335 336 337 338
    if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) {
      $types[] = PhabricatorTransactions::TYPE_SUBTYPE;
    }

epriestley's avatar
epriestley committed
339 340 341 342
    if ($this->object instanceof PhabricatorSubscribableInterface) {
      $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
    }

343 344 345 346
    if ($this->object instanceof PhabricatorCustomFieldInterface) {
      $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
    }

epriestley's avatar
epriestley committed
347 348 349 350
    if ($this->object instanceof PhabricatorTokenReceiverInterface) {
      $types[] = PhabricatorTransactions::TYPE_TOKEN;
    }

351 352
    if ($this->object instanceof PhabricatorProjectInterface ||
        $this->object instanceof PhabricatorMentionableInterface) {
353 354 355
      $types[] = PhabricatorTransactions::TYPE_EDGE;
    }

epriestley's avatar
epriestley committed
356 357 358 359
    if ($this->object instanceof PhabricatorSpacesInterface) {
      $types[] = PhabricatorTransactions::TYPE_SPACE;
    }

360 361
    $types[] = PhabricatorTransactions::TYPE_MFA;

362 363 364 365 366 367 368 369
    $template = $this->object->getApplicationTransactionTemplate();
    if ($template instanceof PhabricatorModularTransaction) {
      $xtypes = $template->newModularTransactionTypes();
      foreach ($xtypes as $xtype) {
        $types[] = $xtype->getTransactionTypeConstant();
      }
    }

370
    if ($template) {
371
      $comment = $template->getApplicationTransactionCommentObject();
372 373 374 375 376
      if ($comment) {
        $types[] = PhabricatorTransactions::TYPE_COMMENT;
      }
    }

epriestley's avatar
epriestley committed
377 378 379 380 381 382
    return $types;
  }

  private function adjustTransactionValues(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
383 384 385 386 387

    if ($xaction->shouldGenerateOldValue()) {
      $old = $this->getTransactionOldValue($object, $xaction);
      $xaction->setOldValue($old);
    }
epriestley's avatar
epriestley committed
388 389 390 391 392 393 394 395

    $new = $this->getTransactionNewValue($object, $xaction);
    $xaction->setNewValue($new);
  }

  private function getTransactionOldValue(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
396 397 398 399 400

    $type = $xaction->getTransactionType();

    $xtype = $this->getModularTransactionType($type);
    if ($xtype) {
401 402
      $xtype = clone $xtype;
      $xtype->setStorage($xaction);
403 404 405 406
      return $xtype->generateOldValue($object);
    }

    switch ($type) {
epriestley's avatar
epriestley committed
407
      case PhabricatorTransactions::TYPE_CREATE:
408
      case PhabricatorTransactions::TYPE_HISTORY:
epriestley's avatar
epriestley committed
409
        return null;
410 411
      case PhabricatorTransactions::TYPE_SUBTYPE:
        return $object->getEditEngineSubtype();
412 413
      case PhabricatorTransactions::TYPE_MFA:
        return null;
epriestley's avatar
epriestley committed
414
      case PhabricatorTransactions::TYPE_SUBSCRIBERS:
415
        return array_values($this->subscribers);
epriestley's avatar
epriestley committed
416
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
417 418 419
        if ($this->getIsNewObject()) {
          return null;
        }
epriestley's avatar
epriestley committed
420 421
        return $object->getViewPolicy();
      case PhabricatorTransactions::TYPE_EDIT_POLICY:
422 423 424
        if ($this->getIsNewObject()) {
          return null;
        }
epriestley's avatar
epriestley committed
425
        return $object->getEditPolicy();
epriestley's avatar
epriestley committed
426
      case PhabricatorTransactions::TYPE_JOIN_POLICY:
427 428 429
        if ($this->getIsNewObject()) {
          return null;
        }
epriestley's avatar
epriestley committed
430
        return $object->getJoinPolicy();
epriestley's avatar
epriestley committed
431
      case PhabricatorTransactions::TYPE_SPACE:
432 433 434 435
        if ($this->getIsNewObject()) {
          return null;
        }

epriestley's avatar
epriestley committed
436 437 438 439 440 441 442
        $space_phid = $object->getSpacePHID();
        if ($space_phid === null) {
          $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
          if ($default_space) {
            $space_phid = $default_space->getPHID();
          }
        }
epriestley's avatar
epriestley committed
443

epriestley's avatar
epriestley committed
444
        return $space_phid;
445 446 447
      case PhabricatorTransactions::TYPE_EDGE:
        $edge_type = $xaction->getMetadataValue('edge:type');
        if (!$edge_type) {
Joshua Spence's avatar
Joshua Spence committed
448 449 450 451
          throw new Exception(
            pht(
              "Edge transaction has no '%s'!",
              'edge:type'));
452 453
        }

454 455 456 457 458 459
        // See T13082. If this is an inverse edit, the parent editor has
        // already populated the transaction values correctly.
        if ($this->getIsInverseEdgeEditor()) {
          return $xaction->getOldValue();
        }

460 461 462 463 464 465 466 467 468 469 470 471 472
        $old_edges = array();
        if ($object->getPHID()) {
          $edge_src = $object->getPHID();

          $old_edges = id(new PhabricatorEdgeQuery())
            ->withSourcePHIDs(array($edge_src))
            ->withEdgeTypes(array($edge_type))
            ->needEdgeData(true)
            ->execute();

          $old_edges = $old_edges[$edge_src][$edge_type];
        }
        return $old_edges;
473
      case PhabricatorTransactions::TYPE_CUSTOMFIELD:
474 475 476
        // NOTE: Custom fields have their old value pre-populated when they are
        // built by PhabricatorCustomFieldList.
        return $xaction->getOldValue();
477 478
      case PhabricatorTransactions::TYPE_COMMENT:
        return null;
epriestley's avatar
epriestley committed
479 480 481 482 483 484 485 486
      default:
        return $this->getCustomTransactionOldValue($object, $xaction);
    }
  }

  private function getTransactionNewValue(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
487 488 489 490 491

    $type = $xaction->getTransactionType();

    $xtype = $this->getModularTransactionType($type);
    if ($xtype) {
492 493
      $xtype = clone $xtype;
      $xtype->setStorage($xaction);
494 495 496 497
      return $xtype->generateNewValue($object, $xaction->getNewValue());
    }

    switch ($type) {
epriestley's avatar
epriestley committed
498 499
      case PhabricatorTransactions::TYPE_CREATE:
        return null;
epriestley's avatar
epriestley committed
500 501 502 503
      case PhabricatorTransactions::TYPE_SUBSCRIBERS:
        return $this->getPHIDTransactionNewValue($xaction);
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
      case PhabricatorTransactions::TYPE_EDIT_POLICY:
epriestley's avatar
epriestley committed
504
      case PhabricatorTransactions::TYPE_JOIN_POLICY:
epriestley's avatar
epriestley committed
505
      case PhabricatorTransactions::TYPE_TOKEN:
506
      case PhabricatorTransactions::TYPE_INLINESTATE:
507
      case PhabricatorTransactions::TYPE_SUBTYPE:
508
      case PhabricatorTransactions::TYPE_HISTORY:
509
        return $xaction->getNewValue();
510 511
      case PhabricatorTransactions::TYPE_MFA:
        return true;
epriestley's avatar
epriestley committed
512 513 514
      case PhabricatorTransactions::TYPE_SPACE:
        $space_phid = $xaction->getNewValue();
        if (!strlen($space_phid)) {
epriestley's avatar
epriestley committed
515 516 517 518 519 520 521
          // If an install has no Spaces or the Spaces controls are not visible
          // to the viewer, we might end up with the empty string here instead
          // of a strict `null`, because some controller just used `getStr()`
          // to read the space PHID from the request.
          // Just make this work like callers might reasonably expect so we
          // don't need to handle this specially in every EditController.
          return $this->getActor()->getDefaultSpacePHID();
epriestley's avatar
epriestley committed
522 523 524
        } else {
          return $space_phid;
        }
525
      case PhabricatorTransactions::TYPE_EDGE:
526 527 528 529 530 531
        // See T13082. If this is an inverse edit, the parent editor has
        // already populated appropriate transaction values.
        if ($this->getIsInverseEdgeEditor()) {
          return $xaction->getNewValue();
        }

532 533 534 535 536 537 538 539 540
        $new_value = $this->getEdgeTransactionNewValue($xaction);

        $edge_type = $xaction->getMetadataValue('edge:type');
        $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
        if ($edge_type == $type_project) {
          $new_value = $this->applyProjectConflictRules($new_value);
        }

        return $new_value;
541 542
      case PhabricatorTransactions::TYPE_CUSTOMFIELD:
        $field = $this->getCustomFieldForTransaction($object, $xaction);
543
        return $field->getNewValueFromApplicationTransactions($xaction);
544 545
      case PhabricatorTransactions::TYPE_COMMENT:
        return null;
epriestley's avatar
epriestley committed
546 547 548 549 550 551 552 553
      default:
        return $this->getCustomTransactionNewValue($object, $xaction);
    }
  }

  protected function getCustomTransactionOldValue(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
Joshua Spence's avatar
Joshua Spence committed
554
    throw new Exception(pht('Capability not supported!'));
epriestley's avatar
epriestley committed
555 556 557 558 559
  }

  protected function getCustomTransactionNewValue(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
Joshua Spence's avatar
Joshua Spence committed
560
    throw new Exception(pht('Capability not supported!'));
epriestley's avatar
epriestley committed
561 562 563 564 565
  }

  protected function transactionHasEffect(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
566 567

    switch ($xaction->getTransactionType()) {
epriestley's avatar
epriestley committed
568
      case PhabricatorTransactions::TYPE_CREATE:
569
      case PhabricatorTransactions::TYPE_HISTORY:
epriestley's avatar
epriestley committed
570
        return true;
571 572 573
      case PhabricatorTransactions::TYPE_CUSTOMFIELD:
        $field = $this->getCustomFieldForTransaction($object, $xaction);
        return $field->getApplicationTransactionHasEffect($xaction);
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
      case PhabricatorTransactions::TYPE_EDGE:
        // A straight value comparison here doesn't always get the right
        // result, because newly added edges aren't fully populated. Instead,
        // compare the changes in a more granular way.
        $old = $xaction->getOldValue();
        $new = $xaction->getNewValue();

        $old_dst = array_keys($old);
        $new_dst = array_keys($new);

        // NOTE: For now, we don't consider edge reordering to be a change.
        // We have very few order-dependent edges and effectively no order
        // oriented UI. This might change in the future.
        sort($old_dst);
        sort($new_dst);

        if ($old_dst !== $new_dst) {
          // We've added or removed edges, so this transaction definitely
          // has an effect.
          return true;
        }

        // We haven't added or removed edges, but we might have changed
        // edge data.
        foreach ($old as $key => $old_value) {
          $new_value = $new[$key];
          if ($old_value['data'] !== $new_value['data']) {
            return true;
          }
        }

        return false;
606 607
    }

608 609 610 611 612 613 614 615 616
    $type = $xaction->getTransactionType();
    $xtype = $this->getModularTransactionType($type);
    if ($xtype) {
      return $xtype->getTransactionHasEffect(
        $object,
        $xaction->getOldValue(),
        $xaction->getNewValue());
    }

617 618 619 620
    if ($xaction->hasComment()) {
      return true;
    }

epriestley's avatar
epriestley committed
621 622 623
    return ($xaction->getOldValue() !== $xaction->getNewValue());
  }

624 625 626 627 628 629 630 631 632
  protected function shouldApplyInitialEffects(
    PhabricatorLiskDAO $object,
    array $xactions) {
    return false;
  }

  protected function applyInitialEffects(
    PhabricatorLiskDAO $object,
    array $xactions) {
633
    throw new PhutilMethodNotImplementedException();
634 635
  }

epriestley's avatar
epriestley committed
636 637 638
  private function applyInternalEffects(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
639

640 641 642 643
    $type = $xaction->getTransactionType();

    $xtype = $this->getModularTransactionType($type);
    if ($xtype) {
644 645
      $xtype = clone $xtype;
      $xtype->setStorage($xaction);
646 647 648 649
      return $xtype->applyInternalEffects($object, $xaction->getNewValue());
    }

    switch ($type) {
650 651 652
      case PhabricatorTransactions::TYPE_CUSTOMFIELD:
        $field = $this->getCustomFieldForTransaction($object, $xaction);
        return $field->applyApplicationTransactionInternalEffects($xaction);
epriestley's avatar
epriestley committed
653
      case PhabricatorTransactions::TYPE_CREATE:
654
      case PhabricatorTransactions::TYPE_HISTORY:
655
      case PhabricatorTransactions::TYPE_SUBTYPE:
656
      case PhabricatorTransactions::TYPE_MFA:
657
      case PhabricatorTransactions::TYPE_TOKEN:
658 659 660
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
      case PhabricatorTransactions::TYPE_EDIT_POLICY:
      case PhabricatorTransactions::TYPE_JOIN_POLICY:
661
      case PhabricatorTransactions::TYPE_SUBSCRIBERS:
662
      case PhabricatorTransactions::TYPE_INLINESTATE:
663
      case PhabricatorTransactions::TYPE_EDGE:
epriestley's avatar
epriestley committed
664
      case PhabricatorTransactions::TYPE_SPACE:
665
      case PhabricatorTransactions::TYPE_COMMENT:
666
        return $this->applyBuiltinInternalTransaction($object, $xaction);
epriestley's avatar
epriestley committed
667
    }
668

Bob Trahan's avatar
Bob Trahan committed
669
    return $this->applyCustomInternalTransaction($object, $xaction);
epriestley's avatar
epriestley committed
670 671 672 673 674
  }

  private function applyExternalEffects(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
675 676 677 678 679

    $type = $xaction->getTransactionType();

    $xtype = $this->getModularTransactionType($type);
    if ($xtype) {
680 681
      $xtype = clone $xtype;
      $xtype->setStorage($xaction);
682 683 684 685
      return $xtype->applyExternalEffects($object, $xaction->getNewValue());
    }

    switch ($type) {
epriestley's avatar
epriestley committed
686 687 688
      case PhabricatorTransactions::TYPE_SUBSCRIBERS:
        $subeditor = id(new PhabricatorSubscriptionsEditor())
          ->setObject($object)
689 690 691 692 693 694 695 696 697 698 699 700 701 702
          ->setActor($this->requireActor());

        $old_map = array_fuse($xaction->getOldValue());
        $new_map = array_fuse($xaction->getNewValue());

        $subeditor->unsubscribe(
          array_keys(
            array_diff_key($old_map, $new_map)));

        $subeditor->subscribeExplicit(
          array_keys(
            array_diff_key($new_map, $old_map)));

        $subeditor->save();
703 704 705 706 707 708 709 710

        // for the rest of these edits, subscribers should include those just
        // added as well as those just removed.
        $subscribers = array_unique(array_merge(
          $this->subscribers,
          $xaction->getOldValue(),
          $xaction->getNewValue()));
        $this->subscribers = $subscribers;
711
        return $this->applyBuiltinExternalTransaction($object, $xaction);
712

713 714 715
      case PhabricatorTransactions::TYPE_CUSTOMFIELD:
        $field = $this->getCustomFieldForTransaction($object, $xaction);
        return $field->applyApplicationTransactionExternalEffects($xaction);
epriestley's avatar
epriestley committed
716
      case PhabricatorTransactions::TYPE_CREATE:
717
      case PhabricatorTransactions::TYPE_HISTORY:
718
      case PhabricatorTransactions::TYPE_SUBTYPE:
719
      case PhabricatorTransactions::TYPE_MFA:
720
      case PhabricatorTransactions::TYPE_EDGE:
721
      case PhabricatorTransactions::TYPE_TOKEN:
722 723 724
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
      case PhabricatorTransactions::TYPE_EDIT_POLICY:
      case PhabricatorTransactions::TYPE_JOIN_POLICY:
725
      case PhabricatorTransactions::TYPE_INLINESTATE:
epriestley's avatar
epriestley committed
726
      case PhabricatorTransactions::TYPE_SPACE:
727
      case PhabricatorTransactions::TYPE_COMMENT:
728
        return $this->applyBuiltinExternalTransaction($object, $xaction);
epriestley's avatar
epriestley committed
729
    }
730

Bob Trahan's avatar
Bob Trahan committed
731
    return $this->applyCustomExternalTransaction($object, $xaction);
epriestley's avatar
epriestley committed
732 733 734 735 736
  }

  protected function applyCustomInternalTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
737 738
    $type = $xaction->getTransactionType();
    throw new Exception(
Joshua Spence's avatar
Joshua Spence committed
739 740 741
      pht(
        "Transaction type '%s' is missing an internal apply implementation!",
        $type));
epriestley's avatar
epriestley committed
742 743 744 745 746
  }

  protected function applyCustomExternalTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
747 748
    $type = $xaction->getTransactionType();
    throw new Exception(
Joshua Spence's avatar
Joshua Spence committed
749 750 751
      pht(
        "Transaction type '%s' is missing an external apply implementation!",
        $type));
epriestley's avatar
epriestley committed
752 753
  }

754 755 756 757 758 759 760 761 762 763 764
  /**
   * @{class:PhabricatorTransactions} provides many built-in transactions
   * which should not require much - if any - code in specific applications.
   *
   * This method is a hook for the exceedingly-rare cases where you may need
   * to do **additional** work for built-in transactions. Developers should
   * extend this method, making sure to return the parent implementation
   * regardless of handling any transactions.
   *
   * See also @{method:applyBuiltinExternalTransaction}.
   */
765 766 767
  protected function applyBuiltinInternalTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
768 769 770 771 772 773 774 775 776 777 778

    switch ($xaction->getTransactionType()) {
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
        $object->setViewPolicy($xaction->getNewValue());
        break;
      case PhabricatorTransactions::TYPE_EDIT_POLICY:
        $object->setEditPolicy($xaction->getNewValue());
        break;
      case PhabricatorTransactions::TYPE_JOIN_POLICY:
        $object->setJoinPolicy($xaction->getNewValue());
        break;
epriestley's avatar
epriestley committed
779 780 781
      case PhabricatorTransactions::TYPE_SPACE:
        $object->setSpacePHID($xaction->getNewValue());
        break;
782 783 784
      case PhabricatorTransactions::TYPE_SUBTYPE:
        $object->setEditEngineSubtype($xaction->getNewValue());
        break;
785
    }
786 787
  }

788 789 790
  /**
   * See @{method::applyBuiltinInternalTransaction}.
   */
791 792 793
  protected function applyBuiltinExternalTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838

    switch ($xaction->getTransactionType()) {
      case PhabricatorTransactions::TYPE_EDGE:
        if ($this->getIsInverseEdgeEditor()) {
          // If we're writing an inverse edge transaction, don't actually
          // do anything. The initiating editor on the other side of the
          // transaction will take care of the edge writes.
          break;
        }

        $old = $xaction->getOldValue();
        $new = $xaction->getNewValue();
        $src = $object->getPHID();
        $const = $xaction->getMetadataValue('edge:type');

        foreach ($new as $dst_phid => $edge) {
          $new[$dst_phid]['src'] = $src;
        }

        $editor = new PhabricatorEdgeEditor();

        foreach ($old as $dst_phid => $edge) {
          if (!empty($new[$dst_phid])) {
            if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
              continue;
            }
          }
          $editor->removeEdge($src, $const, $dst_phid);
        }

        foreach ($new as $dst_phid => $edge) {
          if (!empty($old[$dst_phid])) {
            if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
              continue;
            }
          }

          $data = array(
            'data' => $edge['data'],
          );

          $editor->addEdge($src, $const, $dst_phid, $data);
        }

        $editor->save();
839 840

        $this->updateWorkboardColumns($object, $const, $old, $new);
841
        break;
842 843 844 845
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
      case PhabricatorTransactions::TYPE_SPACE:
        $this->scrambleFileSecrets($object);
        break;
846 847 848
      case PhabricatorTransactions::TYPE_HISTORY:
        $this->sendHistory = true;
        break;
849
    }
850 851
  }

852 853 854 855 856 857 858 859 860 861 862 863
  /**
   * Fill in a transaction's common values, like author and content source.
   */
  protected function populateTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {

    $actor = $this->getActor();

    // TODO: This needs to be more sophisticated once we have meta-policies.
    $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);

864 865 866
    if ($actor->isOmnipotent()) {
      $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
    } else {
867
      $xaction->setEditPolicy($this->getActingAsPHID());
868 869
    }

870 871 872 873 874 875 876
    // If the transaction already has an explicit author PHID, allow it to
    // stand. This is used by applications like Owners that hook into the
    // post-apply change pipeline.
    if (!$xaction->getAuthorPHID()) {
      $xaction->setAuthorPHID($this->getActingAsPHID());
    }

877 878 879 880 881 882 883 884
    $xaction->setContentSource($this->getContentSource());
    $xaction->attachViewer($actor);
    $xaction->attachObject($object);

    if ($object->getPHID()) {
      $xaction->setObjectPHID($object->getPHID());
    }

885 886 887 888
    if ($this->getIsSilent()) {
      $xaction->setIsSilentTransaction(true);
    }

889 890 891
    return $xaction;
  }

892 893 894 895 896
  protected function didApplyInternalEffects(
    PhabricatorLiskDAO $object,
    array $xactions) {
    return $xactions;
  }
897

898 899 900
  protected function applyFinalEffects(
    PhabricatorLiskDAO $object,
    array $xactions) {
901
    return $xactions;
902 903
  }

904 905 906 907 908 909 910
  final protected function didCommitTransactions(
    PhabricatorLiskDAO $object,
    array $xactions) {

    foreach ($xactions as $xaction) {
      $type = $xaction->getTransactionType();

911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934
      // See T13082. When we're writing edges that imply corresponding inverse
      // transactions, apply those inverse transactions now. We have to wait
      // until the object we're editing (with this editor) has committed its
      // transactions to do this. If we don't, the inverse editor may race,
      // build a mail before we actually commit this object, and render "alice
      // added an edge: Unknown Object".

      if ($type === PhabricatorTransactions::TYPE_EDGE) {
        // Don't do anything if we're already an inverse edge editor.
        if ($this->getIsInverseEdgeEditor()) {
          continue;
        }

        $edge_const = $xaction->getMetadataValue('edge:type');
        $edge_type = PhabricatorEdgeType::getByConstant($edge_const);
        if ($edge_type->shouldWriteInverseTransactions()) {
          $this->applyInverseEdgeTransactions(
            $object,
            $xaction,
            $edge_type->getInverseEdgeConstant());
        }
        continue;
      }

935 936 937 938 939 940 941 942 943 944 945
      $xtype = $this->getModularTransactionType($type);
      if (!$xtype) {
        continue;
      }

      $xtype = clone $xtype;
      $xtype->setStorage($xaction);
      $xtype->didCommitTransaction($object, $xaction->getNewValue());
    }
  }

epriestley's avatar
epriestley committed
946 947 948 949 950
  public function setContentSource(PhabricatorContentSource $content_source) {
    $this->contentSource = $content_source;
    return $this;
  }

epriestley's avatar
epriestley committed
951
  public function setContentSourceFromRequest(AphrontRequest $request) {
952
    $this->setRequest($request);
epriestley's avatar
epriestley committed
953
    return $this->setContentSource(
954
      PhabricatorContentSource::newFromRequest($request));
epriestley's avatar
epriestley committed
955 956
  }

epriestley's avatar
epriestley committed
957 958 959 960
  public function getContentSource() {
    return $this->contentSource;
  }

961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978
  public function setRequest(AphrontRequest $request) {
    $this->request = $request;
    return $this;
  }

  public function getRequest() {
    return $this->request;
  }

  public function setCancelURI($cancel_uri) {
    $this->cancelURI = $cancel_uri;
    return $this;
  }

  public function getCancelURI() {
    return $this->cancelURI;
  }

979 980 981 982 983 984 985 986
  protected function getTransactionGroupID() {
    if ($this->transactionGroupID === null) {
      $this->transactionGroupID = Filesystem::readRandomCharacters(32);
    }

    return $this->transactionGroupID;
  }

epriestley's avatar
epriestley committed
987 988 989 990
  final public function applyTransactions(
    PhabricatorLiskDAO $object,
    array $xactions) {

991
    $is_new = ($object->getID() === null);
992
    $this->isNewObject = $is_new;
epriestley's avatar
epriestley committed
993

994 995 996
    $is_preview = $this->getIsPreview();
    $read_locking = false;
    $transaction_open = false;
epriestley's avatar
epriestley committed
997

998 999 1000 1001
    // If we're attempting to apply transactions, lock and reload the object
    // before we go anywhere. If we don't do this at the very beginning, we
    // may be looking at an older version of the object when we populate and
    // filter the transactions. See PHI1165 for an example.
epriestley's avatar
epriestley committed
1002

1003 1004 1005
    if (!$is_preview) {
      if (!$is_new) {
        $this->buildOldRecipientLists($object, $xactions);
epriestley's avatar
epriestley committed
1006

1007 1008
        $object->openTransaction();
        $transaction_open = true;
epriestley's avatar
epriestley committed
1009

1010 1011 1012 1013 1014
        $object->beginReadLocking();
        $read_locking = true;

        $object->reload();
      }
epriestley's avatar
epriestley committed
1015 1016
    }

1017 1018 1019
    try {
      $this->object = $object;
      $this->xactions = $xactions;
1020

1021 1022
      $this->validateEditParameters($object, $xactions);
      $xactions = $this->newMFATransactions($object, $xactions);
1023

1024
      $actor = $this->requireActor();
1025

1026 1027 1028 1029 1030
      // NOTE: Some transaction expansion requires that the edited object be
      // attached.
      foreach ($xactions as $xaction) {
        $xaction->attachObject($object);
        $xaction->attachViewer($actor);
1031 1032
      }

1033 1034 1035 1036 1037 1038
      $xactions = $this->expandTransactions($object, $xactions);
      $xactions = $this->expandSupportTransactions($object, $xactions);
      $xactions = $this->combineTransactions($xactions);

      foreach ($xactions as $xaction) {
        $xaction = $this->populateTransaction($object, $xaction);
1039 1040
      }

1041 1042 1043 1044 1045 1046 1047 1048 1049
      if (!$is_preview) {
        $errors = array();
        $type_map = mgroup($xactions, 'getTransactionType');
        foreach ($this->getTransactionTypes() as $type) {
          $type_xactions = idx($type_map, $type, array());
          $errors[] = $this->validateTransaction(
            $object,
            $type,
            $type_xactions);
1050
        }
1051

1052 1053 1054 1055 1056
        $errors[] = $this->validateAllTransactions($object, $xactions);
        $errors[] = $this->validateTransactionsWithExtensions(
          $object,
          $xactions);
        $errors = array_mergev($errors);
1057

1058 1059 1060 1061 1062 1063
        $continue_on_missing = $this->getContinueOnMissingFields();
        foreach ($errors as $key => $error) {
          if ($continue_on_missing && $error->getIsMissingFieldError()) {
            unset($errors[$key]);
          }
        }
1064

1065 1066 1067 1068
        if ($errors) {
          throw new PhabricatorApplicationTransactionValidationException(
            $errors);
        }
1069

1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
        if ($this->raiseWarnings) {
          $warnings = array();
          foreach ($xactions as $xaction) {
            if ($this->hasWarnings($object, $xaction)) {
              $warnings[] = $xaction;
            }
          }
          if ($warnings) {
            throw new PhabricatorApplicationTransactionWarningException(
              $warnings);
          }
        }
1082 1083
      }

1084 1085 1086
      foreach ($xactions as $xaction) {
        $this->adjustTransactionValues($object, $xaction);
      }
1087

1088 1089 1090 1091 1092 1093
      // Now that we've merged and combined transactions, check for required
      // capabilities. Note that we're doing this before filtering
      // transactions: if you try to apply an edit which you do not have
      // permission to apply, we want to give you a permissions error even
      // if the edit would have no effect.
      $this->applyCapabilityChecks($object, $xactions);
1094

1095
      $xactions = $this->filterTransactions($object, $xactions);
epriestley's avatar
epriestley committed
1096

1097 1098 1099 1100 1101
      if (!$is_preview) {
        $this->hasRequiredMFA = true;
        if ($this->getShouldRequireMFA()) {
          $this->requireMFA($object, $xactions);
        }
1102

1103 1104 1105 1106 1107
        if ($this->shouldApplyInitialEffects($object, $xactions)) {
          if (!$transaction_open) {
            $object->openTransaction();
            $transaction_open = true;
          }
1108 1109 1110
        }
      }

epriestley's avatar
epriestley committed
1111 1112 1113
      if ($this->shouldApplyInitialEffects($object, $xactions)) {
        $this->applyInitialEffects($object, $xactions);
      }
1114