~ubuntu-branches/ubuntu/vivid/phabricator/vivid-proposed

« back to all changes in this revision

Viewing changes to phabricator/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-10-23 20:49:26 UTC
  • Revision ID: package-import@ubuntu.com-20141023204926-ar20vnfjqwxysrce
Tags: upstream-0~git20141023
ImportĀ upstreamĀ versionĀ 0~git20141023

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
    $template
 
1903
      ->setFrom($this->getActingAsPHID())
 
1904
      ->setSubjectPrefix($this->getMailSubjectPrefix())
 
1905
      ->setVarySubjectPrefix('['.$action.']')
 
1906
      ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
 
1907
      ->setRelatedPHID($object->getPHID())
 
1908
      ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
 
1909
      ->setMailTags($mail_tags)
 
1910
      ->setIsBulk(true)
 
1911
      ->setBody($body->render())
 
1912
      ->setHTMLBody($body->renderHTML());
 
1913
 
 
1914
    foreach ($body->getAttachments() as $attachment) {
 
1915
      $template->addAttachment($attachment);
 
1916
    }
 
1917
 
 
1918
    $herald_xscript = $this->getHeraldTranscript();
 
1919
    if ($herald_xscript) {
 
1920
      $herald_header = $herald_xscript->getXHeraldRulesHeader();
 
1921
      $herald_header = HeraldTranscript::saveXHeraldRulesHeader(
 
1922
        $object->getPHID(),
 
1923
        $herald_header);
 
1924
    } else {
 
1925
      $herald_header = HeraldTranscript::loadXHeraldRulesHeader(
 
1926
        $object->getPHID());
 
1927
    }
 
1928
 
 
1929
    if ($herald_header) {
 
1930
      $template->addHeader('X-Herald-Rules', $herald_header);
 
1931
    }
 
1932
 
 
1933
    if ($object instanceof PhabricatorProjectInterface) {
 
1934
      $this->addMailProjectMetadata($object, $template);
 
1935
    }
 
1936
 
 
1937
    if ($this->getParentMessageID()) {
 
1938
      $template->setParentMessageID($this->getParentMessageID());
 
1939
    }
 
1940
 
 
1941
    $mails = $reply_handler->multiplexMail(
 
1942
      $template,
 
1943
      array_select_keys($handles, $email_to),
 
1944
      array_select_keys($handles, $email_cc));
 
1945
 
 
1946
    foreach ($mails as $mail) {
 
1947
      $mail->saveAndSend();
 
1948
    }
 
1949
 
 
1950
    $template->addTos($email_to);
 
1951
    $template->addCCs($email_cc);
 
1952
 
 
1953
    return $template;
 
1954
  }
 
1955
 
 
1956
  private function addMailProjectMetadata(
 
1957
    PhabricatorLiskDAO $object,
 
1958
    PhabricatorMetaMTAMail $template) {
 
1959
 
 
1960
    $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
 
1961
      $object->getPHID(),
 
1962
      PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
1963
 
 
1964
    if (!$project_phids) {
 
1965
      return;
 
1966
    }
 
1967
 
 
1968
    // TODO: This viewer isn't quite right. It would be slightly better to use
 
1969
    // the mail recipient, but that's not very easy given the way rendering
 
1970
    // works today.
 
1971
 
 
1972
    $handles = id(new PhabricatorHandleQuery())
 
1973
      ->setViewer($this->requireActor())
 
1974
      ->withPHIDs($project_phids)
 
1975
      ->execute();
 
1976
 
 
1977
    $project_tags = array();
 
1978
    foreach ($handles as $handle) {
 
1979
      if (!$handle->isComplete()) {
 
1980
        continue;
 
1981
      }
 
1982
      $project_tags[] = '<'.$handle->getObjectName().'>';
 
1983
    }
 
1984
 
 
1985
    if (!$project_tags) {
 
1986
      return;
 
1987
    }
 
1988
 
 
1989
    $project_tags = implode(', ', $project_tags);
 
1990
    $template->addHeader('X-Phabricator-Projects', $project_tags);
 
1991
  }
 
