Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/diff/PhabricatorInlineCommentController.php
12241 views
1
<?php
2
3
abstract class PhabricatorInlineCommentController
4
extends PhabricatorController {
5
6
private $containerObject;
7
8
abstract protected function createComment();
9
abstract protected function newInlineCommentQuery();
10
abstract protected function loadCommentForDone($id);
11
abstract protected function loadObjectOwnerPHID(
12
PhabricatorInlineComment $inline);
13
abstract protected function newContainerObject();
14
15
final protected function getContainerObject() {
16
if ($this->containerObject === null) {
17
$object = $this->newContainerObject();
18
if (!$object) {
19
throw new Exception(
20
pht(
21
'Failed to load container object for inline comment.'));
22
}
23
$this->containerObject = $object;
24
}
25
26
return $this->containerObject;
27
}
28
29
protected function hideComments(array $ids) {
30
throw new PhutilMethodNotImplementedException();
31
}
32
33
protected function showComments(array $ids) {
34
throw new PhutilMethodNotImplementedException();
35
}
36
37
private $changesetID;
38
private $isNewFile;
39
private $isOnRight;
40
private $lineNumber;
41
private $lineLength;
42
private $operation;
43
private $commentID;
44
private $renderer;
45
private $replyToCommentPHID;
46
47
public function getCommentID() {
48
return $this->commentID;
49
}
50
51
public function getOperation() {
52
return $this->operation;
53
}
54
55
public function getLineLength() {
56
return $this->lineLength;
57
}
58
59
public function getLineNumber() {
60
return $this->lineNumber;
61
}
62
63
public function getIsOnRight() {
64
return $this->isOnRight;
65
}
66
67
public function getChangesetID() {
68
return $this->changesetID;
69
}
70
71
public function getIsNewFile() {
72
return $this->isNewFile;
73
}
74
75
public function setRenderer($renderer) {
76
$this->renderer = $renderer;
77
return $this;
78
}
79
80
public function getRenderer() {
81
return $this->renderer;
82
}
83
84
public function setReplyToCommentPHID($phid) {
85
$this->replyToCommentPHID = $phid;
86
return $this;
87
}
88
89
public function getReplyToCommentPHID() {
90
return $this->replyToCommentPHID;
91
}
92
93
public function processRequest() {
94
$request = $this->getRequest();
95
$viewer = $this->getViewer();
96
97
if (!$request->validateCSRF()) {
98
return new Aphront404Response();
99
}
100
101
$this->readRequestParameters();
102
103
$op = $this->getOperation();
104
switch ($op) {
105
case 'hide':
106
case 'show':
107
$ids = $request->getStrList('ids');
108
if ($ids) {
109
if ($op == 'hide') {
110
$this->hideComments($ids);
111
} else {
112
$this->showComments($ids);
113
}
114
}
115
116
return id(new AphrontAjaxResponse())->setContent(array());
117
case 'done':
118
$inline = $this->loadCommentForDone($this->getCommentID());
119
120
$is_draft_state = false;
121
$is_checked = false;
122
switch ($inline->getFixedState()) {
123
case PhabricatorInlineComment::STATE_DRAFT:
124
$next_state = PhabricatorInlineComment::STATE_UNDONE;
125
break;
126
case PhabricatorInlineComment::STATE_UNDRAFT:
127
$next_state = PhabricatorInlineComment::STATE_DONE;
128
$is_checked = true;
129
break;
130
case PhabricatorInlineComment::STATE_DONE:
131
$next_state = PhabricatorInlineComment::STATE_UNDRAFT;
132
$is_draft_state = true;
133
break;
134
default:
135
case PhabricatorInlineComment::STATE_UNDONE:
136
$next_state = PhabricatorInlineComment::STATE_DRAFT;
137
$is_draft_state = true;
138
$is_checked = true;
139
break;
140
}
141
142
$inline->setFixedState($next_state)->save();
143
144
return id(new AphrontAjaxResponse())
145
->setContent(
146
array(
147
'isChecked' => $is_checked,
148
'draftState' => $is_draft_state,
149
));
150
case 'delete':
151
case 'undelete':
152
case 'refdelete':
153
// NOTE: For normal deletes, we just process the delete immediately
154
// and show an "Undo" action. For deletes by reference from the
155
// preview ("refdelete"), we prompt first (because the "Undo" may
156
// not draw, or may not be easy to locate).
157
158
if ($op == 'refdelete') {
159
if (!$request->isFormPost()) {
160
return $this->newDialog()
161
->setTitle(pht('Really delete comment?'))
162
->addHiddenInput('id', $this->getCommentID())
163
->addHiddenInput('op', $op)
164
->appendParagraph(pht('Delete this inline comment?'))
165
->addCancelButton('#')
166
->addSubmitButton(pht('Delete'));
167
}
168
}
169
170
$is_delete = ($op == 'delete' || $op == 'refdelete');
171
172
$inline = $this->loadCommentByIDForEdit($this->getCommentID());
173
174
if ($is_delete) {
175
$inline
176
->setIsEditing(false)
177
->setIsDeleted(1);
178
} else {
179
$inline->setIsDeleted(0);
180
}
181
182
$this->saveComment($inline);
183
184
return $this->buildEmptyResponse();
185
case 'save':
186
$inline = $this->loadCommentByIDForEdit($this->getCommentID());
187
188
$this->updateCommentContentState($inline);
189
190
$inline
191
->setIsEditing(false)
192
->setIsDeleted(0);
193
194
// Since we're saving the comment, update the committed state.
195
$active_state = $inline->getContentState();
196
$inline->setCommittedContentState($active_state);
197
198
$this->saveComment($inline);
199
200
return $this->buildRenderedCommentResponse(
201
$inline,
202
$this->getIsOnRight());
203
case 'edit':
204
$inline = $this->loadCommentByIDForEdit($this->getCommentID());
205
206
// NOTE: At time of writing, the "editing" state of inlines is
207
// preserved by simulating a click on "Edit" when the inline loads.
208
209
// In this case, we don't want to "saveComment()", because it
210
// recalculates object drafts and purges versioned drafts.
211
212
// The recalculation is merely unnecessary (state doesn't change)
213
// but purging drafts means that loading a page and then closing it
214
// discards your drafts.
215
216
// To avoid the purge, only invoke "saveComment()" if we actually
217
// have changes to apply.
218
219
$is_dirty = false;
220
if (!$inline->getIsEditing()) {
221
$inline
222
->setIsDeleted(0)
223
->setIsEditing(true);
224
225
$is_dirty = true;
226
}
227
228
if ($this->hasContentState()) {
229
$this->updateCommentContentState($inline);
230
$is_dirty = true;
231
} else {
232
PhabricatorInlineComment::loadAndAttachVersionedDrafts(
233
$viewer,
234
array($inline));
235
}
236
237
if ($is_dirty) {
238
$this->saveComment($inline);
239
}
240
241
$edit_dialog = $this->buildEditDialog($inline)
242
->setTitle(pht('Edit Inline Comment'));
243
244
$view = $this->buildScaffoldForView($edit_dialog);
245
246
return $this->newInlineResponse($inline, $view, true);
247
case 'cancel':
248
$inline = $this->loadCommentByIDForEdit($this->getCommentID());
249
250
$inline->setIsEditing(false);
251
252
// If the user uses "Undo" to get into an edited state ("AB"), then
253
// clicks cancel to return to the previous state ("A"), we also want
254
// to set the stored state back to "A".
255
$this->updateCommentContentState($inline);
256
257
$this->saveComment($inline);
258
259
return $this->buildEmptyResponse();
260
case 'draft':
261
$inline = $this->loadCommentByIDForEdit($this->getCommentID());
262
263
$versioned_draft = PhabricatorVersionedDraft::loadOrCreateDraft(
264
$inline->getPHID(),
265
$viewer->getPHID(),
266
$inline->getID());
267
268
$map = $this->newRequestContentState($inline)->newStorageMap();
269
$versioned_draft->setProperty('inline.state', $map);
270
$versioned_draft->save();
271
272
// We have to synchronize the draft engine after saving a versioned
273
// draft, because taking an inline comment from "no text, no draft"
274
// to "no text, text in a draft" marks the container object as having
275
// a draft.
276
$draft_engine = $this->newDraftEngine();
277
if ($draft_engine) {
278
$draft_engine->synchronize();
279
}
280
281
return $this->buildEmptyResponse();
282
case 'new':
283
case 'reply':
284
default:
285
// NOTE: We read the values from the client (the display values), not
286
// the values from the database (the original values) when replying.
287
// In particular, when replying to a ghost comment which was moved
288
// across diffs and then moved backward to the most recent visible
289
// line, we want to reply on the display line (which exists), not on
290
// the comment's original line (which may not exist in this changeset).
291
$is_new = $this->getIsNewFile();
292
$number = $this->getLineNumber();
293
$length = $this->getLineLength();
294
295
$inline = $this->createComment()
296
->setChangesetID($this->getChangesetID())
297
->setAuthorPHID($viewer->getPHID())
298
->setIsNewFile($is_new)
299
->setLineNumber($number)
300
->setLineLength($length)
301
->setReplyToCommentPHID($this->getReplyToCommentPHID())
302
->setIsEditing(true)
303
->setStartOffset($request->getInt('startOffset'))
304
->setEndOffset($request->getInt('endOffset'))
305
->setContent('');
306
307
$document_engine_key = $request->getStr('documentEngineKey');
308
if ($document_engine_key !== null) {
309
$inline->setDocumentEngineKey($document_engine_key);
310
}
311
312
// If you own this object, mark your own inlines as "Done" by default.
313
$owner_phid = $this->loadObjectOwnerPHID($inline);
314
if ($owner_phid) {
315
if ($viewer->getPHID() == $owner_phid) {
316
$fixed_state = PhabricatorInlineComment::STATE_DRAFT;
317
$inline->setFixedState($fixed_state);
318
}
319
}
320
321
if ($this->hasContentState()) {
322
$this->updateCommentContentState($inline);
323
}
324
325
// NOTE: We're writing the comment as "deleted", then reloading to
326
// pick up context and undeleting it. This is silly -- we just want
327
// to load and attach context -- but just loading context is currently
328
// complicated (for example, context relies on cache keys that expect
329
// the inline to have an ID).
330
331
$inline->setIsDeleted(1);
332
333
$this->saveComment($inline);
334
335
// Reload the inline to attach context.
336
$inline = $this->loadCommentByIDForEdit($inline->getID());
337
338
// Now, we can read the source file and set the initial state.
339
$state = $inline->getContentState();
340
$default_suggestion = $inline->getDefaultSuggestionText();
341
$state->setContentSuggestionText($default_suggestion);
342
343
$inline->setInitialContentState($state);
344
$inline->setContentState($state);
345
346
$inline->setIsDeleted(0);
347
348
$this->saveComment($inline);
349
350
$edit_dialog = $this->buildEditDialog($inline);
351
352
if ($this->getOperation() == 'reply') {
353
$edit_dialog->setTitle(pht('Reply to Inline Comment'));
354
} else {
355
$edit_dialog->setTitle(pht('New Inline Comment'));
356
}
357
358
$view = $this->buildScaffoldForView($edit_dialog);
359
360
return $this->newInlineResponse($inline, $view, true);
361
}
362
}
363
364
private function readRequestParameters() {
365
$request = $this->getRequest();
366
367
// NOTE: This isn't necessarily a DifferentialChangeset ID, just an
368
// application identifier for the changeset. In Diffusion, it's a Path ID.
369
$this->changesetID = $request->getInt('changesetID');
370
371
$this->isNewFile = (int)$request->getBool('is_new');
372
$this->isOnRight = $request->getBool('on_right');
373
$this->lineNumber = $request->getInt('number');
374
$this->lineLength = $request->getInt('length');
375
$this->commentID = $request->getInt('id');
376
$this->operation = $request->getStr('op');
377
$this->renderer = $request->getStr('renderer');
378
$this->replyToCommentPHID = $request->getStr('replyToCommentPHID');
379
380
if ($this->getReplyToCommentPHID()) {
381
$reply_phid = $this->getReplyToCommentPHID();
382
$reply_comment = $this->loadCommentByPHID($reply_phid);
383
if (!$reply_comment) {
384
throw new Exception(
385
pht('Failed to load comment "%s".', $reply_phid));
386
}
387
388
// When replying, force the new comment into the same location as the
389
// old comment. If we don't do this, replying to a ghost comment from
390
// diff A while viewing diff B can end up placing the two comments in
391
// different places while viewing diff C, because the porting algorithm
392
// makes a different decision. Forcing the comments to bind to the same
393
// place makes sure they stick together no matter which diff is being
394
// viewed. See T10562 for discussion.
395
396
$this->changesetID = $reply_comment->getChangesetID();
397
$this->isNewFile = $reply_comment->getIsNewFile();
398
$this->lineNumber = $reply_comment->getLineNumber();
399
$this->lineLength = $reply_comment->getLineLength();
400
}
401
}
402
403
private function buildEditDialog(PhabricatorInlineComment $inline) {
404
$request = $this->getRequest();
405
$viewer = $this->getViewer();
406
407
$edit_dialog = id(new PHUIDiffInlineCommentEditView())
408
->setViewer($viewer)
409
->setInlineComment($inline)
410
->setIsOnRight($this->getIsOnRight())
411
->setRenderer($this->getRenderer());
412
413
return $edit_dialog;
414
}
415
416
private function buildEmptyResponse() {
417
return id(new AphrontAjaxResponse())
418
->setContent(
419
array(
420
'inline' => array(),
421
'view' => null,
422
));
423
}
424
425
private function buildRenderedCommentResponse(
426
PhabricatorInlineComment $inline,
427
$on_right) {
428
429
$request = $this->getRequest();
430
$viewer = $this->getViewer();
431
432
$engine = new PhabricatorMarkupEngine();
433
$engine->setViewer($viewer);
434
$engine->addObject(
435
$inline,
436
PhabricatorInlineComment::MARKUP_FIELD_BODY);
437
$engine->process();
438
439
$phids = array($viewer->getPHID());
440
441
$handles = $this->loadViewerHandles($phids);
442
$object_owner_phid = $this->loadObjectOwnerPHID($inline);
443
444
$view = id(new PHUIDiffInlineCommentDetailView())
445
->setUser($viewer)
446
->setInlineComment($inline)
447
->setIsOnRight($on_right)
448
->setMarkupEngine($engine)
449
->setHandles($handles)
450
->setEditable(true)
451
->setCanMarkDone(false)
452
->setObjectOwnerPHID($object_owner_phid);
453
454
$view = $this->buildScaffoldForView($view);
455
456
return $this->newInlineResponse($inline, $view, false);
457
}
458
459
private function buildScaffoldForView(PHUIDiffInlineCommentView $view) {
460
$renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey(
461
$this->getRenderer());
462
463
$view = $renderer->getRowScaffoldForInline($view);
464
465
return id(new PHUIDiffInlineCommentTableScaffold())
466
->addRowScaffold($view);
467
}
468
469
private function newInlineResponse(
470
PhabricatorInlineComment $inline,
471
$view,
472
$is_edit) {
473
$viewer = $this->getViewer();
474
475
if ($inline->getReplyToCommentPHID()) {
476
$can_suggest = false;
477
} else {
478
$can_suggest = (bool)$inline->getInlineContext();
479
}
480
481
if ($is_edit) {
482
$state = $inline->getContentStateMapForEdit($viewer);
483
} else {
484
$state = $inline->getContentStateMap();
485
}
486
487
$response = array(
488
'inline' => array(
489
'id' => $inline->getID(),
490
'state' => $state,
491
'canSuggestEdit' => $can_suggest,
492
),
493
'view' => hsprintf('%s', $view),
494
);
495
496
return id(new AphrontAjaxResponse())
497
->setContent($response);
498
}
499
500
final protected function loadCommentByID($id) {
501
$query = $this->newInlineCommentQuery()
502
->withIDs(array($id));
503
504
return $this->loadCommentByQuery($query);
505
}
506
507
final protected function loadCommentByPHID($phid) {
508
$query = $this->newInlineCommentQuery()
509
->withPHIDs(array($phid));
510
511
return $this->loadCommentByQuery($query);
512
}
513
514
final protected function loadCommentByIDForEdit($id) {
515
$viewer = $this->getViewer();
516
517
$query = $this->newInlineCommentQuery()
518
->withIDs(array($id))
519
->needInlineContext(true);
520
521
$inline = $this->loadCommentByQuery($query);
522
523
if (!$inline) {
524
throw new Exception(
525
pht(
526
'Unable to load inline "%s".',
527
$id));
528
}
529
530
if (!$this->canEditInlineComment($viewer, $inline)) {
531
throw new Exception(
532
pht(
533
'Inline comment "%s" is not editable.',
534
$id));
535
}
536
537
return $inline;
538
}
539
540
private function loadCommentByQuery(
541
PhabricatorDiffInlineCommentQuery $query) {
542
$viewer = $this->getViewer();
543
544
$inline = $query
545
->setViewer($viewer)
546
->executeOne();
547
548
if ($inline) {
549
$inline = $inline->newInlineCommentObject();
550
}
551
552
return $inline;
553
}
554
555
private function hasContentState() {
556
$request = $this->getRequest();
557
return (bool)$request->getBool('hasContentState');
558
}
559
560
private function newRequestContentState($inline) {
561
$request = $this->getRequest();
562
return $inline->newContentStateFromRequest($request);
563
}
564
565
private function updateCommentContentState(PhabricatorInlineComment $inline) {
566
if (!$this->hasContentState()) {
567
throw new Exception(
568
pht(
569
'Attempting to update comment content state, but request has no '.
570
'content state.'));
571
}
572
573
$state = $this->newRequestContentState($inline);
574
$inline->setContentState($state);
575
}
576
577
private function saveComment(PhabricatorInlineComment $inline) {
578
$viewer = $this->getViewer();
579
$draft_engine = $this->newDraftEngine();
580
581
$inline->openTransaction();
582
$inline->save();
583
584
PhabricatorVersionedDraft::purgeDrafts(
585
$inline->getPHID(),
586
$viewer->getPHID());
587
588
if ($draft_engine) {
589
$draft_engine->synchronize();
590
}
591
592
$inline->saveTransaction();
593
}
594
595
private function newDraftEngine() {
596
$viewer = $this->getViewer();
597
$object = $this->getContainerObject();
598
599
if (!($object instanceof PhabricatorDraftInterface)) {
600
return null;
601
}
602
603
return $object->newDraftEngine()
604
->setObject($object)
605
->setViewer($viewer);
606
}
607
608
}
609
610