Path: blob/master/src/infrastructure/diff/PhabricatorInlineCommentController.php
12241 views
<?php12abstract class PhabricatorInlineCommentController3extends PhabricatorController {45private $containerObject;67abstract protected function createComment();8abstract protected function newInlineCommentQuery();9abstract protected function loadCommentForDone($id);10abstract protected function loadObjectOwnerPHID(11PhabricatorInlineComment $inline);12abstract protected function newContainerObject();1314final protected function getContainerObject() {15if ($this->containerObject === null) {16$object = $this->newContainerObject();17if (!$object) {18throw new Exception(19pht(20'Failed to load container object for inline comment.'));21}22$this->containerObject = $object;23}2425return $this->containerObject;26}2728protected function hideComments(array $ids) {29throw new PhutilMethodNotImplementedException();30}3132protected function showComments(array $ids) {33throw new PhutilMethodNotImplementedException();34}3536private $changesetID;37private $isNewFile;38private $isOnRight;39private $lineNumber;40private $lineLength;41private $operation;42private $commentID;43private $renderer;44private $replyToCommentPHID;4546public function getCommentID() {47return $this->commentID;48}4950public function getOperation() {51return $this->operation;52}5354public function getLineLength() {55return $this->lineLength;56}5758public function getLineNumber() {59return $this->lineNumber;60}6162public function getIsOnRight() {63return $this->isOnRight;64}6566public function getChangesetID() {67return $this->changesetID;68}6970public function getIsNewFile() {71return $this->isNewFile;72}7374public function setRenderer($renderer) {75$this->renderer = $renderer;76return $this;77}7879public function getRenderer() {80return $this->renderer;81}8283public function setReplyToCommentPHID($phid) {84$this->replyToCommentPHID = $phid;85return $this;86}8788public function getReplyToCommentPHID() {89return $this->replyToCommentPHID;90}9192public function processRequest() {93$request = $this->getRequest();94$viewer = $this->getViewer();9596if (!$request->validateCSRF()) {97return new Aphront404Response();98}99100$this->readRequestParameters();101102$op = $this->getOperation();103switch ($op) {104case 'hide':105case 'show':106$ids = $request->getStrList('ids');107if ($ids) {108if ($op == 'hide') {109$this->hideComments($ids);110} else {111$this->showComments($ids);112}113}114115return id(new AphrontAjaxResponse())->setContent(array());116case 'done':117$inline = $this->loadCommentForDone($this->getCommentID());118119$is_draft_state = false;120$is_checked = false;121switch ($inline->getFixedState()) {122case PhabricatorInlineComment::STATE_DRAFT:123$next_state = PhabricatorInlineComment::STATE_UNDONE;124break;125case PhabricatorInlineComment::STATE_UNDRAFT:126$next_state = PhabricatorInlineComment::STATE_DONE;127$is_checked = true;128break;129case PhabricatorInlineComment::STATE_DONE:130$next_state = PhabricatorInlineComment::STATE_UNDRAFT;131$is_draft_state = true;132break;133default:134case PhabricatorInlineComment::STATE_UNDONE:135$next_state = PhabricatorInlineComment::STATE_DRAFT;136$is_draft_state = true;137$is_checked = true;138break;139}140141$inline->setFixedState($next_state)->save();142143return id(new AphrontAjaxResponse())144->setContent(145array(146'isChecked' => $is_checked,147'draftState' => $is_draft_state,148));149case 'delete':150case 'undelete':151case 'refdelete':152// NOTE: For normal deletes, we just process the delete immediately153// and show an "Undo" action. For deletes by reference from the154// preview ("refdelete"), we prompt first (because the "Undo" may155// not draw, or may not be easy to locate).156157if ($op == 'refdelete') {158if (!$request->isFormPost()) {159return $this->newDialog()160->setTitle(pht('Really delete comment?'))161->addHiddenInput('id', $this->getCommentID())162->addHiddenInput('op', $op)163->appendParagraph(pht('Delete this inline comment?'))164->addCancelButton('#')165->addSubmitButton(pht('Delete'));166}167}168169$is_delete = ($op == 'delete' || $op == 'refdelete');170171$inline = $this->loadCommentByIDForEdit($this->getCommentID());172173if ($is_delete) {174$inline175->setIsEditing(false)176->setIsDeleted(1);177} else {178$inline->setIsDeleted(0);179}180181$this->saveComment($inline);182183return $this->buildEmptyResponse();184case 'save':185$inline = $this->loadCommentByIDForEdit($this->getCommentID());186187$this->updateCommentContentState($inline);188189$inline190->setIsEditing(false)191->setIsDeleted(0);192193// Since we're saving the comment, update the committed state.194$active_state = $inline->getContentState();195$inline->setCommittedContentState($active_state);196197$this->saveComment($inline);198199return $this->buildRenderedCommentResponse(200$inline,201$this->getIsOnRight());202case 'edit':203$inline = $this->loadCommentByIDForEdit($this->getCommentID());204205// NOTE: At time of writing, the "editing" state of inlines is206// preserved by simulating a click on "Edit" when the inline loads.207208// In this case, we don't want to "saveComment()", because it209// recalculates object drafts and purges versioned drafts.210211// The recalculation is merely unnecessary (state doesn't change)212// but purging drafts means that loading a page and then closing it213// discards your drafts.214215// To avoid the purge, only invoke "saveComment()" if we actually216// have changes to apply.217218$is_dirty = false;219if (!$inline->getIsEditing()) {220$inline221->setIsDeleted(0)222->setIsEditing(true);223224$is_dirty = true;225}226227if ($this->hasContentState()) {228$this->updateCommentContentState($inline);229$is_dirty = true;230} else {231PhabricatorInlineComment::loadAndAttachVersionedDrafts(232$viewer,233array($inline));234}235236if ($is_dirty) {237$this->saveComment($inline);238}239240$edit_dialog = $this->buildEditDialog($inline)241->setTitle(pht('Edit Inline Comment'));242243$view = $this->buildScaffoldForView($edit_dialog);244245return $this->newInlineResponse($inline, $view, true);246case 'cancel':247$inline = $this->loadCommentByIDForEdit($this->getCommentID());248249$inline->setIsEditing(false);250251// If the user uses "Undo" to get into an edited state ("AB"), then252// clicks cancel to return to the previous state ("A"), we also want253// to set the stored state back to "A".254$this->updateCommentContentState($inline);255256$this->saveComment($inline);257258return $this->buildEmptyResponse();259case 'draft':260$inline = $this->loadCommentByIDForEdit($this->getCommentID());261262$versioned_draft = PhabricatorVersionedDraft::loadOrCreateDraft(263$inline->getPHID(),264$viewer->getPHID(),265$inline->getID());266267$map = $this->newRequestContentState($inline)->newStorageMap();268$versioned_draft->setProperty('inline.state', $map);269$versioned_draft->save();270271// We have to synchronize the draft engine after saving a versioned272// draft, because taking an inline comment from "no text, no draft"273// to "no text, text in a draft" marks the container object as having274// a draft.275$draft_engine = $this->newDraftEngine();276if ($draft_engine) {277$draft_engine->synchronize();278}279280return $this->buildEmptyResponse();281case 'new':282case 'reply':283default:284// NOTE: We read the values from the client (the display values), not285// the values from the database (the original values) when replying.286// In particular, when replying to a ghost comment which was moved287// across diffs and then moved backward to the most recent visible288// line, we want to reply on the display line (which exists), not on289// the comment's original line (which may not exist in this changeset).290$is_new = $this->getIsNewFile();291$number = $this->getLineNumber();292$length = $this->getLineLength();293294$inline = $this->createComment()295->setChangesetID($this->getChangesetID())296->setAuthorPHID($viewer->getPHID())297->setIsNewFile($is_new)298->setLineNumber($number)299->setLineLength($length)300->setReplyToCommentPHID($this->getReplyToCommentPHID())301->setIsEditing(true)302->setStartOffset($request->getInt('startOffset'))303->setEndOffset($request->getInt('endOffset'))304->setContent('');305306$document_engine_key = $request->getStr('documentEngineKey');307if ($document_engine_key !== null) {308$inline->setDocumentEngineKey($document_engine_key);309}310311// If you own this object, mark your own inlines as "Done" by default.312$owner_phid = $this->loadObjectOwnerPHID($inline);313if ($owner_phid) {314if ($viewer->getPHID() == $owner_phid) {315$fixed_state = PhabricatorInlineComment::STATE_DRAFT;316$inline->setFixedState($fixed_state);317}318}319320if ($this->hasContentState()) {321$this->updateCommentContentState($inline);322}323324// NOTE: We're writing the comment as "deleted", then reloading to325// pick up context and undeleting it. This is silly -- we just want326// to load and attach context -- but just loading context is currently327// complicated (for example, context relies on cache keys that expect328// the inline to have an ID).329330$inline->setIsDeleted(1);331332$this->saveComment($inline);333334// Reload the inline to attach context.335$inline = $this->loadCommentByIDForEdit($inline->getID());336337// Now, we can read the source file and set the initial state.338$state = $inline->getContentState();339$default_suggestion = $inline->getDefaultSuggestionText();340$state->setContentSuggestionText($default_suggestion);341342$inline->setInitialContentState($state);343$inline->setContentState($state);344345$inline->setIsDeleted(0);346347$this->saveComment($inline);348349$edit_dialog = $this->buildEditDialog($inline);350351if ($this->getOperation() == 'reply') {352$edit_dialog->setTitle(pht('Reply to Inline Comment'));353} else {354$edit_dialog->setTitle(pht('New Inline Comment'));355}356357$view = $this->buildScaffoldForView($edit_dialog);358359return $this->newInlineResponse($inline, $view, true);360}361}362363private function readRequestParameters() {364$request = $this->getRequest();365366// NOTE: This isn't necessarily a DifferentialChangeset ID, just an367// application identifier for the changeset. In Diffusion, it's a Path ID.368$this->changesetID = $request->getInt('changesetID');369370$this->isNewFile = (int)$request->getBool('is_new');371$this->isOnRight = $request->getBool('on_right');372$this->lineNumber = $request->getInt('number');373$this->lineLength = $request->getInt('length');374$this->commentID = $request->getInt('id');375$this->operation = $request->getStr('op');376$this->renderer = $request->getStr('renderer');377$this->replyToCommentPHID = $request->getStr('replyToCommentPHID');378379if ($this->getReplyToCommentPHID()) {380$reply_phid = $this->getReplyToCommentPHID();381$reply_comment = $this->loadCommentByPHID($reply_phid);382if (!$reply_comment) {383throw new Exception(384pht('Failed to load comment "%s".', $reply_phid));385}386387// When replying, force the new comment into the same location as the388// old comment. If we don't do this, replying to a ghost comment from389// diff A while viewing diff B can end up placing the two comments in390// different places while viewing diff C, because the porting algorithm391// makes a different decision. Forcing the comments to bind to the same392// place makes sure they stick together no matter which diff is being393// viewed. See T10562 for discussion.394395$this->changesetID = $reply_comment->getChangesetID();396$this->isNewFile = $reply_comment->getIsNewFile();397$this->lineNumber = $reply_comment->getLineNumber();398$this->lineLength = $reply_comment->getLineLength();399}400}401402private function buildEditDialog(PhabricatorInlineComment $inline) {403$request = $this->getRequest();404$viewer = $this->getViewer();405406$edit_dialog = id(new PHUIDiffInlineCommentEditView())407->setViewer($viewer)408->setInlineComment($inline)409->setIsOnRight($this->getIsOnRight())410->setRenderer($this->getRenderer());411412return $edit_dialog;413}414415private function buildEmptyResponse() {416return id(new AphrontAjaxResponse())417->setContent(418array(419'inline' => array(),420'view' => null,421));422}423424private function buildRenderedCommentResponse(425PhabricatorInlineComment $inline,426$on_right) {427428$request = $this->getRequest();429$viewer = $this->getViewer();430431$engine = new PhabricatorMarkupEngine();432$engine->setViewer($viewer);433$engine->addObject(434$inline,435PhabricatorInlineComment::MARKUP_FIELD_BODY);436$engine->process();437438$phids = array($viewer->getPHID());439440$handles = $this->loadViewerHandles($phids);441$object_owner_phid = $this->loadObjectOwnerPHID($inline);442443$view = id(new PHUIDiffInlineCommentDetailView())444->setUser($viewer)445->setInlineComment($inline)446->setIsOnRight($on_right)447->setMarkupEngine($engine)448->setHandles($handles)449->setEditable(true)450->setCanMarkDone(false)451->setObjectOwnerPHID($object_owner_phid);452453$view = $this->buildScaffoldForView($view);454455return $this->newInlineResponse($inline, $view, false);456}457458private function buildScaffoldForView(PHUIDiffInlineCommentView $view) {459$renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey(460$this->getRenderer());461462$view = $renderer->getRowScaffoldForInline($view);463464return id(new PHUIDiffInlineCommentTableScaffold())465->addRowScaffold($view);466}467468private function newInlineResponse(469PhabricatorInlineComment $inline,470$view,471$is_edit) {472$viewer = $this->getViewer();473474if ($inline->getReplyToCommentPHID()) {475$can_suggest = false;476} else {477$can_suggest = (bool)$inline->getInlineContext();478}479480if ($is_edit) {481$state = $inline->getContentStateMapForEdit($viewer);482} else {483$state = $inline->getContentStateMap();484}485486$response = array(487'inline' => array(488'id' => $inline->getID(),489'state' => $state,490'canSuggestEdit' => $can_suggest,491),492'view' => hsprintf('%s', $view),493);494495return id(new AphrontAjaxResponse())496->setContent($response);497}498499final protected function loadCommentByID($id) {500$query = $this->newInlineCommentQuery()501->withIDs(array($id));502503return $this->loadCommentByQuery($query);504}505506final protected function loadCommentByPHID($phid) {507$query = $this->newInlineCommentQuery()508->withPHIDs(array($phid));509510return $this->loadCommentByQuery($query);511}512513final protected function loadCommentByIDForEdit($id) {514$viewer = $this->getViewer();515516$query = $this->newInlineCommentQuery()517->withIDs(array($id))518->needInlineContext(true);519520$inline = $this->loadCommentByQuery($query);521522if (!$inline) {523throw new Exception(524pht(525'Unable to load inline "%s".',526$id));527}528529if (!$this->canEditInlineComment($viewer, $inline)) {530throw new Exception(531pht(532'Inline comment "%s" is not editable.',533$id));534}535536return $inline;537}538539private function loadCommentByQuery(540PhabricatorDiffInlineCommentQuery $query) {541$viewer = $this->getViewer();542543$inline = $query544->setViewer($viewer)545->executeOne();546547if ($inline) {548$inline = $inline->newInlineCommentObject();549}550551return $inline;552}553554private function hasContentState() {555$request = $this->getRequest();556return (bool)$request->getBool('hasContentState');557}558559private function newRequestContentState($inline) {560$request = $this->getRequest();561return $inline->newContentStateFromRequest($request);562}563564private function updateCommentContentState(PhabricatorInlineComment $inline) {565if (!$this->hasContentState()) {566throw new Exception(567pht(568'Attempting to update comment content state, but request has no '.569'content state.'));570}571572$state = $this->newRequestContentState($inline);573$inline->setContentState($state);574}575576private function saveComment(PhabricatorInlineComment $inline) {577$viewer = $this->getViewer();578$draft_engine = $this->newDraftEngine();579580$inline->openTransaction();581$inline->save();582583PhabricatorVersionedDraft::purgeDrafts(584$inline->getPHID(),585$viewer->getPHID());586587if ($draft_engine) {588$draft_engine->synchronize();589}590591$inline->saveTransaction();592}593594private function newDraftEngine() {595$viewer = $this->getViewer();596$object = $this->getContainerObject();597598if (!($object instanceof PhabricatorDraftInterface)) {599return null;600}601602return $object->newDraftEngine()603->setObject($object)604->setViewer($viewer);605}606607}608609610