1992
 
 
1993
 
 
1994
  protected function getMailThreadID(PhabricatorLiskDAO $object) {
 
1995
    return $object->getPHID();
 
1996
  }
 
1997
 
 
1998
 
 
1999
  /**
 
2000
   * @task mail
 
2001
   */
 
2002
  protected function getStrongestAction(
 
2003
    PhabricatorLiskDAO $object,
 
2004
    array $xactions) {
 
2005
    return last(msort($xactions, 'getActionStrength'));
 
2006
  }
 
2007
 
 
2008
 
 
2009
  /**
 
2010
   * @task mail
 
2011
   */
 
2012
  protected function buildReplyHandler(PhabricatorLiskDAO $object) {
 
2013
    throw new Exception('Capability not supported.');
 
2014
  }
 
2015
 
 
2016
 
 
2017
  /**
 
2018
   * @task mail
 
2019
   */
 
2020
  protected function getMailSubjectPrefix() {
 
2021
    throw new Exception('Capability not supported.');
 
2022
  }
 
2023
 
 
2024
 
 
2025
  /**
 
2026
   * @task mail
 
2027
   */
 
2028
  protected function getMailTags(
 
2029
    PhabricatorLiskDAO $object,
 
2030
    array $xactions) {
 
2031
    $tags = array();
 
2032
 
 
2033
    foreach ($xactions as $xaction) {
 
2034
      $tags[] = $xaction->getMailTags();
 
2035
    }
 
2036
 
 
2037
    return array_mergev($tags);
 
2038
  }
 
2039
 
 
2040
  /**
 
2041
   * @task mail
 
2042
   */
 
2043
  public function getMailTagsMap() {
 
2044
    // TODO: We should move shared mail tags, like "comment", here.
 
2045
    return array();
 
2046
  }
 
2047
 
 
2048
 
 
2049
  /**
 
2050
   * @task mail
 
2051
   */
 
2052
  protected function getMailAction(
 
2053
    PhabricatorLiskDAO $object,
 
2054
    array $xactions) {
 
2055
    return $this->getStrongestAction($object, $xactions)->getActionName();
 
2056
  }
 
2057
 
 
2058
 
 
2059
  /**
 
2060
   * @task mail
 
2061
   */
 
2062
  protected function buildMailTemplate(PhabricatorLiskDAO $object) {
 
2063
    throw new Exception('Capability not supported.');
 
2064
  }
 
2065
 
 
2066
 
 
2067
  /**
 
2068
   * @task mail
 
2069
   */
 
2070
  protected function getMailTo(PhabricatorLiskDAO $object) {
 
2071
    throw new Exception('Capability not supported.');
 
2072
  }
 
2073
 
 
2074
 
 
2075
  /**
 
2076
   * @task mail
 
2077
   */
 
2078
  protected function getMailCC(PhabricatorLiskDAO $object) {
 
2079
    $phids = array();
 
2080
    $has_support = false;
 
2081
 
 
2082
    if ($object instanceof PhabricatorSubscribableInterface) {
 
2083
      $phids[] = $this->subscribers;
 
2084
      $has_support = true;
 
2085
    }
 
2086
 
 
2087
    if ($object instanceof PhabricatorProjectInterface) {
 
2088
      $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
 
2089
        $object->getPHID(),
 
2090
        PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
2091
 
 
2092
      if ($project_phids) {
 
2093
        $watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
 
2094
 
 
2095
        $query = id(new PhabricatorEdgeQuery())
 
2096
          ->withSourcePHIDs($project_phids)
 
2097
          ->withEdgeTypes(array($watcher_type));
 
2098
        $query->execute();
 
2099
 
 
2100
        $watcher_phids = $query->getDestinationPHIDs();
 
2101
        if ($watcher_phids) {
 
2102
          // We need to do a visibility check for all the watchers, as
 
2103
          // watching a project is not a guarantee that you can see objects
 
2104
          // associated with it.
 
2105
          $users = id(new PhabricatorPeopleQuery())
 
2106
            ->setViewer($this->requireActor())
 
2107
            ->withPHIDs($watcher_phids)
 
2108
            ->execute();
 
2109
 
 
2110
          $watchers = array();
 
2111
          foreach ($users as $user) {
 
2112
            $can_see = PhabricatorPolicyFilter::hasCapability(
 
2113
              $user,
 
2114
              $object,
 
2115
              PhabricatorPolicyCapability::CAN_VIEW);
 
2116
            if ($can_see) {
 
2117
              $watchers[] = $user->getPHID();
 
2118
            }
 
2119
          }
 
2120
          $phids[] = $watchers;
 
2121
        }
 
2122
      }
 
2123
 
 
2124
      $has_support = true;
 
2125
    }
 
2126
 
 
2127
    if (!$has_support) {
 
2128
      throw new Exception('Capability not supported.');
 
2129
    }
 
2130
 
 
2131
    return array_mergev($phids);
 
2132
  }
 
