@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3abstract class PhabricatorApplicationTransaction
4 extends PhabricatorLiskDAO
5 implements
6 PhabricatorPolicyInterface,
7 PhabricatorDestructibleInterface {
8
9 const TARGET_TEXT = 'text';
10 const TARGET_HTML = 'html';
11
12 protected $phid;
13 protected $objectPHID;
14 protected $authorPHID;
15 protected $viewPolicy;
16 protected $editPolicy;
17
18 protected $commentPHID;
19 protected $commentVersion = 0;
20 protected $transactionType;
21 protected $oldValue;
22 protected $newValue;
23 protected $metadata = array();
24
25 protected $contentSource;
26
27 private $comment;
28 private $commentNotLoaded;
29
30 private $handles;
31 private $renderingTarget = self::TARGET_HTML;
32 private $transactionGroup = array();
33 private $viewer = self::ATTACHABLE;
34 private $object = self::ATTACHABLE;
35 private $oldValueHasBeenSet = false;
36
37 private $ignoreOnNoEffect;
38
39
40 /**
41 * Flag this transaction as a pure side-effect which should be ignored when
42 * applying transactions if it has no effect, even if transaction application
43 * would normally fail. This both provides users with better error messages
44 * and allows transactions to perform optional side effects.
45 */
46 public function setIgnoreOnNoEffect($ignore) {
47 $this->ignoreOnNoEffect = $ignore;
48 return $this;
49 }
50
51 public function getIgnoreOnNoEffect() {
52 return $this->ignoreOnNoEffect;
53 }
54
55 public function shouldGenerateOldValue() {
56 switch ($this->getTransactionType()) {
57 case PhabricatorTransactions::TYPE_TOKEN:
58 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
59 case PhabricatorTransactions::TYPE_INLINESTATE:
60 return false;
61 }
62 return true;
63 }
64
65 abstract public function getApplicationTransactionType();
66
67 private function getApplicationObjectTypeName() {
68 $types = PhabricatorPHIDType::getAllTypes();
69
70 $type = idx($types, $this->getApplicationTransactionType());
71 if ($type) {
72 return $type->getTypeName();
73 }
74
75 return pht('Object');
76 }
77
78 public function getApplicationTransactionCommentObject() {
79 return null;
80 }
81
82 public function getMetadataValue($key, $default = null) {
83 return idx($this->metadata, $key, $default);
84 }
85
86 public function setMetadataValue($key, $value) {
87 $this->metadata[$key] = $value;
88 return $this;
89 }
90
91 public function generatePHID() {
92 $type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
93 $subtype = $this->getApplicationTransactionType();
94
95 return PhabricatorPHID::generateNewPHID($type, $subtype);
96 }
97
98 protected function getConfiguration() {
99 return array(
100 self::CONFIG_AUX_PHID => true,
101 self::CONFIG_SERIALIZATION => array(
102 'oldValue' => self::SERIALIZATION_JSON,
103 'newValue' => self::SERIALIZATION_JSON,
104 'metadata' => self::SERIALIZATION_JSON,
105 ),
106 self::CONFIG_COLUMN_SCHEMA => array(
107 'commentPHID' => 'phid?',
108 'commentVersion' => 'uint32',
109 'contentSource' => 'text',
110 'transactionType' => 'text32',
111 ),
112 self::CONFIG_KEY_SCHEMA => array(
113 'key_object' => array(
114 'columns' => array('objectPHID'),
115 ),
116 ),
117 ) + parent::getConfiguration();
118 }
119
120 public function setContentSource(PhabricatorContentSource $content_source) {
121 $this->contentSource = $content_source->serialize();
122 return $this;
123 }
124
125 public function getContentSource() {
126 return PhabricatorContentSource::newFromSerialized($this->contentSource);
127 }
128
129 public function hasComment() {
130 $comment = $this->getComment();
131 if (!$comment) {
132 return false;
133 }
134
135 if ($comment->isEmptyComment()) {
136 return false;
137 }
138
139 return true;
140 }
141
142 public function getComment() {
143 if ($this->commentNotLoaded) {
144 throw new Exception(pht('Comment for this transaction was not loaded.'));
145 }
146 return $this->comment;
147 }
148
149 public function setIsCreateTransaction($create) {
150 return $this->setMetadataValue('core.create', $create);
151 }
152
153 public function getIsCreateTransaction() {
154 return (bool)$this->getMetadataValue('core.create', false);
155 }
156
157 public function setIsDefaultTransaction($default) {
158 return $this->setMetadataValue('core.default', $default);
159 }
160
161 public function getIsDefaultTransaction() {
162 return (bool)$this->getMetadataValue('core.default', false);
163 }
164
165 public function setIsSilentTransaction($silent) {
166 return $this->setMetadataValue('core.silent', $silent);
167 }
168
169 public function getIsSilentTransaction() {
170 return (bool)$this->getMetadataValue('core.silent', false);
171 }
172
173 public function setIsMFATransaction($mfa) {
174 return $this->setMetadataValue('core.mfa', $mfa);
175 }
176
177 public function getIsMFATransaction() {
178 return (bool)$this->getMetadataValue('core.mfa', false);
179 }
180
181 public function setIsLockOverrideTransaction($override) {
182 return $this->setMetadataValue('core.lock-override', $override);
183 }
184
185 public function getIsLockOverrideTransaction() {
186 return (bool)$this->getMetadataValue('core.lock-override', false);
187 }
188
189 public function setTransactionGroupID($group_id) {
190 return $this->setMetadataValue('core.groupID', $group_id);
191 }
192
193 public function getTransactionGroupID() {
194 return $this->getMetadataValue('core.groupID', null);
195 }
196
197 public function attachComment(
198 PhabricatorApplicationTransactionComment $comment) {
199 $this->comment = $comment;
200 $this->commentNotLoaded = false;
201 return $this;
202 }
203
204 public function setCommentNotLoaded($not_loaded) {
205 $this->commentNotLoaded = $not_loaded;
206 return $this;
207 }
208
209 public function attachObject($object) {
210 $this->object = $object;
211 return $this;
212 }
213
214 public function getObject() {
215 return $this->assertAttached($this->object);
216 }
217
218 public function getRemarkupChanges() {
219 $changes = $this->newRemarkupChanges();
220 assert_instances_of($changes, PhabricatorTransactionRemarkupChange::class);
221
222 // Convert older-style remarkup blocks into newer-style remarkup changes.
223 // This builds changes that do not have the correct "old value", so rules
224 // that operate differently against edits (like @user mentions) won't work
225 // properly.
226 foreach ($this->getRemarkupBlocks() as $block) {
227 $changes[] = $this->newRemarkupChange()
228 ->setOldValue(null)
229 ->setNewValue($block);
230 }
231
232 $comment = $this->getComment();
233 if ($comment) {
234 if ($comment->hasOldComment()) {
235 $old_value = $comment->getOldComment()->getContent();
236 } else {
237 $old_value = null;
238 }
239
240 $new_value = $comment->getContent();
241
242 $changes[] = $this->newRemarkupChange()
243 ->setOldValue($old_value)
244 ->setNewValue($new_value);
245 }
246
247 $metadata = $this->getMetadataValue('remarkup.control');
248
249 if (!is_array($metadata)) {
250 $metadata = array();
251 }
252
253 foreach ($changes as $change) {
254 if (!$change->getMetadata()) {
255 $change->setMetadata($metadata);
256 }
257 }
258
259 return $changes;
260 }
261
262 protected function newRemarkupChanges() {
263 return array();
264 }
265
266 protected function newRemarkupChange() {
267 return id(new PhabricatorTransactionRemarkupChange())
268 ->setTransaction($this);
269 }
270
271 /**
272 * @deprecated
273 */
274 public function getRemarkupBlocks() {
275 $blocks = array();
276
277 switch ($this->getTransactionType()) {
278 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
279 $field = $this->getTransactionCustomField();
280 if ($field) {
281 $custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
282 $this);
283 foreach ($custom_blocks as $custom_block) {
284 $blocks[] = $custom_block;
285 }
286 }
287 break;
288 }
289
290 return $blocks;
291 }
292
293 public function setOldValue($value) {
294 $this->oldValueHasBeenSet = true;
295 $this->writeField('oldValue', $value);
296 return $this;
297 }
298
299 public function hasOldValue() {
300 return $this->oldValueHasBeenSet;
301 }
302
303 public function newChronologicalSortVector() {
304 return id(new PhutilSortVector())
305 ->addInt((int)$this->getDateCreated())
306 ->addInt((int)$this->getID());
307 }
308
309/* -( Rendering )---------------------------------------------------------- */
310
311 public function setRenderingTarget($rendering_target) {
312 $this->renderingTarget = $rendering_target;
313 return $this;
314 }
315
316 public function getRenderingTarget() {
317 return $this->renderingTarget;
318 }
319
320 public function attachViewer(PhabricatorUser $viewer) {
321 $this->viewer = $viewer;
322 return $this;
323 }
324
325 public function getViewer() {
326 return $this->assertAttached($this->viewer);
327 }
328
329 public function getRequiredHandlePHIDs() {
330 $phids = array();
331
332 $old = $this->getOldValue();
333 $new = $this->getNewValue();
334
335 $phids[] = array($this->getAuthorPHID());
336 $phids[] = array($this->getObjectPHID());
337 switch ($this->getTransactionType()) {
338 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
339 $field = $this->getTransactionCustomField();
340 if ($field) {
341 $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
342 $this);
343 }
344 break;
345 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
346 $phids[] = $old;
347 $phids[] = $new;
348 break;
349 case PhabricatorTransactions::TYPE_FILE:
350 $phids[] = array_keys($old + $new);
351 break;
352 case PhabricatorTransactions::TYPE_EDGE:
353 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
354 $phids[] = $record->getChangedPHIDs();
355 break;
356 case PhabricatorTransactions::TYPE_COLUMNS:
357 foreach ($new as $move) {
358 $phids[] = array(
359 $move['columnPHID'],
360 $move['boardPHID'],
361 );
362 $phids[] = $move['fromColumnPHIDs'];
363 }
364 break;
365 case PhabricatorTransactions::TYPE_EDIT_POLICY:
366 case PhabricatorTransactions::TYPE_VIEW_POLICY:
367 case PhabricatorTransactions::TYPE_JOIN_POLICY:
368 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
369 if (!PhabricatorPolicyQuery::isSpecialPolicy($old)) {
370 $phids[] = array($old);
371 }
372 if (!PhabricatorPolicyQuery::isSpecialPolicy($new)) {
373 $phids[] = array($new);
374 }
375 break;
376 case PhabricatorTransactions::TYPE_SPACE:
377 if ($old) {
378 $phids[] = array($old);
379 }
380 if ($new) {
381 $phids[] = array($new);
382 }
383 break;
384 case PhabricatorTransactions::TYPE_TOKEN:
385 break;
386 }
387
388 if ($this->getComment() && $this->getComment()->getAuthorPHID()) {
389 $phids[] = array($this->getComment()->getAuthorPHID());
390 }
391
392 return array_mergev($phids);
393 }
394
395 public function setHandles(array $handles) {
396 $this->handles = $handles;
397 return $this;
398 }
399
400 public function getHandle($phid) {
401 if (empty($this->handles[$phid])) {
402 throw new Exception(
403 pht(
404 'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
405 'did not load.',
406 $this->getPHID(),
407 $this->getTransactionType(),
408 $phid));
409 }
410 return $this->handles[$phid];
411 }
412
413 public function getHandleIfExists($phid) {
414 return idx($this->handles, $phid);
415 }
416
417 public function getHandles() {
418 if ($this->handles === null) {
419 throw new Exception(
420 pht('Transaction requires handles and it did not load them.'));
421 }
422 return $this->handles;
423 }
424
425 public function renderHandleLink($phid) {
426 if ($this->renderingTarget == self::TARGET_HTML) {
427 return $this->getHandle($phid)->renderHovercardLink();
428 } else {
429 return $this->getHandle($phid)->getLinkName();
430 }
431 }
432
433 public function renderHandleList(array $phids) {
434 $links = array();
435 foreach ($phids as $phid) {
436 $links[] = $this->renderHandleLink($phid);
437 }
438 if ($this->renderingTarget == self::TARGET_HTML) {
439 return phutil_implode_html(', ', $links);
440 } else {
441 return implode(', ', $links);
442 }
443 }
444
445 private function renderSubscriberList(array $phids, $change_type) {
446 if ($this->getRenderingTarget() == self::TARGET_TEXT) {
447 return $this->renderHandleList($phids);
448 } else {
449 $handles = array_select_keys($this->getHandles(), $phids);
450 return id(new SubscriptionListStringBuilder())
451 ->setHandles($handles)
452 ->setObjectPHID($this->getPHID())
453 ->buildTransactionString($change_type);
454 }
455 }
456
457 protected function renderPolicyName($phid, $state = 'old') {
458 $policy = PhabricatorPolicy::newFromPolicyAndHandle(
459 $phid,
460 $this->getHandleIfExists($phid));
461
462 $ref = $policy->newRef($this->getViewer());
463
464 if ($this->renderingTarget == self::TARGET_HTML) {
465 $output = $ref->newTransactionLink($state, $this);
466 } else {
467 $output = $ref->getPolicyDisplayName();
468 }
469
470 return $output;
471 }
472
473 /**
474 * @return string
475 */
476 public function getIcon() {
477 switch ($this->getTransactionType()) {
478 case PhabricatorTransactions::TYPE_COMMENT:
479 $comment = $this->getComment();
480 if ($comment && $comment->getIsRemoved()) {
481 return 'fa-trash';
482 }
483 return 'fa-comment';
484 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
485 $old = $this->getOldValue();
486 $new = $this->getNewValue();
487 $add = array_diff($new, $old);
488 $rem = array_diff($old, $new);
489 if ($add && $rem) {
490 return 'fa-user';
491 } else if ($add) {
492 return 'fa-user-plus';
493 } else if ($rem) {
494 return 'fa-user-times';
495 } else {
496 return 'fa-user';
497 }
498 case PhabricatorTransactions::TYPE_VIEW_POLICY:
499 case PhabricatorTransactions::TYPE_EDIT_POLICY:
500 case PhabricatorTransactions::TYPE_JOIN_POLICY:
501 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
502 return 'fa-lock';
503 case PhabricatorTransactions::TYPE_EDGE:
504 switch ($this->getMetadataValue('edge:type')) {
505 case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
506 return 'fa-undo';
507 case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
508 return 'fa-ambulance';
509 }
510 return 'fa-link';
511 case PhabricatorTransactions::TYPE_TOKEN:
512 return 'fa-trophy';
513 case PhabricatorTransactions::TYPE_SPACE:
514 return 'fa-th-large';
515 case PhabricatorTransactions::TYPE_COLUMNS:
516 return 'fa-columns';
517 case PhabricatorTransactions::TYPE_MFA:
518 return 'fa-vcard';
519 }
520
521 return 'fa-pencil';
522 }
523
524 public function getToken() {
525 switch ($this->getTransactionType()) {
526 case PhabricatorTransactions::TYPE_TOKEN:
527 $old = $this->getOldValue();
528 $new = $this->getNewValue();
529 if ($new) {
530 $icon = substr($new, 10);
531 } else {
532 $icon = substr($old, 10);
533 }
534 return array($icon, !$this->getNewValue());
535 }
536
537 return array(null, null);
538 }
539
540 /**
541 * @return string|null
542 */
543 public function getColor() {
544 switch ($this->getTransactionType()) {
545 case PhabricatorTransactions::TYPE_COMMENT:
546 $comment = $this->getComment();
547 if ($comment && $comment->getIsRemoved()) {
548 return 'grey';
549 }
550 break;
551 case PhabricatorTransactions::TYPE_EDGE:
552 switch ($this->getMetadataValue('edge:type')) {
553 case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
554 return 'pink';
555 case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
556 return 'sky';
557 }
558 break;
559 case PhabricatorTransactions::TYPE_MFA:
560 return 'pink';
561 }
562 return null;
563 }
564
565 protected function getTransactionCustomField() {
566 switch ($this->getTransactionType()) {
567 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
568 $key = $this->getMetadataValue('customfield:key');
569 if (!$key) {
570 return null;
571 }
572
573 $object = $this->getObject();
574
575 if (!($object instanceof PhabricatorCustomFieldInterface)) {
576 return null;
577 }
578
579 $field = PhabricatorCustomField::getObjectField(
580 $object,
581 PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
582 $key);
583 if (!$field) {
584 return null;
585 }
586
587 $field->setViewer($this->getViewer());
588 return $field;
589 }
590
591 return null;
592 }
593
594 public function shouldHide() {
595 // Never hide comments.
596 if ($this->hasComment()) {
597 return false;
598 }
599
600 $xaction_type = $this->getTransactionType();
601
602 // Always hide requests for object history.
603 if ($xaction_type === PhabricatorTransactions::TYPE_HISTORY) {
604 return true;
605 }
606
607 // Always hide file attach/detach transactions.
608 if ($xaction_type === PhabricatorTransactions::TYPE_FILE) {
609 if ($this->getMetadataValue('attach.implicit')) {
610 return true;
611 }
612 }
613
614 // Hide creation transactions if the old value is empty. These are
615 // transactions like "alice set the task title to: ...", which are
616 // essentially never interesting.
617 if ($this->getIsCreateTransaction()) {
618 switch ($xaction_type) {
619 case PhabricatorTransactions::TYPE_CREATE:
620 case PhabricatorTransactions::TYPE_VIEW_POLICY:
621 case PhabricatorTransactions::TYPE_EDIT_POLICY:
622 case PhabricatorTransactions::TYPE_JOIN_POLICY:
623 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
624 case PhabricatorTransactions::TYPE_SPACE:
625 break;
626 case PhabricatorTransactions::TYPE_SUBTYPE:
627 return true;
628 default:
629 $old = $this->getOldValue();
630
631 if (is_array($old) && !$old) {
632 return true;
633 }
634
635 if (!is_array($old)) {
636 if ($old === '' || $old === null) {
637 return true;
638 }
639
640 // The integer 0 is also uninteresting by default; this is often
641 // an "off" flag for something like "All Day Event".
642 if ($old === 0) {
643 return true;
644 }
645 }
646
647 break;
648 }
649 }
650
651 // Hide creation transactions setting values to defaults, even if
652 // the old value is not empty. For example, tasks may have a global
653 // default view policy of "All Users", but a particular form sets the
654 // policy to "Administrators". The transaction corresponding to this
655 // change is not interesting, since it is the default behavior of the
656 // form.
657
658 if ($this->getIsCreateTransaction()) {
659 if ($this->getIsDefaultTransaction()) {
660 return true;
661 }
662 }
663
664 switch ($this->getTransactionType()) {
665 case PhabricatorTransactions::TYPE_VIEW_POLICY:
666 case PhabricatorTransactions::TYPE_EDIT_POLICY:
667 case PhabricatorTransactions::TYPE_JOIN_POLICY:
668 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
669 case PhabricatorTransactions::TYPE_SPACE:
670 if ($this->getIsCreateTransaction()) {
671 break;
672 }
673
674 // TODO: Remove this eventually, this is handling old changes during
675 // object creation prior to the introduction of "create" and "default"
676 // transaction display flags.
677
678 // NOTE: We can also hit this case with Space transactions that later
679 // update a default space (`null`) to an explicit space, so handling
680 // the Space case may require some finesse.
681
682 if ($this->getOldValue() === null) {
683 return true;
684 } else {
685 return false;
686 }
687 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
688 $field = $this->getTransactionCustomField();
689 if ($field) {
690 return $field->shouldHideInApplicationTransactions($this);
691 }
692 break;
693 case PhabricatorTransactions::TYPE_COLUMNS:
694 return !$this->getInterestingMoves($this->getNewValue());
695 case PhabricatorTransactions::TYPE_EDGE:
696 $edge_type = $this->getMetadataValue('edge:type');
697 switch ($edge_type) {
698 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
699 case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST:
700 case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST:
701 case PhabricatorMutedEdgeType::EDGECONST:
702 case PhabricatorMutedByEdgeType::EDGECONST:
703 return true;
704 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
705 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
706 $add = $record->getAddedPHIDs();
707 $add_value = reset($add);
708 $add_handle = $this->getHandle($add_value);
709 if ($add_handle->getPolicyFiltered()) {
710 return true;
711 }
712 return false;
713 default:
714 break;
715 }
716 break;
717
718 case PhabricatorTransactions::TYPE_INLINESTATE:
719 list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
720
721 if (!$done && !$undone) {
722 return true;
723 }
724
725 break;
726
727 }
728
729 return false;
730 }
731
732 public function shouldHideForMail(array $xactions) {
733 if ($this->isSelfSubscription()) {
734 return true;
735 }
736
737 switch ($this->getTransactionType()) {
738 case PhabricatorTransactions::TYPE_TOKEN:
739 return true;
740 case PhabricatorTransactions::TYPE_EDGE:
741 $edge_type = $this->getMetadataValue('edge:type');
742 switch ($edge_type) {
743 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
744 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
745 case DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST:
746 case DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST:
747 case ManiphestTaskHasCommitEdgeType::EDGECONST:
748 case DiffusionCommitHasTaskEdgeType::EDGECONST:
749 case DiffusionCommitHasRevisionEdgeType::EDGECONST:
750 case DifferentialRevisionHasCommitEdgeType::EDGECONST:
751 return true;
752 case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
753 // When an object is first created, we hide any corresponding
754 // project transactions in the web UI because you can just look at
755 // the UI element elsewhere on screen to see which projects it
756 // is tagged with. However, in mail there's no other way to get
757 // this information, and it has some amount of value to users, so
758 // we keep the transaction. See T10493.
759 return false;
760 default:
761 break;
762 }
763 break;
764 }
765
766 if ($this->isInlineCommentTransaction()) {
767 $inlines = array();
768
769 // If there's a normal comment, we don't need to publish the inline
770 // transaction, since the normal comment covers things.
771 foreach ($xactions as $xaction) {
772 if ($xaction->isInlineCommentTransaction()) {
773 $inlines[] = $xaction;
774 continue;
775 }
776
777 // We found a normal comment, so hide this inline transaction.
778 if ($xaction->hasComment()) {
779 return true;
780 }
781 }
782
783 // If there are several inline comments, only publish the first one.
784 if ($this !== head($inlines)) {
785 return true;
786 }
787 }
788
789 return $this->shouldHide();
790 }
791
792 public function shouldHideForFeed() {
793 if ($this->isSelfSubscription()) {
794 return true;
795 }
796
797 switch ($this->getTransactionType()) {
798 case PhabricatorTransactions::TYPE_TOKEN:
799 case PhabricatorTransactions::TYPE_MFA:
800 case PhabricatorTransactions::TYPE_INLINESTATE:
801 return true;
802 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
803 // See T8952. When an application (usually Herald) modifies
804 // subscribers, this tends to be very uninteresting.
805 if ($this->isApplicationAuthor()) {
806 return true;
807 }
808 break;
809 case PhabricatorTransactions::TYPE_EDGE:
810 $edge_type = $this->getMetadataValue('edge:type');
811 switch ($edge_type) {
812 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
813 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
814 case DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST:
815 case DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST:
816 case ManiphestTaskHasCommitEdgeType::EDGECONST:
817 case DiffusionCommitHasTaskEdgeType::EDGECONST:
818 case DiffusionCommitHasRevisionEdgeType::EDGECONST:
819 case DifferentialRevisionHasCommitEdgeType::EDGECONST:
820 return true;
821 default:
822 break;
823 }
824 break;
825 }
826
827 return $this->shouldHide();
828 }
829
830 public function shouldHideForNotifications() {
831 return $this->shouldHideForFeed();
832 }
833
834 private function getTitleForMailWithRenderingTarget($new_target) {
835 $old_target = $this->getRenderingTarget();
836 try {
837 $this->setRenderingTarget($new_target);
838 $result = $this->getTitleForMail();
839 } catch (Exception $ex) {
840 $this->setRenderingTarget($old_target);
841 throw $ex;
842 }
843 $this->setRenderingTarget($old_target);
844 return $result;
845 }
846
847 public function getTitleForMail() {
848 return $this->getTitle();
849 }
850
851 public function getTitleForTextMail() {
852 return $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
853 }
854
855 public function getTitleForHTMLMail() {
856 // TODO: For now, rendering this with TARGET_HTML generates links with
857 // bad targets ("/x/y/" instead of "https://dev.example.com/x/y/"). Throw
858 // a rug over the issue for the moment. See T12921.
859
860 $title = $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
861 if ($title === null) {
862 return null;
863 }
864
865 if ($this->hasChangeDetails()) {
866 $details_uri = $this->getChangeDetailsURI();
867 $details_uri = PhabricatorEnv::getProductionURI($details_uri);
868
869 $show_details = phutil_tag(
870 'a',
871 array(
872 'href' => $details_uri,
873 ),
874 pht('(Show Details)'));
875
876 $title = array($title, ' ', $show_details);
877 }
878
879 return $title;
880 }
881
882 public function getChangeDetailsURI() {
883 return '/transactions/detail/'.$this->getPHID().'/';
884 }
885
886 public function getBodyForMail() {
887 if ($this->isInlineCommentTransaction()) {
888 // We don't return inline comment content as mail body content, because
889 // applications need to contextualize it (by adding line numbers, for
890 // example) in order for it to make sense.
891 return null;
892 }
893
894 $comment = $this->getComment();
895 if ($comment && strlen($comment->getContent())) {
896 return $comment->getContent();
897 }
898
899 return null;
900 }
901
902 public function getNoEffectDescription() {
903
904 switch ($this->getTransactionType()) {
905 case PhabricatorTransactions::TYPE_COMMENT:
906 return pht('You can not post an empty comment.');
907 case PhabricatorTransactions::TYPE_VIEW_POLICY:
908 return pht(
909 'This %s already has that view policy.',
910 $this->getApplicationObjectTypeName());
911 case PhabricatorTransactions::TYPE_EDIT_POLICY:
912 return pht(
913 'This %s already has that edit policy.',
914 $this->getApplicationObjectTypeName());
915 case PhabricatorTransactions::TYPE_JOIN_POLICY:
916 return pht(
917 'This %s already has that join policy.',
918 $this->getApplicationObjectTypeName());
919 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
920 return pht(
921 'This %s already has that interact policy.',
922 $this->getApplicationObjectTypeName());
923 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
924 return pht(
925 'All users are already subscribed to this %s.',
926 $this->getApplicationObjectTypeName());
927 case PhabricatorTransactions::TYPE_SPACE:
928 return pht('This object is already in that space.');
929 case PhabricatorTransactions::TYPE_EDGE:
930 return pht('Edges already exist; transaction has no effect.');
931 case PhabricatorTransactions::TYPE_COLUMNS:
932 return pht(
933 'You have not moved this object to any columns it is not '.
934 'already in.');
935 case PhabricatorTransactions::TYPE_MFA:
936 return pht(
937 'You can not sign a transaction group that has no other '.
938 'effects.');
939 }
940
941 return pht(
942 'Transaction (of type "%s") has no effect.',
943 $this->getTransactionType());
944 }
945
946 public function getTitle() {
947 $author_phid = $this->getAuthorPHID();
948
949 $old = $this->getOldValue();
950 $new = $this->getNewValue();
951
952 switch ($this->getTransactionType()) {
953 case PhabricatorTransactions::TYPE_CREATE:
954 return pht(
955 '%s created this object.',
956 $this->renderHandleLink($author_phid));
957 case PhabricatorTransactions::TYPE_COMMENT:
958 return pht(
959 '%s added a comment.',
960 $this->renderHandleLink($author_phid));
961 case PhabricatorTransactions::TYPE_VIEW_POLICY:
962 if ($this->getIsCreateTransaction()) {
963 return pht(
964 '%s created this object with visibility "%s".',
965 $this->renderHandleLink($author_phid),
966 $this->renderPolicyName($new, 'new'));
967 } else {
968 return pht(
969 '%s changed the visibility from "%s" to "%s".',
970 $this->renderHandleLink($author_phid),
971 $this->renderPolicyName($old, 'old'),
972 $this->renderPolicyName($new, 'new'));
973 }
974 case PhabricatorTransactions::TYPE_EDIT_POLICY:
975 if ($this->getIsCreateTransaction()) {
976 return pht(
977 '%s created this object with edit policy "%s".',
978 $this->renderHandleLink($author_phid),
979 $this->renderPolicyName($new, 'new'));
980 } else {
981 return pht(
982 '%s changed the edit policy from "%s" to "%s".',
983 $this->renderHandleLink($author_phid),
984 $this->renderPolicyName($old, 'old'),
985 $this->renderPolicyName($new, 'new'));
986 }
987 case PhabricatorTransactions::TYPE_JOIN_POLICY:
988 if ($this->getIsCreateTransaction()) {
989 return pht(
990 '%s created this object with join policy "%s".',
991 $this->renderHandleLink($author_phid),
992 $this->renderPolicyName($new, 'new'));
993 } else {
994 return pht(
995 '%s changed the join policy from "%s" to "%s".',
996 $this->renderHandleLink($author_phid),
997 $this->renderPolicyName($old, 'old'),
998 $this->renderPolicyName($new, 'new'));
999 }
1000 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
1001 if ($this->getIsCreateTransaction()) {
1002 return pht(
1003 '%s created this object with interact policy "%s".',
1004 $this->renderHandleLink($author_phid),
1005 $this->renderPolicyName($new, 'new'));
1006 } else {
1007 return pht(
1008 '%s changed the interact policy from "%s" to "%s".',
1009 $this->renderHandleLink($author_phid),
1010 $this->renderPolicyName($old, 'old'),
1011 $this->renderPolicyName($new, 'new'));
1012 }
1013 case PhabricatorTransactions::TYPE_SPACE:
1014 if ($this->getIsCreateTransaction()) {
1015 return pht(
1016 '%s created this object in space %s.',
1017 $this->renderHandleLink($author_phid),
1018 $this->renderHandleLink($new));
1019 } else {
1020 return pht(
1021 '%s shifted this object from the %s space to the %s space.',
1022 $this->renderHandleLink($author_phid),
1023 $this->renderHandleLink($old),
1024 $this->renderHandleLink($new));
1025 }
1026 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1027 $add = array_diff($new, $old);
1028 $rem = array_diff($old, $new);
1029
1030 if ($add && $rem) {
1031 return pht(
1032 '%s edited subscriber(s), added %d: %s; removed %d: %s.',
1033 $this->renderHandleLink($author_phid),
1034 count($add),
1035 $this->renderSubscriberList($add, 'add'),
1036 count($rem),
1037 $this->renderSubscriberList($rem, 'rem'));
1038 } else if ($add) {
1039 if ($this->isSelfSubscription()) {
1040 return pht(
1041 '%s subscribed.',
1042 $this->renderHandleLink($author_phid));
1043 }
1044 return pht(
1045 '%s added %d subscriber(s): %s.',
1046 $this->renderHandleLink($author_phid),
1047 count($add),
1048 $this->renderSubscriberList($add, 'add'));
1049 } else if ($rem) {
1050 if ($this->isSelfSubscription()) {
1051 return pht(
1052 '%s unsubscribed.',
1053 $this->renderHandleLink($author_phid));
1054 }
1055 return pht(
1056 '%s removed %d subscriber(s): %s.',
1057 $this->renderHandleLink($author_phid),
1058 count($rem),
1059 $this->renderSubscriberList($rem, 'rem'));
1060 } else {
1061 // This is used when rendering previews, before the user actually
1062 // selects any CCs.
1063 return pht(
1064 '%s updated subscribers...',
1065 $this->renderHandleLink($author_phid));
1066 }
1067 case PhabricatorTransactions::TYPE_FILE:
1068 $add = array_diff_key($new, $old);
1069 $add = array_keys($add);
1070
1071 $rem = array_diff_key($old, $new);
1072 $rem = array_keys($rem);
1073
1074 $mod = array();
1075 foreach ($old + $new as $key => $ignored) {
1076 if (!isset($old[$key])) {
1077 continue;
1078 }
1079
1080 if (!isset($new[$key])) {
1081 continue;
1082 }
1083
1084 if ($old[$key] === $new[$key]) {
1085 continue;
1086 }
1087
1088 $mod[] = $key;
1089 }
1090
1091 // Specialize the specific case of only modifying files and upgrading
1092 // references to attachments. This is accessible via the UI and can
1093 // be shown more clearly than the generic default transaction shows
1094 // it.
1095
1096 $mode_reference = PhabricatorFileAttachment::MODE_REFERENCE;
1097 $mode_attach = PhabricatorFileAttachment::MODE_ATTACH;
1098
1099 $is_refattach = false;
1100 if ($mod && !$add && !$rem) {
1101 $all_refattach = true;
1102 foreach ($mod as $phid) {
1103 if (idx($old, $phid) !== $mode_reference) {
1104 $all_refattach = false;
1105 break;
1106 }
1107 if (idx($new, $phid) !== $mode_attach) {
1108 $all_refattach = false;
1109 break;
1110 }
1111 }
1112 $is_refattach = $all_refattach;
1113 }
1114
1115 if ($is_refattach) {
1116 return pht(
1117 '%s attached %s referenced file(s): %s.',
1118 $this->renderHandleLink($author_phid),
1119 phutil_count($mod),
1120 $this->renderHandleList($mod));
1121 } else if ($add && $rem && $mod) {
1122 return pht(
1123 '%s updated %s attached file(s), added %s: %s; removed %s: %s; '.
1124 'modified %s: %s.',
1125 $this->renderHandleLink($author_phid),
1126 new PhutilNumber(count($add) + count($rem) + count($mod)),
1127 phutil_count($add),
1128 $this->renderHandleList($add),
1129 phutil_count($rem),
1130 $this->renderHandleList($rem),
1131 phutil_count($mod),
1132 $this->renderHandleList($mod));
1133 } else if ($add && $rem) {
1134 return pht(
1135 '%s updated %s attached file(s), added %s: %s; removed %s: %s.',
1136 $this->renderHandleLink($author_phid),
1137 new PhutilNumber(count($add) + count($rem)),
1138 phutil_count($add),
1139 $this->renderHandleList($add),
1140 phutil_count($rem),
1141 $this->renderHandleList($rem));
1142 } else if ($add && $mod) {
1143 return pht(
1144 '%s updated %s attached file(s), added %s: %s; modified %s: %s.',
1145 $this->renderHandleLink($author_phid),
1146 new PhutilNumber(count($add) + count($mod)),
1147 phutil_count($add),
1148 $this->renderHandleList($add),
1149 phutil_count($mod),
1150 $this->renderHandleList($mod));
1151 } else if ($rem && $mod) {
1152 return pht(
1153 '%s updated %s attached file(s), removed %s: %s; modified %s: %s.',
1154 $this->renderHandleLink($author_phid),
1155 new PhutilNumber(count($rem) + count($mod)),
1156 phutil_count($rem),
1157 $this->renderHandleList($rem),
1158 phutil_count($mod),
1159 $this->renderHandleList($mod));
1160 } else if ($add) {
1161 return pht(
1162 '%s attached %s file(s): %s.',
1163 $this->renderHandleLink($author_phid),
1164 phutil_count($add),
1165 $this->renderHandleList($add));
1166 } else if ($rem) {
1167 return pht(
1168 '%s removed %s attached file(s): %s.',
1169 $this->renderHandleLink($author_phid),
1170 phutil_count($rem),
1171 $this->renderHandleList($rem));
1172 } else if ($mod) {
1173 return pht(
1174 '%s modified %s attached file(s): %s.',
1175 $this->renderHandleLink($author_phid),
1176 phutil_count($mod),
1177 $this->renderHandleList($mod));
1178 } else {
1179 return pht(
1180 '%s attached files...',
1181 $this->renderHandleLink($author_phid));
1182 }
1183
1184 case PhabricatorTransactions::TYPE_EDGE:
1185 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
1186 $add = $record->getAddedPHIDs();
1187 $rem = $record->getRemovedPHIDs();
1188
1189 $type = $this->getMetadata('edge:type');
1190 $type = head($type);
1191
1192 try {
1193 $type_obj = PhabricatorEdgeType::getByConstant($type);
1194 } catch (Exception $ex) {
1195 // Recover somewhat gracefully from edge transactions which
1196 // we don't have the classes for.
1197 return pht(
1198 '%s edited an edge.',
1199 $this->renderHandleLink($author_phid));
1200 }
1201
1202 if ($add && $rem) {
1203 return $type_obj->getTransactionEditString(
1204 $this->renderHandleLink($author_phid),
1205 new PhutilNumber(count($add) + count($rem)),
1206 phutil_count($add),
1207 $this->renderHandleList($add),
1208 phutil_count($rem),
1209 $this->renderHandleList($rem));
1210 } else if ($add) {
1211 return $type_obj->getTransactionAddString(
1212 $this->renderHandleLink($author_phid),
1213 phutil_count($add),
1214 $this->renderHandleList($add));
1215 } else if ($rem) {
1216 return $type_obj->getTransactionRemoveString(
1217 $this->renderHandleLink($author_phid),
1218 phutil_count($rem),
1219 $this->renderHandleList($rem));
1220 } else {
1221 return $type_obj->getTransactionPreviewString(
1222 $this->renderHandleLink($author_phid));
1223 }
1224
1225 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1226 $field = $this->getTransactionCustomField();
1227 if ($field) {
1228 return $field->getApplicationTransactionTitle($this);
1229 } else {
1230 $developer_mode = 'phabricator.developer-mode';
1231 $is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
1232 if ($is_developer) {
1233 return pht(
1234 '%s edited a custom field (with key "%s").',
1235 $this->renderHandleLink($author_phid),
1236 $this->getMetadata('customfield:key'));
1237 } else {
1238 return pht(
1239 '%s edited a custom field.',
1240 $this->renderHandleLink($author_phid));
1241 }
1242 }
1243
1244 case PhabricatorTransactions::TYPE_TOKEN:
1245 if ($old && $new) {
1246 return pht(
1247 '%s updated a token.',
1248 $this->renderHandleLink($author_phid));
1249 } else if ($old) {
1250 return pht(
1251 '%s rescinded a token.',
1252 $this->renderHandleLink($author_phid));
1253 } else {
1254 return pht(
1255 '%s awarded a token.',
1256 $this->renderHandleLink($author_phid));
1257 }
1258
1259 case PhabricatorTransactions::TYPE_INLINESTATE:
1260 list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
1261 if ($done && $undone) {
1262 return pht(
1263 '%s marked %s inline comment(s) as done and %s inline comment(s) '.
1264 'as not done.',
1265 $this->renderHandleLink($author_phid),
1266 new PhutilNumber($done),
1267 new PhutilNumber($undone));
1268 } else if ($done) {
1269 return pht(
1270 '%s marked %s inline comment(s) as done.',
1271 $this->renderHandleLink($author_phid),
1272 new PhutilNumber($done));
1273 } else {
1274 return pht(
1275 '%s marked %s inline comment(s) as not done.',
1276 $this->renderHandleLink($author_phid),
1277 new PhutilNumber($undone));
1278 }
1279
1280 case PhabricatorTransactions::TYPE_COLUMNS:
1281 $moves = $this->getInterestingMoves($new);
1282 if (count($moves) == 1) {
1283 $move = head($moves);
1284 $from_columns = $move['fromColumnPHIDs'];
1285 $to_column = $move['columnPHID'];
1286 $board_phid = $move['boardPHID'];
1287 if (count($from_columns) == 1) {
1288 return pht(
1289 '%s moved this task from %s to %s on the %s board.',
1290 $this->renderHandleLink($author_phid),
1291 $this->renderHandleLink(head($from_columns)),
1292 $this->renderHandleLink($to_column),
1293 $this->renderHandleLink($board_phid));
1294 } else {
1295 return pht(
1296 '%s moved this task to %s on the %s board.',
1297 $this->renderHandleLink($author_phid),
1298 $this->renderHandleLink($to_column),
1299 $this->renderHandleLink($board_phid));
1300 }
1301 } else {
1302 $fragments = array();
1303 foreach ($moves as $move) {
1304 $to_column = $move['columnPHID'];
1305 $board_phid = $move['boardPHID'];
1306 $fragments[] = pht(
1307 '%s (%s)',
1308 $this->renderHandleLink($board_phid),
1309 $this->renderHandleLink($to_column));
1310 }
1311
1312 return pht(
1313 '%s moved this task on %s board(s): %s.',
1314 $this->renderHandleLink($author_phid),
1315 phutil_count($moves),
1316 phutil_implode_html(', ', $fragments));
1317 }
1318
1319 case PhabricatorTransactions::TYPE_MFA:
1320 return pht(
1321 '%s signed these changes with MFA.',
1322 $this->renderHandleLink($author_phid));
1323
1324 default:
1325 // In developer mode, provide a better hint here about which string
1326 // we're missing.
1327 $developer_mode = 'phabricator.developer-mode';
1328 $is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
1329 if ($is_developer) {
1330 return pht(
1331 '%s edited this object (transaction type "%s").',
1332 $this->renderHandleLink($author_phid),
1333 $this->getTransactionType());
1334 } else {
1335 return pht(
1336 '%s edited this %s.',
1337 $this->renderHandleLink($author_phid),
1338 $this->getApplicationObjectTypeName());
1339 }
1340 }
1341 }
1342
1343 public function getTitleForFeed() {
1344 $author_phid = $this->getAuthorPHID();
1345 $object_phid = $this->getObjectPHID();
1346
1347 $old = $this->getOldValue();
1348 $new = $this->getNewValue();
1349
1350 switch ($this->getTransactionType()) {
1351 case PhabricatorTransactions::TYPE_CREATE:
1352 return pht(
1353 '%s created %s.',
1354 $this->renderHandleLink($author_phid),
1355 $this->renderHandleLink($object_phid));
1356 case PhabricatorTransactions::TYPE_COMMENT:
1357 return pht(
1358 '%s added a comment to %s.',
1359 $this->renderHandleLink($author_phid),
1360 $this->renderHandleLink($object_phid));
1361 case PhabricatorTransactions::TYPE_VIEW_POLICY:
1362 return pht(
1363 '%s changed the visibility for %s.',
1364 $this->renderHandleLink($author_phid),
1365 $this->renderHandleLink($object_phid));
1366 case PhabricatorTransactions::TYPE_EDIT_POLICY:
1367 return pht(
1368 '%s changed the edit policy for %s.',
1369 $this->renderHandleLink($author_phid),
1370 $this->renderHandleLink($object_phid));
1371 case PhabricatorTransactions::TYPE_JOIN_POLICY:
1372 return pht(
1373 '%s changed the join policy for %s.',
1374 $this->renderHandleLink($author_phid),
1375 $this->renderHandleLink($object_phid));
1376 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
1377 return pht(
1378 '%s changed the interact policy for %s.',
1379 $this->renderHandleLink($author_phid),
1380 $this->renderHandleLink($object_phid));
1381 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1382 return pht(
1383 '%s updated subscribers of %s.',
1384 $this->renderHandleLink($author_phid),
1385 $this->renderHandleLink($object_phid));
1386 case PhabricatorTransactions::TYPE_SPACE:
1387 if ($this->getIsCreateTransaction()) {
1388 return pht(
1389 '%s created %s in the %s space.',
1390 $this->renderHandleLink($author_phid),
1391 $this->renderHandleLink($object_phid),
1392 $this->renderHandleLink($new));
1393 } else {
1394 return pht(
1395 '%s shifted %s from the %s space to the %s space.',
1396 $this->renderHandleLink($author_phid),
1397 $this->renderHandleLink($object_phid),
1398 $this->renderHandleLink($old),
1399 $this->renderHandleLink($new));
1400 }
1401 case PhabricatorTransactions::TYPE_EDGE:
1402 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
1403 $add = $record->getAddedPHIDs();
1404 $rem = $record->getRemovedPHIDs();
1405
1406 $type = $this->getMetadata('edge:type');
1407 $type = head($type);
1408
1409 $type_obj = PhabricatorEdgeType::getByConstant($type);
1410
1411 if ($add && $rem) {
1412 return $type_obj->getFeedEditString(
1413 $this->renderHandleLink($author_phid),
1414 $this->renderHandleLink($object_phid),
1415 new PhutilNumber(count($add) + count($rem)),
1416 phutil_count($add),
1417 $this->renderHandleList($add),
1418 phutil_count($rem),
1419 $this->renderHandleList($rem));
1420 } else if ($add) {
1421 return $type_obj->getFeedAddString(
1422 $this->renderHandleLink($author_phid),
1423 $this->renderHandleLink($object_phid),
1424 phutil_count($add),
1425 $this->renderHandleList($add));
1426 } else if ($rem) {
1427 return $type_obj->getFeedRemoveString(
1428 $this->renderHandleLink($author_phid),
1429 $this->renderHandleLink($object_phid),
1430 phutil_count($rem),
1431 $this->renderHandleList($rem));
1432 } else {
1433 return pht(
1434 '%s edited edge metadata for %s.',
1435 $this->renderHandleLink($author_phid),
1436 $this->renderHandleLink($object_phid));
1437 }
1438
1439 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1440 $field = $this->getTransactionCustomField();
1441 if ($field) {
1442 return $field->getApplicationTransactionTitleForFeed($this);
1443 } else {
1444 return pht(
1445 '%s edited a custom field on %s.',
1446 $this->renderHandleLink($author_phid),
1447 $this->renderHandleLink($object_phid));
1448 }
1449
1450 case PhabricatorTransactions::TYPE_COLUMNS:
1451 $moves = $this->getInterestingMoves($new);
1452 if (count($moves) == 1) {
1453 $move = head($moves);
1454 $from_columns = $move['fromColumnPHIDs'];
1455 $to_column = $move['columnPHID'];
1456 $board_phid = $move['boardPHID'];
1457 if (count($from_columns) == 1) {
1458 return pht(
1459 '%s moved %s from %s to %s on the %s board.',
1460 $this->renderHandleLink($author_phid),
1461 $this->renderHandleLink($object_phid),
1462 $this->renderHandleLink(head($from_columns)),
1463 $this->renderHandleLink($to_column),
1464 $this->renderHandleLink($board_phid));
1465 } else {
1466 return pht(
1467 '%s moved %s to %s on the %s board.',
1468 $this->renderHandleLink($author_phid),
1469 $this->renderHandleLink($object_phid),
1470 $this->renderHandleLink($to_column),
1471 $this->renderHandleLink($board_phid));
1472 }
1473 } else {
1474 $fragments = array();
1475 foreach ($moves as $move) {
1476 $to_column = $move['columnPHID'];
1477 $board_phid = $move['boardPHID'];
1478 $fragments[] = pht(
1479 '%s (%s)',
1480 $this->renderHandleLink($board_phid),
1481 $this->renderHandleLink($to_column));
1482 }
1483
1484 return pht(
1485 '%s moved %s on %s board(s): %s.',
1486 $this->renderHandleLink($author_phid),
1487 $this->renderHandleLink($object_phid),
1488 phutil_count($moves),
1489 phutil_implode_html(', ', $fragments));
1490 }
1491
1492 case PhabricatorTransactions::TYPE_MFA:
1493 return null;
1494
1495 }
1496
1497 return $this->getTitle();
1498 }
1499
1500 public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
1501 $fields = array();
1502
1503 switch ($this->getTransactionType()) {
1504 case PhabricatorTransactions::TYPE_COMMENT:
1505 $text = $this->getComment()->getContent();
1506 if (strlen($text)) {
1507 $fields[] = 'comment/'.$this->getID();
1508 }
1509 break;
1510 }
1511
1512 return $fields;
1513 }
1514
1515 public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
1516 switch ($this->getTransactionType()) {
1517 case PhabricatorTransactions::TYPE_COMMENT:
1518 $text = $this->getComment()->getContent();
1519 return PhabricatorMarkupEngine::summarize($text);
1520 }
1521
1522 return null;
1523 }
1524
1525 public function getBodyForFeed(PhabricatorFeedStory $story) {
1526 $remarkup = $this->getRemarkupBodyForFeed($story);
1527 if ($remarkup !== null) {
1528 $remarkup = PhabricatorMarkupEngine::summarize($remarkup);
1529 return new PHUIRemarkupView($this->viewer, $remarkup);
1530 }
1531
1532 $old = $this->getOldValue();
1533 $new = $this->getNewValue();
1534
1535 $body = null;
1536
1537 switch ($this->getTransactionType()) {
1538 case PhabricatorTransactions::TYPE_COMMENT:
1539 $text = $this->getComment()->getContent();
1540 if (strlen($text)) {
1541 $body = $story->getMarkupFieldOutput('comment/'.$this->getID());
1542 }
1543 break;
1544 }
1545
1546 return $body;
1547 }
1548
1549 public function getRemarkupBodyForFeed(PhabricatorFeedStory $story) {
1550 return null;
1551 }
1552
1553 public function getActionStrength() {
1554 if ($this->isInlineCommentTransaction()) {
1555 return 25;
1556 }
1557
1558 switch ($this->getTransactionType()) {
1559 case PhabricatorTransactions::TYPE_COMMENT:
1560 return 50;
1561 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1562 if ($this->isSelfSubscription()) {
1563 // Make this weaker than TYPE_COMMENT.
1564 return 25;
1565 }
1566
1567 // In other cases, subscriptions are more interesting than comments
1568 // (which are shown anyway) but less interesting than any other type of
1569 // transaction.
1570 return 75;
1571 case PhabricatorTransactions::TYPE_MFA:
1572 // We want MFA signatures to render at the top of transaction groups,
1573 // on top of the things they signed.
1574 return 1000;
1575 }
1576
1577 return 100;
1578 }
1579
1580 /**
1581 * Whether the transaction concerns a comment (e.g. add, edit, remove)
1582 * @return bool True if the transaction concerns a comment
1583 */
1584 public function isCommentTransaction() {
1585 if ($this->hasComment()) {
1586 return true;
1587 }
1588
1589 switch ($this->getTransactionType()) {
1590 case PhabricatorTransactions::TYPE_COMMENT:
1591 return true;
1592 }
1593
1594 return false;
1595 }
1596
1597 public function isInlineCommentTransaction() {
1598 return false;
1599 }
1600
1601 public function getActionName() {
1602 switch ($this->getTransactionType()) {
1603 case PhabricatorTransactions::TYPE_COMMENT:
1604 return pht('Commented On');
1605 case PhabricatorTransactions::TYPE_VIEW_POLICY:
1606 case PhabricatorTransactions::TYPE_EDIT_POLICY:
1607 case PhabricatorTransactions::TYPE_JOIN_POLICY:
1608 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
1609 return pht('Changed Policy');
1610 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1611 return pht('Changed Subscribers');
1612 case PhabricatorTransactions::TYPE_CREATE:
1613 return pht('Created');
1614 default:
1615 return pht('Updated');
1616 }
1617 }
1618
1619 public function getMailTags() {
1620 return array();
1621 }
1622
1623 public function hasChangeDetails() {
1624 switch ($this->getTransactionType()) {
1625 case PhabricatorTransactions::TYPE_FILE:
1626 return true;
1627 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1628 $field = $this->getTransactionCustomField();
1629 if ($field) {
1630 return $field->getApplicationTransactionHasChangeDetails($this);
1631 }
1632 break;
1633 }
1634 return false;
1635 }
1636
1637 public function hasChangeDetailsForMail() {
1638 return $this->hasChangeDetails();
1639 }
1640
1641 public function renderChangeDetailsForMail(PhabricatorUser $viewer) {
1642 switch ($this->getTransactionType()) {
1643 case PhabricatorTransactions::TYPE_FILE:
1644 return false;
1645 }
1646
1647 $view = $this->renderChangeDetails($viewer);
1648 if ($view instanceof PhabricatorApplicationTransactionTextDiffDetailView) {
1649 return $view->renderForMail();
1650 }
1651 return null;
1652 }
1653
1654 public function renderChangeDetails(PhabricatorUser $viewer) {
1655 switch ($this->getTransactionType()) {
1656 case PhabricatorTransactions::TYPE_FILE:
1657 return $this->newFileTransactionChangeDetails($viewer);
1658 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1659 $field = $this->getTransactionCustomField();
1660 if ($field) {
1661 return $field->getApplicationTransactionChangeDetails($this, $viewer);
1662 }
1663 break;
1664 }
1665
1666 return $this->renderTextCorpusChangeDetails(
1667 $viewer,
1668 $this->getOldValue(),
1669 $this->getNewValue());
1670 }
1671
1672 public function renderTextCorpusChangeDetails(
1673 PhabricatorUser $viewer,
1674 $old,
1675 $new) {
1676 return id(new PhabricatorApplicationTransactionTextDiffDetailView())
1677 ->setUser($viewer)
1678 ->setOldText($old)
1679 ->setNewText($new);
1680 }
1681
1682 /**
1683 * @param array<PhabricatorApplicationTransaction> $group
1684 */
1685 public function attachTransactionGroup(array $group) {
1686 assert_instances_of($group, self::class);
1687 $this->transactionGroup = $group;
1688 return $this;
1689 }
1690
1691 public function getTransactionGroup() {
1692 return $this->transactionGroup;
1693 }
1694
1695 /**
1696 * Should this transaction be visually grouped with an existing transaction
1697 * group?
1698 *
1699 * @param list<PhabricatorApplicationTransaction> $group List of transactions.
1700 * @return bool True to display in a group with the other transactions.
1701 */
1702 public function shouldDisplayGroupWith(array $group) {
1703 $this_source = null;
1704 if ($this->getContentSource()) {
1705 $this_source = $this->getContentSource()->getSource();
1706 }
1707
1708 $type_mfa = PhabricatorTransactions::TYPE_MFA;
1709
1710 foreach ($group as $xaction) {
1711 // Don't group transactions by different authors.
1712 if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
1713 return false;
1714 }
1715
1716 // Don't group transactions for different objects.
1717 if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
1718 return false;
1719 }
1720
1721 // Don't group anything into a group which already has a comment.
1722 if ($xaction->isCommentTransaction()) {
1723 return false;
1724 }
1725
1726 // Don't group transactions from different content sources.
1727 $other_source = null;
1728 if ($xaction->getContentSource()) {
1729 $other_source = $xaction->getContentSource()->getSource();
1730 }
1731
1732 if ($other_source != $this_source) {
1733 return false;
1734 }
1735
1736 // Don't group transactions which happened more than 2 minutes apart.
1737 $apart = abs($xaction->getDateCreated() - $this->getDateCreated());
1738 if ($apart > (60 * 2)) {
1739 return false;
1740 }
1741
1742 // Don't group silent and nonsilent transactions together.
1743 $is_silent = $this->getIsSilentTransaction();
1744 if ($is_silent != $xaction->getIsSilentTransaction()) {
1745 return false;
1746 }
1747
1748 // Don't group MFA and non-MFA transactions together.
1749 $is_mfa = $this->getIsMFATransaction();
1750 if ($is_mfa != $xaction->getIsMFATransaction()) {
1751 return false;
1752 }
1753
1754 // Don't group two "Sign with MFA" transactions together.
1755 if ($this->getTransactionType() === $type_mfa) {
1756 if ($xaction->getTransactionType() === $type_mfa) {
1757 return false;
1758 }
1759 }
1760
1761 // Don't group lock override and non-override transactions together.
1762 $is_override = $this->getIsLockOverrideTransaction();
1763 if ($is_override != $xaction->getIsLockOverrideTransaction()) {
1764 return false;
1765 }
1766 }
1767
1768 return true;
1769 }
1770
1771 public function renderExtraInformationLink() {
1772 $herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
1773
1774 if ($herald_xscript_id) {
1775 return phutil_tag(
1776 'a',
1777 array(
1778 'href' => '/herald/transcript/'.$herald_xscript_id.'/',
1779 ),
1780 pht('View Herald Transcript'));
1781 }
1782
1783 return null;
1784 }
1785
1786 public function renderAsTextForDoorkeeper(
1787 DoorkeeperFeedStoryPublisher $publisher,
1788 PhabricatorFeedStory $story,
1789 array $xactions) {
1790
1791 $text = array();
1792 $body = array();
1793
1794 foreach ($xactions as $xaction) {
1795 $xaction_body = $xaction->getBodyForMail();
1796 if ($xaction_body !== null) {
1797 $body[] = $xaction_body;
1798 }
1799
1800 if ($xaction->shouldHideForMail($xactions)) {
1801 continue;
1802 }
1803
1804 $old_target = $xaction->getRenderingTarget();
1805 $new_target = self::TARGET_TEXT;
1806 $xaction->setRenderingTarget($new_target);
1807
1808 if ($publisher->getRenderWithImpliedContext()) {
1809 $text[] = $xaction->getTitle();
1810 } else {
1811 $text[] = $xaction->getTitleForFeed();
1812 }
1813
1814 $xaction->setRenderingTarget($old_target);
1815 }
1816
1817 $text = implode("\n", $text);
1818 $body = implode("\n\n", $body);
1819
1820 return rtrim($text."\n\n".$body);
1821 }
1822
1823 /**
1824 * Test if this transaction is just a user subscribing or unsubscribing
1825 * themselves.
1826 *
1827 * @return bool
1828 */
1829 private function isSelfSubscription() {
1830 $type = $this->getTransactionType();
1831 if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
1832 return false;
1833 }
1834
1835 $old = $this->getOldValue();
1836 $new = $this->getNewValue();
1837
1838 $add = array_diff($old, $new);
1839 $rem = array_diff($new, $old);
1840
1841 if ((count($add) + count($rem)) != 1) {
1842 // More than one user affected.
1843 return false;
1844 }
1845
1846 $affected_phid = head(array_merge($add, $rem));
1847 if ($affected_phid != $this->getAuthorPHID()) {
1848 // Affected user is someone else.
1849 return false;
1850 }
1851
1852 return true;
1853 }
1854
1855 private function isApplicationAuthor() {
1856 $author_phid = $this->getAuthorPHID();
1857 $author_type = phid_get_type($author_phid);
1858 $application_type = PhabricatorApplicationApplicationPHIDType::TYPECONST;
1859 return ($author_type == $application_type);
1860 }
1861
1862
1863 private function getInterestingMoves(array $moves) {
1864 // Remove moves which only shift the position of a task within a column.
1865 foreach ($moves as $key => $move) {
1866 $from_phids = array_fuse($move['fromColumnPHIDs']);
1867 if (isset($from_phids[$move['columnPHID']])) {
1868 unset($moves[$key]);
1869 }
1870 }
1871
1872 return $moves;
1873 }
1874
1875 private function getInterestingInlineStateChangeCounts() {
1876 // See PHI995. Newer inline state transactions have additional details
1877 // which we use to tailor the rendering behavior. These details are not
1878 // present on older transactions.
1879 $details = $this->getMetadataValue('inline.details', array());
1880
1881 $new = $this->getNewValue();
1882
1883 $done = 0;
1884 $undone = 0;
1885 foreach ($new as $phid => $state) {
1886 $is_done = ($state == PhabricatorInlineComment::STATE_DONE);
1887
1888 // See PHI995. If you're marking your own inline comments as "Done",
1889 // don't count them when rendering a timeline story. In the case where
1890 // you're only affecting your own comments, this will hide the
1891 // "alice marked X comments as done" story entirely.
1892
1893 // Usually, this happens when you pre-mark inlines as "done" and submit
1894 // them yourself. We'll still generate an "alice added inline comments"
1895 // story (in most cases/contexts), but the state change story is largely
1896 // just clutter and slightly confusing/misleading.
1897
1898 $inline_details = idx($details, $phid, array());
1899 $inline_author_phid = idx($inline_details, 'authorPHID');
1900 if ($inline_author_phid) {
1901 if ($inline_author_phid == $this->getAuthorPHID()) {
1902 if ($is_done) {
1903 continue;
1904 }
1905 }
1906 }
1907
1908 if ($is_done) {
1909 $done++;
1910 } else {
1911 $undone++;
1912 }
1913 }
1914
1915 return array($done, $undone);
1916 }
1917
1918 public function newGlobalSortVector() {
1919 return id(new PhutilSortVector())
1920 ->addInt(-$this->getDateCreated())
1921 ->addString($this->getPHID());
1922 }
1923
1924 public function newActionStrengthSortVector() {
1925 return id(new PhutilSortVector())
1926 ->addInt(-$this->getActionStrength());
1927 }
1928
1929 private function newFileTransactionChangeDetails(PhabricatorUser $viewer) {
1930 $old = $this->getOldValue();
1931 $new = $this->getNewValue();
1932
1933 $phids = array_keys($old + $new);
1934 $handles = $viewer->loadHandles($phids);
1935
1936 $names = array(
1937 PhabricatorFileAttachment::MODE_REFERENCE => pht('Referenced'),
1938 PhabricatorFileAttachment::MODE_ATTACH => pht('Attached'),
1939 );
1940
1941 $rows = array();
1942 foreach ($old + $new as $phid => $ignored) {
1943 $handle = $handles[$phid];
1944
1945 $old_mode = idx($old, $phid);
1946 $new_mode = idx($new, $phid);
1947
1948 if ($old_mode === null) {
1949 $old_name = pht('None');
1950 } else if (isset($names[$old_mode])) {
1951 $old_name = $names[$old_mode];
1952 } else {
1953 $old_name = pht('Unknown ("%s")', $old_mode);
1954 }
1955
1956 if ($new_mode === null) {
1957 $new_name = pht('Detached');
1958 } else if (isset($names[$new_mode])) {
1959 $new_name = $names[$new_mode];
1960 } else {
1961 $new_name = pht('Unknown ("%s")', $new_mode);
1962 }
1963
1964 $rows[] = array(
1965 $handle->renderLink(),
1966 $old_name,
1967 $new_name,
1968 );
1969 }
1970
1971 $table = id(new AphrontTableView($rows))
1972 ->setHeaders(
1973 array(
1974 pht('File'),
1975 pht('Old Mode'),
1976 pht('New Mode'),
1977 ))
1978 ->setColumnClasses(
1979 array(
1980 'pri',
1981 ));
1982
1983 return id(new PHUIBoxView())
1984 ->addMargin(PHUI::MARGIN_SMALL)
1985 ->appendChild($table);
1986 }
1987
1988
1989
1990/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
1991
1992
1993 public function getCapabilities() {
1994 return array(
1995 PhabricatorPolicyCapability::CAN_VIEW,
1996 PhabricatorPolicyCapability::CAN_EDIT,
1997 );
1998 }
1999
2000 public function getPolicy($capability) {
2001 switch ($capability) {
2002 case PhabricatorPolicyCapability::CAN_VIEW:
2003 return $this->getViewPolicy();
2004 case PhabricatorPolicyCapability::CAN_EDIT:
2005 return $this->getEditPolicy();
2006 }
2007 }
2008
2009 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
2010 return ($viewer->getPHID() == $this->getAuthorPHID());
2011 }
2012
2013 public function describeAutomaticCapability($capability) {
2014 return pht(
2015 'Transactions are visible to users that can see the object which was '.
2016 'acted upon. Some transactions - in particular, comments - are '.
2017 'editable by the transaction author.');
2018 }
2019
2020 public function getModularType() {
2021 return null;
2022 }
2023
2024 public function setForceNotifyPHIDs(array $phids) {
2025 $this->setMetadataValue('notify.force', $phids);
2026 return $this;
2027 }
2028
2029 public function getForceNotifyPHIDs() {
2030 return $this->getMetadataValue('notify.force', array());
2031 }
2032
2033
2034/* -( PhabricatorDestructibleInterface )----------------------------------- */
2035
2036
2037 public function destroyObjectPermanently(
2038 PhabricatorDestructionEngine $engine) {
2039
2040 $this->openTransaction();
2041 $comment_template = $this->getApplicationTransactionCommentObject();
2042
2043 if ($comment_template) {
2044 $comments = $comment_template->loadAllWhere(
2045 'transactionPHID = %s',
2046 $this->getPHID());
2047 foreach ($comments as $comment) {
2048 $engine->destroyObject($comment);
2049 }
2050 }
2051
2052 $this->delete();
2053 $this->saveTransaction();
2054 }
2055
2056}