@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 PhabricatorApplicationTransactionCommentEditor
4 extends PhabricatorEditor {
5
6 private $contentSource;
7 private $actingAsPHID;
8 private $request;
9 private $cancelURI;
10 private $isNewComment;
11
12 public function setActingAsPHID($acting_as_phid) {
13 $this->actingAsPHID = $acting_as_phid;
14 return $this;
15 }
16
17 public function getActingAsPHID() {
18 if ($this->actingAsPHID) {
19 return $this->actingAsPHID;
20 }
21 return $this->getActor()->getPHID();
22 }
23
24 public function setContentSource(PhabricatorContentSource $content_source) {
25 $this->contentSource = $content_source;
26 return $this;
27 }
28
29 public function getContentSource() {
30 return $this->contentSource;
31 }
32
33 public function setRequest(AphrontRequest $request) {
34 $this->request = $request;
35 return $this;
36 }
37
38 public function getRequest() {
39 return $this->request;
40 }
41
42 public function setCancelURI($cancel_uri) {
43 $this->cancelURI = $cancel_uri;
44 return $this;
45 }
46
47 public function getCancelURI() {
48 return $this->cancelURI;
49 }
50
51 public function setIsNewComment($is_new) {
52 $this->isNewComment = $is_new;
53 return $this;
54 }
55
56 public function getIsNewComment() {
57 return $this->isNewComment;
58 }
59
60 /**
61 * Edit a transaction's comment. This method effects the required create,
62 * update or delete to set the transaction's comment to the provided comment.
63 */
64 public function applyEdit(
65 PhabricatorApplicationTransaction $xaction,
66 PhabricatorApplicationTransactionComment $comment) {
67
68 $this->validateEdit($xaction, $comment);
69
70 $actor = $this->requireActor();
71
72 $this->applyMFAChecks($xaction, $comment);
73
74 $comment->setContentSource($this->getContentSource());
75 $comment->setAuthorPHID($this->getActingAsPHID());
76
77 // TODO: This needs to be more sophisticated once we have meta-policies.
78 $comment->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
79 $comment->setEditPolicy($this->getActingAsPHID());
80
81 $xaction->openTransaction();
82 $xaction->beginReadLocking();
83 if ($xaction->getID()) {
84 $xaction->reload();
85 }
86
87 $new_version = $xaction->getCommentVersion() + 1;
88
89 $comment->setCommentVersion($new_version);
90 $comment->setTransactionPHID($xaction->getPHID());
91 $comment->save();
92
93 $old_comment = $xaction->getComment();
94 $comment->attachOldComment($old_comment);
95
96 $xaction->setCommentVersion($new_version);
97 $xaction->setCommentPHID($comment->getPHID());
98 $xaction->setViewPolicy($comment->getViewPolicy());
99 $xaction->setEditPolicy($comment->getEditPolicy());
100 $xaction->save();
101 $xaction->attachComment($comment);
102
103 // For comment edits, we need to make sure there are no automagical
104 // transactions like adding mentions or projects.
105 if ($new_version > 1) {
106 $object = id(new PhabricatorObjectQuery())
107 ->withPHIDs(array($xaction->getObjectPHID()))
108 ->setViewer($this->getActor())
109 ->executeOne();
110 if ($object &&
111 $object instanceof PhabricatorApplicationTransactionInterface) {
112 $editor = $object->getApplicationTransactionEditor();
113 $editor->setActor($this->getActor());
114 $support_xactions = $editor->getExpandedSupportTransactions(
115 $object,
116 $xaction);
117 if ($support_xactions) {
118 $editor
119 ->setContentSource($this->getContentSource())
120 ->setContinueOnNoEffect(true)
121 ->setContinueOnMissingFields(true)
122 ->applyTransactions($object, $support_xactions);
123 }
124 }
125 }
126 $xaction->endReadLocking();
127 $xaction->saveTransaction();
128
129 return $this;
130 }
131
132 /**
133 * Validate that the edit is permissible, and the actor has permission to
134 * perform it.
135 */
136 private function validateEdit(
137 PhabricatorApplicationTransaction $xaction,
138 PhabricatorApplicationTransactionComment $comment) {
139
140 if (!$xaction->getPHID()) {
141 throw new Exception(
142 pht(
143 'Transaction must have a PHID before calling %s!',
144 'applyEdit()'));
145 }
146
147 $type_comment = PhabricatorTransactions::TYPE_COMMENT;
148 if ($xaction->getTransactionType() == $type_comment) {
149 if ($comment->getPHID()) {
150 throw new Exception(
151 pht('Transaction comment must not yet have a PHID!'));
152 }
153 }
154
155 if (!$this->getContentSource()) {
156 throw new PhutilInvalidStateException('applyEdit');
157 }
158
159 $actor = $this->requireActor();
160
161 PhabricatorPolicyFilter::requireCapability(
162 $actor,
163 $xaction,
164 PhabricatorPolicyCapability::CAN_VIEW);
165
166 if ($comment->getIsRemoved() && $actor->getIsAdmin()) {
167 // NOTE: Administrators can remove comments by any user, and don't need
168 // to pass the edit check.
169 } else {
170 PhabricatorPolicyFilter::requireCapability(
171 $actor,
172 $xaction,
173 PhabricatorPolicyCapability::CAN_EDIT);
174
175 PhabricatorPolicyFilter::requireCanInteract(
176 $actor,
177 $xaction->getObject());
178 }
179 }
180
181 private function applyMFAChecks(
182 PhabricatorApplicationTransaction $xaction,
183 PhabricatorApplicationTransactionComment $comment) {
184 $actor = $this->requireActor();
185
186 // We don't do any MFA checks here when you're creating a comment for the
187 // first time (the parent editor handles them for us), so we can just bail
188 // out if this is the creation flow.
189 if ($this->getIsNewComment()) {
190 return;
191 }
192
193 $request = $this->getRequest();
194 if (!$request) {
195 throw new PhutilInvalidStateException('setRequest');
196 }
197
198 $cancel_uri = $this->getCancelURI();
199 if (!strlen($cancel_uri)) {
200 throw new PhutilInvalidStateException('setCancelURI');
201 }
202
203 // If you're deleting a comment, we try to prompt you for MFA if you have
204 // it configured, but do not require that you have it configured. In most
205 // cases, this is administrators removing content.
206
207 // See PHI1173. If you're editing a comment you authored and the original
208 // comment was signed with MFA, you MUST have MFA on your account and you
209 // MUST sign the edit with MFA. Otherwise, we can end up with an MFA badge
210 // on different content than what was signed.
211
212 $want_mfa = false;
213 $need_mfa = false;
214
215 if ($comment->getIsRemoved()) {
216 // Try to prompt on removal.
217 $want_mfa = true;
218 }
219
220 if ($xaction->getIsMFATransaction()) {
221 if ($actor->getPHID() === $xaction->getAuthorPHID()) {
222 // Strictly require MFA if the original transaction was signed and
223 // you're the author.
224 $want_mfa = true;
225 $need_mfa = true;
226 }
227 }
228
229 if (!$want_mfa) {
230 return;
231 }
232
233 if ($need_mfa) {
234 $factors = id(new PhabricatorAuthFactorConfigQuery())
235 ->setViewer($actor)
236 ->withUserPHIDs(array($this->getActingAsPHID()))
237 ->withFactorProviderStatuses(
238 array(
239 PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
240 PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
241 ))
242 ->execute();
243 if (!$factors) {
244 $error = new PhabricatorApplicationTransactionValidationError(
245 $xaction->getTransactionType(),
246 pht('No MFA'),
247 pht(
248 'This comment was signed with MFA, so edits to it must also be '.
249 'signed with MFA. You do not have any MFA factors attached to '.
250 'your account, so you can not sign this edit. Add MFA to your '.
251 'account in Settings.'),
252 $xaction);
253
254 throw new PhabricatorApplicationTransactionValidationException(
255 array(
256 $error,
257 ));
258 }
259 }
260
261 $workflow_key = sprintf(
262 'comment.edit(%s, %d)',
263 $xaction->getPHID(),
264 $xaction->getComment()->getID());
265
266 $hisec_token = id(new PhabricatorAuthSessionEngine())
267 ->setWorkflowKey($workflow_key)
268 ->requireHighSecurityToken($actor, $request, $cancel_uri);
269 }
270
271}