2133
 
 
2134
 
 
2135
  /**
 
2136
   * @task mail
 
2137
   */
 
2138
  protected function buildMailBody(
 
2139
    PhabricatorLiskDAO $object,
 
2140
    array $xactions) {
 
2141
 
 
2142
    $headers = array();
 
2143
    $comments = array();
 
2144
 
 
2145
    foreach ($xactions as $xaction) {
 
2146
      if ($xaction->shouldHideForMail($xactions)) {
 
2147
        continue;
 
2148
      }
 
2149
 
 
2150
      $header = $xaction->getTitleForMail();
 
2151
      if ($header !== null) {
 
2152
        $headers[] = $header;
 
2153
      }
 
2154
 
 
2155
      $comment = $xaction->getBodyForMail();
 
2156
      if ($comment !== null) {
 
2157
        $comments[] = $comment;
 
2158
      }
 
2159
    }
 
2160
 
 
2161
    $body = new PhabricatorMetaMTAMailBody();
 
2162
    $body->addRawSection(implode("\n", $headers));
 
2163
 
 
2164
    foreach ($comments as $comment) {
 
2165
      $body->addRawSection($comment);
 
2166
    }
 
2167
 
 
2168
    if ($object instanceof PhabricatorCustomFieldInterface) {
 
2169
      $field_list = PhabricatorCustomField::getObjectFields(
 
2170
        $object,
 
2171
        PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
 
2172
      $field_list->setViewer($this->getActor());
 
2173
      $field_list->readFieldsFromStorage($object);
 
2174
 
 
2175
      foreach ($field_list->getFields() as $field) {
 
2176
        $field->updateTransactionMailBody(
 
2177
          $body,
 
2178
          $this,
 
2179
          $xactions);
 
2180
      }
 
2181
    }
 
2182
 
 
2183
    return $body;
 
2184
  }
 
2185
 
 
2186
 
 
2187
/* -(  Publishing Feed Stories  )-------------------------------------------- */
 
2188
 
 
2189
 
 
2190
  /**
 
2191
   * @task feed
 
2192
   */
 
2193
  protected function shouldPublishFeedStory(
 
2194
    PhabricatorLiskDAO $object,
 
2195
    array $xactions) {
 
2196
    return false;
 
2197
  }
 
2198
 
 
2199
 
 
2200
  /**
 
2201
   * @task feed
 
2202
   */
 
2203
  protected function getFeedStoryType() {
 
2204
    return 'PhabricatorApplicationTransactionFeedStory';
 
2205
  }
 
2206
 
 
2207
 
 
2208
  /**
 
2209
   * @task feed
 
2210
   */
 
2211
  protected function getFeedRelatedPHIDs(
 
2212
    PhabricatorLiskDAO $object,
 
2213
    array $xactions) {
 
2214
 
 
2215
    $phids = array(
 
2216
      $object->getPHID(),
 
2217
      $this->getActingAsPHID(),
 
2218
    );
 
2219
 
 
2220
    if ($object instanceof PhabricatorProjectInterface) {
 
2221
      $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
 
2222
        $object->getPHID(),
 
2223
        PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
2224
      foreach ($project_phids as $project_phid) {
 
2225
        $phids[] = $project_phid;
 
2226
      }
 
2227
    }
 
2228
 
 
2229
    return $phids;
 
2230
  }
 
2231
 
 
2232
 
 
2233
  /**
 
2234
   * @task feed
 
2235
   */
 
2236
  protected function getFeedNotifyPHIDs(
 
2237
    PhabricatorLiskDAO $object,
 
2238
    array $xactions) {
 
2239
 
 
2240
    return array_unique(array_merge(
 
2241
      $this->getMailTo($object),
 
2242
      $this->getMailCC($object)));
 
2243
  }
 
2244
 
 
2245
 
 
2246
  /**
 
2247
   * @task feed
 
2248
   */
 
2249
  protected function getFeedStoryData(
 
2250
    PhabricatorLiskDAO $object,
 
2251
    array $xactions) {
 
2252
 
 
2253
    $xactions = msort($xactions, 'getActionStrength');
 
2254
    $xactions = array_reverse($xactions);
 
2255
 
 
2256
    return array(
 
2257
      'objectPHID'        => $object->getPHID(),
 
2258
      'transactionPHIDs'  => mpull($xactions, 'getPHID'),
 
2259
    );
 
2260
  }
 
2261
 
 
2262
 
 
2263
  /**
 
2264
   * @task feed
 
2265
   */
 
2266
  protected function publishFeedStory(
 
2267
    PhabricatorLiskDAO $object,
 
2268
    array $xactions,
 
2269
    array $mailed_phids) {
 
2270
 
 
2271
    $xactions = mfilter($xactions, 'shouldHideForFeed', true);
 
2272
 
 
2273
    if (!$xactions) {
 
2274
      return;
 
2275
    }
 
2276
 
 
2277
    $related_phids = $this->getFeedRelatedPHIDs($object, $xactions);
 
2278
    $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions);
 
2279
 
 
2280
    $story_type = $this->getFeedStoryType();
 
2281
    $story_data = $this->getFeedStoryData($object, $xactions);
 
2282
 
 
2283
    id(new PhabricatorFeedStoryPublisher())
 
2284
      ->setStoryType($story_type)
 
2285
      ->setStoryData($story_data)
 
2286
      ->setStoryTime(time())
 
2287
      ->setStoryAuthorPHID($this->getActingAsPHID())
 
2288
      ->setRelatedPHIDs($related_phids)
 
2289
      ->setPrimaryObjectPHID($object->getPHID())
 
2290
      ->setSubscribedPHIDs($subscribed_phids)
 
2291
      ->setMailRecipientPHIDs($mailed_phids)
 
2292
      ->setMailTags($this->getMailTags($object, $xactions))
 
2293
      ->publish();
 
2294
  }
 
