[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @task mail Sending Mail 5 * @task feed Publishing Feed Stories 6 * @task search Search Index 7 * @task files Integration with Files 8 */ 9 abstract class PhabricatorApplicationTransactionEditor 10 extends PhabricatorEditor { 11 12 private $contentSource; 13 private $object; 14 private $xactions; 15 16 private $isNewObject; 17 private $mentionedPHIDs; 18 private $continueOnNoEffect; 19 private $continueOnMissingFields; 20 private $parentMessageID; 21 private $heraldAdapter; 22 private $heraldTranscript; 23 private $subscribers; 24 private $unmentionablePHIDMap = array(); 25 26 private $isPreview; 27 private $isHeraldEditor; 28 private $isInverseEdgeEditor; 29 private $actingAsPHID; 30 private $disableEmail; 31 32 33 /** 34 * Get the class name for the application this editor is a part of. 35 * 36 * Uninstalling the application will disable the editor. 37 * 38 * @return string Editor's application class name. 39 */ 40 abstract public function getEditorApplicationClass(); 41 42 43 /** 44 * Get a description of the objects this editor edits, like "Differential 45 * Revisions". 46 * 47 * @return string Human readable description of edited objects. 48 */ 49 abstract public function getEditorObjectsDescription(); 50 51 52 public function setActingAsPHID($acting_as_phid) { 53 $this->actingAsPHID = $acting_as_phid; 54 return $this; 55 } 56 57 public function getActingAsPHID() { 58 if ($this->actingAsPHID) { 59 return $this->actingAsPHID; 60 } 61 return $this->getActor()->getPHID(); 62 } 63 64 65 /** 66 * When the editor tries to apply transactions that have no effect, should 67 * it raise an exception (default) or drop them and continue? 68 * 69 * Generally, you will set this flag for edits coming from "Edit" interfaces, 70 * and leave it cleared for edits coming from "Comment" interfaces, so the 71 * user will get a useful error if they try to submit a comment that does 72 * nothing (e.g., empty comment with a status change that has already been 73 * performed by another user). 74 * 75 * @param bool True to drop transactions without effect and continue. 76 * @return this 77 */ 78 public function setContinueOnNoEffect($continue) { 79 $this->continueOnNoEffect = $continue; 80 return $this; 81 } 82 83 public function getContinueOnNoEffect() { 84 return $this->continueOnNoEffect; 85 } 86 87 88 /** 89 * When the editor tries to apply transactions which don't populate all of 90 * an object's required fields, should it raise an exception (default) or 91 * drop them and continue? 92 * 93 * For example, if a user adds a new required custom field (like "Severity") 94 * to a task, all existing tasks won't have it populated. When users 95 * manually edit existing tasks, it's usually desirable to have them provide 96 * a severity. However, other operations (like batch editing just the 97 * owner of a task) will fail by default. 98 * 99 * By setting this flag for edit operations which apply to specific fields 100 * (like the priority, batch, and merge editors in Maniphest), these 101 * operations can continue to function even if an object is outdated. 102 * 103 * @param bool True to continue when transactions don't completely satisfy 104 * all required fields. 105 * @return this 106 */ 107 public function setContinueOnMissingFields($continue_on_missing_fields) { 108 $this->continueOnMissingFields = $continue_on_missing_fields; 109 return $this; 110 } 111 112 public function getContinueOnMissingFields() { 113 return $this->continueOnMissingFields; 114 } 115 116 117 /** 118 * Not strictly necessary, but reply handlers ideally set this value to 119 * make email threading work better. 120 */ 121 public function setParentMessageID($parent_message_id) { 122 $this->parentMessageID = $parent_message_id; 123 return $this; 124 } 125 public function getParentMessageID() { 126 return $this->parentMessageID; 127 } 128 129 public function getIsNewObject() { 130 return $this->isNewObject; 131 } 132 133 protected function getMentionedPHIDs() { 134 return $this->mentionedPHIDs; 135 } 136 137 public function setIsPreview($is_preview) { 138 $this->isPreview = $is_preview; 139 return $this; 140 } 141 142 public function getIsPreview() { 143 return $this->isPreview; 144 } 145 146 public function setIsInverseEdgeEditor($is_inverse_edge_editor) { 147 $this->isInverseEdgeEditor = $is_inverse_edge_editor; 148 return $this; 149 } 150 151 public function getIsInverseEdgeEditor() { 152 return $this->isInverseEdgeEditor; 153 } 154 155 public function setIsHeraldEditor($is_herald_editor) { 156 $this->isHeraldEditor = $is_herald_editor; 157 return $this; 158 } 159 160 public function getIsHeraldEditor() { 161 return $this->isHeraldEditor; 162 } 163 164 /** 165 * Prevent this editor from generating email when applying transactions. 166 * 167 * @param bool True to disable email. 168 * @return this 169 */ 170 public function setDisableEmail($disable_email) { 171 $this->disableEmail = $disable_email; 172 return $this; 173 } 174 175 public function getDisableEmail() { 176 return $this->disableEmail; 177 } 178 179 public function setUnmentionablePHIDMap(array $map) { 180 $this->unmentionablePHIDMap = $map; 181 return $this; 182 } 183 184 public function getUnmentionablePHIDMap() { 185 return $this->unmentionablePHIDMap; 186 } 187 188 public function getTransactionTypes() { 189 $types = array(); 190 191 if ($this->object instanceof PhabricatorSubscribableInterface) { 192 $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS; 193 } 194 195 if ($this->object instanceof PhabricatorCustomFieldInterface) { 196 $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD; 197 } 198 199 if ($this->object instanceof HarbormasterBuildableInterface) { 200 $types[] = PhabricatorTransactions::TYPE_BUILDABLE; 201 } 202 203 if ($this->object instanceof PhabricatorTokenReceiverInterface) { 204 $types[] = PhabricatorTransactions::TYPE_TOKEN; 205 } 206 207 if ($this->object instanceof PhabricatorProjectInterface) { 208 $types[] = PhabricatorTransactions::TYPE_EDGE; 209 } 210 211 return $types; 212 } 213 214 private function adjustTransactionValues( 215 PhabricatorLiskDAO $object, 216 PhabricatorApplicationTransaction $xaction) { 217 218 if ($xaction->shouldGenerateOldValue()) { 219 $old = $this->getTransactionOldValue($object, $xaction); 220 $xaction->setOldValue($old); 221 } 222 223 $new = $this->getTransactionNewValue($object, $xaction); 224 $xaction->setNewValue($new); 225 } 226 227 private function getTransactionOldValue( 228 PhabricatorLiskDAO $object, 229 PhabricatorApplicationTransaction $xaction) { 230 switch ($xaction->getTransactionType()) { 231 case PhabricatorTransactions::TYPE_SUBSCRIBERS: 232 return array_values($this->subscribers); 233 case PhabricatorTransactions::TYPE_VIEW_POLICY: 234 return $object->getViewPolicy(); 235 case PhabricatorTransactions::TYPE_EDIT_POLICY: 236 return $object->getEditPolicy(); 237 case PhabricatorTransactions::TYPE_JOIN_POLICY: 238 return $object->getJoinPolicy(); 239 case PhabricatorTransactions::TYPE_EDGE: 240 $edge_type = $xaction->getMetadataValue('edge:type'); 241 if (!$edge_type) { 242 throw new Exception("Edge transaction has no 'edge:type'!"); 243 } 244 245 $old_edges = array(); 246 if ($object->getPHID()) { 247 $edge_src = $object->getPHID(); 248 249 $old_edges = id(new PhabricatorEdgeQuery()) 250 ->withSourcePHIDs(array($edge_src)) 251 ->withEdgeTypes(array($edge_type)) 252 ->needEdgeData(true) 253 ->execute(); 254 255 $old_edges = $old_edges[$edge_src][$edge_type]; 256 } 257 return $old_edges; 258 case PhabricatorTransactions::TYPE_CUSTOMFIELD: 259 // NOTE: Custom fields have their old value pre-populated when they are 260 // built by PhabricatorCustomFieldList. 261 return $xaction->getOldValue(); 262 case PhabricatorTransactions::TYPE_COMMENT: 263 return null; 264 default: 265 return $this->getCustomTransactionOldValue($object, $xaction); 266 } 267 } 268 269 private function getTransactionNewValue( 270 PhabricatorLiskDAO $object, 271 PhabricatorApplicationTransaction $xaction) { 272 switch ($xaction->getTransactionType()) { 273 case PhabricatorTransactions::TYPE_SUBSCRIBERS: 274 return $this->getPHIDTransactionNewValue($xaction); 275 case PhabricatorTransactions::TYPE_VIEW_POLICY: 276 case PhabricatorTransactions::TYPE_EDIT_POLICY: 277 case PhabricatorTransactions::TYPE_JOIN_POLICY: 278 case PhabricatorTransactions::TYPE_BUILDABLE: 279 case PhabricatorTransactions::TYPE_TOKEN: 280 return $xaction->getNewValue(); 281 case PhabricatorTransactions::TYPE_EDGE: 282 return $this->getEdgeTransactionNewValue($xaction); 283 case PhabricatorTransactions::TYPE_CUSTOMFIELD: 284 $field = $this->getCustomFieldForTransaction($object, $xaction); 285 return $field->getNewValueFromApplicationTransactions($xaction); 286 case PhabricatorTransactions::TYPE_COMMENT: 287 return null; 288 default: 289 return $this->getCustomTransactionNewValue($object, $xaction); 290 } 291 } 292 293 protected function getCustomTransactionOldValue( 294 PhabricatorLiskDAO $object, 295 PhabricatorApplicationTransaction $xaction) { 296 throw new Exception('Capability not supported!'); 297 } 298 299 protected function getCustomTransactionNewValue( 300 PhabricatorLiskDAO $object, 301 PhabricatorApplicationTransaction $xaction) { 302 throw new Exception('Capability not supported!'); 303 } 304 305 protected function transactionHasEffect( 306 PhabricatorLiskDAO $object, 307 PhabricatorApplicationTransaction $xaction) { 308 309 switch ($xaction->getTransactionType()) { 310 case PhabricatorTransactions::TYPE_COMMENT: 311 return $xaction->hasComment(); 312 case PhabricatorTransactions::TYPE_CUSTOMFIELD: 313 $field = $this->getCustomFieldForTransaction($object, $xaction); 314 return $field->getApplicationTransactionHasEffect($xaction); 315 case PhabricatorTransactions::TYPE_EDGE: 316 // A straight value comparison here doesn't always get the right 317 // result, because newly added edges aren't fully populated. Instead, 318 // compare the changes in a more granular way. 319 $old = $xaction->getOldValue(); 320 $new = $xaction->getNewValue(); 321 322 $old_dst = array_keys($old); 323 $new_dst = array_keys($new); 324 325 // NOTE: For now, we don't consider edge reordering to be a change. 326 // We have very few order-dependent edges and effectively no order 327 // oriented UI. This might change in the future. 328 sort($old_dst); 329 sort($new_dst); 330 331 if ($old_dst !== $new_dst) { 332 // We've added or removed edges, so this transaction definitely 333 // has an effect. 334 return true; 335 } 336 337 // We haven't added or removed edges, but we might have changed 338 // edge data. 339 foreach ($old as $key => $old_value) { 340 $new_value = $new[$key]; 341 if ($old_value['data'] !== $new_value['data']) { 342 return true; 343 } 344 } 345 346 return false; 347 } 348 349 return ($xaction->getOldValue() !== $xaction->getNewValue()); 350 } 351 352 protected function shouldApplyInitialEffects( 353 PhabricatorLiskDAO $object, 354 array $xactions) { 355 return false; 356 } 357 358 protected function applyInitialEffects( 359 PhabricatorLiskDAO $object, 360 array $xactions) { 361 throw new PhutilMethodNotImplementedException(); 362 } 363 364 private function applyInternalEffects( 365 PhabricatorLiskDAO $object, 366 PhabricatorApplicationTransaction $xaction) { 367 368 switch ($xaction->getTransactionType()) { 369 case PhabricatorTransactions::TYPE_BUILDABLE: 370 case PhabricatorTransactions::TYPE_TOKEN: 371 return; 372 case PhabricatorTransactions::TYPE_VIEW_POLICY: 373 $object->setViewPolicy($xaction->getNewValue()); 374 break; 375 case PhabricatorTransactions::TYPE_EDIT_POLICY: 376 $object->setEditPolicy($xaction->getNewValue()); 377 break; 378 case PhabricatorTransactions::TYPE_CUSTOMFIELD: 379 $field = $this->getCustomFieldForTransaction($object, $xaction); 380 return $field->applyApplicationTransactionInternalEffects($xaction); 381 } 382 383 return $this->applyCustomInternalTransaction($object, $xaction); 384 } 385 386 private function applyExternalEffects( 387 PhabricatorLiskDAO $object, 388 PhabricatorApplicationTransaction $xaction) { 389 switch ($xaction->getTransactionType()) { 390 case PhabricatorTransactions::TYPE_BUILDABLE: 391 case PhabricatorTransactions::TYPE_TOKEN: 392 return; 393 case PhabricatorTransactions::TYPE_SUBSCRIBERS: 394 $subeditor = id(new PhabricatorSubscriptionsEditor()) 395 ->setObject($object) 396 ->setActor($this->requireActor()); 397 398 $old_map = array_fuse($xaction->getOldValue()); 399 $new_map = array_fuse($xaction->getNewValue()); 400 401 $subeditor->unsubscribe( 402 array_keys( 403 array_diff_key($old_map, $new_map))); 404 405 $subeditor->subscribeExplicit( 406 array_keys( 407 array_diff_key($new_map, $old_map))); 408 409 $subeditor->save(); 410 411 // for the rest of these edits, subscribers should include those just 412 // added as well as those just removed. 413 $subscribers = array_unique(array_merge( 414 $this->subscribers, 415 $xaction->getOldValue(), 416 $xaction->getNewValue())); 417 $this->subscribers = $subscribers; 418 419 break; 420 case PhabricatorTransactions::TYPE_EDGE: 421 if ($this->getIsInverseEdgeEditor()) { 422 // If we're writing an inverse edge transaction, don't actually 423 // do anything. The initiating editor on the other side of the 424 // transaction will take care of the edge writes. 425 break; 426 } 427 428 $old = $xaction->getOldValue(); 429 $new = $xaction->getNewValue(); 430 $src = $object->getPHID(); 431 $const = $xaction->getMetadataValue('edge:type'); 432 433 $type = PhabricatorEdgeType::getByConstant($const); 434 if ($type->shouldWriteInverseTransactions()) { 435 $this->applyInverseEdgeTransactions( 436 $object, 437 $xaction, 438 $type->getInverseEdgeConstant()); 439 } 440 441 foreach ($new as $dst_phid => $edge) { 442 $new[$dst_phid]['src'] = $src; 443 } 444 445 $editor = new PhabricatorEdgeEditor(); 446 447 foreach ($old as $dst_phid => $edge) { 448 if (!empty($new[$dst_phid])) { 449 if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { 450 continue; 451 } 452 } 453 $editor->removeEdge($src, $const, $dst_phid); 454 } 455 456 foreach ($new as $dst_phid => $edge) { 457 if (!empty($old[$dst_phid])) { 458 if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) { 459 continue; 460 } 461 } 462 463 $data = array( 464 'data' => $edge['data'], 465 ); 466 467 $editor->addEdge($src, $const, $dst_phid, $data); 468 } 469 470 $editor->save(); 471 break; 472 case PhabricatorTransactions::TYPE_CUSTOMFIELD: 473 $field = $this->getCustomFieldForTransaction($object, $xaction); 474 return $field->applyApplicationTransactionExternalEffects($xaction); 475 } 476 477 return $this->applyCustomExternalTransaction($object, $xaction); 478 } 479 480 protected function applyCustomInternalTransaction( 481 PhabricatorLiskDAO $object, 482 PhabricatorApplicationTransaction $xaction) { 483 $type = $xaction->getTransactionType(); 484 throw new Exception( 485 "Transaction type '{$type}' is missing an internal apply ". 486 "implementation!"); 487 } 488 489 protected function applyCustomExternalTransaction( 490 PhabricatorLiskDAO $object, 491 PhabricatorApplicationTransaction $xaction) { 492 $type = $xaction->getTransactionType(); 493 throw new Exception( 494 "Transaction type '{$type}' is missing an external apply ". 495 "implementation!"); 496 } 497 498 /** 499 * Fill in a transaction's common values, like author and content source. 500 */ 501 protected function populateTransaction( 502 PhabricatorLiskDAO $object, 503 PhabricatorApplicationTransaction $xaction) { 504 505 $actor = $this->getActor(); 506 507 // TODO: This needs to be more sophisticated once we have meta-policies. 508 $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC); 509 510 if ($actor->isOmnipotent()) { 511 $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); 512 } else { 513 $xaction->setEditPolicy($this->getActingAsPHID()); 514 } 515 516 $xaction->setAuthorPHID($this->getActingAsPHID()); 517 $xaction->setContentSource($this->getContentSource()); 518 $xaction->attachViewer($actor); 519 $xaction->attachObject($object); 520 521 if ($object->getPHID()) { 522 $xaction->setObjectPHID($object->getPHID()); 523 } 524 525 return $xaction; 526 } 527 528 529 protected function applyFinalEffects( 530 PhabricatorLiskDAO $object, 531 array $xactions) { 532 return $xactions; 533 } 534 535 public function setContentSource(PhabricatorContentSource $content_source) { 536 $this->contentSource = $content_source; 537 return $this; 538 } 539 540 public function setContentSourceFromRequest(AphrontRequest $request) { 541 return $this->setContentSource( 542 PhabricatorContentSource::newFromRequest($request)); 543 } 544 545 public function setContentSourceFromConduitRequest( 546 ConduitAPIRequest $request) { 547 548 $content_source = PhabricatorContentSource::newForSource( 549 PhabricatorContentSource::SOURCE_CONDUIT, 550 array()); 551 552 return $this->setContentSource($content_source); 553 } 554 555 public function getContentSource() { 556 return $this->contentSource; 557 } 558 559 final public function applyTransactions( 560 PhabricatorLiskDAO $object, 561 array $xactions) { 562 563 $this->object = $object; 564 $this->xactions = $xactions; 565 $this->isNewObject = ($object->getPHID() === null); 566 567 $this->validateEditParameters($object, $xactions); 568 569 $actor = $this->requireActor(); 570 571 // NOTE: Some transaction expansion requires that the edited object be 572 // attached. 573 foreach ($xactions as $xaction) { 574 $xaction->attachObject($object); 575 $xaction->attachViewer($actor); 576 } 577 578 $xactions = $this->expandTransactions($object, $xactions); 579 $xactions = $this->expandSupportTransactions($object, $xactions); 580 $xactions = $this->combineTransactions($xactions); 581 582 foreach ($xactions as $xaction) { 583 $xaction = $this->populateTransaction($object, $xaction); 584 } 585 586 $is_preview = $this->getIsPreview(); 587 $read_locking = false; 588 $transaction_open = false; 589 590 if (!$is_preview) { 591 $errors = array(); 592 $type_map = mgroup($xactions, 'getTransactionType'); 593 foreach ($this->getTransactionTypes() as $type) { 594 $type_xactions = idx($type_map, $type, array()); 595 $errors[] = $this->validateTransaction($object, $type, $type_xactions); 596 } 597 598 $errors = array_mergev($errors); 599 600 $continue_on_missing = $this->getContinueOnMissingFields(); 601 foreach ($errors as $key => $error) { 602 if ($continue_on_missing && $error->getIsMissingFieldError()) { 603 unset($errors[$key]); 604 } 605 } 606 607 if ($errors) { 608 throw new PhabricatorApplicationTransactionValidationException($errors); 609 } 610 611 $file_phids = $this->extractFilePHIDs($object, $xactions); 612 613 if ($object->getID()) { 614 foreach ($xactions as $xaction) { 615 616 // If any of the transactions require a read lock, hold one and 617 // reload the object. We need to do this fairly early so that the 618 // call to `adjustTransactionValues()` (which populates old values) 619 // is based on the synchronized state of the object, which may differ 620 // from the state when it was originally loaded. 621 622 if ($this->shouldReadLock($object, $xaction)) { 623 $object->openTransaction(); 624 $object->beginReadLocking(); 625 $transaction_open = true; 626 $read_locking = true; 627 $object->reload(); 628 break; 629 } 630 } 631 } 632 633 if ($this->shouldApplyInitialEffects($object, $xactions)) { 634 if (!$transaction_open) { 635 $object->openTransaction(); 636 $transaction_open = true; 637 } 638 } 639 } 640 641 if ($this->shouldApplyInitialEffects($object, $xactions)) { 642 $this->applyInitialEffects($object, $xactions); 643 } 644 645 foreach ($xactions as $xaction) { 646 $this->adjustTransactionValues($object, $xaction); 647 } 648 649 $xactions = $this->filterTransactions($object, $xactions); 650 651 if (!$xactions) { 652 if ($read_locking) { 653 $object->endReadLocking(); 654 $read_locking = false; 655 } 656 if ($transaction_open) { 657 $object->killTransaction(); 658 $transaction_open = false; 659 } 660 return array(); 661 } 662 663 // Now that we've merged, filtered, and combined transactions, check for 664 // required capabilities. 665 foreach ($xactions as $xaction) { 666 $this->requireCapabilities($object, $xaction); 667 } 668 669 $xactions = $this->sortTransactions($xactions); 670 671 if ($is_preview) { 672 $this->loadHandles($xactions); 673 return $xactions; 674 } 675 676 $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor()) 677 ->setActor($actor) 678 ->setActingAsPHID($this->getActingAsPHID()) 679 ->setContentSource($this->getContentSource()); 680 681 if (!$transaction_open) { 682 $object->openTransaction(); 683 } 684 685 foreach ($xactions as $xaction) { 686 $this->applyInternalEffects($object, $xaction); 687 } 688 689 $object->save(); 690 691 foreach ($xactions as $xaction) { 692 $xaction->setObjectPHID($object->getPHID()); 693 if ($xaction->getComment()) { 694 $xaction->setPHID($xaction->generatePHID()); 695 $comment_editor->applyEdit($xaction, $xaction->getComment()); 696 } else { 697 $xaction->save(); 698 } 699 } 700 701 if ($file_phids) { 702 $this->attachFiles($object, $file_phids); 703 } 704 705 foreach ($xactions as $xaction) { 706 $this->applyExternalEffects($object, $xaction); 707 } 708 709 $xactions = $this->applyFinalEffects($object, $xactions); 710 711 if ($read_locking) { 712 $object->endReadLocking(); 713 $read_locking = false; 714 } 715 716 $object->saveTransaction(); 717 718 // Now that we've completely applied the core transaction set, try to apply 719 // Herald rules. Herald rules are allowed to either take direct actions on 720 // the database (like writing flags), or take indirect actions (like saving 721 // some targets for CC when we generate mail a little later), or return 722 // transactions which we'll apply normally using another Editor. 723 724 // First, check if *this* is a sub-editor which is itself applying Herald 725 // rules: if it is, stop working and return so we don't descend into 726 // madness. 727 728 // Otherwise, we're not a Herald editor, so process Herald rules (possibly 729 // using a Herald editor to apply resulting transactions) and then send out 730 // mail, notifications, and feed updates about everything. 731 732 if ($this->getIsHeraldEditor()) { 733 // We are the Herald editor, so stop work here and return the updated 734 // transactions. 735 return $xactions; 736 } else if ($this->shouldApplyHeraldRules($object, $xactions)) { 737 // We are not the Herald editor, so try to apply Herald rules. 738 $herald_xactions = $this->applyHeraldRules($object, $xactions); 739 740 if ($herald_xactions) { 741 $xscript_id = $this->getHeraldTranscript()->getID(); 742 foreach ($herald_xactions as $herald_xaction) { 743 $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id); 744 } 745 746 // NOTE: We're acting as the omnipotent user because rules deal with 747 // their own policy issues. We use a synthetic author PHID (the 748 // Herald application) as the author of record, so that transactions 749 // will render in a reasonable way ("Herald assigned this task ..."). 750 $herald_actor = PhabricatorUser::getOmnipotentUser(); 751 $herald_phid = id(new PhabricatorHeraldApplication())->getPHID(); 752 753 // TODO: It would be nice to give transactions a more specific source 754 // which points at the rule which generated them. You can figure this 755 // out from transcripts, but it would be cleaner if you didn't have to. 756 757 $herald_source = PhabricatorContentSource::newForSource( 758 PhabricatorContentSource::SOURCE_HERALD, 759 array()); 760 761 $herald_editor = newv(get_class($this), array()) 762 ->setContinueOnNoEffect(true) 763 ->setContinueOnMissingFields(true) 764 ->setParentMessageID($this->getParentMessageID()) 765 ->setIsHeraldEditor(true) 766 ->setActor($herald_actor) 767 ->setActingAsPHID($herald_phid) 768 ->setContentSource($herald_source); 769 770 $herald_xactions = $herald_editor->applyTransactions( 771 $object, 772 $herald_xactions); 773 774 // Merge the new transactions into the transaction list: we want to 775 // send email and publish feed stories about them, too. 776 $xactions = array_merge($xactions, $herald_xactions); 777 } 778 } 779 780 // Before sending mail or publishing feed stories, reload the object 781 // subscribers to pick up changes caused by Herald (or by other side effects 782 // in various transaction phases). 783 $this->loadSubscribers($object); 784 785 $this->loadHandles($xactions); 786 787 $mail = null; 788 if (!$this->getDisableEmail()) { 789 if ($this->shouldSendMail($object, $xactions)) { 790 $mail = $this->sendMail($object, $xactions); 791 } 792 } 793 794 if ($this->supportsSearch()) { 795 id(new PhabricatorSearchIndexer()) 796 ->queueDocumentForIndexing($object->getPHID()); 797 } 798 799 if ($this->shouldPublishFeedStory($object, $xactions)) { 800 $mailed = array(); 801 if ($mail) { 802 $mailed = $mail->buildRecipientList(); 803 } 804 $this->publishFeedStory( 805 $object, 806 $xactions, 807 $mailed); 808 } 809 810 $this->didApplyTransactions($xactions); 811 812 if ($object instanceof PhabricatorCustomFieldInterface) { 813 // Maybe this makes more sense to move into the search index itself? For 814 // now I'm putting it here since I think we might end up with things that 815 // need it to be up to date once the next page loads, but if we don't go 816 // there we we could move it into search once search moves to the daemons. 817 818 // It now happens in the search indexer as well, but the search indexer is 819 // always daemonized, so the logic above still potentially holds. We could 820 // possibly get rid of this. The major motivation for putting it in the 821 // indexer was to enable reindexing to work. 822 823 $fields = PhabricatorCustomField::getObjectFields( 824 $object, 825 PhabricatorCustomField::ROLE_APPLICATIONSEARCH); 826 $fields->readFieldsFromStorage($object); 827 $fields->rebuildIndexes($object); 828 } 829 830 return $xactions; 831 } 832 833 protected function didApplyTransactions(array $xactions) { 834 // Hook for subclasses. 835 return; 836 } 837 838 839 /** 840 * Determine if the editor should hold a read lock on the object while 841 * applying a transaction. 842 * 843 * If the editor does not hold a lock, two editors may read an object at the 844 * same time, then apply their changes without any synchronization. For most 845 * transactions, this does not matter much. However, it is important for some 846 * transactions. For example, if an object has a transaction count on it, both 847 * editors may read the object with `count = 23`, then independently update it 848 * and save the object with `count = 24` twice. This will produce the wrong 849 * state: the object really has 25 transactions, but the count is only 24. 850 * 851 * Generally, transactions fall into one of four buckets: 852 * 853 * - Append operations: Actions like adding a comment to an object purely 854 * add information to its state, and do not depend on the current object 855 * state in any way. These transactions never need to hold locks. 856 * - Overwrite operations: Actions like changing the title or description 857 * of an object replace the current value with a new value, so the end 858 * state is consistent without a lock. We currently do not lock these 859 * transactions, although we may in the future. 860 * - Edge operations: Edge and subscription operations have internal 861 * synchronization which limits the damage race conditions can cause. 862 * We do not currently lock these transactions, although we may in the 863 * future. 864 * - Update operations: Actions like incrementing a count on an object. 865 * These operations generally should use locks, unless it is not 866 * important that the state remain consistent in the presence of races. 867 * 868 * @param PhabricatorLiskDAO Object being updated. 869 * @param PhabricatorApplicationTransaction Transaction being applied. 870 * @return bool True to synchronize the edit with a lock. 871 */ 872 protected function shouldReadLock( 873 PhabricatorLiskDAO $object, 874 PhabricatorApplicationTransaction $xaction) { 875 return false; 876 } 877 878 private function loadHandles(array $xactions) { 879 $phids = array(); 880 foreach ($xactions as $key => $xaction) { 881 $phids[$key] = $xaction->getRequiredHandlePHIDs(); 882 } 883 $handles = array(); 884 $merged = array_mergev($phids); 885 if ($merged) { 886 $handles = id(new PhabricatorHandleQuery()) 887 ->setViewer($this->requireActor()) 888 ->withPHIDs($merged) 889 ->execute(); 890 } 891 foreach ($xactions as $key => $xaction) { 892 $xaction->setHandles(array_select_keys($handles, $phids[$key])); 893 } 894 } 895 896 private function loadSubscribers(PhabricatorLiskDAO $object) { 897 if ($object->getPHID() && 898 ($object instanceof PhabricatorSubscribableInterface)) { 899 $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID( 900 $object->getPHID()); 901 $this->subscribers = array_fuse($subs); 902 } else { 903 $this->subscribers = array(); 904 } 905 } 906 907 private function validateEditParameters( 908 PhabricatorLiskDAO $object, 909 array $xactions) { 910 911 if (!$this->getContentSource()) { 912 throw new Exception( 913 'Call setContentSource() before applyTransactions()!'); 914 } 915 916 // Do a bunch of sanity checks that the incoming transactions are fresh. 917 // They should be unsaved and have only "transactionType" and "newValue" 918 // set. 919 920 $types = array_fill_keys($this->getTransactionTypes(), true); 921 922 assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); 923 foreach ($xactions as $xaction) { 924 if ($xaction->getPHID() || $xaction->getID()) { 925 throw new PhabricatorApplicationTransactionStructureException( 926 $xaction, 927 pht( 928 'You can not apply transactions which already have IDs/PHIDs!')); 929 } 930 if ($xaction->getObjectPHID()) { 931 throw new PhabricatorApplicationTransactionStructureException( 932 $xaction, 933 pht( 934 'You can not apply transactions which already have objectPHIDs!')); 935 } 936 if ($xaction->getAuthorPHID()) { 937 throw new PhabricatorApplicationTransactionStructureException( 938 $xaction, 939 pht( 940 'You can not apply transactions which already have authorPHIDs!')); 941 } 942 if ($xaction->getCommentPHID()) { 943 throw new PhabricatorApplicationTransactionStructureException( 944 $xaction, 945 pht( 946 'You can not apply transactions which already have '. 947 'commentPHIDs!')); 948 } 949 if ($xaction->getCommentVersion() !== 0) { 950 throw new PhabricatorApplicationTransactionStructureException( 951 $xaction, 952 pht( 953 'You can not apply transactions which already have '. 954 'commentVersions!')); 955 } 956 957 $expect_value = !$xaction->shouldGenerateOldValue(); 958 $has_value = $xaction->hasOldValue(); 959 960 if ($expect_value && !$has_value) { 961 throw new PhabricatorApplicationTransactionStructureException( 962 $xaction, 963 pht( 964 'This transaction is supposed to have an oldValue set, but '. 965 'it does not!')); 966 } 967 968 if ($has_value && !$expect_value) { 969 throw new PhabricatorApplicationTransactionStructureException( 970 $xaction, 971 pht( 972 'This transaction should generate its oldValue automatically, '. 973 'but has already had one set!')); 974 } 975 976 $type = $xaction->getTransactionType(); 977 if (empty($types[$type])) { 978 throw new PhabricatorApplicationTransactionStructureException( 979 $xaction, 980 pht( 981 'Transaction has type "%s", but that transaction type is not '. 982 'supported by this editor (%s).', 983 $type, 984 get_class($this))); 985 } 986 } 987 } 988 989 protected function requireCapabilities( 990 PhabricatorLiskDAO $object, 991 PhabricatorApplicationTransaction $xaction) { 992 993 if ($this->getIsNewObject()) { 994 return; 995 } 996 997 $actor = $this->requireActor(); 998 switch ($xaction->getTransactionType()) { 999 case PhabricatorTransactions::TYPE_COMMENT: 1000 PhabricatorPolicyFilter::requireCapability( 1001 $actor, 1002 $object, 1003 PhabricatorPolicyCapability::CAN_VIEW); 1004 break; 1005 case PhabricatorTransactions::TYPE_VIEW_POLICY: 1006 PhabricatorPolicyFilter::requireCapability( 1007 $actor, 1008 $object, 1009 PhabricatorPolicyCapability::CAN_EDIT); 1010 break; 1011 case PhabricatorTransactions::TYPE_EDIT_POLICY: 1012 PhabricatorPolicyFilter::requireCapability( 1013 $actor, 1014 $object, 1015 PhabricatorPolicyCapability::CAN_EDIT); 1016 break; 1017 case PhabricatorTransactions::TYPE_JOIN_POLICY: 1018 PhabricatorPolicyFilter::requireCapability( 1019 $actor, 1020 $object, 1021 PhabricatorPolicyCapability::CAN_EDIT); 1022 break; 1023 } 1024 } 1025 1026 private function buildSubscribeTransaction( 1027 PhabricatorLiskDAO $object, 1028 array $xactions, 1029 array $blocks) { 1030 1031 if (!($object instanceof PhabricatorSubscribableInterface)) { 1032 return null; 1033 } 1034 1035 $texts = array_mergev($blocks); 1036 $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( 1037 $this->getActor(), 1038 $texts); 1039 1040 $this->mentionedPHIDs = $phids; 1041 1042 if ($object->getPHID()) { 1043 // Don't try to subscribe already-subscribed mentions: we want to generate 1044 // a dialog about an action having no effect if the user explicitly adds 1045 // existing CCs, but not if they merely mention existing subscribers. 1046 $phids = array_diff($phids, $this->subscribers); 1047 } 1048 1049 foreach ($phids as $key => $phid) { 1050 if ($object->isAutomaticallySubscribed($phid)) { 1051 unset($phids[$key]); 1052 } 1053 } 1054 $phids = array_values($phids); 1055 1056 if (!$phids) { 1057 return null; 1058 } 1059 1060 $xaction = newv(get_class(head($xactions)), array()); 1061 $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); 1062 $xaction->setNewValue(array('+' => $phids)); 1063 1064 return $xaction; 1065 } 1066 1067 protected function getRemarkupBlocksFromTransaction( 1068 PhabricatorApplicationTransaction $transaction) { 1069 return $transaction->getRemarkupBlocks(); 1070 } 1071 1072 protected function mergeTransactions( 1073 PhabricatorApplicationTransaction $u, 1074 PhabricatorApplicationTransaction $v) { 1075 1076 $type = $u->getTransactionType(); 1077 1078 switch ($type) { 1079 case PhabricatorTransactions::TYPE_SUBSCRIBERS: 1080 return $this->mergePHIDOrEdgeTransactions($u, $v); 1081 case PhabricatorTransactions::TYPE_EDGE: 1082 $u_type = $u->getMetadataValue('edge:type'); 1083 $v_type = $v->getMetadataValue('edge:type'); 1084 if ($u_type == $v_type) { 1085 return $this->mergePHIDOrEdgeTransactions($u, $v); 1086 } 1087 return null; 1088 } 1089 1090 // By default, do not merge the transactions. 1091 return null; 1092 } 1093 1094 /** 1095 * Optionally expand transactions which imply other effects. For example, 1096 * resigning from a revision in Differential implies removing yourself as 1097 * a reviewer. 1098 */ 1099 private function expandTransactions( 1100 PhabricatorLiskDAO $object, 1101 array $xactions) { 1102 1103 $results = array(); 1104 foreach ($xactions as $xaction) { 1105 foreach ($this->expandTransaction($object, $xaction) as $expanded) { 1106 $results[] = $expanded; 1107 } 1108 } 1109 1110 return $results; 1111 } 1112 1113 protected function expandTransaction( 1114 PhabricatorLiskDAO $object, 1115 PhabricatorApplicationTransaction $xaction) { 1116 return array($xaction); 1117 } 1118 1119 1120 private function expandSupportTransactions( 1121 PhabricatorLiskDAO $object, 1122 array $xactions) { 1123 $this->loadSubscribers($object); 1124 1125 $xactions = $this->applyImplicitCC($object, $xactions); 1126 1127 $blocks = array(); 1128 foreach ($xactions as $key => $xaction) { 1129 $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction); 1130 } 1131 1132 $subscribe_xaction = $this->buildSubscribeTransaction( 1133 $object, 1134 $xactions, 1135 $blocks); 1136 if ($subscribe_xaction) { 1137 $xactions[] = $subscribe_xaction; 1138 } 1139 1140 // TODO: For now, this is just a placeholder. 1141 $engine = PhabricatorMarkupEngine::getEngine('extract'); 1142 $engine->setConfig('viewer', $this->requireActor()); 1143 1144 $block_xactions = $this->expandRemarkupBlockTransactions( 1145 $object, 1146 $xactions, 1147 $blocks, 1148 $engine); 1149 1150 foreach ($block_xactions as $xaction) { 1151 $xactions[] = $xaction; 1152 } 1153 1154 return $xactions; 1155 } 1156 1157 private function expandRemarkupBlockTransactions( 1158 PhabricatorLiskDAO $object, 1159 array $xactions, 1160 $blocks, 1161 PhutilMarkupEngine $engine) { 1162 1163 $block_xactions = $this->expandCustomRemarkupBlockTransactions( 1164 $object, 1165 $xactions, 1166 $blocks, 1167 $engine); 1168 1169 $mentioned_phids = array(); 1170 foreach ($blocks as $key => $xaction_blocks) { 1171 foreach ($xaction_blocks as $block) { 1172 $engine->markupText($block); 1173 $mentioned_phids += $engine->getTextMetadata( 1174 PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS, 1175 array()); 1176 } 1177 } 1178 1179 if (!$mentioned_phids) { 1180 return $block_xactions; 1181 } 1182 1183 if ($object instanceof PhabricatorProjectInterface) { 1184 $phids = $mentioned_phids; 1185 $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; 1186 foreach ($phids as $key => $phid) { 1187 if (phid_get_type($phid) != $project_type) { 1188 unset($phids[$key]); 1189 } 1190 } 1191 1192 if ($phids) { 1193 $edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; 1194 $block_xactions[] = newv(get_class(head($xactions)), array()) 1195 ->setIgnoreOnNoEffect(true) 1196 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 1197 ->setMetadataValue('edge:type', $edge_type) 1198 ->setNewValue(array('+' => $phids)); 1199 } 1200 } 1201 1202 $mentioned_objects = id(new PhabricatorObjectQuery()) 1203 ->setViewer($this->getActor()) 1204 ->withPHIDs($mentioned_phids) 1205 ->execute(); 1206 1207 $mentionable_phids = array(); 1208 foreach ($mentioned_objects as $mentioned_object) { 1209 if ($mentioned_object instanceof PhabricatorMentionableInterface) { 1210 $mentioned_phid = $mentioned_object->getPHID(); 1211 if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) { 1212 continue; 1213 } 1214 // don't let objects mention themselves 1215 if ($object->getPHID() && $mentioned_phid == $object->getPHID()) { 1216 continue; 1217 } 1218 $mentionable_phids[$mentioned_phid] = $mentioned_phid; 1219 } 1220 } 1221 if ($mentionable_phids) { 1222 $edge_type = PhabricatorObjectMentionsObject::EDGECONST; 1223 $block_xactions[] = newv(get_class(head($xactions)), array()) 1224 ->setIgnoreOnNoEffect(true) 1225 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 1226 ->setMetadataValue('edge:type', $edge_type) 1227 ->setNewValue(array('+' => $mentionable_phids)); 1228 } 1229 1230 return $block_xactions; 1231 } 1232 1233 protected function expandCustomRemarkupBlockTransactions( 1234 PhabricatorLiskDAO $object, 1235 array $xactions, 1236 $blocks, 1237 PhutilMarkupEngine $engine) { 1238 return array(); 1239 } 1240 1241 1242 /** 1243 * Attempt to combine similar transactions into a smaller number of total 1244 * transactions. For example, two transactions which edit the title of an 1245 * object can be merged into a single edit. 1246 */ 1247 private function combineTransactions(array $xactions) { 1248 $stray_comments = array(); 1249 1250 $result = array(); 1251 $types = array(); 1252 foreach ($xactions as $key => $xaction) { 1253 $type = $xaction->getTransactionType(); 1254 if (isset($types[$type])) { 1255 foreach ($types[$type] as $other_key) { 1256 $merged = $this->mergeTransactions($result[$other_key], $xaction); 1257 if ($merged) { 1258 $result[$other_key] = $merged; 1259 1260 if ($xaction->getComment() && 1261 ($xaction->getComment() !== $merged->getComment())) { 1262 $stray_comments[] = $xaction->getComment(); 1263 } 1264 1265 if ($result[$other_key]->getComment() && 1266 ($result[$other_key]->getComment() !== $merged->getComment())) { 1267 $stray_comments[] = $result[$other_key]->getComment(); 1268 } 1269 1270 // Move on to the next transaction. 1271 continue 2; 1272 } 1273 } 1274 } 1275 $result[$key] = $xaction; 1276 $types[$type][] = $key; 1277 } 1278 1279 // If we merged any comments away, restore them. 1280 foreach ($stray_comments as $comment) { 1281 $xaction = newv(get_class(head($result)), array()); 1282 $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT); 1283 $xaction->setComment($comment); 1284 $result[] = $xaction; 1285 } 1286 1287 return array_values($result); 1288 } 1289 1290 protected function mergePHIDOrEdgeTransactions( 1291 PhabricatorApplicationTransaction $u, 1292 PhabricatorApplicationTransaction $v) { 1293 1294 $result = $u->getNewValue(); 1295 foreach ($v->getNewValue() as $key => $value) { 1296 if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) { 1297 if (empty($result[$key])) { 1298 $result[$key] = $value; 1299 } else { 1300 // We're merging two lists of edge adds, sets, or removes. Merge 1301 // them by merging individual PHIDs within them. 1302 $merged = $result[$key]; 1303 1304 foreach ($value as $dst => $v_spec) { 1305 if (empty($merged[$dst])) { 1306 $merged[$dst] = $v_spec; 1307 } else { 1308 // Two transactions are trying to perform the same operation on 1309 // the same edge. Normalize the edge data and then merge it. This 1310 // allows transactions to specify how data merges execute in a 1311 // precise way. 1312 1313 $u_spec = $merged[$dst]; 1314 1315 if (!is_array($u_spec)) { 1316 $u_spec = array('dst' => $u_spec); 1317 } 1318 if (!is_array($v_spec)) { 1319 $v_spec = array('dst' => $v_spec); 1320 } 1321 1322 $ux_data = idx($u_spec, 'data', array()); 1323 $vx_data = idx($v_spec, 'data', array()); 1324 1325 $merged_data = $this->mergeEdgeData( 1326 $u->getMetadataValue('edge:type'), 1327 $ux_data, 1328 $vx_data); 1329 1330 $u_spec['data'] = $merged_data; 1331 $merged[$dst] = $u_spec; 1332 } 1333 } 1334 1335 $result[$key] = $merged; 1336 } 1337 } else { 1338 $result[$key] = array_merge($value, idx($result, $key, array())); 1339 } 1340 } 1341 $u->setNewValue($result); 1342 1343 // When combining an "ignore" transaction with a normal transaction, make 1344 // sure we don't propagate the "ignore" flag. 1345 if (!$v->getIgnoreOnNoEffect()) { 1346 $u->setIgnoreOnNoEffect(false); 1347 } 1348 1349 return $u; 1350 } 1351 1352 protected function mergeEdgeData($type, array $u, array $v) { 1353 return $v + $u; 1354 } 1355 1356 protected function getPHIDTransactionNewValue( 1357 PhabricatorApplicationTransaction $xaction) { 1358 1359 $old = array_fuse($xaction->getOldValue()); 1360 1361 $new = $xaction->getNewValue(); 1362 $new_add = idx($new, '+', array()); 1363 unset($new['+']); 1364 $new_rem = idx($new, '-', array()); 1365 unset($new['-']); 1366 $new_set = idx($new, '=', null); 1367 if ($new_set !== null) { 1368 $new_set = array_fuse($new_set); 1369 } 1370 unset($new['=']); 1371 1372 if ($new) { 1373 throw new Exception( 1374 "Invalid 'new' value for PHID transaction. Value should contain only ". 1375 "keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS)."); 1376 } 1377 1378 $result = array(); 1379 1380 foreach ($old as $phid) { 1381 if ($new_set !== null && empty($new_set[$phid])) { 1382 continue; 1383 } 1384 $result[$phid] = $phid; 1385 } 1386 1387 if ($new_set !== null) { 1388 foreach ($new_set as $phid) { 1389 $result[$phid] = $phid; 1390 } 1391 } 1392 1393 foreach ($new_add as $phid) { 1394 $result[$phid] = $phid; 1395 } 1396 1397 foreach ($new_rem as $phid) { 1398 unset($result[$phid]); 1399 } 1400 1401 return array_values($result); 1402 } 1403 1404 protected function getEdgeTransactionNewValue( 1405 PhabricatorApplicationTransaction $xaction) { 1406 1407 $new = $xaction->getNewValue(); 1408 $new_add = idx($new, '+', array()); 1409 unset($new['+']); 1410 $new_rem = idx($new, '-', array()); 1411 unset($new['-']); 1412 $new_set = idx($new, '=', null); 1413 unset($new['=']); 1414 1415 if ($new) { 1416 throw new Exception( 1417 "Invalid 'new' value for Edge transaction. Value should contain only ". 1418 "keys '+' (add edges), '-' (remove edges) and '=' (set edges)."); 1419 } 1420 1421 $old = $xaction->getOldValue(); 1422 1423 $lists = array($new_set, $new_add, $new_rem); 1424 foreach ($lists as $list) { 1425 $this->checkEdgeList($list); 1426 } 1427 1428 $result = array(); 1429 foreach ($old as $dst_phid => $edge) { 1430 if ($new_set !== null && empty($new_set[$dst_phid])) { 1431 continue; 1432 } 1433 $result[$dst_phid] = $this->normalizeEdgeTransactionValue( 1434 $xaction, 1435 $edge, 1436 $dst_phid); 1437 } 1438 1439 if ($new_set !== null) { 1440 foreach ($new_set as $dst_phid => $edge) { 1441 $result[$dst_phid] = $this->normalizeEdgeTransactionValue( 1442 $xaction, 1443 $edge, 1444 $dst_phid); 1445 } 1446 } 1447 1448 foreach ($new_add as $dst_phid => $edge) { 1449 $result[$dst_phid] = $this->normalizeEdgeTransactionValue( 1450 $xaction, 1451 $edge, 1452 $dst_phid); 1453 } 1454 1455 foreach ($new_rem as $dst_phid => $edge) { 1456 unset($result[$dst_phid]); 1457 } 1458 1459 return $result; 1460 } 1461 1462 private function checkEdgeList($list) { 1463 if (!$list) { 1464 return; 1465 } 1466 foreach ($list as $key => $item) { 1467 if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { 1468 throw new Exception( 1469 "Edge transactions must have destination PHIDs as in edge ". 1470 "lists (found key '{$key}')."); 1471 } 1472 if (!is_array($item) && $item !== $key) { 1473 throw new Exception( 1474 "Edge transactions must have PHIDs or edge specs as values ". 1475 "(found value '{$item}')."); 1476 } 1477 } 1478 } 1479 1480 private function normalizeEdgeTransactionValue( 1481 PhabricatorApplicationTransaction $xaction, 1482 $edge, 1483 $dst_phid) { 1484 1485 if (!is_array($edge)) { 1486 if ($edge != $dst_phid) { 1487 throw new Exception( 1488 pht( 1489 'Transaction edge data must either be the edge PHID or an edge '. 1490 'specification dictionary.')); 1491 } 1492 $edge = array(); 1493 } else { 1494 foreach ($edge as $key => $value) { 1495 switch ($key) { 1496 case 'src': 1497 case 'dst': 1498 case 'type': 1499 case 'data': 1500 case 'dateCreated': 1501 case 'dateModified': 1502 case 'seq': 1503 case 'dataID': 1504 break; 1505 default: 1506 throw new Exception( 1507 pht( 1508 'Transaction edge specification contains unexpected key '. 1509 '"%s".', 1510 $key)); 1511 } 1512 } 1513 } 1514 1515 $edge['dst'] = $dst_phid; 1516 1517 $edge_type = $xaction->getMetadataValue('edge:type'); 1518 if (empty($edge['type'])) { 1519 $edge['type'] = $edge_type; 1520 } else { 1521 if ($edge['type'] != $edge_type) { 1522 $this_type = $edge['type']; 1523 throw new Exception( 1524 "Edge transaction includes edge of type '{$this_type}', but ". 1525 "transaction is of type '{$edge_type}'. Each edge transaction must ". 1526 "alter edges of only one type."); 1527 } 1528 } 1529 1530 if (!isset($edge['data'])) { 1531 $edge['data'] = array(); 1532 } 1533 1534 return $edge; 1535 } 1536 1537 protected function sortTransactions(array $xactions) { 1538 $head = array(); 1539 $tail = array(); 1540 1541 // Move bare comments to the end, so the actions precede them. 1542 foreach ($xactions as $xaction) { 1543 $type = $xaction->getTransactionType(); 1544 if ($type == PhabricatorTransactions::TYPE_COMMENT) { 1545 $tail[] = $xaction; 1546 } else { 1547 $head[] = $xaction; 1548 } 1549 } 1550 1551 return array_values(array_merge($head, $tail)); 1552 } 1553 1554 1555 protected function filterTransactions( 1556 PhabricatorLiskDAO $object, 1557 array $xactions) { 1558 1559 $type_comment = PhabricatorTransactions::TYPE_COMMENT; 1560 1561 $no_effect = array(); 1562 $has_comment = false; 1563 $any_effect = false; 1564 foreach ($xactions as $key => $xaction) { 1565 if ($this->transactionHasEffect($object, $xaction)) { 1566 if ($xaction->getTransactionType() != $type_comment) { 1567 $any_effect = true; 1568 } 1569 } else if ($xaction->getIgnoreOnNoEffect()) { 1570 unset($xactions[$key]); 1571 } else { 1572 $no_effect[$key] = $xaction; 1573 } 1574 if ($xaction->hasComment()) { 1575 $has_comment = true; 1576 } 1577 } 1578 1579 if (!$no_effect) { 1580 return $xactions; 1581 } 1582 1583 if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) { 1584 throw new PhabricatorApplicationTransactionNoEffectException( 1585 $no_effect, 1586 $any_effect, 1587 $has_comment); 1588 } 1589 1590 if (!$any_effect && !$has_comment) { 1591 // If we only have empty comment transactions, just drop them all. 1592 return array(); 1593 } 1594 1595 foreach ($no_effect as $key => $xaction) { 1596 if ($xaction->getComment()) { 1597 $xaction->setTransactionType($type_comment); 1598 $xaction->setOldValue(null); 1599 $xaction->setNewValue(null); 1600 } else { 1601 unset($xactions[$key]); 1602 } 1603 } 1604 1605 return $xactions; 1606 } 1607 1608 1609 /** 1610 * Hook for validating transactions. This callback will be invoked for each 1611 * available transaction type, even if an edit does not apply any transactions 1612 * of that type. This allows you to raise exceptions when required fields are 1613 * missing, by detecting that the object has no field value and there is no 1614 * transaction which sets one. 1615 * 1616 * @param PhabricatorLiskDAO Object being edited. 1617 * @param string Transaction type to validate. 1618 * @param list<PhabricatorApplicationTransaction> Transactions of given type, 1619 * which may be empty if the edit does not apply any transactions of the 1620 * given type. 1621 * @return list<PhabricatorApplicationTransactionValidationError> List of 1622 * validation errors. 1623 */ 1624 protected function validateTransaction( 1625 PhabricatorLiskDAO $object, 1626 $type, 1627 array $xactions) { 1628 1629 $errors = array(); 1630 switch ($type) { 1631 case PhabricatorTransactions::TYPE_VIEW_POLICY: 1632 $errors[] = $this->validatePolicyTransaction( 1633 $object, 1634 $xactions, 1635 $type, 1636 PhabricatorPolicyCapability::CAN_VIEW); 1637 break; 1638 case PhabricatorTransactions::TYPE_EDIT_POLICY: 1639 $errors[] = $this->validatePolicyTransaction( 1640 $object, 1641 $xactions, 1642 $type, 1643 PhabricatorPolicyCapability::CAN_EDIT); 1644 break; 1645 case PhabricatorTransactions::TYPE_CUSTOMFIELD: 1646 $groups = array(); 1647 foreach ($xactions as $xaction) { 1648 $groups[$xaction->getMetadataValue('customfield:key')][] = $xaction; 1649 } 1650 1651 $field_list = PhabricatorCustomField::getObjectFields( 1652 $object, 1653 PhabricatorCustomField::ROLE_EDIT); 1654 $field_list->setViewer($this->getActor()); 1655 1656 $role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS; 1657 foreach ($field_list->getFields() as $field) { 1658 if (!$field->shouldEnableForRole($role_xactions)) { 1659 continue; 1660 } 1661 $errors[] = $field->validateApplicationTransactions( 1662 $this, 1663 $type, 1664 idx($groups, $field->getFieldKey(), array())); 1665 } 1666 break; 1667 } 1668 1669 return array_mergev($errors); 1670 } 1671 1672 private function validatePolicyTransaction( 1673 PhabricatorLiskDAO $object, 1674 array $xactions, 1675 $transaction_type, 1676 $capability) { 1677 1678 $actor = $this->requireActor(); 1679 $errors = array(); 1680 // Note $this->xactions is necessary; $xactions is $this->xactions of 1681 // $transaction_type 1682 $policy_object = $this->adjustObjectForPolicyChecks( 1683 $object, 1684 $this->xactions); 1685 1686 // Make sure the user isn't editing away their ability to $capability this 1687 // object. 1688 foreach ($xactions as $xaction) { 1689 try { 1690 PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy( 1691 $actor, 1692 $policy_object, 1693 $capability, 1694 $xaction->getNewValue()); 1695 } catch (PhabricatorPolicyException $ex) { 1696 $errors[] = new PhabricatorApplicationTransactionValidationError( 1697 $transaction_type, 1698 pht('Invalid'), 1699 pht( 1700 'You can not select this %s policy, because you would no longer '. 1701 'be able to %s the object.', 1702 $capability, 1703 $capability), 1704 $xaction); 1705 } 1706 } 1707 1708 if ($this->getIsNewObject()) { 1709 if (!$xactions) { 1710 $has_capability = PhabricatorPolicyFilter::hasCapability( 1711 $actor, 1712 $policy_object, 1713 $capability); 1714 if (!$has_capability) { 1715 $errors[] = new PhabricatorApplicationTransactionValidationError( 1716 $transaction_type, 1717 pht('Invalid'), 1718 pht('The selected %s policy excludes you. Choose a %s policy '. 1719 'which allows you to %s the object.', 1720 $capability, 1721 $capability, 1722 $capability)); 1723 } 1724 } 1725 } 1726 1727 return $errors; 1728 } 1729 1730 protected function adjustObjectForPolicyChecks( 1731 PhabricatorLiskDAO $object, 1732 array $xactions) { 1733 1734 return clone $object; 1735 } 1736 1737 /** 1738 * Check for a missing text field. 1739 * 1740 * A text field is missing if the object has no value and there are no 1741 * transactions which set a value, or if the transactions remove the value. 1742 * This method is intended to make implementing @{method:validateTransaction} 1743 * more convenient: 1744 * 1745 * $missing = $this->validateIsEmptyTextField( 1746 * $object->getName(), 1747 * $xactions); 1748 * 1749 * This will return `true` if the net effect of the object and transactions 1750 * is an empty field. 1751 * 1752 * @param wild Current field value. 1753 * @param list<PhabricatorApplicationTransaction> Transactions editing the 1754 * field. 1755 * @return bool True if the field will be an empty text field after edits. 1756 */ 1757 protected function validateIsEmptyTextField($field_value, array $xactions) { 1758 if (strlen($field_value) && empty($xactions)) { 1759 return false; 1760 } 1761 1762 if ($xactions && strlen(last($xactions)->getNewValue())) { 1763 return false; 1764 } 1765 1766 return true; 1767 } 1768 1769 1770 /* -( Implicit CCs )------------------------------------------------------- */ 1771 1772 1773 /** 1774 * When a user interacts with an object, we might want to add them to CC. 1775 */ 1776 final public function applyImplicitCC( 1777 PhabricatorLiskDAO $object, 1778 array $xactions) { 1779 1780 if (!($object instanceof PhabricatorSubscribableInterface)) { 1781 // If the object isn't subscribable, we can't CC them. 1782 return $xactions; 1783 } 1784 1785 $actor_phid = $this->getActingAsPHID(); 1786 1787 $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; 1788 if (phid_get_type($actor_phid) != $type_user) { 1789 // Transactions by application actors like Herald, Harbormaster and 1790 // Diffusion should not CC the applications. 1791 return $xactions; 1792 } 1793 1794 if ($object->isAutomaticallySubscribed($actor_phid)) { 1795 // If they're auto-subscribed, don't CC them. 1796 return $xactions; 1797 } 1798 1799 $should_cc = false; 1800 foreach ($xactions as $xaction) { 1801 if ($this->shouldImplyCC($object, $xaction)) { 1802 $should_cc = true; 1803 break; 1804 } 1805 } 1806 1807 if (!$should_cc) { 1808 // Only some types of actions imply a CC (like adding a comment). 1809 return $xactions; 1810 } 1811 1812 if ($object->getPHID()) { 1813 if (isset($this->subscribers[$actor_phid])) { 1814 // If the user is already subscribed, don't implicitly CC them. 1815 return $xactions; 1816 } 1817 1818 $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs( 1819 $object->getPHID(), 1820 PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); 1821 $unsub = array_fuse($unsub); 1822 if (isset($unsub[$actor_phid])) { 1823 // If the user has previously unsubscribed from this object explicitly, 1824 // don't implicitly CC them. 1825 return $xactions; 1826 } 1827 } 1828 1829 $xaction = newv(get_class(head($xactions)), array()); 1830 $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); 1831 $xaction->setNewValue(array('+' => array($actor_phid))); 1832 1833 array_unshift($xactions, $xaction); 1834 1835 return $xactions; 1836 } 1837 1838 protected function shouldImplyCC( 1839 PhabricatorLiskDAO $object, 1840 PhabricatorApplicationTransaction $xaction) { 1841 1842 return $xaction->isCommentTransaction(); 1843 } 1844 1845 1846 /* -( Sending Mail )------------------------------------------------------- */ 1847 1848 1849 /** 1850 * @task mail 1851 */ 1852 protected function shouldSendMail( 1853 PhabricatorLiskDAO $object, 1854 array $xactions) { 1855 return false; 1856 } 1857 1858 1859 /** 1860 * @task mail 1861 */ 1862 protected function sendMail( 1863 PhabricatorLiskDAO $object, 1864 array $xactions) { 1865 1866 // Check if any of the transactions are visible. If we don't have any 1867 // visible transactions, don't send the mail. 1868 1869 $any_visible = false; 1870 foreach ($xactions as $xaction) { 1871 if (!$xaction->shouldHideForMail($xactions)) { 1872 $any_visible = true; 1873 break; 1874 } 1875 } 1876 1877 if (!$any_visible) { 1878 return; 1879 } 1880 1881 $email_to = array_filter(array_unique($this->getMailTo($object))); 1882 $email_cc = array_filter(array_unique($this->getMailCC($object))); 1883 1884 $phids = array_merge($email_to, $email_cc); 1885 $handles = id(new PhabricatorHandleQuery()) 1886 ->setViewer($this->requireActor()) 1887 ->withPHIDs($phids) 1888 ->execute(); 1889 1890 $template = $this->buildMailTemplate($object); 1891 $body = $this->buildMailBody($object, $xactions); 1892 1893 $mail_tags = $this->getMailTags($object, $xactions); 1894 $action = $this->getMailAction($object, $xactions); 1895 1896 $reply_handler = $this->buildReplyHandler($object); 1897 $reply_section = $reply_handler->getReplyHandlerInstructions(); 1898 if ($reply_section !== null) { 1899 $body->addReplySection($reply_section); 1900 } 1901 1902 $body->addEmailPreferenceSection(); 1903 1904 $template 1905 ->setFrom($this->getActingAsPHID()) 1906 ->setSubjectPrefix($this->getMailSubjectPrefix()) 1907 ->setVarySubjectPrefix('['.$action.']') 1908 ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) 1909 ->setRelatedPHID($object->getPHID()) 1910 ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) 1911 ->setMailTags($mail_tags) 1912 ->setIsBulk(true) 1913 ->setBody($body->render()) 1914 ->setHTMLBody($body->renderHTML()); 1915 1916 foreach ($body->getAttachments() as $attachment) { 1917 $template->addAttachment($attachment); 1918 } 1919 1920 $herald_xscript = $this->getHeraldTranscript(); 1921 if ($herald_xscript) { 1922 $herald_header = $herald_xscript->getXHeraldRulesHeader(); 1923 $herald_header = HeraldTranscript::saveXHeraldRulesHeader( 1924 $object->getPHID(), 1925 $herald_header); 1926 } else { 1927 $herald_header = HeraldTranscript::loadXHeraldRulesHeader( 1928 $object->getPHID()); 1929 } 1930 1931 if ($herald_header) { 1932 $template->addHeader('X-Herald-Rules', $herald_header); 1933 } 1934 1935 if ($object instanceof PhabricatorProjectInterface) { 1936 $this->addMailProjectMetadata($object, $template); 1937 } 1938 1939 if ($this->getParentMessageID()) { 1940 $template->setParentMessageID($this->getParentMessageID()); 1941 } 1942 1943 $mails = $reply_handler->multiplexMail( 1944 $template, 1945 array_select_keys($handles, $email_to), 1946 array_select_keys($handles, $email_cc)); 1947 1948 foreach ($mails as $mail) { 1949 $mail->saveAndSend(); 1950 } 1951 1952 $template->addTos($email_to); 1953 $template->addCCs($email_cc); 1954 1955 return $template; 1956 } 1957 1958 private function addMailProjectMetadata( 1959 PhabricatorLiskDAO $object, 1960 PhabricatorMetaMTAMail $template) { 1961 1962 $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 1963 $object->getPHID(), 1964 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 1965 1966 if (!$project_phids) { 1967 return; 1968 } 1969 1970 // TODO: This viewer isn't quite right. It would be slightly better to use 1971 // the mail recipient, but that's not very easy given the way rendering 1972 // works today. 1973 1974 $handles = id(new PhabricatorHandleQuery()) 1975 ->setViewer($this->requireActor()) 1976 ->withPHIDs($project_phids) 1977 ->execute(); 1978 1979 $project_tags = array(); 1980 foreach ($handles as $handle) { 1981 if (!$handle->isComplete()) { 1982 continue; 1983 } 1984 $project_tags[] = '<'.$handle->getObjectName().'>'; 1985 } 1986 1987 if (!$project_tags) { 1988 return; 1989 } 1990 1991 $project_tags = implode(', ', $project_tags); 1992 $template->addHeader('X-Phabricator-Projects', $project_tags); 1993 } 1994 1995 1996 protected function getMailThreadID(PhabricatorLiskDAO $object) { 1997 return $object->getPHID(); 1998 } 1999 2000 2001 /** 2002 * @task mail 2003 */ 2004 protected function getStrongestAction( 2005 PhabricatorLiskDAO $object, 2006 array $xactions) { 2007 return last(msort($xactions, 'getActionStrength')); 2008 } 2009 2010 2011 /** 2012 * @task mail 2013 */ 2014 protected function buildReplyHandler(PhabricatorLiskDAO $object) { 2015 throw new Exception('Capability not supported.'); 2016 } 2017 2018 /** 2019 * @task mail 2020 */ 2021 protected function getMailSubjectPrefix() { 2022 throw new Exception('Capability not supported.'); 2023 } 2024 2025 2026 /** 2027 * @task mail 2028 */ 2029 protected function getMailTags( 2030 PhabricatorLiskDAO $object, 2031 array $xactions) { 2032 $tags = array(); 2033 2034 foreach ($xactions as $xaction) { 2035 $tags[] = $xaction->getMailTags(); 2036 } 2037 2038 return array_mergev($tags); 2039 } 2040 2041 /** 2042 * @task mail 2043 */ 2044 public function getMailTagsMap() { 2045 // TODO: We should move shared mail tags, like "comment", here. 2046 return array(); 2047 } 2048 2049 2050 /** 2051 * @task mail 2052 */ 2053 protected function getMailAction( 2054 PhabricatorLiskDAO $object, 2055 array $xactions) { 2056 return $this->getStrongestAction($object, $xactions)->getActionName(); 2057 } 2058 2059 2060 /** 2061 * @task mail 2062 */ 2063 protected function buildMailTemplate(PhabricatorLiskDAO $object) { 2064 throw new Exception('Capability not supported.'); 2065 } 2066 2067 2068 /** 2069 * @task mail 2070 */ 2071 protected function getMailTo(PhabricatorLiskDAO $object) { 2072 throw new Exception('Capability not supported.'); 2073 } 2074 2075 2076 /** 2077 * @task mail 2078 */ 2079 protected function getMailCC(PhabricatorLiskDAO $object) { 2080 $phids = array(); 2081 $has_support = false; 2082 2083 if ($object instanceof PhabricatorSubscribableInterface) { 2084 $phids[] = $this->subscribers; 2085 $has_support = true; 2086 } 2087 2088 if ($object instanceof PhabricatorProjectInterface) { 2089 $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 2090 $object->getPHID(), 2091 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 2092 2093 if ($project_phids) { 2094 $watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER; 2095 2096 $query = id(new PhabricatorEdgeQuery()) 2097 ->withSourcePHIDs($project_phids) 2098 ->withEdgeTypes(array($watcher_type)); 2099 $query->execute(); 2100 2101 $watcher_phids = $query->getDestinationPHIDs(); 2102 if ($watcher_phids) { 2103 // We need to do a visibility check for all the watchers, as 2104 // watching a project is not a guarantee that you can see objects 2105 // associated with it. 2106 $users = id(new PhabricatorPeopleQuery()) 2107 ->setViewer($this->requireActor()) 2108 ->withPHIDs($watcher_phids) 2109 ->execute(); 2110 2111 $watchers = array(); 2112 foreach ($users as $user) { 2113 $can_see = PhabricatorPolicyFilter::hasCapability( 2114 $user, 2115 $object, 2116 PhabricatorPolicyCapability::CAN_VIEW); 2117 if ($can_see) { 2118 $watchers[] = $user->getPHID(); 2119 } 2120 } 2121 $phids[] = $watchers; 2122 } 2123 } 2124 2125 $has_support = true; 2126 } 2127 2128 if (!$has_support) { 2129 throw new Exception('Capability not supported.'); 2130 } 2131 2132 return array_mergev($phids); 2133 } 2134 2135 2136 /** 2137 * @task mail 2138 */ 2139 protected function buildMailBody( 2140 PhabricatorLiskDAO $object, 2141 array $xactions) { 2142 2143 $headers = array(); 2144 $comments = array(); 2145 2146 foreach ($xactions as $xaction) { 2147 if ($xaction->shouldHideForMail($xactions)) { 2148 continue; 2149 } 2150 2151 $header = $xaction->getTitleForMail(); 2152 if ($header !== null) { 2153 $headers[] = $header; 2154 } 2155 2156 $comment = $xaction->getBodyForMail(); 2157 if ($comment !== null) { 2158 $comments[] = $comment; 2159 } 2160 } 2161 2162 $body = new PhabricatorMetaMTAMailBody(); 2163 $body->setViewer($this->requireActor()); 2164 $body->addRawSection(implode("\n", $headers)); 2165 2166 foreach ($comments as $comment) { 2167 $body->addRemarkupSection($comment); 2168 } 2169 2170 if ($object instanceof PhabricatorCustomFieldInterface) { 2171 $field_list = PhabricatorCustomField::getObjectFields( 2172 $object, 2173 PhabricatorCustomField::ROLE_TRANSACTIONMAIL); 2174 $field_list->setViewer($this->getActor()); 2175 $field_list->readFieldsFromStorage($object); 2176 2177 foreach ($field_list->getFields() as $field) { 2178 $field->updateTransactionMailBody( 2179 $body, 2180 $this, 2181 $xactions); 2182 } 2183 } 2184 2185 return $body; 2186 } 2187 2188 2189 /* -( Publishing Feed Stories )-------------------------------------------- */ 2190 2191 2192 /** 2193 * @task feed 2194 */ 2195 protected function shouldPublishFeedStory( 2196 PhabricatorLiskDAO $object, 2197 array $xactions) { 2198 return false; 2199 } 2200 2201 2202 /** 2203 * @task feed 2204 */ 2205 protected function getFeedStoryType() { 2206 return 'PhabricatorApplicationTransactionFeedStory'; 2207 } 2208 2209 2210 /** 2211 * @task feed 2212 */ 2213 protected function getFeedRelatedPHIDs( 2214 PhabricatorLiskDAO $object, 2215 array $xactions) { 2216 2217 $phids = array( 2218 $object->getPHID(), 2219 $this->getActingAsPHID(), 2220 ); 2221 2222 if ($object instanceof PhabricatorProjectInterface) { 2223 $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 2224 $object->getPHID(), 2225 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 2226 foreach ($project_phids as $project_phid) { 2227 $phids[] = $project_phid; 2228 } 2229 } 2230 2231 return $phids; 2232 } 2233 2234 2235 /** 2236 * @task feed 2237 */ 2238 protected function getFeedNotifyPHIDs( 2239 PhabricatorLiskDAO $object, 2240 array $xactions) { 2241 2242 return array_unique(array_merge( 2243 $this->getMailTo($object), 2244 $this->getMailCC($object))); 2245 } 2246 2247 2248 /** 2249 * @task feed 2250 */ 2251 protected function getFeedStoryData( 2252 PhabricatorLiskDAO $object, 2253 array $xactions) { 2254 2255 $xactions = msort($xactions, 'getActionStrength'); 2256 $xactions = array_reverse($xactions); 2257 2258 return array( 2259 'objectPHID' => $object->getPHID(), 2260 'transactionPHIDs' => mpull($xactions, 'getPHID'), 2261 ); 2262 } 2263 2264 2265 /** 2266 * @task feed 2267 */ 2268 protected function publishFeedStory( 2269 PhabricatorLiskDAO $object, 2270 array $xactions, 2271 array $mailed_phids) { 2272 2273 $xactions = mfilter($xactions, 'shouldHideForFeed', true); 2274 2275 if (!$xactions) { 2276 return; 2277 } 2278 2279 $related_phids = $this->getFeedRelatedPHIDs($object, $xactions); 2280 $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions); 2281 2282 $story_type = $this->getFeedStoryType(); 2283 $story_data = $this->getFeedStoryData($object, $xactions); 2284 2285 id(new PhabricatorFeedStoryPublisher()) 2286 ->setStoryType($story_type) 2287 ->setStoryData($story_data) 2288 ->setStoryTime(time()) 2289 ->setStoryAuthorPHID($this->getActingAsPHID()) 2290 ->setRelatedPHIDs($related_phids) 2291 ->setPrimaryObjectPHID($object->getPHID()) 2292 ->setSubscribedPHIDs($subscribed_phids) 2293 ->setMailRecipientPHIDs($mailed_phids) 2294 ->setMailTags($this->getMailTags($object, $xactions)) 2295 ->publish(); 2296 } 2297 2298 2299 /* -( Search Index )------------------------------------------------------- */ 2300 2301 2302 /** 2303 * @task search 2304 */ 2305 protected function supportsSearch() { 2306 return false; 2307 } 2308 2309 2310 /* -( Herald Integration )-------------------------------------------------- */ 2311 2312 2313 protected function shouldApplyHeraldRules( 2314 PhabricatorLiskDAO $object, 2315 array $xactions) { 2316 return false; 2317 } 2318 2319 protected function buildHeraldAdapter( 2320 PhabricatorLiskDAO $object, 2321 array $xactions) { 2322 throw new Exception('No herald adapter specified.'); 2323 } 2324 2325 private function setHeraldAdapter(HeraldAdapter $adapter) { 2326 $this->heraldAdapter = $adapter; 2327 return $this; 2328 } 2329 2330 protected function getHeraldAdapter() { 2331 return $this->heraldAdapter; 2332 } 2333 2334 private function setHeraldTranscript(HeraldTranscript $transcript) { 2335 $this->heraldTranscript = $transcript; 2336 return $this; 2337 } 2338 2339 protected function getHeraldTranscript() { 2340 return $this->heraldTranscript; 2341 } 2342 2343 private function applyHeraldRules( 2344 PhabricatorLiskDAO $object, 2345 array $xactions) { 2346 2347 $adapter = $this->buildHeraldAdapter($object, $xactions); 2348 $adapter->setContentSource($this->getContentSource()); 2349 $adapter->setIsNewObject($this->getIsNewObject()); 2350 $xscript = HeraldEngine::loadAndApplyRules($adapter); 2351 2352 $this->setHeraldAdapter($adapter); 2353 $this->setHeraldTranscript($xscript); 2354 2355 return array_merge( 2356 $this->didApplyHeraldRules($object, $adapter, $xscript), 2357 $adapter->getQueuedTransactions()); 2358 } 2359 2360 protected function didApplyHeraldRules( 2361 PhabricatorLiskDAO $object, 2362 HeraldAdapter $adapter, 2363 HeraldTranscript $transcript) { 2364 return array(); 2365 } 2366 2367 2368 /* -( Custom Fields )------------------------------------------------------ */ 2369 2370 2371 /** 2372 * @task customfield 2373 */ 2374 private function getCustomFieldForTransaction( 2375 PhabricatorLiskDAO $object, 2376 PhabricatorApplicationTransaction $xaction) { 2377 2378 $field_key = $xaction->getMetadataValue('customfield:key'); 2379 if (!$field_key) { 2380 throw new Exception( 2381 "Custom field transaction has no 'customfield:key'!"); 2382 } 2383 2384 $field = PhabricatorCustomField::getObjectField( 2385 $object, 2386 PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, 2387 $field_key); 2388 2389 if (!$field) { 2390 throw new Exception( 2391 "Custom field transaction has invalid 'customfield:key'; field ". 2392 "'{$field_key}' is disabled or does not exist."); 2393 } 2394 2395 if (!$field->shouldAppearInApplicationTransactions()) { 2396 throw new Exception( 2397 "Custom field transaction '{$field_key}' does not implement ". 2398 "integration for ApplicationTransactions."); 2399 } 2400 2401 $field->setViewer($this->getActor()); 2402 2403 return $field; 2404 } 2405 2406 2407 /* -( Files )-------------------------------------------------------------- */ 2408 2409 2410 /** 2411 * Extract the PHIDs of any files which these transactions attach. 2412 * 2413 * @task files 2414 */ 2415 private function extractFilePHIDs( 2416 PhabricatorLiskDAO $object, 2417 array $xactions) { 2418 2419 $blocks = array(); 2420 foreach ($xactions as $xaction) { 2421 $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction); 2422 } 2423 $blocks = array_mergev($blocks); 2424 2425 $phids = array(); 2426 if ($blocks) { 2427 $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles( 2428 $this->getActor(), 2429 $blocks); 2430 } 2431 2432 foreach ($xactions as $xaction) { 2433 $phids[] = $this->extractFilePHIDsFromCustomTransaction( 2434 $object, 2435 $xaction); 2436 } 2437 2438 $phids = array_unique(array_filter(array_mergev($phids))); 2439 if (!$phids) { 2440 return array(); 2441 } 2442 2443 // Only let a user attach files they can actually see, since this would 2444 // otherwise let you access any file by attaching it to an object you have 2445 // view permission on. 2446 2447 $files = id(new PhabricatorFileQuery()) 2448 ->setViewer($this->getActor()) 2449 ->withPHIDs($phids) 2450 ->execute(); 2451 2452 return mpull($files, 'getPHID'); 2453 } 2454 2455 /** 2456 * @task files 2457 */ 2458 protected function extractFilePHIDsFromCustomTransaction( 2459 PhabricatorLiskDAO $object, 2460 PhabricatorApplicationTransaction $xaction) { 2461 return array(); 2462 } 2463 2464 2465 /** 2466 * @task files 2467 */ 2468 private function attachFiles( 2469 PhabricatorLiskDAO $object, 2470 array $file_phids) { 2471 2472 if (!$file_phids) { 2473 return; 2474 } 2475 2476 $editor = new PhabricatorEdgeEditor(); 2477 2478 $src = $object->getPHID(); 2479 $type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; 2480 foreach ($file_phids as $dst) { 2481 $editor->addEdge($src, $type, $dst); 2482 } 2483 2484 $editor->save(); 2485 } 2486 2487 private function applyInverseEdgeTransactions( 2488 PhabricatorLiskDAO $object, 2489 PhabricatorApplicationTransaction $xaction, 2490 $inverse_type) { 2491 2492 $old = $xaction->getOldValue(); 2493 $new = $xaction->getNewValue(); 2494 2495 $add = array_keys(array_diff_key($new, $old)); 2496 $rem = array_keys(array_diff_key($old, $new)); 2497 2498 $add = array_fuse($add); 2499 $rem = array_fuse($rem); 2500 $all = $add + $rem; 2501 2502 $nodes = id(new PhabricatorObjectQuery()) 2503 ->setViewer($this->requireActor()) 2504 ->withPHIDs($all) 2505 ->execute(); 2506 2507 foreach ($nodes as $node) { 2508 if (!($node instanceof PhabricatorApplicationTransactionInterface)) { 2509 continue; 2510 } 2511 2512 $editor = $node->getApplicationTransactionEditor(); 2513 $template = $node->getApplicationTransactionTemplate(); 2514 $target = $node->getApplicationTransactionObject(); 2515 2516 if (isset($add[$node->getPHID()])) { 2517 $edge_edit_type = '+'; 2518 } else { 2519 $edge_edit_type = '-'; 2520 } 2521 2522 $template 2523 ->setTransactionType($xaction->getTransactionType()) 2524 ->setMetadataValue('edge:type', $inverse_type) 2525 ->setNewValue( 2526 array( 2527 $edge_edit_type => array($object->getPHID() => $object->getPHID()), 2528 )); 2529 2530 $editor 2531 ->setContinueOnNoEffect(true) 2532 ->setContinueOnMissingFields(true) 2533 ->setParentMessageID($this->getParentMessageID()) 2534 ->setIsInverseEdgeEditor(true) 2535 ->setActor($this->requireActor()) 2536 ->setActingAsPHID($this->getActingAsPHID()) 2537 ->setContentSource($this->getContentSource()); 2538 2539 $editor->applyTransactions($target, array($template)); 2540 } 2541 } 2542 2543 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |