@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<?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}