2295
 
 
2296
 
 
2297
/* -(  Search Index  )------------------------------------------------------- */
 
2298
 
 
2299
 
 
2300
  /**
 
2301
   * @task search
 
2302
   */
 
2303
  protected function supportsSearch() {
 
2304
    return false;
 
2305
  }
 
2306
 
 
2307
 
 
2308
/* -(  Herald Integration )-------------------------------------------------- */
 
2309
 
 
2310
 
 
2311
  protected function shouldApplyHeraldRules(
 
2312
    PhabricatorLiskDAO $object,
 
2313
    array $xactions) {
 
2314
    return false;
 
2315
  }
 
2316
 
 
2317
  protected function buildHeraldAdapter(
 
2318
    PhabricatorLiskDAO $object,
 
2319
    array $xactions) {
 
2320
    throw new Exception('No herald adapter specified.');
 
2321
  }
 
2322
 
 
2323
  private function setHeraldAdapter(HeraldAdapter $adapter) {
 
2324
    $this->heraldAdapter = $adapter;
 
2325
    return $this;
 
2326
  }
 
2327
 
 
2328
  protected function getHeraldAdapter() {
 
2329
    return $this->heraldAdapter;
 
2330
  }
 
2331
 
 
2332
  private function setHeraldTranscript(HeraldTranscript $transcript) {
 
2333
    $this->heraldTranscript = $transcript;
 
2334
    return $this;
 
2335
  }
 
