Remove all "FileHasObject" edge reads and writes
[phabricator.git] / src / applications / transactions / storage / PhabricatorApplicationTransaction.php
blobf4f4c5a1d7cd500a285912f29120742c15fcd7c7
1 <?php
3 abstract class PhabricatorApplicationTransaction
4 extends PhabricatorLiskDAO
5 implements
6 PhabricatorPolicyInterface,
7 PhabricatorDestructibleInterface {
9 const TARGET_TEXT = 'text';
10 const TARGET_HTML = 'html';
12 protected $phid;
13 protected $objectPHID;
14 protected $authorPHID;
15 protected $viewPolicy;
16 protected $editPolicy;
18 protected $commentPHID;
19 protected $commentVersion = 0;
20 protected $transactionType;
21 protected $oldValue;
22 protected $newValue;
23 protected $metadata = array();
25 protected $contentSource;
27 private $comment;
28 private $commentNotLoaded;
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;
37 private $ignoreOnNoEffect;
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.
46 public function setIgnoreOnNoEffect($ignore) {
47 $this->ignoreOnNoEffect = $ignore;
48 return $this;
51 public function getIgnoreOnNoEffect() {
52 return $this->ignoreOnNoEffect;
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;
62 return true;
65 abstract public function getApplicationTransactionType();
67 private function getApplicationObjectTypeName() {
68 $types = PhabricatorPHIDType::getAllTypes();
70 $type = idx($types, $this->getApplicationTransactionType());
71 if ($type) {
72 return $type->getTypeName();
75 return pht('Object');
78 public function getApplicationTransactionCommentObject() {
79 return null;
82 public function getMetadataValue($key, $default = null) {
83 return idx($this->metadata, $key, $default);
86 public function setMetadataValue($key, $value) {
87 $this->metadata[$key] = $value;
88 return $this;
91 public function generatePHID() {
92 $type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
93 $subtype = $this->getApplicationTransactionType();
95 return PhabricatorPHID::generateNewPHID($type, $subtype);
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,
106 self::CONFIG_COLUMN_SCHEMA => array(
107 'commentPHID' => 'phid?',
108 'commentVersion' => 'uint32',
109 'contentSource' => 'text',
110 'transactionType' => 'text32',
112 self::CONFIG_KEY_SCHEMA => array(
113 'key_object' => array(
114 'columns' => array('objectPHID'),
117 ) + parent::getConfiguration();
120 public function setContentSource(PhabricatorContentSource $content_source) {
121 $this->contentSource = $content_source->serialize();
122 return $this;
125 public function getContentSource() {
126 return PhabricatorContentSource::newFromSerialized($this->contentSource);
129 public function hasComment() {
130 $comment = $this->getComment();
131 if (!$comment) {
132 return false;
135 if ($comment->isEmptyComment()) {
136 return false;
139 return true;
142 public function getComment() {
143 if ($this->commentNotLoaded) {
144 throw new Exception(pht('Comment for this transaction was not loaded.'));
146 return $this->comment;
149 public function setIsCreateTransaction($create) {
150 return $this->setMetadataValue('core.create', $create);
153 public function getIsCreateTransaction() {
154 return (bool)$this->getMetadataValue('core.create', false);
157 public function setIsDefaultTransaction($default) {
158 return $this->setMetadataValue('core.default', $default);
161 public function getIsDefaultTransaction() {
162 return (bool)$this->getMetadataValue('core.default', false);
165 public function setIsSilentTransaction($silent) {
166 return $this->setMetadataValue('core.silent', $silent);
169 public function getIsSilentTransaction() {
170 return (bool)$this->getMetadataValue('core.silent', false);
173 public function setIsMFATransaction($mfa) {
174 return $this->setMetadataValue('core.mfa', $mfa);
177 public function getIsMFATransaction() {
178 return (bool)$this->getMetadataValue('core.mfa', false);
181 public function setIsLockOverrideTransaction($override) {
182 return $this->setMetadataValue('core.lock-override', $override);
185 public function getIsLockOverrideTransaction() {
186 return (bool)$this->getMetadataValue('core.lock-override', false);
189 public function setTransactionGroupID($group_id) {
190 return $this->setMetadataValue('core.groupID', $group_id);
193 public function getTransactionGroupID() {
194 return $this->getMetadataValue('core.groupID', null);
197 public function attachComment(
198 PhabricatorApplicationTransactionComment $comment) {
199 $this->comment = $comment;
200 $this->commentNotLoaded = false;
201 return $this;
204 public function setCommentNotLoaded($not_loaded) {
205 $this->commentNotLoaded = $not_loaded;
206 return $this;
209 public function attachObject($object) {
210 $this->object = $object;
211 return $this;
214 public function getObject() {
215 return $this->assertAttached($this->object);
218 public function getRemarkupChanges() {
219 $changes = $this->newRemarkupChanges();
220 assert_instances_of($changes, 'PhabricatorTransactionRemarkupChange');
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);
232 $comment = $this->getComment();
233 if ($comment) {
234 if ($comment->hasOldComment()) {
235 $old_value = $comment->getOldComment()->getContent();
236 } else {
237 $old_value = null;
240 $new_value = $comment->getContent();
242 $changes[] = $this->newRemarkupChange()
243 ->setOldValue($old_value)
244 ->setNewValue($new_value);
247 return $changes;
250 protected function newRemarkupChanges() {
251 return array();
254 protected function newRemarkupChange() {
255 return id(new PhabricatorTransactionRemarkupChange())
256 ->setTransaction($this);
260 * @deprecated
262 public function getRemarkupBlocks() {
263 $blocks = array();
265 switch ($this->getTransactionType()) {
266 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
267 $field = $this->getTransactionCustomField();
268 if ($field) {
269 $custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
270 $this);
271 foreach ($custom_blocks as $custom_block) {
272 $blocks[] = $custom_block;
275 break;
278 return $blocks;
281 public function setOldValue($value) {
282 $this->oldValueHasBeenSet = true;
283 $this->writeField('oldValue', $value);
284 return $this;
287 public function hasOldValue() {
288 return $this->oldValueHasBeenSet;
291 public function newChronologicalSortVector() {
292 return id(new PhutilSortVector())
293 ->addInt((int)$this->getDateCreated())
294 ->addInt((int)$this->getID());
297 /* -( Rendering )---------------------------------------------------------- */
299 public function setRenderingTarget($rendering_target) {
300 $this->renderingTarget = $rendering_target;
301 return $this;
304 public function getRenderingTarget() {
305 return $this->renderingTarget;
308 public function attachViewer(PhabricatorUser $viewer) {
309 $this->viewer = $viewer;
310 return $this;
313 public function getViewer() {
314 return $this->assertAttached($this->viewer);
317 public function getRequiredHandlePHIDs() {
318 $phids = array();
320 $old = $this->getOldValue();
321 $new = $this->getNewValue();
323 $phids[] = array($this->getAuthorPHID());
324 $phids[] = array($this->getObjectPHID());
325 switch ($this->getTransactionType()) {
326 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
327 $field = $this->getTransactionCustomField();
328 if ($field) {
329 $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
330 $this);
332 break;
333 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
334 $phids[] = $old;
335 $phids[] = $new;
336 break;
337 case PhabricatorTransactions::TYPE_EDGE:
338 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
339 $phids[] = $record->getChangedPHIDs();
340 break;
341 case PhabricatorTransactions::TYPE_COLUMNS:
342 foreach ($new as $move) {
343 $phids[] = array(
344 $move['columnPHID'],
345 $move['boardPHID'],
347 $phids[] = $move['fromColumnPHIDs'];
349 break;
350 case PhabricatorTransactions::TYPE_EDIT_POLICY:
351 case PhabricatorTransactions::TYPE_VIEW_POLICY:
352 case PhabricatorTransactions::TYPE_JOIN_POLICY:
353 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
354 if (!PhabricatorPolicyQuery::isSpecialPolicy($old)) {
355 $phids[] = array($old);
357 if (!PhabricatorPolicyQuery::isSpecialPolicy($new)) {
358 $phids[] = array($new);
360 break;
361 case PhabricatorTransactions::TYPE_SPACE:
362 if ($old) {
363 $phids[] = array($old);
365 if ($new) {
366 $phids[] = array($new);
368 break;
369 case PhabricatorTransactions::TYPE_TOKEN:
370 break;
373 if ($this->getComment()) {
374 $phids[] = array($this->getComment()->getAuthorPHID());
377 return array_mergev($phids);
380 public function setHandles(array $handles) {
381 $this->handles = $handles;
382 return $this;
385 public function getHandle($phid) {
386 if (empty($this->handles[$phid])) {
387 throw new Exception(
388 pht(
389 'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
390 'did not load.',
391 $this->getPHID(),
392 $this->getTransactionType(),
393 $phid));
395 return $this->handles[$phid];
398 public function getHandleIfExists($phid) {
399 return idx($this->handles, $phid);
402 public function getHandles() {
403 if ($this->handles === null) {
404 throw new Exception(
405 pht('Transaction requires handles and it did not load them.'));
407 return $this->handles;
410 public function renderHandleLink($phid) {
411 if ($this->renderingTarget == self::TARGET_HTML) {
412 return $this->getHandle($phid)->renderHovercardLink();
413 } else {
414 return $this->getHandle($phid)->getLinkName();
418 public function renderHandleList(array $phids) {
419 $links = array();
420 foreach ($phids as $phid) {
421 $links[] = $this->renderHandleLink($phid);
423 if ($this->renderingTarget == self::TARGET_HTML) {
424 return phutil_implode_html(', ', $links);
425 } else {
426 return implode(', ', $links);
430 private function renderSubscriberList(array $phids, $change_type) {
431 if ($this->getRenderingTarget() == self::TARGET_TEXT) {
432 return $this->renderHandleList($phids);
433 } else {
434 $handles = array_select_keys($this->getHandles(), $phids);
435 return id(new SubscriptionListStringBuilder())
436 ->setHandles($handles)
437 ->setObjectPHID($this->getPHID())
438 ->buildTransactionString($change_type);
442 protected function renderPolicyName($phid, $state = 'old') {
443 $policy = PhabricatorPolicy::newFromPolicyAndHandle(
444 $phid,
445 $this->getHandleIfExists($phid));
447 $ref = $policy->newRef($this->getViewer());
449 if ($this->renderingTarget == self::TARGET_HTML) {
450 $output = $ref->newTransactionLink($state, $this);
451 } else {
452 $output = $ref->getPolicyDisplayName();
455 return $output;
458 public function getIcon() {
459 switch ($this->getTransactionType()) {
460 case PhabricatorTransactions::TYPE_COMMENT:
461 $comment = $this->getComment();
462 if ($comment && $comment->getIsRemoved()) {
463 return 'fa-trash';
465 return 'fa-comment';
466 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
467 $old = $this->getOldValue();
468 $new = $this->getNewValue();
469 $add = array_diff($new, $old);
470 $rem = array_diff($old, $new);
471 if ($add && $rem) {
472 return 'fa-user';
473 } else if ($add) {
474 return 'fa-user-plus';
475 } else if ($rem) {
476 return 'fa-user-times';
477 } else {
478 return 'fa-user';
480 case PhabricatorTransactions::TYPE_VIEW_POLICY:
481 case PhabricatorTransactions::TYPE_EDIT_POLICY:
482 case PhabricatorTransactions::TYPE_JOIN_POLICY:
483 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
484 return 'fa-lock';
485 case PhabricatorTransactions::TYPE_EDGE:
486 switch ($this->getMetadataValue('edge:type')) {
487 case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
488 return 'fa-undo';
489 case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
490 return 'fa-ambulance';
492 return 'fa-link';
493 case PhabricatorTransactions::TYPE_TOKEN:
494 return 'fa-trophy';
495 case PhabricatorTransactions::TYPE_SPACE:
496 return 'fa-th-large';
497 case PhabricatorTransactions::TYPE_COLUMNS:
498 return 'fa-columns';
499 case PhabricatorTransactions::TYPE_MFA:
500 return 'fa-vcard';
503 return 'fa-pencil';
506 public function getToken() {
507 switch ($this->getTransactionType()) {
508 case PhabricatorTransactions::TYPE_TOKEN:
509 $old = $this->getOldValue();
510 $new = $this->getNewValue();
511 if ($new) {
512 $icon = substr($new, 10);
513 } else {
514 $icon = substr($old, 10);
516 return array($icon, !$this->getNewValue());
519 return array(null, null);
522 public function getColor() {
523 switch ($this->getTransactionType()) {
524 case PhabricatorTransactions::TYPE_COMMENT;
525 $comment = $this->getComment();
526 if ($comment && $comment->getIsRemoved()) {
527 return 'black';
529 break;
530 case PhabricatorTransactions::TYPE_EDGE:
531 switch ($this->getMetadataValue('edge:type')) {
532 case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
533 return 'pink';
534 case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
535 return 'sky';
537 break;
538 case PhabricatorTransactions::TYPE_MFA;
539 return 'pink';
541 return null;
544 protected function getTransactionCustomField() {
545 switch ($this->getTransactionType()) {
546 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
547 $key = $this->getMetadataValue('customfield:key');
548 if (!$key) {
549 return null;
552 $object = $this->getObject();
554 if (!($object instanceof PhabricatorCustomFieldInterface)) {
555 return null;
558 $field = PhabricatorCustomField::getObjectField(
559 $object,
560 PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
561 $key);
562 if (!$field) {
563 return null;
566 $field->setViewer($this->getViewer());
567 return $field;
570 return null;
573 public function shouldHide() {
574 // Never hide comments.
575 if ($this->hasComment()) {
576 return false;
579 $xaction_type = $this->getTransactionType();
581 // Always hide requests for object history.
582 if ($xaction_type === PhabricatorTransactions::TYPE_HISTORY) {
583 return true;
586 // Always hide file attach/detach transactions.
587 if ($xaction_type === PhabricatorTransactions::TYPE_FILE) {
588 return true;
591 // Hide creation transactions if the old value is empty. These are
592 // transactions like "alice set the task title to: ...", which are
593 // essentially never interesting.
594 if ($this->getIsCreateTransaction()) {
595 switch ($xaction_type) {
596 case PhabricatorTransactions::TYPE_CREATE:
597 case PhabricatorTransactions::TYPE_VIEW_POLICY:
598 case PhabricatorTransactions::TYPE_EDIT_POLICY:
599 case PhabricatorTransactions::TYPE_JOIN_POLICY:
600 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
601 case PhabricatorTransactions::TYPE_SPACE:
602 break;
603 case PhabricatorTransactions::TYPE_SUBTYPE:
604 return true;
605 default:
606 $old = $this->getOldValue();
608 if (is_array($old) && !$old) {
609 return true;
612 if (!is_array($old)) {
613 if ($old === '' || $old === null) {
614 return true;
617 // The integer 0 is also uninteresting by default; this is often
618 // an "off" flag for something like "All Day Event".
619 if ($old === 0) {
620 return true;
624 break;
628 // Hide creation transactions setting values to defaults, even if
629 // the old value is not empty. For example, tasks may have a global
630 // default view policy of "All Users", but a particular form sets the
631 // policy to "Administrators". The transaction corresponding to this
632 // change is not interesting, since it is the default behavior of the
633 // form.
635 if ($this->getIsCreateTransaction()) {
636 if ($this->getIsDefaultTransaction()) {
637 return true;
641 switch ($this->getTransactionType()) {
642 case PhabricatorTransactions::TYPE_VIEW_POLICY:
643 case PhabricatorTransactions::TYPE_EDIT_POLICY:
644 case PhabricatorTransactions::TYPE_JOIN_POLICY:
645 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
646 case PhabricatorTransactions::TYPE_SPACE:
647 if ($this->getIsCreateTransaction()) {
648 break;
651 // TODO: Remove this eventually, this is handling old changes during
652 // object creation prior to the introduction of "create" and "default"
653 // transaction display flags.
655 // NOTE: We can also hit this case with Space transactions that later
656 // update a default space (`null`) to an explicit space, so handling
657 // the Space case may require some finesse.
659 if ($this->getOldValue() === null) {
660 return true;
661 } else {
662 return false;
664 break;
665 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
666 $field = $this->getTransactionCustomField();
667 if ($field) {
668 return $field->shouldHideInApplicationTransactions($this);
670 break;
671 case PhabricatorTransactions::TYPE_COLUMNS:
672 return !$this->getInterestingMoves($this->getNewValue());
673 case PhabricatorTransactions::TYPE_EDGE:
674 $edge_type = $this->getMetadataValue('edge:type');
675 switch ($edge_type) {
676 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
677 case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST:
678 case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST:
679 case PhabricatorMutedEdgeType::EDGECONST:
680 case PhabricatorMutedByEdgeType::EDGECONST:
681 return true;
682 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
683 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
684 $add = $record->getAddedPHIDs();
685 $add_value = reset($add);
686 $add_handle = $this->getHandle($add_value);
687 if ($add_handle->getPolicyFiltered()) {
688 return true;
690 return false;
691 break;
692 default:
693 break;
695 break;
697 case PhabricatorTransactions::TYPE_INLINESTATE:
698 list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
700 if (!$done && !$undone) {
701 return true;
704 break;
708 return false;
711 public function shouldHideForMail(array $xactions) {
712 if ($this->isSelfSubscription()) {
713 return true;
716 switch ($this->getTransactionType()) {
717 case PhabricatorTransactions::TYPE_TOKEN:
718 return true;
719 case PhabricatorTransactions::TYPE_EDGE:
720 $edge_type = $this->getMetadataValue('edge:type');
721 switch ($edge_type) {
722 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
723 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
724 case DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST:
725 case DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST:
726 case ManiphestTaskHasCommitEdgeType::EDGECONST:
727 case DiffusionCommitHasTaskEdgeType::EDGECONST:
728 case DiffusionCommitHasRevisionEdgeType::EDGECONST:
729 case DifferentialRevisionHasCommitEdgeType::EDGECONST:
730 return true;
731 case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
732 // When an object is first created, we hide any corresponding
733 // project transactions in the web UI because you can just look at
734 // the UI element elsewhere on screen to see which projects it
735 // is tagged with. However, in mail there's no other way to get
736 // this information, and it has some amount of value to users, so
737 // we keep the transaction. See T10493.
738 return false;
739 default:
740 break;
742 break;
745 if ($this->isInlineCommentTransaction()) {
746 $inlines = array();
748 // If there's a normal comment, we don't need to publish the inline
749 // transaction, since the normal comment covers things.
750 foreach ($xactions as $xaction) {
751 if ($xaction->isInlineCommentTransaction()) {
752 $inlines[] = $xaction;
753 continue;
756 // We found a normal comment, so hide this inline transaction.
757 if ($xaction->hasComment()) {
758 return true;
762 // If there are several inline comments, only publish the first one.
763 if ($this !== head($inlines)) {
764 return true;
768 return $this->shouldHide();
771 public function shouldHideForFeed() {
772 if ($this->isSelfSubscription()) {
773 return true;
776 switch ($this->getTransactionType()) {
777 case PhabricatorTransactions::TYPE_TOKEN:
778 case PhabricatorTransactions::TYPE_MFA:
779 return true;
780 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
781 // See T8952. When an application (usually Herald) modifies
782 // subscribers, this tends to be very uninteresting.
783 if ($this->isApplicationAuthor()) {
784 return true;
786 break;
787 case PhabricatorTransactions::TYPE_EDGE:
788 $edge_type = $this->getMetadataValue('edge:type');
789 switch ($edge_type) {
790 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
791 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
792 case DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST:
793 case DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST:
794 case ManiphestTaskHasCommitEdgeType::EDGECONST:
795 case DiffusionCommitHasTaskEdgeType::EDGECONST:
796 case DiffusionCommitHasRevisionEdgeType::EDGECONST:
797 case DifferentialRevisionHasCommitEdgeType::EDGECONST:
798 return true;
799 default:
800 break;
802 break;
803 case PhabricatorTransactions::TYPE_INLINESTATE:
804 return true;
807 return $this->shouldHide();
810 public function shouldHideForNotifications() {
811 return $this->shouldHideForFeed();
814 private function getTitleForMailWithRenderingTarget($new_target) {
815 $old_target = $this->getRenderingTarget();
816 try {
817 $this->setRenderingTarget($new_target);
818 $result = $this->getTitleForMail();
819 } catch (Exception $ex) {
820 $this->setRenderingTarget($old_target);
821 throw $ex;
823 $this->setRenderingTarget($old_target);
824 return $result;
827 public function getTitleForMail() {
828 return $this->getTitle();
831 public function getTitleForTextMail() {
832 return $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
835 public function getTitleForHTMLMail() {
836 // TODO: For now, rendering this with TARGET_HTML generates links with
837 // bad targets ("/x/y/" instead of "https://dev.example.com/x/y/"). Throw
838 // a rug over the issue for the moment. See T12921.
840 $title = $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
841 if ($title === null) {
842 return null;
845 if ($this->hasChangeDetails()) {
846 $details_uri = $this->getChangeDetailsURI();
847 $details_uri = PhabricatorEnv::getProductionURI($details_uri);
849 $show_details = phutil_tag(
850 'a',
851 array(
852 'href' => $details_uri,
854 pht('(Show Details)'));
856 $title = array($title, ' ', $show_details);
859 return $title;
862 public function getChangeDetailsURI() {
863 return '/transactions/detail/'.$this->getPHID().'/';
866 public function getBodyForMail() {
867 if ($this->isInlineCommentTransaction()) {
868 // We don't return inline comment content as mail body content, because
869 // applications need to contextualize it (by adding line numbers, for
870 // example) in order for it to make sense.
871 return null;
874 $comment = $this->getComment();
875 if ($comment && strlen($comment->getContent())) {
876 return $comment->getContent();
879 return null;
882 public function getNoEffectDescription() {
884 switch ($this->getTransactionType()) {
885 case PhabricatorTransactions::TYPE_COMMENT:
886 return pht('You can not post an empty comment.');
887 case PhabricatorTransactions::TYPE_VIEW_POLICY:
888 return pht(
889 'This %s already has that view policy.',
890 $this->getApplicationObjectTypeName());
891 case PhabricatorTransactions::TYPE_EDIT_POLICY:
892 return pht(
893 'This %s already has that edit policy.',
894 $this->getApplicationObjectTypeName());
895 case PhabricatorTransactions::TYPE_JOIN_POLICY:
896 return pht(
897 'This %s already has that join policy.',
898 $this->getApplicationObjectTypeName());
899 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
900 return pht(
901 'This %s already has that interact policy.',
902 $this->getApplicationObjectTypeName());
903 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
904 return pht(
905 'All users are already subscribed to this %s.',
906 $this->getApplicationObjectTypeName());
907 case PhabricatorTransactions::TYPE_SPACE:
908 return pht('This object is already in that space.');
909 case PhabricatorTransactions::TYPE_EDGE:
910 return pht('Edges already exist; transaction has no effect.');
911 case PhabricatorTransactions::TYPE_COLUMNS:
912 return pht(
913 'You have not moved this object to any columns it is not '.
914 'already in.');
915 case PhabricatorTransactions::TYPE_MFA:
916 return pht(
917 'You can not sign a transaction group that has no other '.
918 'effects.');
921 return pht(
922 'Transaction (of type "%s") has no effect.',
923 $this->getTransactionType());
926 public function getTitle() {
927 $author_phid = $this->getAuthorPHID();
929 $old = $this->getOldValue();
930 $new = $this->getNewValue();
932 switch ($this->getTransactionType()) {
933 case PhabricatorTransactions::TYPE_CREATE:
934 return pht(
935 '%s created this object.',
936 $this->renderHandleLink($author_phid));
937 case PhabricatorTransactions::TYPE_COMMENT:
938 return pht(
939 '%s added a comment.',
940 $this->renderHandleLink($author_phid));
941 case PhabricatorTransactions::TYPE_VIEW_POLICY:
942 if ($this->getIsCreateTransaction()) {
943 return pht(
944 '%s created this object with visibility "%s".',
945 $this->renderHandleLink($author_phid),
946 $this->renderPolicyName($new, 'new'));
947 } else {
948 return pht(
949 '%s changed the visibility from "%s" to "%s".',
950 $this->renderHandleLink($author_phid),
951 $this->renderPolicyName($old, 'old'),
952 $this->renderPolicyName($new, 'new'));
954 case PhabricatorTransactions::TYPE_EDIT_POLICY:
955 if ($this->getIsCreateTransaction()) {
956 return pht(
957 '%s created this object with edit policy "%s".',
958 $this->renderHandleLink($author_phid),
959 $this->renderPolicyName($new, 'new'));
960 } else {
961 return pht(
962 '%s changed the edit policy from "%s" to "%s".',
963 $this->renderHandleLink($author_phid),
964 $this->renderPolicyName($old, 'old'),
965 $this->renderPolicyName($new, 'new'));
967 case PhabricatorTransactions::TYPE_JOIN_POLICY:
968 if ($this->getIsCreateTransaction()) {
969 return pht(
970 '%s created this object with join policy "%s".',
971 $this->renderHandleLink($author_phid),
972 $this->renderPolicyName($new, 'new'));
973 } else {
974 return pht(
975 '%s changed the join policy from "%s" to "%s".',
976 $this->renderHandleLink($author_phid),
977 $this->renderPolicyName($old, 'old'),
978 $this->renderPolicyName($new, 'new'));
980 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
981 if ($this->getIsCreateTransaction()) {
982 return pht(
983 '%s created this object with interact policy "%s".',
984 $this->renderHandleLink($author_phid),
985 $this->renderPolicyName($new, 'new'));
986 } else {
987 return pht(
988 '%s changed the interact policy from "%s" to "%s".',
989 $this->renderHandleLink($author_phid),
990 $this->renderPolicyName($old, 'old'),
991 $this->renderPolicyName($new, 'new'));
993 case PhabricatorTransactions::TYPE_SPACE:
994 if ($this->getIsCreateTransaction()) {
995 return pht(
996 '%s created this object in space %s.',
997 $this->renderHandleLink($author_phid),
998 $this->renderHandleLink($new));
999 } else {
1000 return pht(
1001 '%s shifted this object from the %s space to the %s space.',
1002 $this->renderHandleLink($author_phid),
1003 $this->renderHandleLink($old),
1004 $this->renderHandleLink($new));
1006 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1007 $add = array_diff($new, $old);
1008 $rem = array_diff($old, $new);
1010 if ($add && $rem) {
1011 return pht(
1012 '%s edited subscriber(s), added %d: %s; removed %d: %s.',
1013 $this->renderHandleLink($author_phid),
1014 count($add),
1015 $this->renderSubscriberList($add, 'add'),
1016 count($rem),
1017 $this->renderSubscriberList($rem, 'rem'));
1018 } else if ($add) {
1019 return pht(
1020 '%s added %d subscriber(s): %s.',
1021 $this->renderHandleLink($author_phid),
1022 count($add),
1023 $this->renderSubscriberList($add, 'add'));
1024 } else if ($rem) {
1025 return pht(
1026 '%s removed %d subscriber(s): %s.',
1027 $this->renderHandleLink($author_phid),
1028 count($rem),
1029 $this->renderSubscriberList($rem, 'rem'));
1030 } else {
1031 // This is used when rendering previews, before the user actually
1032 // selects any CCs.
1033 return pht(
1034 '%s updated subscribers...',
1035 $this->renderHandleLink($author_phid));
1037 break;
1038 case PhabricatorTransactions::TYPE_EDGE:
1039 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
1040 $add = $record->getAddedPHIDs();
1041 $rem = $record->getRemovedPHIDs();
1043 $type = $this->getMetadata('edge:type');
1044 $type = head($type);
1046 try {
1047 $type_obj = PhabricatorEdgeType::getByConstant($type);
1048 } catch (Exception $ex) {
1049 // Recover somewhat gracefully from edge transactions which
1050 // we don't have the classes for.
1051 return pht(
1052 '%s edited an edge.',
1053 $this->renderHandleLink($author_phid));
1056 if ($add && $rem) {
1057 return $type_obj->getTransactionEditString(
1058 $this->renderHandleLink($author_phid),
1059 new PhutilNumber(count($add) + count($rem)),
1060 phutil_count($add),
1061 $this->renderHandleList($add),
1062 phutil_count($rem),
1063 $this->renderHandleList($rem));
1064 } else if ($add) {
1065 return $type_obj->getTransactionAddString(
1066 $this->renderHandleLink($author_phid),
1067 phutil_count($add),
1068 $this->renderHandleList($add));
1069 } else if ($rem) {
1070 return $type_obj->getTransactionRemoveString(
1071 $this->renderHandleLink($author_phid),
1072 phutil_count($rem),
1073 $this->renderHandleList($rem));
1074 } else {
1075 return $type_obj->getTransactionPreviewString(
1076 $this->renderHandleLink($author_phid));
1079 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1080 $field = $this->getTransactionCustomField();
1081 if ($field) {
1082 return $field->getApplicationTransactionTitle($this);
1083 } else {
1084 $developer_mode = 'phabricator.developer-mode';
1085 $is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
1086 if ($is_developer) {
1087 return pht(
1088 '%s edited a custom field (with key "%s").',
1089 $this->renderHandleLink($author_phid),
1090 $this->getMetadata('customfield:key'));
1091 } else {
1092 return pht(
1093 '%s edited a custom field.',
1094 $this->renderHandleLink($author_phid));
1098 case PhabricatorTransactions::TYPE_TOKEN:
1099 if ($old && $new) {
1100 return pht(
1101 '%s updated a token.',
1102 $this->renderHandleLink($author_phid));
1103 } else if ($old) {
1104 return pht(
1105 '%s rescinded a token.',
1106 $this->renderHandleLink($author_phid));
1107 } else {
1108 return pht(
1109 '%s awarded a token.',
1110 $this->renderHandleLink($author_phid));
1113 case PhabricatorTransactions::TYPE_INLINESTATE:
1114 list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
1115 if ($done && $undone) {
1116 return pht(
1117 '%s marked %s inline comment(s) as done and %s inline comment(s) '.
1118 'as not done.',
1119 $this->renderHandleLink($author_phid),
1120 new PhutilNumber($done),
1121 new PhutilNumber($undone));
1122 } else if ($done) {
1123 return pht(
1124 '%s marked %s inline comment(s) as done.',
1125 $this->renderHandleLink($author_phid),
1126 new PhutilNumber($done));
1127 } else {
1128 return pht(
1129 '%s marked %s inline comment(s) as not done.',
1130 $this->renderHandleLink($author_phid),
1131 new PhutilNumber($undone));
1133 break;
1135 case PhabricatorTransactions::TYPE_COLUMNS:
1136 $moves = $this->getInterestingMoves($new);
1137 if (count($moves) == 1) {
1138 $move = head($moves);
1139 $from_columns = $move['fromColumnPHIDs'];
1140 $to_column = $move['columnPHID'];
1141 $board_phid = $move['boardPHID'];
1142 if (count($from_columns) == 1) {
1143 return pht(
1144 '%s moved this task from %s to %s on the %s board.',
1145 $this->renderHandleLink($author_phid),
1146 $this->renderHandleLink(head($from_columns)),
1147 $this->renderHandleLink($to_column),
1148 $this->renderHandleLink($board_phid));
1149 } else {
1150 return pht(
1151 '%s moved this task to %s on the %s board.',
1152 $this->renderHandleLink($author_phid),
1153 $this->renderHandleLink($to_column),
1154 $this->renderHandleLink($board_phid));
1156 } else {
1157 $fragments = array();
1158 foreach ($moves as $move) {
1159 $to_column = $move['columnPHID'];
1160 $board_phid = $move['boardPHID'];
1161 $fragments[] = pht(
1162 '%s (%s)',
1163 $this->renderHandleLink($board_phid),
1164 $this->renderHandleLink($to_column));
1167 return pht(
1168 '%s moved this task on %s board(s): %s.',
1169 $this->renderHandleLink($author_phid),
1170 phutil_count($moves),
1171 phutil_implode_html(', ', $fragments));
1173 break;
1176 case PhabricatorTransactions::TYPE_MFA:
1177 return pht(
1178 '%s signed these changes with MFA.',
1179 $this->renderHandleLink($author_phid));
1181 default:
1182 // In developer mode, provide a better hint here about which string
1183 // we're missing.
1184 $developer_mode = 'phabricator.developer-mode';
1185 $is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
1186 if ($is_developer) {
1187 return pht(
1188 '%s edited this object (transaction type "%s").',
1189 $this->renderHandleLink($author_phid),
1190 $this->getTransactionType());
1191 } else {
1192 return pht(
1193 '%s edited this %s.',
1194 $this->renderHandleLink($author_phid),
1195 $this->getApplicationObjectTypeName());
1200 public function getTitleForFeed() {
1201 $author_phid = $this->getAuthorPHID();
1202 $object_phid = $this->getObjectPHID();
1204 $old = $this->getOldValue();
1205 $new = $this->getNewValue();
1207 switch ($this->getTransactionType()) {
1208 case PhabricatorTransactions::TYPE_CREATE:
1209 return pht(
1210 '%s created %s.',
1211 $this->renderHandleLink($author_phid),
1212 $this->renderHandleLink($object_phid));
1213 case PhabricatorTransactions::TYPE_COMMENT:
1214 return pht(
1215 '%s added a comment to %s.',
1216 $this->renderHandleLink($author_phid),
1217 $this->renderHandleLink($object_phid));
1218 case PhabricatorTransactions::TYPE_VIEW_POLICY:
1219 return pht(
1220 '%s changed the visibility for %s.',
1221 $this->renderHandleLink($author_phid),
1222 $this->renderHandleLink($object_phid));
1223 case PhabricatorTransactions::TYPE_EDIT_POLICY:
1224 return pht(
1225 '%s changed the edit policy for %s.',
1226 $this->renderHandleLink($author_phid),
1227 $this->renderHandleLink($object_phid));
1228 case PhabricatorTransactions::TYPE_JOIN_POLICY:
1229 return pht(
1230 '%s changed the join policy for %s.',
1231 $this->renderHandleLink($author_phid),
1232 $this->renderHandleLink($object_phid));
1233 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
1234 return pht(
1235 '%s changed the interact policy for %s.',
1236 $this->renderHandleLink($author_phid),
1237 $this->renderHandleLink($object_phid));
1238 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1239 return pht(
1240 '%s updated subscribers of %s.',
1241 $this->renderHandleLink($author_phid),
1242 $this->renderHandleLink($object_phid));
1243 case PhabricatorTransactions::TYPE_SPACE:
1244 if ($this->getIsCreateTransaction()) {
1245 return pht(
1246 '%s created %s in the %s space.',
1247 $this->renderHandleLink($author_phid),
1248 $this->renderHandleLink($object_phid),
1249 $this->renderHandleLink($new));
1250 } else {
1251 return pht(
1252 '%s shifted %s from the %s space to the %s space.',
1253 $this->renderHandleLink($author_phid),
1254 $this->renderHandleLink($object_phid),
1255 $this->renderHandleLink($old),
1256 $this->renderHandleLink($new));
1258 case PhabricatorTransactions::TYPE_EDGE:
1259 $record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
1260 $add = $record->getAddedPHIDs();
1261 $rem = $record->getRemovedPHIDs();
1263 $type = $this->getMetadata('edge:type');
1264 $type = head($type);
1266 $type_obj = PhabricatorEdgeType::getByConstant($type);
1268 if ($add && $rem) {
1269 return $type_obj->getFeedEditString(
1270 $this->renderHandleLink($author_phid),
1271 $this->renderHandleLink($object_phid),
1272 new PhutilNumber(count($add) + count($rem)),
1273 phutil_count($add),
1274 $this->renderHandleList($add),
1275 phutil_count($rem),
1276 $this->renderHandleList($rem));
1277 } else if ($add) {
1278 return $type_obj->getFeedAddString(
1279 $this->renderHandleLink($author_phid),
1280 $this->renderHandleLink($object_phid),
1281 phutil_count($add),
1282 $this->renderHandleList($add));
1283 } else if ($rem) {
1284 return $type_obj->getFeedRemoveString(
1285 $this->renderHandleLink($author_phid),
1286 $this->renderHandleLink($object_phid),
1287 phutil_count($rem),
1288 $this->renderHandleList($rem));
1289 } else {
1290 return pht(
1291 '%s edited edge metadata for %s.',
1292 $this->renderHandleLink($author_phid),
1293 $this->renderHandleLink($object_phid));
1296 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1297 $field = $this->getTransactionCustomField();
1298 if ($field) {
1299 return $field->getApplicationTransactionTitleForFeed($this);
1300 } else {
1301 return pht(
1302 '%s edited a custom field on %s.',
1303 $this->renderHandleLink($author_phid),
1304 $this->renderHandleLink($object_phid));
1307 case PhabricatorTransactions::TYPE_COLUMNS:
1308 $moves = $this->getInterestingMoves($new);
1309 if (count($moves) == 1) {
1310 $move = head($moves);
1311 $from_columns = $move['fromColumnPHIDs'];
1312 $to_column = $move['columnPHID'];
1313 $board_phid = $move['boardPHID'];
1314 if (count($from_columns) == 1) {
1315 return pht(
1316 '%s moved %s from %s to %s on the %s board.',
1317 $this->renderHandleLink($author_phid),
1318 $this->renderHandleLink($object_phid),
1319 $this->renderHandleLink(head($from_columns)),
1320 $this->renderHandleLink($to_column),
1321 $this->renderHandleLink($board_phid));
1322 } else {
1323 return pht(
1324 '%s moved %s to %s on the %s board.',
1325 $this->renderHandleLink($author_phid),
1326 $this->renderHandleLink($object_phid),
1327 $this->renderHandleLink($to_column),
1328 $this->renderHandleLink($board_phid));
1330 } else {
1331 $fragments = array();
1332 foreach ($moves as $move) {
1333 $fragments[] = pht(
1334 '%s (%s)',
1335 $this->renderHandleLink($board_phid),
1336 $this->renderHandleLink($to_column));
1339 return pht(
1340 '%s moved %s on %s board(s): %s.',
1341 $this->renderHandleLink($author_phid),
1342 $this->renderHandleLink($object_phid),
1343 phutil_count($moves),
1344 phutil_implode_html(', ', $fragments));
1346 break;
1348 case PhabricatorTransactions::TYPE_MFA:
1349 return null;
1353 return $this->getTitle();
1356 public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
1357 $fields = array();
1359 switch ($this->getTransactionType()) {
1360 case PhabricatorTransactions::TYPE_COMMENT:
1361 $text = $this->getComment()->getContent();
1362 if (strlen($text)) {
1363 $fields[] = 'comment/'.$this->getID();
1365 break;
1368 return $fields;
1371 public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
1372 switch ($this->getTransactionType()) {
1373 case PhabricatorTransactions::TYPE_COMMENT:
1374 $text = $this->getComment()->getContent();
1375 return PhabricatorMarkupEngine::summarize($text);
1378 return null;
1381 public function getBodyForFeed(PhabricatorFeedStory $story) {
1382 $remarkup = $this->getRemarkupBodyForFeed($story);
1383 if ($remarkup !== null) {
1384 $remarkup = PhabricatorMarkupEngine::summarize($remarkup);
1385 return new PHUIRemarkupView($this->viewer, $remarkup);
1388 $old = $this->getOldValue();
1389 $new = $this->getNewValue();
1391 $body = null;
1393 switch ($this->getTransactionType()) {
1394 case PhabricatorTransactions::TYPE_COMMENT:
1395 $text = $this->getComment()->getContent();
1396 if (strlen($text)) {
1397 $body = $story->getMarkupFieldOutput('comment/'.$this->getID());
1399 break;
1402 return $body;
1405 public function getRemarkupBodyForFeed(PhabricatorFeedStory $story) {
1406 return null;
1409 public function getActionStrength() {
1410 if ($this->isInlineCommentTransaction()) {
1411 return 25;
1414 switch ($this->getTransactionType()) {
1415 case PhabricatorTransactions::TYPE_COMMENT:
1416 return 50;
1417 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1418 if ($this->isSelfSubscription()) {
1419 // Make this weaker than TYPE_COMMENT.
1420 return 25;
1423 // In other cases, subscriptions are more interesting than comments
1424 // (which are shown anyway) but less interesting than any other type of
1425 // transaction.
1426 return 75;
1427 case PhabricatorTransactions::TYPE_MFA:
1428 // We want MFA signatures to render at the top of transaction groups,
1429 // on top of the things they signed.
1430 return 1000;
1433 return 100;
1436 public function isCommentTransaction() {
1437 if ($this->hasComment()) {
1438 return true;
1441 switch ($this->getTransactionType()) {
1442 case PhabricatorTransactions::TYPE_COMMENT:
1443 return true;
1446 return false;
1449 public function isInlineCommentTransaction() {
1450 return false;
1453 public function getActionName() {
1454 switch ($this->getTransactionType()) {
1455 case PhabricatorTransactions::TYPE_COMMENT:
1456 return pht('Commented On');
1457 case PhabricatorTransactions::TYPE_VIEW_POLICY:
1458 case PhabricatorTransactions::TYPE_EDIT_POLICY:
1459 case PhabricatorTransactions::TYPE_JOIN_POLICY:
1460 case PhabricatorTransactions::TYPE_INTERACT_POLICY:
1461 return pht('Changed Policy');
1462 case PhabricatorTransactions::TYPE_SUBSCRIBERS:
1463 return pht('Changed Subscribers');
1464 default:
1465 return pht('Updated');
1469 public function getMailTags() {
1470 return array();
1473 public function hasChangeDetails() {
1474 switch ($this->getTransactionType()) {
1475 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1476 $field = $this->getTransactionCustomField();
1477 if ($field) {
1478 return $field->getApplicationTransactionHasChangeDetails($this);
1480 break;
1482 return false;
1485 public function hasChangeDetailsForMail() {
1486 return $this->hasChangeDetails();
1489 public function renderChangeDetailsForMail(PhabricatorUser $viewer) {
1490 $view = $this->renderChangeDetails($viewer);
1491 if ($view instanceof PhabricatorApplicationTransactionTextDiffDetailView) {
1492 return $view->renderForMail();
1494 return null;
1497 public function renderChangeDetails(PhabricatorUser $viewer) {
1498 switch ($this->getTransactionType()) {
1499 case PhabricatorTransactions::TYPE_CUSTOMFIELD:
1500 $field = $this->getTransactionCustomField();
1501 if ($field) {
1502 return $field->getApplicationTransactionChangeDetails($this, $viewer);
1504 break;
1507 return $this->renderTextCorpusChangeDetails(
1508 $viewer,
1509 $this->getOldValue(),
1510 $this->getNewValue());
1513 public function renderTextCorpusChangeDetails(
1514 PhabricatorUser $viewer,
1515 $old,
1516 $new) {
1517 return id(new PhabricatorApplicationTransactionTextDiffDetailView())
1518 ->setUser($viewer)
1519 ->setOldText($old)
1520 ->setNewText($new);
1523 public function attachTransactionGroup(array $group) {
1524 assert_instances_of($group, __CLASS__);
1525 $this->transactionGroup = $group;
1526 return $this;
1529 public function getTransactionGroup() {
1530 return $this->transactionGroup;
1534 * Should this transaction be visually grouped with an existing transaction
1535 * group?
1537 * @param list<PhabricatorApplicationTransaction> List of transactions.
1538 * @return bool True to display in a group with the other transactions.
1540 public function shouldDisplayGroupWith(array $group) {
1541 $this_source = null;
1542 if ($this->getContentSource()) {
1543 $this_source = $this->getContentSource()->getSource();
1546 $type_mfa = PhabricatorTransactions::TYPE_MFA;
1548 foreach ($group as $xaction) {
1549 // Don't group transactions by different authors.
1550 if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
1551 return false;
1554 // Don't group transactions for different objects.
1555 if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
1556 return false;
1559 // Don't group anything into a group which already has a comment.
1560 if ($xaction->isCommentTransaction()) {
1561 return false;
1564 // Don't group transactions from different content sources.
1565 $other_source = null;
1566 if ($xaction->getContentSource()) {
1567 $other_source = $xaction->getContentSource()->getSource();
1570 if ($other_source != $this_source) {
1571 return false;
1574 // Don't group transactions which happened more than 2 minutes apart.
1575 $apart = abs($xaction->getDateCreated() - $this->getDateCreated());
1576 if ($apart > (60 * 2)) {
1577 return false;
1580 // Don't group silent and nonsilent transactions together.
1581 $is_silent = $this->getIsSilentTransaction();
1582 if ($is_silent != $xaction->getIsSilentTransaction()) {
1583 return false;
1586 // Don't group MFA and non-MFA transactions together.
1587 $is_mfa = $this->getIsMFATransaction();
1588 if ($is_mfa != $xaction->getIsMFATransaction()) {
1589 return false;
1592 // Don't group two "Sign with MFA" transactions together.
1593 if ($this->getTransactionType() === $type_mfa) {
1594 if ($xaction->getTransactionType() === $type_mfa) {
1595 return false;
1599 // Don't group lock override and non-override transactions together.
1600 $is_override = $this->getIsLockOverrideTransaction();
1601 if ($is_override != $xaction->getIsLockOverrideTransaction()) {
1602 return false;
1606 return true;
1609 public function renderExtraInformationLink() {
1610 $herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
1612 if ($herald_xscript_id) {
1613 return phutil_tag(
1614 'a',
1615 array(
1616 'href' => '/herald/transcript/'.$herald_xscript_id.'/',
1618 pht('View Herald Transcript'));
1621 return null;
1624 public function renderAsTextForDoorkeeper(
1625 DoorkeeperFeedStoryPublisher $publisher,
1626 PhabricatorFeedStory $story,
1627 array $xactions) {
1629 $text = array();
1630 $body = array();
1632 foreach ($xactions as $xaction) {
1633 $xaction_body = $xaction->getBodyForMail();
1634 if ($xaction_body !== null) {
1635 $body[] = $xaction_body;
1638 if ($xaction->shouldHideForMail($xactions)) {
1639 continue;
1642 $old_target = $xaction->getRenderingTarget();
1643 $new_target = self::TARGET_TEXT;
1644 $xaction->setRenderingTarget($new_target);
1646 if ($publisher->getRenderWithImpliedContext()) {
1647 $text[] = $xaction->getTitle();
1648 } else {
1649 $text[] = $xaction->getTitleForFeed();
1652 $xaction->setRenderingTarget($old_target);
1655 $text = implode("\n", $text);
1656 $body = implode("\n\n", $body);
1658 return rtrim($text."\n\n".$body);
1662 * Test if this transaction is just a user subscribing or unsubscribing
1663 * themselves.
1665 private function isSelfSubscription() {
1666 $type = $this->getTransactionType();
1667 if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
1668 return false;
1671 $old = $this->getOldValue();
1672 $new = $this->getNewValue();
1674 $add = array_diff($old, $new);
1675 $rem = array_diff($new, $old);
1677 if ((count($add) + count($rem)) != 1) {
1678 // More than one user affected.
1679 return false;
1682 $affected_phid = head(array_merge($add, $rem));
1683 if ($affected_phid != $this->getAuthorPHID()) {
1684 // Affected user is someone else.
1685 return false;
1688 return true;
1691 private function isApplicationAuthor() {
1692 $author_phid = $this->getAuthorPHID();
1693 $author_type = phid_get_type($author_phid);
1694 $application_type = PhabricatorApplicationApplicationPHIDType::TYPECONST;
1695 return ($author_type == $application_type);
1699 private function getInterestingMoves(array $moves) {
1700 // Remove moves which only shift the position of a task within a column.
1701 foreach ($moves as $key => $move) {
1702 $from_phids = array_fuse($move['fromColumnPHIDs']);
1703 if (isset($from_phids[$move['columnPHID']])) {
1704 unset($moves[$key]);
1708 return $moves;
1711 private function getInterestingInlineStateChangeCounts() {
1712 // See PHI995. Newer inline state transactions have additional details
1713 // which we use to tailor the rendering behavior. These details are not
1714 // present on older transactions.
1715 $details = $this->getMetadataValue('inline.details', array());
1717 $new = $this->getNewValue();
1719 $done = 0;
1720 $undone = 0;
1721 foreach ($new as $phid => $state) {
1722 $is_done = ($state == PhabricatorInlineComment::STATE_DONE);
1724 // See PHI995. If you're marking your own inline comments as "Done",
1725 // don't count them when rendering a timeline story. In the case where
1726 // you're only affecting your own comments, this will hide the
1727 // "alice marked X comments as done" story entirely.
1729 // Usually, this happens when you pre-mark inlines as "done" and submit
1730 // them yourself. We'll still generate an "alice added inline comments"
1731 // story (in most cases/contexts), but the state change story is largely
1732 // just clutter and slightly confusing/misleading.
1734 $inline_details = idx($details, $phid, array());
1735 $inline_author_phid = idx($inline_details, 'authorPHID');
1736 if ($inline_author_phid) {
1737 if ($inline_author_phid == $this->getAuthorPHID()) {
1738 if ($is_done) {
1739 continue;
1744 if ($is_done) {
1745 $done++;
1746 } else {
1747 $undone++;
1751 return array($done, $undone);
1754 public function newGlobalSortVector() {
1755 return id(new PhutilSortVector())
1756 ->addInt(-$this->getDateCreated())
1757 ->addString($this->getPHID());
1760 public function newActionStrengthSortVector() {
1761 return id(new PhutilSortVector())
1762 ->addInt(-$this->getActionStrength());
1766 /* -( PhabricatorPolicyInterface Implementation )-------------------------- */
1769 public function getCapabilities() {
1770 return array(
1771 PhabricatorPolicyCapability::CAN_VIEW,
1772 PhabricatorPolicyCapability::CAN_EDIT,
1776 public function getPolicy($capability) {
1777 switch ($capability) {
1778 case PhabricatorPolicyCapability::CAN_VIEW:
1779 return $this->getViewPolicy();
1780 case PhabricatorPolicyCapability::CAN_EDIT:
1781 return $this->getEditPolicy();
1785 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
1786 return ($viewer->getPHID() == $this->getAuthorPHID());
1789 public function describeAutomaticCapability($capability) {
1790 return pht(
1791 'Transactions are visible to users that can see the object which was '.
1792 'acted upon. Some transactions - in particular, comments - are '.
1793 'editable by the transaction author.');
1796 public function getModularType() {
1797 return null;
1800 public function setForceNotifyPHIDs(array $phids) {
1801 $this->setMetadataValue('notify.force', $phids);
1802 return $this;
1805 public function getForceNotifyPHIDs() {
1806 return $this->getMetadataValue('notify.force', array());
1810 /* -( PhabricatorDestructibleInterface )----------------------------------- */
1813 public function destroyObjectPermanently(
1814 PhabricatorDestructionEngine $engine) {
1816 $this->openTransaction();
1817 $comment_template = $this->getApplicationTransactionCommentObject();
1819 if ($comment_template) {
1820 $comments = $comment_template->loadAllWhere(
1821 'transactionPHID = %s',
1822 $this->getPHID());
1823 foreach ($comments as $comment) {
1824 $engine->destroyObject($comment);
1828 $this->delete();
1829 $this->saveTransaction();