Remove all "FileHasObject" edge reads and writes
[phabricator.git] / src / applications / maniphest / editor / ManiphestTransactionEditor.php
blob01fc0af83d6be1958c9f493601d39402391dcc9e
1 <?php
3 final class ManiphestTransactionEditor
4 extends PhabricatorApplicationTransactionEditor {
6 private $oldProjectPHIDs;
7 private $moreValidationErrors = array();
9 public function getEditorApplicationClass() {
10 return 'PhabricatorManiphestApplication';
13 public function getEditorObjectsDescription() {
14 return pht('Maniphest Tasks');
17 public function getTransactionTypes() {
18 $types = parent::getTransactionTypes();
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;
26 return $types;
29 public function getCreateObjectTitle($author, $object) {
30 return pht('%s created this task.', $author);
33 public function getCreateObjectTitleForFeed($author, $object) {
34 return pht('%s created %s.', $author, $object);
37 protected function getCustomTransactionOldValue(
38 PhabricatorLiskDAO $object,
39 PhabricatorApplicationTransaction $xaction) {
41 switch ($xaction->getTransactionType()) {
42 case PhabricatorTransactions::TYPE_COLUMNS:
43 return null;
47 protected function getCustomTransactionNewValue(
48 PhabricatorLiskDAO $object,
49 PhabricatorApplicationTransaction $xaction) {
51 switch ($xaction->getTransactionType()) {
52 case PhabricatorTransactions::TYPE_COLUMNS:
53 return $xaction->getNewValue();
57 protected function transactionHasEffect(
58 PhabricatorLiskDAO $object,
59 PhabricatorApplicationTransaction $xaction) {
61 $old = $xaction->getOldValue();
62 $new = $xaction->getNewValue();
64 switch ($xaction->getTransactionType()) {
65 case PhabricatorTransactions::TYPE_COLUMNS:
66 return (bool)$new;
69 return parent::transactionHasEffect($object, $xaction);
72 protected function applyCustomInternalTransaction(
73 PhabricatorLiskDAO $object,
74 PhabricatorApplicationTransaction $xaction) {
76 switch ($xaction->getTransactionType()) {
77 case PhabricatorTransactions::TYPE_COLUMNS:
78 return;
82 protected function applyCustomExternalTransaction(
83 PhabricatorLiskDAO $object,
84 PhabricatorApplicationTransaction $xaction) {
86 switch ($xaction->getTransactionType()) {
87 case PhabricatorTransactions::TYPE_COLUMNS:
88 foreach ($xaction->getNewValue() as $move) {
89 $this->applyBoardMove($object, $move);
91 break;
95 protected function applyFinalEffects(
96 PhabricatorLiskDAO $object,
97 array $xactions) {
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 ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
105 $unblock_xaction = $xaction;
106 break;
110 if ($unblock_xaction !== null) {
111 $blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
112 $object->getPHID(),
113 ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
114 if ($blocked_phids) {
115 // In theory we could apply these through policies, but that seems a
116 // little bit surprising. For now, use the actor's vision.
117 $blocked_tasks = id(new ManiphestTaskQuery())
118 ->setViewer($this->getActor())
119 ->withPHIDs($blocked_phids)
120 ->needSubscriberPHIDs(true)
121 ->needProjectPHIDs(true)
122 ->execute();
124 $old = $unblock_xaction->getOldValue();
125 $new = $unblock_xaction->getNewValue();
127 foreach ($blocked_tasks as $blocked_task) {
128 $parent_xaction = id(new ManiphestTransaction())
129 ->setTransactionType(
130 ManiphestTaskUnblockTransaction::TRANSACTIONTYPE)
131 ->setOldValue(array($object->getPHID() => $old))
132 ->setNewValue(array($object->getPHID() => $new));
134 if ($this->getIsNewObject()) {
135 $parent_xaction->setMetadataValue('blocker.new', true);
138 $this->newSubEditor()
139 ->setContinueOnNoEffect(true)
140 ->setContinueOnMissingFields(true)
141 ->applyTransactions($blocked_task, array($parent_xaction));
146 return $xactions;
149 protected function shouldSendMail(
150 PhabricatorLiskDAO $object,
151 array $xactions) {
152 return true;
155 protected function getMailSubjectPrefix() {
156 return pht('[Maniphest]');
159 protected function getMailThreadID(PhabricatorLiskDAO $object) {
160 return 'maniphest-task-'.$object->getPHID();
163 protected function getMailTo(PhabricatorLiskDAO $object) {
164 $phids = array();
166 if ($object->getOwnerPHID()) {
167 $phids[] = $object->getOwnerPHID();
169 $phids[] = $this->getActingAsPHID();
171 return $phids;
174 public function getMailTagsMap() {
175 return array(
176 ManiphestTransaction::MAILTAG_STATUS =>
177 pht("A task's status changes."),
178 ManiphestTransaction::MAILTAG_OWNER =>
179 pht("A task's owner changes."),
180 ManiphestTransaction::MAILTAG_PRIORITY =>
181 pht("A task's priority changes."),
182 ManiphestTransaction::MAILTAG_CC =>
183 pht("A task's subscribers change."),
184 ManiphestTransaction::MAILTAG_PROJECTS =>
185 pht("A task's associated projects change."),
186 ManiphestTransaction::MAILTAG_UNBLOCK =>
187 pht("One of a task's subtasks changes status."),
188 ManiphestTransaction::MAILTAG_COLUMN =>
189 pht('A task is moved between columns on a workboard.'),
190 ManiphestTransaction::MAILTAG_COMMENT =>
191 pht('Someone comments on a task.'),
192 ManiphestTransaction::MAILTAG_OTHER =>
193 pht('Other task activity not listed above occurs.'),
197 protected function buildReplyHandler(PhabricatorLiskDAO $object) {
198 return id(new ManiphestReplyHandler())
199 ->setMailReceiver($object);
202 protected function buildMailTemplate(PhabricatorLiskDAO $object) {
203 $id = $object->getID();
204 $title = $object->getTitle();
206 return id(new PhabricatorMetaMTAMail())
207 ->setSubject("T{$id}: {$title}");
210 protected function getObjectLinkButtonLabelForMail(
211 PhabricatorLiskDAO $object) {
212 return pht('View Task');
215 protected function buildMailBody(
216 PhabricatorLiskDAO $object,
217 array $xactions) {
219 $body = parent::buildMailBody($object, $xactions);
221 if ($this->getIsNewObject()) {
222 $body->addRemarkupSection(
223 pht('TASK DESCRIPTION'),
224 $object->getDescription());
227 $body->addLinkSection(
228 pht('TASK DETAIL'),
229 $this->getObjectLinkButtonURIForMail($object));
232 $board_phids = array();
233 $type_columns = PhabricatorTransactions::TYPE_COLUMNS;
234 foreach ($xactions as $xaction) {
235 if ($xaction->getTransactionType() == $type_columns) {
236 $moves = $xaction->getNewValue();
237 foreach ($moves as $move) {
238 $board_phids[] = $move['boardPHID'];
243 if ($board_phids) {
244 $projects = id(new PhabricatorProjectQuery())
245 ->setViewer($this->requireActor())
246 ->withPHIDs($board_phids)
247 ->execute();
249 foreach ($projects as $project) {
250 $body->addLinkSection(
251 pht('WORKBOARD'),
252 PhabricatorEnv::getProductionURI($project->getWorkboardURI()));
257 return $body;
260 protected function shouldPublishFeedStory(
261 PhabricatorLiskDAO $object,
262 array $xactions) {
263 return true;
266 protected function supportsSearch() {
267 return true;
270 protected function shouldApplyHeraldRules(
271 PhabricatorLiskDAO $object,
272 array $xactions) {
273 return true;
276 protected function buildHeraldAdapter(
277 PhabricatorLiskDAO $object,
278 array $xactions) {
280 return id(new HeraldManiphestTaskAdapter())
281 ->setTask($object);
284 protected function adjustObjectForPolicyChecks(
285 PhabricatorLiskDAO $object,
286 array $xactions) {
288 $copy = parent::adjustObjectForPolicyChecks($object, $xactions);
289 foreach ($xactions as $xaction) {
290 switch ($xaction->getTransactionType()) {
291 case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
292 $copy->setOwnerPHID($xaction->getNewValue());
293 break;
294 default:
295 break;
299 return $copy;
302 protected function validateAllTransactions(
303 PhabricatorLiskDAO $object,
304 array $xactions) {
306 $errors = parent::validateAllTransactions($object, $xactions);
308 if ($this->moreValidationErrors) {
309 $errors = array_merge($errors, $this->moreValidationErrors);
312 foreach ($this->getLockValidationErrors($object, $xactions) as $error) {
313 $errors[] = $error;
316 return $errors;
319 protected function expandTransactions(
320 PhabricatorLiskDAO $object,
321 array $xactions) {
323 $actor = $this->getActor();
324 $actor_phid = $actor->getPHID();
326 $results = parent::expandTransactions($object, $xactions);
328 $is_unassigned = ($object->getOwnerPHID() === null);
330 $any_assign = false;
331 foreach ($xactions as $xaction) {
332 if ($xaction->getTransactionType() ==
333 ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) {
334 $any_assign = true;
335 break;
339 $is_open = !$object->isClosed();
341 $new_status = null;
342 foreach ($xactions as $xaction) {
343 switch ($xaction->getTransactionType()) {
344 case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
345 $new_status = $xaction->getNewValue();
346 break;
350 if ($new_status === null) {
351 $is_closing = false;
352 } else {
353 $is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
356 // If the task is not assigned, not being assigned, currently open, and
357 // being closed, try to assign the actor as the owner.
358 if ($is_unassigned && !$any_assign && $is_open && $is_closing) {
359 $is_claim = ManiphestTaskStatus::isClaimStatus($new_status);
361 // Don't assign the actor if they aren't a real user.
362 // Don't claim the task if the status is configured to not claim.
363 if ($actor_phid && $is_claim) {
364 $results[] = id(new ManiphestTransaction())
365 ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
366 ->setNewValue($actor_phid);
370 // Automatically subscribe the author when they create a task.
371 if ($this->getIsNewObject()) {
372 if ($actor_phid) {
373 $results[] = id(new ManiphestTransaction())
374 ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
375 ->setNewValue(
376 array(
377 '+' => array($actor_phid => $actor_phid),
382 $send_notifications = PhabricatorNotificationClient::isEnabled();
383 if ($send_notifications) {
384 $this->oldProjectPHIDs = $this->loadProjectPHIDs($object);
387 return $results;
390 protected function expandTransaction(
391 PhabricatorLiskDAO $object,
392 PhabricatorApplicationTransaction $xaction) {
394 $results = parent::expandTransaction($object, $xaction);
396 $type = $xaction->getTransactionType();
397 switch ($type) {
398 case PhabricatorTransactions::TYPE_COLUMNS:
399 try {
400 $more_xactions = $this->buildMoveTransaction($object, $xaction);
401 foreach ($more_xactions as $more_xaction) {
402 $results[] = $more_xaction;
404 } catch (Exception $ex) {
405 $error = new PhabricatorApplicationTransactionValidationError(
406 $type,
407 pht('Invalid'),
408 $ex->getMessage(),
409 $xaction);
410 $this->moreValidationErrors[] = $error;
412 break;
413 case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
414 // If this is a no-op update, don't expand it.
415 $old_value = $object->getOwnerPHID();
416 $new_value = $xaction->getNewValue();
417 if ($old_value === $new_value) {
418 break;
421 // When a task is reassigned, move the old owner to the subscriber
422 // list so they're still in the loop.
423 if ($old_value) {
424 $results[] = id(new ManiphestTransaction())
425 ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
426 ->setIgnoreOnNoEffect(true)
427 ->setNewValue(
428 array(
429 '+' => array($old_value => $old_value),
432 break;
435 return $results;
438 private function buildMoveTransaction(
439 PhabricatorLiskDAO $object,
440 PhabricatorApplicationTransaction $xaction) {
441 $actor = $this->getActor();
443 $new = $xaction->getNewValue();
444 if (!is_array($new)) {
445 $this->validateColumnPHID($new);
446 $new = array($new);
449 $relative_phids = array();
450 foreach ($new as $key => $value) {
451 if (!is_array($value)) {
452 $this->validateColumnPHID($value);
453 $value = array(
454 'columnPHID' => $value,
458 PhutilTypeSpec::checkMap(
459 $value,
460 array(
461 'columnPHID' => 'string',
462 'beforePHIDs' => 'optional list<string>',
463 'afterPHIDs' => 'optional list<string>',
465 // Deprecated older variations of "beforePHIDs" and "afterPHIDs".
466 'beforePHID' => 'optional string',
467 'afterPHID' => 'optional string',
470 $value = $value + array(
471 'beforePHIDs' => array(),
472 'afterPHIDs' => array(),
475 // Normalize the legacy keys "beforePHID" and "afterPHID" keys to the
476 // modern format.
477 if (!empty($value['afterPHID'])) {
478 if ($value['afterPHIDs']) {
479 throw new Exception(
480 pht(
481 'Transaction specifies both "afterPHID" and "afterPHIDs". '.
482 'Specify only "afterPHIDs".'));
484 $value['afterPHIDs'] = array($value['afterPHID']);
485 unset($value['afterPHID']);
488 if (isset($value['beforePHID'])) {
489 if ($value['beforePHIDs']) {
490 throw new Exception(
491 pht(
492 'Transaction specifies both "beforePHID" and "beforePHIDs". '.
493 'Specify only "beforePHIDs".'));
495 $value['beforePHIDs'] = array($value['beforePHID']);
496 unset($value['beforePHID']);
499 foreach ($value['beforePHIDs'] as $phid) {
500 $relative_phids[] = $phid;
503 foreach ($value['afterPHIDs'] as $phid) {
504 $relative_phids[] = $phid;
507 $new[$key] = $value;
510 // We require that objects you specify in "beforePHIDs" or "afterPHIDs"
511 // are real objects which exist and which you have permission to view.
512 // If you provide other objects, we remove them from the specification.
514 if ($relative_phids) {
515 $objects = id(new PhabricatorObjectQuery())
516 ->setViewer($actor)
517 ->withPHIDs($relative_phids)
518 ->execute();
519 $objects = mpull($objects, null, 'getPHID');
520 } else {
521 $objects = array();
524 foreach ($new as $key => $value) {
525 $value['afterPHIDs'] = $this->filterValidPHIDs(
526 $value['afterPHIDs'],
527 $objects);
528 $value['beforePHIDs'] = $this->filterValidPHIDs(
529 $value['beforePHIDs'],
530 $objects);
532 $new[$key] = $value;
535 $column_phids = ipull($new, 'columnPHID');
536 if ($column_phids) {
537 $columns = id(new PhabricatorProjectColumnQuery())
538 ->setViewer($actor)
539 ->withPHIDs($column_phids)
540 ->execute();
541 $columns = mpull($columns, null, 'getPHID');
542 } else {
543 $columns = array();
546 $board_phids = mpull($columns, 'getProjectPHID');
547 $object_phid = $object->getPHID();
549 // Note that we may not have an object PHID if we're creating a new
550 // object.
551 $object_phids = array();
552 if ($object_phid) {
553 $object_phids[] = $object_phid;
556 if ($object_phids) {
557 $layout_engine = id(new PhabricatorBoardLayoutEngine())
558 ->setViewer($this->getActor())
559 ->setBoardPHIDs($board_phids)
560 ->setObjectPHIDs($object_phids)
561 ->setFetchAllBoards(true)
562 ->executeLayout();
565 foreach ($new as $key => $spec) {
566 $column_phid = $spec['columnPHID'];
567 $column = idx($columns, $column_phid);
568 if (!$column) {
569 throw new Exception(
570 pht(
571 'Column move transaction specifies column PHID "%s", but there '.
572 'is no corresponding column with this PHID.',
573 $column_phid));
576 $board_phid = $column->getProjectPHID();
578 if ($object_phid) {
579 $old_columns = $layout_engine->getObjectColumns(
580 $board_phid,
581 $object_phid);
582 $old_column_phids = mpull($old_columns, 'getPHID');
583 } else {
584 $old_column_phids = array();
587 $spec += array(
588 'boardPHID' => $board_phid,
589 'fromColumnPHIDs' => $old_column_phids,
592 // Check if the object is already in this column, and isn't being moved.
593 // We can just drop this column change if it has no effect.
594 $from_map = array_fuse($spec['fromColumnPHIDs']);
595 $already_here = isset($from_map[$column_phid]);
597 $is_reordering = ($spec['afterPHIDs'] || $spec['beforePHIDs']);
598 if ($already_here && !$is_reordering) {
599 unset($new[$key]);
600 } else {
601 $new[$key] = $spec;
605 $new = array_values($new);
606 $xaction->setNewValue($new);
609 $more = array();
611 // If we're moving the object into a column and it does not already belong
612 // in the column, add the appropriate board. For normal columns, this
613 // is the board PHID. For proxy columns, it is the proxy PHID, unless the
614 // object is already a member of some descendant of the proxy PHID.
616 // The major case where this can happen is moves via the API, but it also
617 // happens when a user drags a task from the "Backlog" to a milestone
618 // column.
620 if ($object_phid) {
621 $current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
622 $object_phid,
623 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
624 $current_phids = array_fuse($current_phids);
625 } else {
626 $current_phids = array();
629 $add_boards = array();
630 foreach ($new as $move) {
631 $column_phid = $move['columnPHID'];
632 $board_phid = $move['boardPHID'];
633 $column = $columns[$column_phid];
634 $proxy_phid = $column->getProxyPHID();
636 // If this is a normal column, add the board if the object isn't already
637 // associated.
638 if (!$proxy_phid) {
639 if (!isset($current_phids[$board_phid])) {
640 $add_boards[] = $board_phid;
642 continue;
645 // If this is a proxy column but the object is already associated with
646 // the proxy board, we don't need to do anything.
647 if (isset($current_phids[$proxy_phid])) {
648 continue;
651 // If this a proxy column and the object is already associated with some
652 // descendant of the proxy board, we also don't need to do anything.
653 $descendants = id(new PhabricatorProjectQuery())
654 ->setViewer(PhabricatorUser::getOmnipotentUser())
655 ->withAncestorProjectPHIDs(array($proxy_phid))
656 ->execute();
658 $found_descendant = false;
659 foreach ($descendants as $descendant) {
660 if (isset($current_phids[$descendant->getPHID()])) {
661 $found_descendant = true;
662 break;
666 if ($found_descendant) {
667 continue;
670 // Otherwise, we're moving the object to a proxy column which it is not
671 // a member of yet, so add an association to the column's proxy board.
673 $add_boards[] = $proxy_phid;
676 if ($add_boards) {
677 $more[] = id(new ManiphestTransaction())
678 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
679 ->setMetadataValue(
680 'edge:type',
681 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
682 ->setIgnoreOnNoEffect(true)
683 ->setNewValue(
684 array(
685 '+' => array_fuse($add_boards),
689 return $more;
692 private function applyBoardMove($object, array $move) {
693 $board_phid = $move['boardPHID'];
694 $column_phid = $move['columnPHID'];
696 $before_phids = $move['beforePHIDs'];
697 $after_phids = $move['afterPHIDs'];
699 $object_phid = $object->getPHID();
701 // We're doing layout with the omnipotent viewer to make sure we don't
702 // remove positions in columns that exist, but which the actual actor
703 // can't see.
704 $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
706 $select_phids = array($board_phid);
708 $descendants = id(new PhabricatorProjectQuery())
709 ->setViewer($omnipotent_viewer)
710 ->withAncestorProjectPHIDs($select_phids)
711 ->execute();
712 foreach ($descendants as $descendant) {
713 $select_phids[] = $descendant->getPHID();
716 $board_tasks = id(new ManiphestTaskQuery())
717 ->setViewer($omnipotent_viewer)
718 ->withEdgeLogicPHIDs(
719 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
720 PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
721 array($select_phids))
722 ->execute();
724 $board_tasks = mpull($board_tasks, null, 'getPHID');
725 $board_tasks[$object_phid] = $object;
727 // Make sure tasks are sorted by ID, so we lay out new positions in
728 // a consistent way.
729 $board_tasks = msort($board_tasks, 'getID');
731 $object_phids = array_keys($board_tasks);
733 $engine = id(new PhabricatorBoardLayoutEngine())
734 ->setViewer($omnipotent_viewer)
735 ->setBoardPHIDs(array($board_phid))
736 ->setObjectPHIDs($object_phids)
737 ->executeLayout();
739 // TODO: This logic needs to be revised when we legitimately support
740 // multiple column positions.
741 $columns = $engine->getObjectColumns($board_phid, $object_phid);
742 foreach ($columns as $column) {
743 $engine->queueRemovePosition(
744 $board_phid,
745 $column->getPHID(),
746 $object_phid);
749 $engine->queueAddPosition(
750 $board_phid,
751 $column_phid,
752 $object_phid,
753 $after_phids,
754 $before_phids);
756 $engine->applyPositionUpdates();
760 private function validateColumnPHID($value) {
761 if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
762 return;
765 throw new Exception(
766 pht(
767 'When moving objects between columns on a board, columns must '.
768 'be identified by PHIDs. This transaction uses "%s" to identify '.
769 'a column, but that is not a valid column PHID.',
770 $value));
774 private function getLockValidationErrors($object, array $xactions) {
775 $errors = array();
777 $old_owner = $object->getOwnerPHID();
778 $old_status = $object->getStatus();
780 $new_owner = $old_owner;
781 $new_status = $old_status;
783 $owner_xaction = null;
784 $status_xaction = null;
786 foreach ($xactions as $xaction) {
787 switch ($xaction->getTransactionType()) {
788 case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
789 $new_owner = $xaction->getNewValue();
790 $owner_xaction = $xaction;
791 break;
792 case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
793 $new_status = $xaction->getNewValue();
794 $status_xaction = $xaction;
795 break;
799 $actor_phid = $this->getActingAsPHID();
801 $was_locked = ManiphestTaskStatus::areEditsLockedInStatus(
802 $old_status);
803 $now_locked = ManiphestTaskStatus::areEditsLockedInStatus(
804 $new_status);
806 if (!$now_locked) {
807 // If we're not ending in an edit-locked status, everything is good.
808 } else if ($new_owner !== null) {
809 // If we ending the edit with some valid owner, this is allowed for
810 // now. We might need to revisit this.
811 } else {
812 // The edits end with the task locked and unowned. No one will be able
813 // to edit it, so we forbid this. We try to be specific about what the
814 // user did wrong.
816 $owner_changed = ($old_owner && !$new_owner);
817 $status_changed = ($was_locked !== $now_locked);
818 $message = null;
820 if ($status_changed && $owner_changed) {
821 $message = pht(
822 'You can not lock this task and unassign it at the same time '.
823 'because no one will be able to edit it anymore. Lock the task '.
824 'or remove the owner, but not both.');
825 $problem_xaction = $status_xaction;
826 } else if ($status_changed) {
827 $message = pht(
828 'You can not lock this task because it does not have an owner. '.
829 'No one would be able to edit the task. Assign the task to an '.
830 'owner before locking it.');
831 $problem_xaction = $status_xaction;
832 } else if ($owner_changed) {
833 $message = pht(
834 'You can not remove the owner of this task because it is locked '.
835 'and no one would be able to edit the task. Reassign the task or '.
836 'unlock it before removing the owner.');
837 $problem_xaction = $owner_xaction;
838 } else {
839 // If the task was already broken, we don't have a transaction to
840 // complain about so just let it through. In theory, this is
841 // impossible since policy rules should kick in before we get here.
844 if ($message) {
845 $errors[] = new PhabricatorApplicationTransactionValidationError(
846 $problem_xaction->getTransactionType(),
847 pht('Lock Error'),
848 $message,
849 $problem_xaction);
853 return $errors;
856 private function filterValidPHIDs($phid_list, array $object_map) {
857 foreach ($phid_list as $key => $phid) {
858 if (isset($object_map[$phid])) {
859 continue;
862 unset($phid_list[$key]);
865 return array_values($phid_list);
868 protected function didApplyTransactions($object, array $xactions) {
869 $send_notifications = PhabricatorNotificationClient::isEnabled();
870 if ($send_notifications) {
871 $old_phids = $this->oldProjectPHIDs;
872 $new_phids = $this->loadProjectPHIDs($object);
874 // We want to emit update notifications for all old and new tagged
875 // projects, and all parents of those projects. For example, if an
876 // edit removes project "A > B" from a task, the "A" workboard should
877 // receive an update event.
879 $project_phids = array_fuse($old_phids) + array_fuse($new_phids);
880 $project_phids = array_keys($project_phids);
882 if ($project_phids) {
883 $projects = id(new PhabricatorProjectQuery())
884 ->setViewer(PhabricatorUser::getOmnipotentUser())
885 ->withPHIDs($project_phids)
886 ->execute();
888 $notify_projects = array();
889 foreach ($projects as $project) {
890 $notify_projects[$project->getPHID()] = $project;
891 foreach ($project->getAncestorProjects() as $ancestor) {
892 $notify_projects[$ancestor->getPHID()] = $ancestor;
896 foreach ($notify_projects as $key => $project) {
897 if (!$project->getHasWorkboard()) {
898 unset($notify_projects[$key]);
902 $notify_phids = array_keys($notify_projects);
904 if ($notify_phids) {
905 $data = array(
906 'type' => 'workboards',
907 'subscribers' => $notify_phids,
910 PhabricatorNotificationClient::tryToPostMessage($data);
915 return $xactions;
918 private function loadProjectPHIDs(ManiphestTask $task) {
919 if (!$task->getPHID()) {
920 return array();
923 $edge_query = id(new PhabricatorEdgeQuery())
924 ->withSourcePHIDs(array($task->getPHID()))
925 ->withEdgeTypes(
926 array(
927 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
930 $edge_query->execute();
932 return $edge_query->getDestinationPHIDs();