@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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