Something went wrong. Try again.
@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
Something went wrong. Try again.
18 kB · 608 lines
PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609<?php
abstract class PhabricatorInlineCommentController extends PhabricatorController {
private $containerObject;
abstract protected function createComment(); abstract protected function newInlineCommentQuery(); abstract protected function loadCommentForDone($id); abstract protected function loadObjectOwnerPHID( PhabricatorInlineComment $inline); abstract protected function newContainerObject();
final protected function getContainerObject() { if ($this->containerObject === null) { $object = $this->newContainerObject(); if (!$object) { throw new Exception( pht( 'Failed to load container object for inline comment.')); } $this->containerObject = $object; }
return $this->containerObject; }
protected function hideComments(array $ids) { throw new PhutilMethodNotImplementedException(); }
protected function showComments(array $ids) { throw new PhutilMethodNotImplementedException(); }
private $changesetID; private $isNewFile; private $isOnRight; private $lineNumber; private $lineLength; private $operation; private $commentID; private $renderer; private $replyToCommentPHID;
public function getCommentID() { return $this->commentID; }
public function getOperation() { return $this->operation; }
public function getLineLength() { return $this->lineLength; }
public function getLineNumber() { return $this->lineNumber; }
public function getIsOnRight() { return $this->isOnRight; }
public function getChangesetID() { return $this->changesetID; }
public function getIsNewFile() { return $this->isNewFile; }
public function setRenderer($renderer) { $this->renderer = $renderer; return $this; }
public function getRenderer() { return $this->renderer; }
public function setReplyToCommentPHID($phid) { $this->replyToCommentPHID = $phid; return $this; }
public function getReplyToCommentPHID() { return $this->replyToCommentPHID; }
public function processRequest() { $request = $this->getRequest(); $viewer = $this->getViewer();
if (!$request->validateCSRF()) { return new Aphront404Response(); }
$this->readRequestParameters();
$op = $this->getOperation(); switch ($op) { case 'hide': case 'show': $ids = $request->getStrList('ids'); if ($ids) { if ($op == 'hide') { $this->hideComments($ids); } else { $this->showComments($ids); } }
return id(new AphrontAjaxResponse())->setContent(array()); case 'done': $inline = $this->loadCommentForDone($this->getCommentID());
$is_draft_state = false; $is_checked = false; switch ($inline->getFixedState()) { case PhabricatorInlineComment::STATE_DRAFT: $next_state = PhabricatorInlineComment::STATE_UNDONE; break; case PhabricatorInlineComment::STATE_UNDRAFT: $next_state = PhabricatorInlineComment::STATE_DONE; $is_checked = true; break; case PhabricatorInlineComment::STATE_DONE: $next_state = PhabricatorInlineComment::STATE_UNDRAFT; $is_draft_state = true; break; default: case PhabricatorInlineComment::STATE_UNDONE: $next_state = PhabricatorInlineComment::STATE_DRAFT; $is_draft_state = true; $is_checked = true; break; }
$inline->setFixedState($next_state)->save();
return id(new AphrontAjaxResponse()) ->setContent( array( 'isChecked' => $is_checked, 'draftState' => $is_draft_state, )); case 'delete': case 'undelete': case 'refdelete': // NOTE: For normal deletes, we just process the delete immediately // and show an "Undo" action. For deletes by reference from the // preview ("refdelete"), we prompt first (because the "Undo" may // not draw, or may not be easy to locate).
if ($op == 'refdelete') { if (!$request->isFormPost()) { return $this->newDialog() ->setTitle(pht('Really delete comment?')) ->addHiddenInput('id', $this->getCommentID()) ->addHiddenInput('op', $op) ->appendParagraph(pht('Delete this inline comment?')) ->addCancelButton('#') ->addSubmitButton(pht('Delete')); } }
$is_delete = ($op == 'delete' || $op == 'refdelete');
$inline = $this->loadCommentByIDForEdit($this->getCommentID());
if ($is_delete) { $inline ->setIsEditing(false) ->setIsDeleted(1); } else { $inline->setIsDeleted(0); }
$this->saveComment($inline);
return $this->buildEmptyResponse(); case 'save': $inline = $this->loadCommentByIDForEdit($this->getCommentID());
$this->updateCommentContentState($inline);
$inline ->setIsEditing(false) ->setIsDeleted(0);
// Since we're saving the comment, update the committed state. $active_state = $inline->getContentState(); $inline->setCommittedContentState($active_state);
$this->saveComment($inline);
return $this->buildRenderedCommentResponse( $inline, $this->getIsOnRight()); case 'edit': $inline = $this->loadCommentByIDForEdit($this->getCommentID());
// NOTE: At time of writing, the "editing" state of inlines is // preserved by simulating a click on "Edit" when the inline loads.
// In this case, we don't want to "saveComment()", because it // recalculates object drafts and purges versioned drafts.
// The recalculation is merely unnecessary (state doesn't change) // but purging drafts means that loading a page and then closing it // discards your drafts.
// To avoid the purge, only invoke "saveComment()" if we actually // have changes to apply.
$is_dirty = false; if (!$inline->getIsEditing()) { $inline ->setIsDeleted(0) ->setIsEditing(true);
$is_dirty = true; }
if ($this->hasContentState()) { $this->updateCommentContentState($inline); $is_dirty = true; } else { PhabricatorInlineComment::loadAndAttachVersionedDrafts( $viewer, array($inline)); }
if ($is_dirty) { $this->saveComment($inline); }
$edit_dialog = $this->buildEditDialog($inline) ->setTitle(pht('Edit Inline Comment'));
$view = $this->buildScaffoldForView($edit_dialog);
return $this->newInlineResponse($inline, $view, true); case 'cancel': $inline = $this->loadCommentByIDForEdit($this->getCommentID());
$inline->setIsEditing(false);
// If the user uses "Undo" to get into an edited state ("AB"), then // clicks cancel to return to the previous state ("A"), we also want // to set the stored state back to "A". $this->updateCommentContentState($inline);
$this->saveComment($inline);
return $this->buildEmptyResponse(); case 'draft': $inline = $this->loadCommentByIDForEdit($this->getCommentID());
$versioned_draft = PhabricatorVersionedDraft::loadOrCreateDraft( $inline->getPHID(), $viewer->getPHID(), $inline->getID());
$map = $this->newRequestContentState($inline)->newStorageMap(); $versioned_draft->setProperty('inline.state', $map); $versioned_draft->save();
// We have to synchronize the draft engine after saving a versioned // draft, because taking an inline comment from "no text, no draft" // to "no text, text in a draft" marks the container object as having // a draft. $draft_engine = $this->newDraftEngine(); if ($draft_engine) { $draft_engine->synchronize(); }
return $this->buildEmptyResponse(); case 'new': case 'reply': default: // NOTE: We read the values from the client (the display values), not // the values from the database (the original values) when replying. // In particular, when replying to a ghost comment which was moved // across diffs and then moved backward to the most recent visible // line, we want to reply on the display line (which exists), not on // the comment's original line (which may not exist in this changeset). $is_new = $this->getIsNewFile(); $number = $this->getLineNumber(); $length = $this->getLineLength();
$inline = $this->createComment() ->setChangesetID($this->getChangesetID()) ->setAuthorPHID($viewer->getPHID()) ->setIsNewFile($is_new) ->setLineNumber($number) ->setLineLength($length) ->setReplyToCommentPHID($this->getReplyToCommentPHID()) ->setIsEditing(true) ->setStartOffset($request->getInt('startOffset')) ->setEndOffset($request->getInt('endOffset')) ->setContent('');
$document_engine_key = $request->getStr('documentEngineKey'); if ($document_engine_key !== null) { $inline->setDocumentEngineKey($document_engine_key); }
// If you own this object, mark your own inlines as "Done" by default. $owner_phid = $this->loadObjectOwnerPHID($inline); if ($owner_phid) { if ($viewer->getPHID() == $owner_phid) { $fixed_state = PhabricatorInlineComment::STATE_DRAFT; $inline->setFixedState($fixed_state); } }
if ($this->hasContentState()) { $this->updateCommentContentState($inline); }
// NOTE: We're writing the comment as "deleted", then reloading to // pick up context and undeleting it. This is silly -- we just want // to load and attach context -- but just loading context is currently // complicated (for example, context relies on cache keys that expect // the inline to have an ID).
$inline->setIsDeleted(1);
$this->saveComment($inline);
// Reload the inline to attach context. $inline = $this->loadCommentByIDForEdit($inline->getID());
// Now, we can read the source file and set the initial state. $state = $inline->getContentState(); $default_suggestion = $inline->getDefaultSuggestionText(); $state->setContentSuggestionText($default_suggestion);
$inline->setInitialContentState($state); $inline->setContentState($state);
$inline->setIsDeleted(0);
$this->saveComment($inline);
$edit_dialog = $this->buildEditDialog($inline);
if ($this->getOperation() == 'reply') { $edit_dialog->setTitle(pht('Reply to Inline Comment')); } else { $edit_dialog->setTitle(pht('New Inline Comment')); }
$view = $this->buildScaffoldForView($edit_dialog);
return $this->newInlineResponse($inline, $view, true); } }
private function readRequestParameters() { $request = $this->getRequest();
// NOTE: This isn't necessarily a DifferentialChangeset ID, just an // application identifier for the changeset. In Diffusion, it's a Path ID. $this->changesetID = $request->getInt('changesetID');
$this->isNewFile = (int)$request->getBool('is_new'); $this->isOnRight = $request->getBool('on_right'); $this->lineNumber = $request->getInt('number'); $this->lineLength = $request->getInt('length'); $this->commentID = $request->getInt('id'); $this->operation = $request->getStr('op'); $this->renderer = $request->getStr('renderer'); $this->replyToCommentPHID = $request->getStr('replyToCommentPHID');
if ($this->getReplyToCommentPHID()) { $reply_phid = $this->getReplyToCommentPHID(); $reply_comment = $this->loadCommentByPHID($reply_phid); if (!$reply_comment) { throw new Exception( pht('Failed to load comment "%s".', $reply_phid)); }
// When replying, force the new comment into the same location as the // old comment. If we don't do this, replying to a ghost comment from // diff A while viewing diff B can end up placing the two comments in // different places while viewing diff C, because the porting algorithm // makes a different decision. Forcing the comments to bind to the same // place makes sure they stick together no matter which diff is being // viewed. See T10562 for discussion.
$this->changesetID = $reply_comment->getChangesetID(); $this->isNewFile = $reply_comment->getIsNewFile(); $this->lineNumber = $reply_comment->getLineNumber(); $this->lineLength = $reply_comment->getLineLength(); } }
private function buildEditDialog(PhabricatorInlineComment $inline) { $request = $this->getRequest(); $viewer = $this->getViewer();
$edit_dialog = id(new PHUIDiffInlineCommentEditView()) ->setViewer($viewer) ->setInlineComment($inline) ->setIsOnRight($this->getIsOnRight()) ->setRenderer($this->getRenderer());
return $edit_dialog; }
private function buildEmptyResponse() { return id(new AphrontAjaxResponse()) ->setContent( array( 'inline' => array(), 'view' => null, )); }
private function buildRenderedCommentResponse( PhabricatorInlineComment $inline, $on_right) {
$request = $this->getRequest(); $viewer = $this->getViewer();
$engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); $engine->addObject( $inline, PhabricatorInlineComment::MARKUP_FIELD_BODY); $engine->process();
$phids = array($viewer->getPHID());
$handles = $this->loadViewerHandles($phids); $object_owner_phid = $this->loadObjectOwnerPHID($inline);
$view = id(new PHUIDiffInlineCommentDetailView()) ->setUser($viewer) ->setInlineComment($inline) ->setIsOnRight($on_right) ->setMarkupEngine($engine) ->setHandles($handles) ->setEditable(true) ->setCanMarkDone(false) ->setObjectOwnerPHID($object_owner_phid);
$view = $this->buildScaffoldForView($view);
return $this->newInlineResponse($inline, $view, false); }
private function buildScaffoldForView(PHUIDiffInlineCommentView $view) { $renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey( $this->getRenderer());
$view = $renderer->getRowScaffoldForInline($view);
return id(new PHUIDiffInlineCommentTableScaffold()) ->addRowScaffold($view); }
private function newInlineResponse( PhabricatorInlineComment $inline, $view, $is_edit) { $viewer = $this->getViewer();
if ($inline->getReplyToCommentPHID()) { $can_suggest = false; } else { $can_suggest = (bool)$inline->getInlineContext(); }
if ($is_edit) { $state = $inline->getContentStateMapForEdit($viewer); } else { $state = $inline->getContentStateMap(); }
$response = array( 'inline' => array( 'id' => $inline->getID(), 'state' => $state, 'canSuggestEdit' => $can_suggest, ), 'view' => hsprintf('%s', $view), );
return id(new AphrontAjaxResponse()) ->setContent($response); }
final protected function loadCommentByID($id) { $query = $this->newInlineCommentQuery() ->withIDs(array($id));
return $this->loadCommentByQuery($query); }
final protected function loadCommentByPHID($phid) { $query = $this->newInlineCommentQuery() ->withPHIDs(array($phid));
return $this->loadCommentByQuery($query); }
final protected function loadCommentByIDForEdit($id) { $viewer = $this->getViewer();
$query = $this->newInlineCommentQuery() ->withIDs(array($id)) ->needInlineContext(true);
$inline = $this->loadCommentByQuery($query);
if (!$inline) { throw new Exception( pht( 'Unable to load inline "%s".', $id)); }
if (!$this->canEditInlineComment($viewer, $inline)) { throw new Exception( pht( 'Inline comment "%s" is not editable.', $id)); }
return $inline; }
private function loadCommentByQuery( PhabricatorDiffInlineCommentQuery $query) { $viewer = $this->getViewer();
$inline = $query ->setViewer($viewer) ->executeOne();
if ($inline) { $inline = $inline->newInlineCommentObject(); }
return $inline; }
private function hasContentState() { $request = $this->getRequest(); return (bool)$request->getBool('hasContentState'); }
private function newRequestContentState($inline) { $request = $this->getRequest(); return $inline->newContentStateFromRequest($request); }
private function updateCommentContentState(PhabricatorInlineComment $inline) { if (!$this->hasContentState()) { throw new Exception( pht( 'Attempting to update comment content state, but request has no '. 'content state.')); }
$state = $this->newRequestContentState($inline); $inline->setContentState($state); }
private function saveComment(PhabricatorInlineComment $inline) { $viewer = $this->getViewer(); $draft_engine = $this->newDraftEngine();
$inline->openTransaction(); $inline->save();
PhabricatorVersionedDraft::purgeDrafts( $inline->getPHID(), $viewer->getPHID());
if ($draft_engine) { $draft_engine->synchronize(); }
$inline->saveTransaction(); }
private function newDraftEngine() { $viewer = $this->getViewer(); $object = $this->getContainerObject();
if (!($object instanceof PhabricatorDraftInterface)) { return null; }
return $object->newDraftEngine() ->setObject($object) ->setViewer($viewer); }
}