Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/maniphest/editor/ManiphestTransactionEditor.php
12256 views
1
<?php
2
3
final class ManiphestTransactionEditor
4
extends PhabricatorApplicationTransactionEditor {
5
6
private $oldProjectPHIDs;
7
private $moreValidationErrors = array();
8
9
public function getEditorApplicationClass() {
10
return 'PhabricatorManiphestApplication';
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 ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
105
$unblock_xaction = $xaction;
106
break;
107
}
108
}
109
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();
123
124
$old = $unblock_xaction->getOldValue();
125
$new = $unblock_xaction->getNewValue();
126
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));
133
134
if ($this->getIsNewObject()) {
135
$parent_xaction->setMetadataValue('blocker.new', true);
136
}
137
138
$this->newSubEditor()
139
->setContinueOnNoEffect(true)
140
->setContinueOnMissingFields(true)
141
->applyTransactions($blocked_task, array($parent_xaction));
142
}
143
}
144
}
145
146
return $xactions;
147
}
148
149
protected function shouldSendMail(
150
PhabricatorLiskDAO $object,
151
array $xactions) {
152
return true;
153
}
154
155
protected function getMailSubjectPrefix() {
156
return pht('[Maniphest]');
157
}
158
159
protected function getMailThreadID(PhabricatorLiskDAO $object) {
160
return 'maniphest-task-'.$object->getPHID();
161
}
162
163
protected function getMailTo(PhabricatorLiskDAO $object) {
164
$phids = array();
165
166
if ($object->getOwnerPHID()) {
167
$phids[] = $object->getOwnerPHID();
168
}
169
$phids[] = $this->getActingAsPHID();
170
171
return $phids;
172
}
173
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.'),
194
);
195
}
196
197
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
198
return id(new ManiphestReplyHandler())
199
->setMailReceiver($object);
200
}
201
202
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
203
$id = $object->getID();
204
$title = $object->getTitle();
205
206
return id(new PhabricatorMetaMTAMail())
207
->setSubject("T{$id}: {$title}");
208
}
209
210
protected function getObjectLinkButtonLabelForMail(
211
PhabricatorLiskDAO $object) {
212
return pht('View Task');
213
}
214
215
protected function buildMailBody(
216
PhabricatorLiskDAO $object,
217
array $xactions) {
218
219
$body = parent::buildMailBody($object, $xactions);
220
221
if ($this->getIsNewObject()) {
222
$body->addRemarkupSection(
223
pht('TASK DESCRIPTION'),
224
$object->getDescription());
225
}
226
227
$body->addLinkSection(
228
pht('TASK DETAIL'),
229
$this->getObjectLinkButtonURIForMail($object));
230
231
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'];
239
}
240
}
241
}
242
243
if ($board_phids) {
244
$projects = id(new PhabricatorProjectQuery())
245
->setViewer($this->requireActor())
246
->withPHIDs($board_phids)
247
->execute();
248
249
foreach ($projects as $project) {
250
$body->addLinkSection(
251
pht('WORKBOARD'),
252
PhabricatorEnv::getProductionURI($project->getWorkboardURI()));
253
}
254
}
255
256
257
return $body;
258
}
259
260
protected function shouldPublishFeedStory(
261
PhabricatorLiskDAO $object,
262
array $xactions) {
263
return true;
264
}
265
266
protected function supportsSearch() {
267
return true;
268
}
269
270
protected function shouldApplyHeraldRules(
271
PhabricatorLiskDAO $object,
272
array $xactions) {
273
return true;
274
}
275
276
protected function buildHeraldAdapter(
277
PhabricatorLiskDAO $object,
278
array $xactions) {
279
280
return id(new HeraldManiphestTaskAdapter())
281
->setTask($object);
282
}
283
284
protected function adjustObjectForPolicyChecks(
285
PhabricatorLiskDAO $object,
286
array $xactions) {
287
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;
296
}
297
}
298
299
return $copy;
300
}
301
302
protected function validateAllTransactions(
303
PhabricatorLiskDAO $object,
304
array $xactions) {
305
306
$errors = parent::validateAllTransactions($object, $xactions);
307
308
if ($this->moreValidationErrors) {
309
$errors = array_merge($errors, $this->moreValidationErrors);
310
}
311
312
foreach ($this->getLockValidationErrors($object, $xactions) as $error) {
313
$errors[] = $error;
314
}
315
316
return $errors;
317
}
318
319
protected function expandTransactions(
320
PhabricatorLiskDAO $object,
321
array $xactions) {
322
323
$actor = $this->getActor();
324
$actor_phid = $actor->getPHID();
325
326
$results = parent::expandTransactions($object, $xactions);
327
328
$is_unassigned = ($object->getOwnerPHID() === null);
329
330
$any_assign = false;
331
foreach ($xactions as $xaction) {
332
if ($xaction->getTransactionType() ==
333
ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) {
334
$any_assign = true;
335
break;
336
}
337
}
338
339
$is_open = !$object->isClosed();
340
341
$new_status = null;
342
foreach ($xactions as $xaction) {
343
switch ($xaction->getTransactionType()) {
344
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
345
$new_status = $xaction->getNewValue();
346
break;
347
}
348
}
349
350
if ($new_status === null) {
351
$is_closing = false;
352
} else {
353
$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
354
}
355
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);
360
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);
367
}
368
}
369
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),
378
));
379
}
380
}
381
382
$send_notifications = PhabricatorNotificationClient::isEnabled();
383
if ($send_notifications) {
384
$this->oldProjectPHIDs = $this->loadProjectPHIDs($object);
385
}
386
387
return $results;
388
}
389
390
protected function expandTransaction(
391
PhabricatorLiskDAO $object,
392
PhabricatorApplicationTransaction $xaction) {
393
394
$results = parent::expandTransaction($object, $xaction);
395
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;
403
}
404
} catch (Exception $ex) {
405
$error = new PhabricatorApplicationTransactionValidationError(
406
$type,
407
pht('Invalid'),
408
$ex->getMessage(),
409
$xaction);
410
$this->moreValidationErrors[] = $error;
411
}
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;
419
}
420
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),
430
));
431
}
432
break;
433
}
434
435
return $results;
436
}
437
438
private function buildMoveTransaction(
439
PhabricatorLiskDAO $object,
440
PhabricatorApplicationTransaction $xaction) {
441
$actor = $this->getActor();
442
443
$new = $xaction->getNewValue();
444
if (!is_array($new)) {
445
$this->validateColumnPHID($new);
446
$new = array($new);
447
}
448
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,
455
);
456
}
457
458
PhutilTypeSpec::checkMap(
459
$value,
460
array(
461
'columnPHID' => 'string',
462
'beforePHIDs' => 'optional list<string>',
463
'afterPHIDs' => 'optional list<string>',
464
465
// Deprecated older variations of "beforePHIDs" and "afterPHIDs".
466
'beforePHID' => 'optional string',
467
'afterPHID' => 'optional string',
468
));
469
470
$value = $value + array(
471
'beforePHIDs' => array(),
472
'afterPHIDs' => array(),
473
);
474
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".'));
483
}
484
$value['afterPHIDs'] = array($value['afterPHID']);
485
unset($value['afterPHID']);
486
}
487
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".'));
494
}
495
$value['beforePHIDs'] = array($value['beforePHID']);
496
unset($value['beforePHID']);
497
}
498
499
foreach ($value['beforePHIDs'] as $phid) {
500
$relative_phids[] = $phid;
501
}
502
503
foreach ($value['afterPHIDs'] as $phid) {
504
$relative_phids[] = $phid;
505
}
506
507
$new[$key] = $value;
508
}
509
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.
513
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();
522
}
523
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);
531
532
$new[$key] = $value;
533
}
534
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();
544
}
545
546
$board_phids = mpull($columns, 'getProjectPHID');
547
$object_phid = $object->getPHID();
548
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;
554
}
555
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();
563
}
564
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));
574
}
575
576
$board_phid = $column->getProjectPHID();
577
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();
585
}
586
587
$spec += array(
588
'boardPHID' => $board_phid,
589
'fromColumnPHIDs' => $old_column_phids,
590
);
591
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]);
596
597
$is_reordering = ($spec['afterPHIDs'] || $spec['beforePHIDs']);
598
if ($already_here && !$is_reordering) {
599
unset($new[$key]);
600
} else {
601
$new[$key] = $spec;
602
}
603
}
604
605
$new = array_values($new);
606
$xaction->setNewValue($new);
607
608
609
$more = array();
610
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.
615
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.
619
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();
627
}
628
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();
635
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;
641
}
642
continue;
643
}
644
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;
649
}
650
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();
657
658
$found_descendant = false;
659
foreach ($descendants as $descendant) {
660
if (isset($current_phids[$descendant->getPHID()])) {
661
$found_descendant = true;
662
break;
663
}
664
}
665
666
if ($found_descendant) {
667
continue;
668
}
669
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.
672
673
$add_boards[] = $proxy_phid;
674
}
675
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),
686
));
687
}
688
689
return $more;
690
}
691
692
private function applyBoardMove($object, array $move) {
693
$board_phid = $move['boardPHID'];
694
$column_phid = $move['columnPHID'];
695
696
$before_phids = $move['beforePHIDs'];
697
$after_phids = $move['afterPHIDs'];
698
699
$object_phid = $object->getPHID();
700
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();
705
706
$select_phids = array($board_phid);
707
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();
714
}
715
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();
723
724
$board_tasks = mpull($board_tasks, null, 'getPHID');
725
$board_tasks[$object_phid] = $object;
726
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');
730
731
$object_phids = array_keys($board_tasks);
732
733
$engine = id(new PhabricatorBoardLayoutEngine())
734
->setViewer($omnipotent_viewer)
735
->setBoardPHIDs(array($board_phid))
736
->setObjectPHIDs($object_phids)
737
->executeLayout();
738
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);
747
}
748
749
$engine->queueAddPosition(
750
$board_phid,
751
$column_phid,
752
$object_phid,
753
$after_phids,
754
$before_phids);
755
756
$engine->applyPositionUpdates();
757
}
758
759
760
private function validateColumnPHID($value) {
761
if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
762
return;
763
}
764
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));
771
}
772
773
774
private function getLockValidationErrors($object, array $xactions) {
775
$errors = array();
776
777
$old_owner = $object->getOwnerPHID();
778
$old_status = $object->getStatus();
779
780
$new_owner = $old_owner;
781
$new_status = $old_status;
782
783
$owner_xaction = null;
784
$status_xaction = null;
785
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;
796
}
797
}
798
799
$actor_phid = $this->getActingAsPHID();
800
801
$was_locked = ManiphestTaskStatus::areEditsLockedInStatus(
802
$old_status);
803
$now_locked = ManiphestTaskStatus::areEditsLockedInStatus(
804
$new_status);
805
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.
815
816
$owner_changed = ($old_owner && !$new_owner);
817
$status_changed = ($was_locked !== $now_locked);
818
$message = null;
819
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.
842
}
843
844
if ($message) {
845
$errors[] = new PhabricatorApplicationTransactionValidationError(
846
$problem_xaction->getTransactionType(),
847
pht('Lock Error'),
848
$message,
849
$problem_xaction);
850
}
851
}
852
853
return $errors;
854
}
855
856
private function filterValidPHIDs($phid_list, array $object_map) {
857
foreach ($phid_list as $key => $phid) {
858
if (isset($object_map[$phid])) {
859
continue;
860
}
861
862
unset($phid_list[$key]);
863
}
864
865
return array_values($phid_list);
866
}
867
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);
873
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.
878
879
$project_phids = array_fuse($old_phids) + array_fuse($new_phids);
880
$project_phids = array_keys($project_phids);
881
882
if ($project_phids) {
883
$projects = id(new PhabricatorProjectQuery())
884
->setViewer(PhabricatorUser::getOmnipotentUser())
885
->withPHIDs($project_phids)
886
->execute();
887
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;
893
}
894
}
895
896
foreach ($notify_projects as $key => $project) {
897
if (!$project->getHasWorkboard()) {
898
unset($notify_projects[$key]);
899
}
900
}
901
902
$notify_phids = array_keys($notify_projects);
903
904
if ($notify_phids) {
905
$data = array(
906
'type' => 'workboards',
907
'subscribers' => $notify_phids,
908
);
909
910
PhabricatorNotificationClient::tryToPostMessage($data);
911
}
912
}
913
}
914
915
return $xactions;
916
}
917
918
private function loadProjectPHIDs(ManiphestTask $task) {
919
if (!$task->getPHID()) {
920
return array();
921
}
922
923
$edge_query = id(new PhabricatorEdgeQuery())
924
->withSourcePHIDs(array($task->getPHID()))
925
->withEdgeTypes(
926
array(
927
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
928
));
929
930
$edge_query->execute();
931
932
return $edge_query->getDestinationPHIDs();
933
}
934
935
}
936
937