2336
 
 
2337
  protected function getHeraldTranscript() {
 
2338
    return $this->heraldTranscript;
 
2339
  }
 
2340
 
 
2341
  private function applyHeraldRules(
 
2342
    PhabricatorLiskDAO $object,
 
2343
    array $xactions) {
 
2344
 
 
2345
    $adapter = $this->buildHeraldAdapter($object, $xactions);
 
2346
    $adapter->setContentSource($this->getContentSource());
 
2347
    $adapter->setIsNewObject($this->getIsNewObject());
 
2348
    $xscript = HeraldEngine::loadAndApplyRules($adapter);
 
2349
 
 
2350
    $this->setHeraldAdapter($adapter);
 
2351
    $this->setHeraldTranscript($xscript);
 
2352
 
 
2353
    return array_merge(
 
2354
      $this->didApplyHeraldRules($object, $adapter, $xscript),
 
2355
      $adapter->getQueuedTransactions());
 
2356
  }
 
2357
 
 
2358
  protected function didApplyHeraldRules(
 
2359
    PhabricatorLiskDAO $object,
 
2360
    HeraldAdapter $adapter,
 
2361
    HeraldTranscript $transcript) {
 
2362
    return array();
 
2363
  }
 
2364
 
 
2365
 
 
2366
/* -(  Custom Fields  )------------------------------------------------------ */
 
2367
 
 
2368
 
 
2369
  /**
 
2370
   * @task customfield
 
2371
   */
 
2372
  private function getCustomFieldForTransaction(
 
2373
    PhabricatorLiskDAO $object,
 
2374
    PhabricatorApplicationTransaction $xaction) {
 
2375
 
 
2376
    $field_key = $xaction->getMetadataValue('customfield:key');
 
2377
    if (!$field_key) {
 
2378
      throw new Exception(
 
2379
        "Custom field transaction has no 'customfield:key'!");
 
2380
    }
 
2381
 
 
2382
    $field = PhabricatorCustomField::getObjectField(
 
2383
      $object,
 
2384
      PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
 
2385
      $field_key);
 
2386
 
 
2387
    if (!$field) {
 
2388
      throw new Exception(
 
2389
        "Custom field transaction has invalid 'customfield:key'; field ".
 
2390
        "'{$field_key}' is disabled or does not exist.");
 
2391
    }
 
2392
 
 
2393
    if (!$field->shouldAppearInApplicationTransactions()) {
 
2394
      throw new Exception(
 
2395
        "Custom field transaction '{$field_key}' does not implement ".
 
2396
        "integration for ApplicationTransactions.");
 
2397
    }
 
2398
 
 
2399
    $field->setViewer($this->getActor());
 
2400
 
 
2401
    return $field;
 
2402
  }
 
2403
 
 
2404
 
 
2405
/* -(  Files  )-------------------------------------------------------------- */
 
2406
 
 
2407
 
 
2408
  /**
 
2409
   * Extract the PHIDs of any files which these transactions attach.
 
2410
   *
 
2411
   * @task files
 
2412
   */
 
2413
  private function extractFilePHIDs(
 
2414
    PhabricatorLiskDAO $object,
 
2415
    array $xactions) {
 
2416
 
 
2417
    $blocks = array();
 
2418
    foreach ($xactions as $xaction) {
 
2419
      $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction);
 
2420
    }
 
2421
    $blocks = array_mergev($blocks);
 
2422
 
 
2423
    $phids = array();
 
2424
    if ($blocks) {
 
2425
      $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
 
2426
        $this->getActor(),
 
2427
        $blocks);
 
2428
    }
 
2429
 
 
2430
    foreach ($xactions as $xaction) {
 
2431
      $phids[] = $this->extractFilePHIDsFromCustomTransaction(
 
2432
        $object,
 
2433
        $xaction);
 
2434
    }
 
2435
 
 
2436
    $phids = array_unique(array_filter(array_mergev($phids)));
 
2437
    if (!$phids) {
 
2438
      return array();
 
2439
    }
 
2440
 
 
2441
    // Only let a user attach files they can actually see, since this would
 
2442
    // otherwise let you access any file by attaching it to an object you have
 
2443
    // view permission on.
 
2444
 
 
2445
    $files = id(new PhabricatorFileQuery())
 
2446
      ->setViewer($this->getActor())
 
2447
      ->withPHIDs($phids)
 
2448
      ->execute();
 
2449
 
 
2450
    return mpull($files, 'getPHID');
 
2451
  }
 
2452
 
 
2453
  /**
 
2454
   * @task files
 
2455
   */
 
2456
  protected function extractFilePHIDsFromCustomTransaction(
 
2457
    PhabricatorLiskDAO $object,
 
2458
    PhabricatorApplicationTransaction $xaction) {
 
2459
    return array();
 
2460
  }
 
2461
 
 
2462
 
 
2463
  /**
 
2464
   * @task files
 
2465
   */
 
2466
  private function attachFiles(
 
2467
    PhabricatorLiskDAO $object,
 
2468
    array $file_phids) {
 
2469
 
 
2470
    if (!$file_phids) {
 
2471
      return;
 
2472
    }
 
2473
 
 
2474
    $editor = new PhabricatorEdgeEditor();
 
2475
 
 
2476
    $src = $object->getPHID();
 
2477
    $type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
 
2478
    foreach ($file_phids as $dst) {
 
2479
      $editor->addEdge($src, $type, $dst);
 
2480
    }
 
2481
 
 
2482
    $editor->save();
 
2483
  }
 
2484
 
 
2485
  private function applyInverseEdgeTransactions(
 
2486
    PhabricatorLiskDAO $object,
 
2487
    PhabricatorApplicationTransaction $xaction,
 
2488
    $inverse_type) {
 
2489
 
 
2490
    $old = $xaction->getOldValue();
 
2491
    $new = $xaction->getNewValue();
 
2492
 
 
2493
    $add = array_keys(array_diff_key($new, $old));
 
2494
    $rem = array_keys(array_diff_key($old, $new));
 
2495
 
 
2496
    $add = array_fuse($add);
 
2497
    $rem = array_fuse($rem);
 
2498
    $all = $add + $rem;
 
2499
 
 
2500
    $nodes = id(new PhabricatorObjectQuery())
 
2501
      ->setViewer($this->requireActor())
 
2502
      ->withPHIDs($all)
 
2503
      ->execute();
 
2504
 
 
2505
    foreach ($nodes as $node) {
 
2506
      if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
 
2507
        continue;
 
2508
      }
 
2509
 
 
2510
      $editor = $node->getApplicationTransactionEditor();
 
2511
      $template = $node->getApplicationTransactionTemplate();
 
2512
      $target = $node->getApplicationTransactionObject();
 
2513
 
 
2514
      if (isset($add[$node->getPHID()])) {
 
2515
        $edge_edit_type = '+';
 
2516
      } else {
 
2517
        $edge_edit_type = '-';
 
2518
      }
 
2519
 
 
2520
      $template
 
2521
        ->setTransactionType($xaction->getTransactionType())
 
2522
        ->setMetadataValue('edge:type', $inverse_type)
 
2523
        ->setNewValue(
 
2524
          array(
 
2525
            $edge_edit_type => array($object->getPHID() => $object->getPHID()),
 
2526
          ));
 
2527
 
 
2528
      $editor
 
2529
        ->setContinueOnNoEffect(true)
 
2530
        ->setContinueOnMissingFields(true)
 
2531
        ->setParentMessageID($this->getParentMessageID())
 
2532
        ->setIsInverseEdgeEditor(true)
 
2533
        ->setActor($this->requireActor())
 
2534
        ->setActingAsPHID($this->getActingAsPHID())
 
2535
        ->setContentSource($this->getContentSource());
 
2536
 
 
2537
      $editor->applyTransactions($target, array($template));
 
2538
    }
 
2539
  }
 
2540
 
 
2541
}