3 abstract class PhabricatorApplicationTransaction
4 extends PhabricatorLiskDAO
6 PhabricatorPolicyInterface
,
7 PhabricatorDestructibleInterface
{
9 const TARGET_TEXT
= 'text';
10 const TARGET_HTML
= 'html';
13 protected $objectPHID;
14 protected $authorPHID;
15 protected $viewPolicy;
16 protected $editPolicy;
18 protected $commentPHID;
19 protected $commentVersion = 0;
20 protected $transactionType;
23 protected $metadata = array();
25 protected $contentSource;
28 private $commentNotLoaded;
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;
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;
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
:
65 abstract public function getApplicationTransactionType();
67 private function getApplicationObjectTypeName() {
68 $types = PhabricatorPHIDType
::getAllTypes();
70 $type = idx($types, $this->getApplicationTransactionType());
72 return $type->getTypeName();
78 public function getApplicationTransactionCommentObject() {
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;
91 public function generatePHID() {
92 $type = PhabricatorApplicationTransactionTransactionPHIDType
::TYPECONST
;
93 $subtype = $this->getApplicationTransactionType();
95 return PhabricatorPHID
::generateNewPHID($type, $subtype);
98 protected function getConfiguration() {
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();
125 public function getContentSource() {
126 return PhabricatorContentSource
::newFromSerialized($this->contentSource
);
129 public function hasComment() {
130 $comment = $this->getComment();
135 if ($comment->isEmptyComment()) {
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;
204 public function setCommentNotLoaded($not_loaded) {
205 $this->commentNotLoaded
= $not_loaded;
209 public function attachObject($object) {
210 $this->object = $object;
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
226 foreach ($this->getRemarkupBlocks() as $block) {
227 $changes[] = $this->newRemarkupChange()
229 ->setNewValue($block);
232 $comment = $this->getComment();
234 if ($comment->hasOldComment()) {
235 $old_value = $comment->getOldComment()->getContent();
240 $new_value = $comment->getContent();
242 $changes[] = $this->newRemarkupChange()
243 ->setOldValue($old_value)
244 ->setNewValue($new_value);
250 protected function newRemarkupChanges() {
254 protected function newRemarkupChange() {
255 return id(new PhabricatorTransactionRemarkupChange())
256 ->setTransaction($this);
262 public function getRemarkupBlocks() {
265 switch ($this->getTransactionType()) {
266 case PhabricatorTransactions
::TYPE_CUSTOMFIELD
:
267 $field = $this->getTransactionCustomField();
269 $custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
271 foreach ($custom_blocks as $custom_block) {
272 $blocks[] = $custom_block;
281 public function setOldValue($value) {
282 $this->oldValueHasBeenSet
= true;
283 $this->writeField('oldValue', $value);
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;
304 public function getRenderingTarget() {
305 return $this->renderingTarget
;
308 public function attachViewer(PhabricatorUser
$viewer) {
309 $this->viewer
= $viewer;
313 public function getViewer() {
314 return $this->assertAttached($this->viewer
);
317 public function getRequiredHandlePHIDs() {
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();
329 $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
333 case PhabricatorTransactions
::TYPE_SUBSCRIBERS
:
337 case PhabricatorTransactions
::TYPE_EDGE
:
338 $record = PhabricatorEdgeChangeRecord
::newFromTransaction($this);
339 $phids[] = $record->getChangedPHIDs();
341 case PhabricatorTransactions
::TYPE_COLUMNS
:
342 foreach ($new as $move) {
347 $phids[] = $move['fromColumnPHIDs'];
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);
361 case PhabricatorTransactions
::TYPE_SPACE
:
363 $phids[] = array($old);
366 $phids[] = array($new);
369 case PhabricatorTransactions
::TYPE_TOKEN
:
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;
385 public function getHandle($phid) {
386 if (empty($this->handles
[$phid])) {
389 'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
392 $this->getTransactionType(),
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) {
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();
414 return $this->getHandle($phid)->getLinkName();
418 public function renderHandleList(array $phids) {
420 foreach ($phids as $phid) {
421 $links[] = $this->renderHandleLink($phid);
423 if ($this->renderingTarget
== self
::TARGET_HTML
) {
424 return phutil_implode_html(', ', $links);
426 return implode(', ', $links);
430 private function renderSubscriberList(array $phids, $change_type) {
431 if ($this->getRenderingTarget() == self
::TARGET_TEXT
) {
432 return $this->renderHandleList($phids);
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(
445 $this->getHandleIfExists($phid));
447 $ref = $policy->newRef($this->getViewer());
449 if ($this->renderingTarget
== self
::TARGET_HTML
) {
450 $output = $ref->newTransactionLink($state, $this);
452 $output = $ref->getPolicyDisplayName();
458 public function getIcon() {
459 switch ($this->getTransactionType()) {
460 case PhabricatorTransactions
::TYPE_COMMENT
:
461 $comment = $this->getComment();
462 if ($comment && $comment->getIsRemoved()) {
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);
474 return 'fa-user-plus';
476 return 'fa-user-times';
480 case PhabricatorTransactions
::TYPE_VIEW_POLICY
:
481 case PhabricatorTransactions
::TYPE_EDIT_POLICY
:
482 case PhabricatorTransactions
::TYPE_JOIN_POLICY
:
483 case PhabricatorTransactions
::TYPE_INTERACT_POLICY
:
485 case PhabricatorTransactions
::TYPE_EDGE
:
486 switch ($this->getMetadataValue('edge:type')) {
487 case DiffusionCommitRevertedByCommitEdgeType
::EDGECONST
:
489 case DiffusionCommitRevertsCommitEdgeType
::EDGECONST
:
490 return 'fa-ambulance';
493 case PhabricatorTransactions
::TYPE_TOKEN
:
495 case PhabricatorTransactions
::TYPE_SPACE
:
496 return 'fa-th-large';
497 case PhabricatorTransactions
::TYPE_COLUMNS
:
499 case PhabricatorTransactions
::TYPE_MFA
:
506 public function getToken() {
507 switch ($this->getTransactionType()) {
508 case PhabricatorTransactions
::TYPE_TOKEN
:
509 $old = $this->getOldValue();
510 $new = $this->getNewValue();
512 $icon = substr($new, 10);
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()) {
530 case PhabricatorTransactions
::TYPE_EDGE
:
531 switch ($this->getMetadataValue('edge:type')) {
532 case DiffusionCommitRevertedByCommitEdgeType
::EDGECONST
:
534 case DiffusionCommitRevertsCommitEdgeType
::EDGECONST
:
538 case PhabricatorTransactions
::TYPE_MFA
;
544 protected function getTransactionCustomField() {
545 switch ($this->getTransactionType()) {
546 case PhabricatorTransactions
::TYPE_CUSTOMFIELD
:
547 $key = $this->getMetadataValue('customfield:key');
552 $object = $this->getObject();
554 if (!($object instanceof PhabricatorCustomFieldInterface
)) {
558 $field = PhabricatorCustomField
::getObjectField(
560 PhabricatorCustomField
::ROLE_APPLICATIONTRANSACTIONS
,
566 $field->setViewer($this->getViewer());
573 public function shouldHide() {
574 // Never hide comments.
575 if ($this->hasComment()) {
579 $xaction_type = $this->getTransactionType();
581 // Always hide requests for object history.
582 if ($xaction_type === PhabricatorTransactions
::TYPE_HISTORY
) {
586 // Always hide file attach/detach transactions.
587 if ($xaction_type === PhabricatorTransactions
::TYPE_FILE
) {
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
:
603 case PhabricatorTransactions
::TYPE_SUBTYPE
:
606 $old = $this->getOldValue();
608 if (is_array($old) && !$old) {
612 if (!is_array($old)) {
613 if ($old === '' ||
$old === null) {
617 // The integer 0 is also uninteresting by default; this is often
618 // an "off" flag for something like "All Day Event".
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
635 if ($this->getIsCreateTransaction()) {
636 if ($this->getIsDefaultTransaction()) {
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()) {
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) {
665 case PhabricatorTransactions
::TYPE_CUSTOMFIELD
:
666 $field = $this->getTransactionCustomField();
668 return $field->shouldHideInApplicationTransactions($this);
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
:
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()) {
697 case PhabricatorTransactions
::TYPE_INLINESTATE
:
698 list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
700 if (!$done && !$undone) {
711 public function shouldHideForMail(array $xactions) {
712 if ($this->isSelfSubscription()) {
716 switch ($this->getTransactionType()) {
717 case PhabricatorTransactions
::TYPE_TOKEN
:
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
:
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.
745 if ($this->isInlineCommentTransaction()) {
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;
756 // We found a normal comment, so hide this inline transaction.
757 if ($xaction->hasComment()) {
762 // If there are several inline comments, only publish the first one.
763 if ($this !== head($inlines)) {
768 return $this->shouldHide();
771 public function shouldHideForFeed() {
772 if ($this->isSelfSubscription()) {
776 switch ($this->getTransactionType()) {
777 case PhabricatorTransactions
::TYPE_TOKEN
:
778 case PhabricatorTransactions
::TYPE_MFA
:
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()) {
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
:
803 case PhabricatorTransactions
::TYPE_INLINESTATE
:
807 return $this->shouldHide();
810 public function shouldHideForNotifications() {
811 return $this->shouldHideForFeed();
814 private function getTitleForMailWithRenderingTarget($new_target) {
815 $old_target = $this->getRenderingTarget();
817 $this->setRenderingTarget($new_target);
818 $result = $this->getTitleForMail();
819 } catch (Exception
$ex) {
820 $this->setRenderingTarget($old_target);
823 $this->setRenderingTarget($old_target);
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) {
845 if ($this->hasChangeDetails()) {
846 $details_uri = $this->getChangeDetailsURI();
847 $details_uri = PhabricatorEnv
::getProductionURI($details_uri);
849 $show_details = phutil_tag(
852 'href' => $details_uri,
854 pht('(Show Details)'));
856 $title = array($title, ' ', $show_details);
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.
874 $comment = $this->getComment();
875 if ($comment && strlen($comment->getContent())) {
876 return $comment->getContent();
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
:
889 'This %s already has that view policy.',
890 $this->getApplicationObjectTypeName());
891 case PhabricatorTransactions
::TYPE_EDIT_POLICY
:
893 'This %s already has that edit policy.',
894 $this->getApplicationObjectTypeName());
895 case PhabricatorTransactions
::TYPE_JOIN_POLICY
:
897 'This %s already has that join policy.',
898 $this->getApplicationObjectTypeName());
899 case PhabricatorTransactions
::TYPE_INTERACT_POLICY
:
901 'This %s already has that interact policy.',
902 $this->getApplicationObjectTypeName());
903 case PhabricatorTransactions
::TYPE_SUBSCRIBERS
:
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
:
913 'You have not moved this object to any columns it is not '.
915 case PhabricatorTransactions
::TYPE_MFA
:
917 'You can not sign a transaction group that has no other '.
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
:
935 '%s created this object.',
936 $this->renderHandleLink($author_phid));
937 case PhabricatorTransactions
::TYPE_COMMENT
:
939 '%s added a comment.',
940 $this->renderHandleLink($author_phid));
941 case PhabricatorTransactions
::TYPE_VIEW_POLICY
:
942 if ($this->getIsCreateTransaction()) {
944 '%s created this object with visibility "%s".',
945 $this->renderHandleLink($author_phid),
946 $this->renderPolicyName($new, 'new'));
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()) {
957 '%s created this object with edit policy "%s".',
958 $this->renderHandleLink($author_phid),
959 $this->renderPolicyName($new, 'new'));
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()) {
970 '%s created this object with join policy "%s".',
971 $this->renderHandleLink($author_phid),
972 $this->renderPolicyName($new, 'new'));
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()) {
983 '%s created this object with interact policy "%s".',
984 $this->renderHandleLink($author_phid),
985 $this->renderPolicyName($new, 'new'));
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()) {
996 '%s created this object in space %s.',
997 $this->renderHandleLink($author_phid),
998 $this->renderHandleLink($new));
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);
1012 '%s edited subscriber(s), added %d: %s; removed %d: %s.',
1013 $this->renderHandleLink($author_phid),
1015 $this->renderSubscriberList($add, 'add'),
1017 $this->renderSubscriberList($rem, 'rem'));
1020 '%s added %d subscriber(s): %s.',
1021 $this->renderHandleLink($author_phid),
1023 $this->renderSubscriberList($add, 'add'));
1026 '%s removed %d subscriber(s): %s.',
1027 $this->renderHandleLink($author_phid),
1029 $this->renderSubscriberList($rem, 'rem'));
1031 // This is used when rendering previews, before the user actually
1034 '%s updated subscribers...',
1035 $this->renderHandleLink($author_phid));
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);
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.
1052 '%s edited an edge.',
1053 $this->renderHandleLink($author_phid));
1057 return $type_obj->getTransactionEditString(
1058 $this->renderHandleLink($author_phid),
1059 new PhutilNumber(count($add) +
count($rem)),
1061 $this->renderHandleList($add),
1063 $this->renderHandleList($rem));
1065 return $type_obj->getTransactionAddString(
1066 $this->renderHandleLink($author_phid),
1068 $this->renderHandleList($add));
1070 return $type_obj->getTransactionRemoveString(
1071 $this->renderHandleLink($author_phid),
1073 $this->renderHandleList($rem));
1075 return $type_obj->getTransactionPreviewString(
1076 $this->renderHandleLink($author_phid));
1079 case PhabricatorTransactions
::TYPE_CUSTOMFIELD
:
1080 $field = $this->getTransactionCustomField();
1082 return $field->getApplicationTransactionTitle($this);
1084 $developer_mode = 'phabricator.developer-mode';
1085 $is_developer = PhabricatorEnv
::getEnvConfig($developer_mode);
1086 if ($is_developer) {
1088 '%s edited a custom field (with key "%s").',
1089 $this->renderHandleLink($author_phid),
1090 $this->getMetadata('customfield:key'));
1093 '%s edited a custom field.',
1094 $this->renderHandleLink($author_phid));
1098 case PhabricatorTransactions
::TYPE_TOKEN
:
1101 '%s updated a token.',
1102 $this->renderHandleLink($author_phid));
1105 '%s rescinded a token.',
1106 $this->renderHandleLink($author_phid));
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) {
1117 '%s marked %s inline comment(s) as done and %s inline comment(s) '.
1119 $this->renderHandleLink($author_phid),
1120 new PhutilNumber($done),
1121 new PhutilNumber($undone));
1124 '%s marked %s inline comment(s) as done.',
1125 $this->renderHandleLink($author_phid),
1126 new PhutilNumber($done));
1129 '%s marked %s inline comment(s) as not done.',
1130 $this->renderHandleLink($author_phid),
1131 new PhutilNumber($undone));
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) {
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));
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));
1157 $fragments = array();
1158 foreach ($moves as $move) {
1159 $to_column = $move['columnPHID'];
1160 $board_phid = $move['boardPHID'];
1163 $this->renderHandleLink($board_phid),
1164 $this->renderHandleLink($to_column));
1168 '%s moved this task on %s board(s): %s.',
1169 $this->renderHandleLink($author_phid),
1170 phutil_count($moves),
1171 phutil_implode_html(', ', $fragments));
1176 case PhabricatorTransactions
::TYPE_MFA
:
1178 '%s signed these changes with MFA.',
1179 $this->renderHandleLink($author_phid));
1182 // In developer mode, provide a better hint here about which string
1184 $developer_mode = 'phabricator.developer-mode';
1185 $is_developer = PhabricatorEnv
::getEnvConfig($developer_mode);
1186 if ($is_developer) {
1188 '%s edited this object (transaction type "%s").',
1189 $this->renderHandleLink($author_phid),
1190 $this->getTransactionType());
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
:
1211 $this->renderHandleLink($author_phid),
1212 $this->renderHandleLink($object_phid));
1213 case PhabricatorTransactions
::TYPE_COMMENT
:
1215 '%s added a comment to %s.',
1216 $this->renderHandleLink($author_phid),
1217 $this->renderHandleLink($object_phid));
1218 case PhabricatorTransactions
::TYPE_VIEW_POLICY
:
1220 '%s changed the visibility for %s.',
1221 $this->renderHandleLink($author_phid),
1222 $this->renderHandleLink($object_phid));
1223 case PhabricatorTransactions
::TYPE_EDIT_POLICY
:
1225 '%s changed the edit policy for %s.',
1226 $this->renderHandleLink($author_phid),
1227 $this->renderHandleLink($object_phid));
1228 case PhabricatorTransactions
::TYPE_JOIN_POLICY
:
1230 '%s changed the join policy for %s.',
1231 $this->renderHandleLink($author_phid),
1232 $this->renderHandleLink($object_phid));
1233 case PhabricatorTransactions
::TYPE_INTERACT_POLICY
:
1235 '%s changed the interact policy for %s.',
1236 $this->renderHandleLink($author_phid),
1237 $this->renderHandleLink($object_phid));
1238 case PhabricatorTransactions
::TYPE_SUBSCRIBERS
:
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()) {
1246 '%s created %s in the %s space.',
1247 $this->renderHandleLink($author_phid),
1248 $this->renderHandleLink($object_phid),
1249 $this->renderHandleLink($new));
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);
1269 return $type_obj->getFeedEditString(
1270 $this->renderHandleLink($author_phid),
1271 $this->renderHandleLink($object_phid),
1272 new PhutilNumber(count($add) +
count($rem)),
1274 $this->renderHandleList($add),
1276 $this->renderHandleList($rem));
1278 return $type_obj->getFeedAddString(
1279 $this->renderHandleLink($author_phid),
1280 $this->renderHandleLink($object_phid),
1282 $this->renderHandleList($add));
1284 return $type_obj->getFeedRemoveString(
1285 $this->renderHandleLink($author_phid),
1286 $this->renderHandleLink($object_phid),
1288 $this->renderHandleList($rem));
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();
1299 return $field->getApplicationTransactionTitleForFeed($this);
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) {
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));
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));
1331 $fragments = array();
1332 foreach ($moves as $move) {
1335 $this->renderHandleLink($board_phid),
1336 $this->renderHandleLink($to_column));
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));
1348 case PhabricatorTransactions
::TYPE_MFA
:
1353 return $this->getTitle();
1356 public function getMarkupFieldsForFeed(PhabricatorFeedStory
$story) {
1359 switch ($this->getTransactionType()) {
1360 case PhabricatorTransactions
::TYPE_COMMENT
:
1361 $text = $this->getComment()->getContent();
1362 if (strlen($text)) {
1363 $fields[] = 'comment/'.$this->getID();
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);
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();
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());
1405 public function getRemarkupBodyForFeed(PhabricatorFeedStory
$story) {
1409 public function getActionStrength() {
1410 if ($this->isInlineCommentTransaction()) {
1414 switch ($this->getTransactionType()) {
1415 case PhabricatorTransactions
::TYPE_COMMENT
:
1417 case PhabricatorTransactions
::TYPE_SUBSCRIBERS
:
1418 if ($this->isSelfSubscription()) {
1419 // Make this weaker than TYPE_COMMENT.
1423 // In other cases, subscriptions are more interesting than comments
1424 // (which are shown anyway) but less interesting than any other type of
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.
1436 public function isCommentTransaction() {
1437 if ($this->hasComment()) {
1441 switch ($this->getTransactionType()) {
1442 case PhabricatorTransactions
::TYPE_COMMENT
:
1449 public function isInlineCommentTransaction() {
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');
1465 return pht('Updated');
1469 public function getMailTags() {
1473 public function hasChangeDetails() {
1474 switch ($this->getTransactionType()) {
1475 case PhabricatorTransactions
::TYPE_CUSTOMFIELD
:
1476 $field = $this->getTransactionCustomField();
1478 return $field->getApplicationTransactionHasChangeDetails($this);
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();
1497 public function renderChangeDetails(PhabricatorUser
$viewer) {
1498 switch ($this->getTransactionType()) {
1499 case PhabricatorTransactions
::TYPE_CUSTOMFIELD
:
1500 $field = $this->getTransactionCustomField();
1502 return $field->getApplicationTransactionChangeDetails($this, $viewer);
1507 return $this->renderTextCorpusChangeDetails(
1509 $this->getOldValue(),
1510 $this->getNewValue());
1513 public function renderTextCorpusChangeDetails(
1514 PhabricatorUser
$viewer,
1517 return id(new PhabricatorApplicationTransactionTextDiffDetailView())
1523 public function attachTransactionGroup(array $group) {
1524 assert_instances_of($group, __CLASS__
);
1525 $this->transactionGroup
= $group;
1529 public function getTransactionGroup() {
1530 return $this->transactionGroup
;
1534 * Should this transaction be visually grouped with an existing transaction
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()) {
1554 // Don't group transactions for different objects.
1555 if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
1559 // Don't group anything into a group which already has a comment.
1560 if ($xaction->isCommentTransaction()) {
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) {
1574 // Don't group transactions which happened more than 2 minutes apart.
1575 $apart = abs($xaction->getDateCreated() - $this->getDateCreated());
1576 if ($apart > (60 * 2)) {
1580 // Don't group silent and nonsilent transactions together.
1581 $is_silent = $this->getIsSilentTransaction();
1582 if ($is_silent != $xaction->getIsSilentTransaction()) {
1586 // Don't group MFA and non-MFA transactions together.
1587 $is_mfa = $this->getIsMFATransaction();
1588 if ($is_mfa != $xaction->getIsMFATransaction()) {
1592 // Don't group two "Sign with MFA" transactions together.
1593 if ($this->getTransactionType() === $type_mfa) {
1594 if ($xaction->getTransactionType() === $type_mfa) {
1599 // Don't group lock override and non-override transactions together.
1600 $is_override = $this->getIsLockOverrideTransaction();
1601 if ($is_override != $xaction->getIsLockOverrideTransaction()) {
1609 public function renderExtraInformationLink() {
1610 $herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
1612 if ($herald_xscript_id) {
1616 'href' => '/herald/transcript/'.$herald_xscript_id.'/',
1618 pht('View Herald Transcript'));
1624 public function renderAsTextForDoorkeeper(
1625 DoorkeeperFeedStoryPublisher
$publisher,
1626 PhabricatorFeedStory
$story,
1632 foreach ($xactions as $xaction) {
1633 $xaction_body = $xaction->getBodyForMail();
1634 if ($xaction_body !== null) {
1635 $body[] = $xaction_body;
1638 if ($xaction->shouldHideForMail($xactions)) {
1642 $old_target = $xaction->getRenderingTarget();
1643 $new_target = self
::TARGET_TEXT
;
1644 $xaction->setRenderingTarget($new_target);
1646 if ($publisher->getRenderWithImpliedContext()) {
1647 $text[] = $xaction->getTitle();
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
1665 private function isSelfSubscription() {
1666 $type = $this->getTransactionType();
1667 if ($type != PhabricatorTransactions
::TYPE_SUBSCRIBERS
) {
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.
1682 $affected_phid = head(array_merge($add, $rem));
1683 if ($affected_phid != $this->getAuthorPHID()) {
1684 // Affected user is someone else.
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]);
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();
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()) {
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() {
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) {
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() {
1800 public function setForceNotifyPHIDs(array $phids) {
1801 $this->setMetadataValue('notify.force', $phids);
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',
1823 foreach ($comments as $comment) {
1824 $engine->destroyObject($comment);
1829 $this->saveTransaction();