@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 recaptime-dev/main 943 lines 28 kB view raw
1<?php 2 3final class ManiphestTransactionEditor 4 extends PhabricatorApplicationTransactionEditor { 5 6 private $oldProjectPHIDs; 7 private $moreValidationErrors = array(); 8 9 public function getEditorApplicationClass() { 10 return PhabricatorManiphestApplication::class; 11 } 12 13 public function getEditorObjectsDescription() { 14 return pht('Maniphest Tasks'); 15 } 16 17 public function getTransactionTypes() { 18 $types = parent::getTransactionTypes(); 19 20 $types[] = PhabricatorTransactions::TYPE_COMMENT; 21 $types[] = PhabricatorTransactions::TYPE_EDGE; 22 $types[] = PhabricatorTransactions::TYPE_COLUMNS; 23 $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 24 $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; 25 26 return $types; 27 } 28 29 public function getCreateObjectTitle($author, $object) { 30 return pht('%s created this task.', $author); 31 } 32 33 public function getCreateObjectTitleForFeed($author, $object) { 34 return pht('%s created %s.', $author, $object); 35 } 36 37 protected function getCustomTransactionOldValue( 38 PhabricatorLiskDAO $object, 39 PhabricatorApplicationTransaction $xaction) { 40 41 switch ($xaction->getTransactionType()) { 42 case PhabricatorTransactions::TYPE_COLUMNS: 43 return null; 44 } 45 } 46 47 protected function getCustomTransactionNewValue( 48 PhabricatorLiskDAO $object, 49 PhabricatorApplicationTransaction $xaction) { 50 51 switch ($xaction->getTransactionType()) { 52 case PhabricatorTransactions::TYPE_COLUMNS: 53 return $xaction->getNewValue(); 54 } 55 } 56 57 protected function transactionHasEffect( 58 PhabricatorLiskDAO $object, 59 PhabricatorApplicationTransaction $xaction) { 60 61 $old = $xaction->getOldValue(); 62 $new = $xaction->getNewValue(); 63 64 switch ($xaction->getTransactionType()) { 65 case PhabricatorTransactions::TYPE_COLUMNS: 66 return (bool)$new; 67 } 68 69 return parent::transactionHasEffect($object, $xaction); 70 } 71 72 protected function applyCustomInternalTransaction( 73 PhabricatorLiskDAO $object, 74 PhabricatorApplicationTransaction $xaction) { 75 76 switch ($xaction->getTransactionType()) { 77 case PhabricatorTransactions::TYPE_COLUMNS: 78 return; 79 } 80 } 81 82 protected function applyCustomExternalTransaction( 83 PhabricatorLiskDAO $object, 84 PhabricatorApplicationTransaction $xaction) { 85 86 switch ($xaction->getTransactionType()) { 87 case PhabricatorTransactions::TYPE_COLUMNS: 88 foreach ($xaction->getNewValue() as $move) { 89 $this->applyBoardMove($object, $move); 90 } 91 break; 92 } 93 } 94 95 protected function applyFinalEffects( 96 PhabricatorLiskDAO $object, 97 array $xactions) { 98 99 // When we change the status of a task, update tasks this tasks blocks 100 // with a message to the effect of "alincoln resolved blocking task Txxx." 101 $unblock_xaction = null; 102 foreach ($xactions as $xaction) { 103 switch ($xaction->getTransactionType()) { 104 case ManiphestTaskParentTransaction::TRANSACTIONTYPE: 105 case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: 106 $unblock_xaction = $xaction; 107 break; 108 } 109 } 110 111 if ($unblock_xaction !== null) { 112 $blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 113 $object->getPHID(), 114 ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); 115 if ($blocked_phids) { 116 // In theory we could apply these through policies, but that seems a 117 // little bit surprising. For now, use the actor's vision. 118 $blocked_tasks = id(new ManiphestTaskQuery()) 119 ->setViewer($this->getActor()) 120 ->withPHIDs($blocked_phids) 121 ->needSubscriberPHIDs(true) 122 ->needProjectPHIDs(true) 123 ->execute(); 124 125 $old = $unblock_xaction->getOldValue(); 126 $new = $unblock_xaction->getNewValue(); 127 128 foreach ($blocked_tasks as $blocked_task) { 129 $parent_xaction = id(new ManiphestTransaction()) 130 ->setTransactionType( 131 ManiphestTaskUnblockTransaction::TRANSACTIONTYPE) 132 ->setOldValue(array($object->getPHID() => $old)) 133 ->setNewValue(array($object->getPHID() => $new)); 134 135 if ($this->getIsNewObject()) { 136 $parent_xaction->setMetadataValue('blocker.new', true); 137 } 138 139 $this->newSubEditor() 140 ->setContinueOnNoEffect(true) 141 ->setContinueOnMissingFields(true) 142 ->applyTransactions($blocked_task, array($parent_xaction)); 143 } 144 } 145 } 146 147 return $xactions; 148 } 149 150 protected function shouldSendMail( 151 PhabricatorLiskDAO $object, 152 array $xactions) { 153 return true; 154 } 155 156 protected function getMailSubjectPrefix() { 157 return pht('[Maniphest]'); 158 } 159 160 protected function getMailThreadID(PhabricatorLiskDAO $object) { 161 return 'maniphest-task-'.$object->getPHID(); 162 } 163 164 protected function getMailTo(PhabricatorLiskDAO $object) { 165 $phids = array(); 166 167 if ($object->getOwnerPHID()) { 168 $phids[] = $object->getOwnerPHID(); 169 } 170 $phids[] = $this->getActingAsPHID(); 171 172 return $phids; 173 } 174 175 public function getMailTagsMap() { 176 return array( 177 ManiphestTransaction::MAILTAG_STATUS => 178 pht("A task's status changes."), 179 ManiphestTransaction::MAILTAG_OWNER => 180 pht("A task's assignee changes."), 181 ManiphestTransaction::MAILTAG_PRIORITY => 182 pht("A task's priority changes."), 183 ManiphestTransaction::MAILTAG_CC => 184 pht("A task's subscribers change."), 185 ManiphestTransaction::MAILTAG_PROJECTS => 186 pht("A task's associated projects change."), 187 ManiphestTransaction::MAILTAG_UNBLOCK => 188 pht("One of a task's subtasks changes status."), 189 ManiphestTransaction::MAILTAG_COLUMN => 190 pht('A task is moved between columns on a workboard.'), 191 ManiphestTransaction::MAILTAG_COMMENT => 192 pht('Someone comments on a task.'), 193 ManiphestTransaction::MAILTAG_OTHER => 194 pht('Other task activity not listed above occurs.'), 195 ); 196 } 197 198 protected function buildReplyHandler(PhabricatorLiskDAO $object) { 199 return id(new ManiphestReplyHandler()) 200 ->setMailReceiver($object); 201 } 202 203 protected function buildMailTemplate(PhabricatorLiskDAO $object) { 204 $id = $object->getID(); 205 $title = $object->getTitle(); 206 207 return id(new PhabricatorMetaMTAMail()) 208 ->setSubject("T{$id}: {$title}"); 209 } 210 211 protected function getObjectLinkButtonLabelForMail( 212 PhabricatorLiskDAO $object) { 213 return pht('View Task'); 214 } 215 216 protected function buildMailBody( 217 PhabricatorLiskDAO $object, 218 array $xactions) { 219 220 $body = parent::buildMailBody($object, $xactions); 221 222 if ($this->getIsNewObject()) { 223 $body->addRemarkupSection( 224 pht('TASK DESCRIPTION'), 225 $object->getDescription()); 226 } 227 228 $body->addLinkSection( 229 pht('TASK DETAIL'), 230 $this->getObjectLinkButtonURIForMail($object)); 231 232 233 $board_phids = array(); 234 $type_columns = PhabricatorTransactions::TYPE_COLUMNS; 235 foreach ($xactions as $xaction) { 236 if ($xaction->getTransactionType() == $type_columns) { 237 $moves = $xaction->getNewValue(); 238 foreach ($moves as $move) { 239 $board_phids[] = $move['boardPHID']; 240 } 241 } 242 } 243 244 if ($board_phids) { 245 $projects = id(new PhabricatorProjectQuery()) 246 ->setViewer($this->requireActor()) 247 ->withPHIDs($board_phids) 248 ->execute(); 249 250 foreach ($projects as $project) { 251 $body->addLinkSection( 252 pht('WORKBOARD'), 253 PhabricatorEnv::getProductionURI($project->getWorkboardURI())); 254 } 255 } 256 257 258 return $body; 259 } 260 261 protected function shouldPublishFeedStory( 262 PhabricatorLiskDAO $object, 263 array $xactions) { 264 return true; 265 } 266 267 protected function supportsSearch() { 268 return true; 269 } 270 271 protected function shouldApplyHeraldRules( 272 PhabricatorLiskDAO $object, 273 array $xactions) { 274 return true; 275 } 276 277 protected function buildHeraldAdapter( 278 PhabricatorLiskDAO $object, 279 array $xactions) { 280 281 return id(new HeraldManiphestTaskAdapter()) 282 ->setTask($object); 283 } 284 285 protected function adjustObjectForPolicyChecks( 286 PhabricatorLiskDAO $object, 287 array $xactions) { 288 289 $copy = parent::adjustObjectForPolicyChecks($object, $xactions); 290 foreach ($xactions as $xaction) { 291 switch ($xaction->getTransactionType()) { 292 case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: 293 $copy->setOwnerPHID($xaction->getNewValue()); 294 break; 295 default: 296 break; 297 } 298 } 299 300 return $copy; 301 } 302 303 protected function validateAllTransactions( 304 PhabricatorLiskDAO $object, 305 array $xactions) { 306 307 $errors = parent::validateAllTransactions($object, $xactions); 308 309 if ($this->moreValidationErrors) { 310 $errors = array_merge($errors, $this->moreValidationErrors); 311 } 312 313 foreach ($this->getLockValidationErrors($object, $xactions) as $error) { 314 $errors[] = $error; 315 } 316 317 return $errors; 318 } 319 320 protected function expandTransactions( 321 PhabricatorLiskDAO $object, 322 array $xactions) { 323 324 $actor = $this->getActor(); 325 $actor_phid = $actor->getPHID(); 326 327 $results = parent::expandTransactions($object, $xactions); 328 329 $is_unassigned = ($object->getOwnerPHID() === null); 330 331 $any_xassign = null; 332 foreach ($xactions as $xaction) { 333 if ($xaction->getTransactionType() == 334 ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) { 335 $any_xassign = $xaction; 336 break; 337 } 338 } 339 340 $is_open = !$object->isClosed(); 341 342 $new_status = null; 343 foreach ($xactions as $xaction) { 344 switch ($xaction->getTransactionType()) { 345 case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: 346 $new_status = $xaction->getNewValue(); 347 break; 348 } 349 } 350 351 if ($new_status === null) { 352 $is_closing = false; 353 } else { 354 $is_closing = ManiphestTaskStatus::isClosedStatus($new_status); 355 } 356 357 // If the task is not assigned, not being assigned, currently open, and 358 // being closed, try to assign the actor as the owner. 359 // Don't assign the actor if they aren't a real user. 360 if ($is_unassigned && $is_open && $is_closing && $actor_phid) { 361 $is_autoclaim = ManiphestTaskStatus::isClaimStatus($new_status); 362 if ($is_autoclaim) { 363 if ($any_xassign === null) { 364 $results[] = id(new ManiphestTransaction()) 365 ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) 366 ->setNewValue($actor_phid); 367 } else if ($any_xassign->getNewValue() === null) { 368 // We have an explicit "Assign / Claim" = nothing in the frontend. 369 // The user is trying to "undo" the above automatic auto-claim. 370 // When saving, this would cause the "no effect" warning. 371 // So we suppress that confusing warning. 372 // https://we.phorge.it/T15164 373 $any_xassign->setIgnoreOnNoEffect(true); 374 } 375 } 376 } 377 378 // Automatically subscribe the author when they create a task. 379 if ($this->getIsNewObject()) { 380 if ($actor_phid) { 381 $results[] = id(new ManiphestTransaction()) 382 ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 383 ->setNewValue( 384 array( 385 '+' => array($actor_phid => $actor_phid), 386 )); 387 } 388 } 389 390 $send_notifications = PhabricatorNotificationClient::isEnabled(); 391 if ($send_notifications) { 392 $this->oldProjectPHIDs = $this->loadProjectPHIDs($object); 393 } 394 395 return $results; 396 } 397 398 protected function expandTransaction( 399 PhabricatorLiskDAO $object, 400 PhabricatorApplicationTransaction $xaction) { 401 402 $results = parent::expandTransaction($object, $xaction); 403 404 $type = $xaction->getTransactionType(); 405 switch ($type) { 406 case PhabricatorTransactions::TYPE_COLUMNS: 407 try { 408 $more_xactions = $this->buildMoveTransaction($object, $xaction); 409 foreach ($more_xactions as $more_xaction) { 410 $results[] = $more_xaction; 411 } 412 } catch (Exception $ex) { 413 $error = new PhabricatorApplicationTransactionValidationError( 414 $type, 415 pht('Invalid'), 416 $ex->getMessage(), 417 $xaction); 418 $this->moreValidationErrors[] = $error; 419 } 420 break; 421 case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: 422 // If this is a no-op update, don't expand it. 423 $old_value = $object->getOwnerPHID(); 424 $new_value = $xaction->getNewValue(); 425 if ($old_value === $new_value) { 426 break; 427 } 428 429 // When a task is reassigned, move the old owner to the subscriber 430 // list so they're still in the loop. 431 if ($old_value) { 432 $results[] = id(new ManiphestTransaction()) 433 ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 434 ->setIgnoreOnNoEffect(true) 435 ->setNewValue( 436 array( 437 '+' => array($old_value => $old_value), 438 )); 439 } 440 break; 441 } 442 443 return $results; 444 } 445 446 private function buildMoveTransaction( 447 PhabricatorLiskDAO $object, 448 PhabricatorApplicationTransaction $xaction) { 449 $actor = $this->getActor(); 450 451 $new = $xaction->getNewValue(); 452 if (!is_array($new)) { 453 $this->validateColumnPHID($new); 454 $new = array($new); 455 } 456 457 $relative_phids = array(); 458 foreach ($new as $key => $value) { 459 if (!is_array($value)) { 460 $this->validateColumnPHID($value); 461 $value = array( 462 'columnPHID' => $value, 463 ); 464 } 465 466 PhutilTypeSpec::checkMap( 467 $value, 468 array( 469 'columnPHID' => 'string', 470 'beforePHIDs' => 'optional list<string>', 471 'afterPHIDs' => 'optional list<string>', 472 473 // Deprecated older variations of "beforePHIDs" and "afterPHIDs". 474 'beforePHID' => 'optional string', 475 'afterPHID' => 'optional string', 476 )); 477 478 $value = $value + array( 479 'beforePHIDs' => array(), 480 'afterPHIDs' => array(), 481 ); 482 483 // Normalize the legacy keys "beforePHID" and "afterPHID" keys to the 484 // modern format. 485 if (!empty($value['afterPHID'])) { 486 if ($value['afterPHIDs']) { 487 throw new Exception( 488 pht( 489 'Transaction specifies both "afterPHID" and "afterPHIDs". '. 490 'Specify only "afterPHIDs".')); 491 } 492 $value['afterPHIDs'] = array($value['afterPHID']); 493 unset($value['afterPHID']); 494 } 495 496 if (isset($value['beforePHID'])) { 497 if ($value['beforePHIDs']) { 498 throw new Exception( 499 pht( 500 'Transaction specifies both "beforePHID" and "beforePHIDs". '. 501 'Specify only "beforePHIDs".')); 502 } 503 $value['beforePHIDs'] = array($value['beforePHID']); 504 unset($value['beforePHID']); 505 } 506 507 foreach ($value['beforePHIDs'] as $phid) { 508 $relative_phids[] = $phid; 509 } 510 511 foreach ($value['afterPHIDs'] as $phid) { 512 $relative_phids[] = $phid; 513 } 514 515 $new[$key] = $value; 516 } 517 518 // We require that objects you specify in "beforePHIDs" or "afterPHIDs" 519 // are real objects which exist and which you have permission to view. 520 // If you provide other objects, we remove them from the specification. 521 522 if ($relative_phids) { 523 $objects = id(new PhabricatorObjectQuery()) 524 ->setViewer($actor) 525 ->withPHIDs($relative_phids) 526 ->execute(); 527 $objects = mpull($objects, null, 'getPHID'); 528 } else { 529 $objects = array(); 530 } 531 532 foreach ($new as $key => $value) { 533 $value['afterPHIDs'] = $this->filterValidPHIDs( 534 $value['afterPHIDs'], 535 $objects); 536 $value['beforePHIDs'] = $this->filterValidPHIDs( 537 $value['beforePHIDs'], 538 $objects); 539 540 $new[$key] = $value; 541 } 542 543 $column_phids = ipull($new, 'columnPHID'); 544 if ($column_phids) { 545 $columns = id(new PhabricatorProjectColumnQuery()) 546 ->setViewer($actor) 547 ->withPHIDs($column_phids) 548 ->execute(); 549 $columns = mpull($columns, null, 'getPHID'); 550 } else { 551 $columns = array(); 552 } 553 554 $board_phids = mpull($columns, 'getProjectPHID'); 555 $object_phid = $object->getPHID(); 556 557 // Note that we may not have an object PHID if we're creating a new 558 // object. 559 $object_phids = array(); 560 if ($object_phid) { 561 $object_phids[] = $object_phid; 562 } 563 564 if ($object_phids) { 565 $layout_engine = id(new PhabricatorBoardLayoutEngine()) 566 ->setViewer($this->getActor()) 567 ->setBoardPHIDs($board_phids) 568 ->setObjectPHIDs($object_phids) 569 ->setFetchAllBoards(true) 570 ->executeLayout(); 571 } 572 573 foreach ($new as $key => $spec) { 574 $column_phid = $spec['columnPHID']; 575 $column = idx($columns, $column_phid); 576 if (!$column) { 577 throw new Exception( 578 pht( 579 'Column move transaction specifies column PHID "%s", but there '. 580 'is no corresponding column with this PHID.', 581 $column_phid)); 582 } 583 584 $board_phid = $column->getProjectPHID(); 585 586 if ($object_phid) { 587 $old_columns = $layout_engine->getObjectColumns( 588 $board_phid, 589 $object_phid); 590 $old_column_phids = mpull($old_columns, 'getPHID'); 591 } else { 592 $old_column_phids = array(); 593 } 594 595 $spec += array( 596 'boardPHID' => $board_phid, 597 'fromColumnPHIDs' => $old_column_phids, 598 ); 599 600 // Check if the object is already in this column, and isn't being moved. 601 // We can just drop this column change if it has no effect. 602 $from_map = array_fuse($spec['fromColumnPHIDs']); 603 $already_here = isset($from_map[$column_phid]); 604 605 $is_reordering = ($spec['afterPHIDs'] || $spec['beforePHIDs']); 606 if ($already_here && !$is_reordering) { 607 unset($new[$key]); 608 } else { 609 $new[$key] = $spec; 610 } 611 } 612 613 $new = array_values($new); 614 $xaction->setNewValue($new); 615 616 617 $more = array(); 618 619 // If we're moving the object into a column and it does not already belong 620 // in the column, add the appropriate board. For normal columns, this 621 // is the board PHID. For proxy columns, it is the proxy PHID, unless the 622 // object is already a member of some descendant of the proxy PHID. 623 624 // The major case where this can happen is moves via the API, but it also 625 // happens when a user drags a task from the "Backlog" to a milestone 626 // column. 627 628 if ($object_phid) { 629 $current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 630 $object_phid, 631 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 632 $current_phids = array_fuse($current_phids); 633 } else { 634 $current_phids = array(); 635 } 636 637 $add_boards = array(); 638 foreach ($new as $move) { 639 $column_phid = $move['columnPHID']; 640 $board_phid = $move['boardPHID']; 641 $column = $columns[$column_phid]; 642 $proxy_phid = $column->getProxyPHID(); 643 644 // If this is a normal column, add the board if the object isn't already 645 // associated. 646 if (!$proxy_phid) { 647 if (!isset($current_phids[$board_phid])) { 648 $add_boards[] = $board_phid; 649 } 650 continue; 651 } 652 653 // If this is a proxy column but the object is already associated with 654 // the proxy board, we don't need to do anything. 655 if (isset($current_phids[$proxy_phid])) { 656 continue; 657 } 658 659 // If this a proxy column and the object is already associated with some 660 // descendant of the proxy board, we also don't need to do anything. 661 $descendants = id(new PhabricatorProjectQuery()) 662 ->setViewer(PhabricatorUser::getOmnipotentUser()) 663 ->withAncestorProjectPHIDs(array($proxy_phid)) 664 ->execute(); 665 666 $found_descendant = false; 667 foreach ($descendants as $descendant) { 668 if (isset($current_phids[$descendant->getPHID()])) { 669 $found_descendant = true; 670 break; 671 } 672 } 673 674 if ($found_descendant) { 675 continue; 676 } 677 678 // Otherwise, we're moving the object to a proxy column which it is not 679 // a member of yet, so add an association to the column's proxy board. 680 681 $add_boards[] = $proxy_phid; 682 } 683 684 if ($add_boards) { 685 $more[] = id(new ManiphestTransaction()) 686 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 687 ->setMetadataValue( 688 'edge:type', 689 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) 690 ->setIgnoreOnNoEffect(true) 691 ->setNewValue( 692 array( 693 '+' => array_fuse($add_boards), 694 )); 695 } 696 697 return $more; 698 } 699 700 private function applyBoardMove($object, array $move) { 701 $board_phid = $move['boardPHID']; 702 $column_phid = $move['columnPHID']; 703 704 $before_phids = $move['beforePHIDs']; 705 $after_phids = $move['afterPHIDs']; 706 707 $object_phid = $object->getPHID(); 708 709 // We're doing layout with the omnipotent viewer to make sure we don't 710 // remove positions in columns that exist, but which the actual actor 711 // can't see. 712 $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); 713 714 $select_phids = array($board_phid); 715 716 $descendants = id(new PhabricatorProjectQuery()) 717 ->setViewer($omnipotent_viewer) 718 ->withAncestorProjectPHIDs($select_phids) 719 ->execute(); 720 foreach ($descendants as $descendant) { 721 $select_phids[] = $descendant->getPHID(); 722 } 723 724 $board_tasks = id(new ManiphestTaskQuery()) 725 ->setViewer($omnipotent_viewer) 726 ->withEdgeLogicPHIDs( 727 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 728 PhabricatorQueryConstraint::OPERATOR_ANCESTOR, 729 array($select_phids)) 730 ->execute(); 731 732 $board_tasks = mpull($board_tasks, null, 'getPHID'); 733 $board_tasks[$object_phid] = $object; 734 735 // Make sure tasks are sorted by ID, so we lay out new positions in 736 // a consistent way. 737 $board_tasks = msort($board_tasks, 'getID'); 738 739 $object_phids = array_keys($board_tasks); 740 741 $engine = id(new PhabricatorBoardLayoutEngine()) 742 ->setViewer($omnipotent_viewer) 743 ->setBoardPHIDs(array($board_phid)) 744 ->setObjectPHIDs($object_phids) 745 ->executeLayout(); 746 747 // TODO: This logic needs to be revised when we legitimately support 748 // multiple column positions. 749 $columns = $engine->getObjectColumns($board_phid, $object_phid); 750 foreach ($columns as $column) { 751 $engine->queueRemovePosition( 752 $board_phid, 753 $column->getPHID(), 754 $object_phid); 755 } 756 757 $engine->queueAddPosition( 758 $board_phid, 759 $column_phid, 760 $object_phid, 761 $after_phids, 762 $before_phids); 763 764 $engine->applyPositionUpdates(); 765 } 766 767 768 private function validateColumnPHID($value) { 769 if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) { 770 return; 771 } 772 773 throw new Exception( 774 pht( 775 'When moving objects between columns on a board, columns must '. 776 'be identified by PHIDs. This transaction uses "%s" to identify '. 777 'a column, but that is not a valid column PHID.', 778 $value)); 779 } 780 781 782 private function getLockValidationErrors($object, array $xactions) { 783 $errors = array(); 784 785 $old_owner = $object->getOwnerPHID(); 786 $old_status = $object->getStatus(); 787 788 $new_owner = $old_owner; 789 $new_status = $old_status; 790 791 $owner_xaction = null; 792 $status_xaction = null; 793 794 foreach ($xactions as $xaction) { 795 switch ($xaction->getTransactionType()) { 796 case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: 797 $new_owner = $xaction->getNewValue(); 798 $owner_xaction = $xaction; 799 break; 800 case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: 801 $new_status = $xaction->getNewValue(); 802 $status_xaction = $xaction; 803 break; 804 } 805 } 806 807 $actor_phid = $this->getActingAsPHID(); 808 809 $was_locked = ManiphestTaskStatus::areEditsLockedInStatus( 810 $old_status); 811 $now_locked = ManiphestTaskStatus::areEditsLockedInStatus( 812 $new_status); 813 814 if (!$now_locked) { 815 // If we're not ending in an edit-locked status, everything is good. 816 } else if ($new_owner !== null) { 817 // If we ending the edit with some valid owner, this is allowed for 818 // now. We might need to revisit this. 819 } else { 820 // The edits end with the task locked and unowned. No one will be able 821 // to edit it, so we forbid this. We try to be specific about what the 822 // user did wrong. 823 824 $owner_changed = ($old_owner && !$new_owner); 825 $status_changed = ($was_locked !== $now_locked); 826 $message = null; 827 828 if ($status_changed && $owner_changed) { 829 $message = pht( 830 'You can not lock this task and unassign it at the same time '. 831 'because no one will be able to edit it anymore. Lock the task '. 832 'or remove the assignee, but not both.'); 833 $problem_xaction = $status_xaction; 834 } else if ($status_changed) { 835 $message = pht( 836 'You can not lock this task because it does not have an assignee. '. 837 'No one would be able to edit the task. Assign the task to an '. 838 'assignee before locking it.'); 839 $problem_xaction = $status_xaction; 840 } else if ($owner_changed) { 841 $message = pht( 842 'You can not remove the assignee of this task because it is locked '. 843 'and no one would be able to edit the task. Reassign the task or '. 844 'unlock it before removing the assignee.'); 845 $problem_xaction = $owner_xaction; 846 } else { 847 // If the task was already broken, we don't have a transaction to 848 // complain about so just let it through. In theory, this is 849 // impossible since policy rules should kick in before we get here. 850 } 851 852 if ($message) { 853 $errors[] = new PhabricatorApplicationTransactionValidationError( 854 $problem_xaction->getTransactionType(), 855 pht('Lock Error'), 856 $message, 857 $problem_xaction); 858 } 859 } 860 861 return $errors; 862 } 863 864 private function filterValidPHIDs($phid_list, array $object_map) { 865 foreach ($phid_list as $key => $phid) { 866 if (isset($object_map[$phid])) { 867 continue; 868 } 869 870 unset($phid_list[$key]); 871 } 872 873 return array_values($phid_list); 874 } 875 876 protected function didApplyTransactions($object, array $xactions) { 877 $send_notifications = PhabricatorNotificationClient::isEnabled(); 878 if ($send_notifications) { 879 $old_phids = $this->oldProjectPHIDs; 880 $new_phids = $this->loadProjectPHIDs($object); 881 882 // We want to emit update notifications for all old and new tagged 883 // projects, and all parents of those projects. For example, if an 884 // edit removes project "A > B" from a task, the "A" workboard should 885 // receive an update event. 886 887 $project_phids = array_fuse($old_phids) + array_fuse($new_phids); 888 $project_phids = array_keys($project_phids); 889 890 if ($project_phids) { 891 $projects = id(new PhabricatorProjectQuery()) 892 ->setViewer(PhabricatorUser::getOmnipotentUser()) 893 ->withPHIDs($project_phids) 894 ->execute(); 895 896 $notify_projects = array(); 897 foreach ($projects as $project) { 898 $notify_projects[$project->getPHID()] = $project; 899 foreach ($project->getAncestorProjects() as $ancestor) { 900 $notify_projects[$ancestor->getPHID()] = $ancestor; 901 } 902 } 903 904 foreach ($notify_projects as $key => $project) { 905 if (!$project->getHasWorkboard()) { 906 unset($notify_projects[$key]); 907 } 908 } 909 910 $notify_phids = array_keys($notify_projects); 911 912 if ($notify_phids) { 913 $data = array( 914 'type' => 'workboards', 915 'subscribers' => $notify_phids, 916 ); 917 918 PhabricatorNotificationClient::tryToPostMessage($data); 919 } 920 } 921 } 922 923 return $xactions; 924 } 925 926 private function loadProjectPHIDs(ManiphestTask $task) { 927 if (!$task->getPHID()) { 928 return array(); 929 } 930 931 $edge_query = id(new PhabricatorEdgeQuery()) 932 ->withSourcePHIDs(array($task->getPHID())) 933 ->withEdgeTypes( 934 array( 935 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 936 )); 937 938 $edge_query->execute(); 939 940 return $edge_query->getDestinationPHIDs(); 941 } 942 943}