@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
3abstract class PhabricatorDiffInlineCommentQuery
4 extends PhabricatorApplicationTransactionCommentQuery {
5
6 const INLINE_CONTEXT_CACHE_VERSION = 1;
7
8 private $fixedStates;
9 private $needReplyToComments;
10 private $publishedComments;
11 private $publishableComments;
12 private $needHidden;
13 private $needAppliedDrafts;
14 private $needInlineContext;
15
16 abstract protected function buildInlineCommentWhereClauseParts(
17 AphrontDatabaseConnection $conn);
18 abstract public function withObjectPHIDs(array $phids);
19 abstract protected function loadHiddenCommentIDs(
20 $viewer_phid,
21 array $comments);
22
23 abstract protected function newInlineContextMap(array $inlines);
24 abstract protected function newInlineContextFromCacheData(array $map);
25
26 final public function withFixedStates(array $states) {
27 $this->fixedStates = $states;
28 return $this;
29 }
30
31 final public function needReplyToComments($need_reply_to) {
32 $this->needReplyToComments = $need_reply_to;
33 return $this;
34 }
35
36 final public function withPublishableComments($with_publishable) {
37 $this->publishableComments = $with_publishable;
38 return $this;
39 }
40
41 final public function withPublishedComments($with_published) {
42 $this->publishedComments = $with_published;
43 return $this;
44 }
45
46 final public function needHidden($need_hidden) {
47 $this->needHidden = $need_hidden;
48 return $this;
49 }
50
51 final public function needInlineContext($need_context) {
52 $this->needInlineContext = $need_context;
53 return $this;
54 }
55
56 final public function needAppliedDrafts($need_applied) {
57 $this->needAppliedDrafts = $need_applied;
58 return $this;
59 }
60
61 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
62 $where = parent::buildWhereClauseParts($conn);
63 $alias = $this->getPrimaryTableAlias();
64
65 foreach ($this->buildInlineCommentWhereClauseParts($conn) as $part) {
66 $where[] = $part;
67 }
68
69 if ($this->fixedStates !== null) {
70 $where[] = qsprintf(
71 $conn,
72 '%T.fixedState IN (%Ls)',
73 $alias,
74 $this->fixedStates);
75 }
76
77 $show_published = false;
78 $show_publishable = false;
79
80 if ($this->publishableComments !== null) {
81 if (!$this->publishableComments) {
82 throw new Exception(
83 pht(
84 'Querying for comments that are "not publishable" is '.
85 'not supported.'));
86 }
87 $show_publishable = true;
88 }
89
90 if ($this->publishedComments !== null) {
91 if (!$this->publishedComments) {
92 throw new Exception(
93 pht(
94 'Querying for comments that are "not published" is '.
95 'not supported.'));
96 }
97 $show_published = true;
98 }
99
100 if ($show_publishable || $show_published) {
101 $clauses = array();
102
103 if ($show_published) {
104 $clauses[] = qsprintf(
105 $conn,
106 '%T.transactionPHID IS NOT NULL',
107 $alias);
108 }
109
110 if ($show_publishable) {
111 $viewer = $this->getViewer();
112 $viewer_phid = $viewer->getPHID();
113
114 // If the viewer has a PHID, unpublished comments they authored and
115 // have not deleted are visible.
116 if ($viewer_phid) {
117 $clauses[] = qsprintf(
118 $conn,
119 '%T.authorPHID = %s
120 AND %T.isDeleted = 0
121 AND %T.transactionPHID IS NULL ',
122 $alias,
123 $viewer_phid,
124 $alias,
125 $alias);
126 }
127 }
128
129 // We can end up with a known-empty query if we (for example) query for
130 // publishable comments and the viewer is logged-out.
131 if (!$clauses) {
132 throw new PhabricatorEmptyQueryException();
133 }
134
135 $where[] = qsprintf(
136 $conn,
137 '%LO',
138 $clauses);
139 }
140
141 return $where;
142 }
143
144 protected function willFilterPage(array $inlines) {
145 $viewer = $this->getViewer();
146
147 if ($this->needReplyToComments) {
148 $reply_phids = array();
149 foreach ($inlines as $inline) {
150 $reply_phid = $inline->getReplyToCommentPHID();
151 if ($reply_phid) {
152 $reply_phids[] = $reply_phid;
153 }
154 }
155
156 if ($reply_phids) {
157 $reply_inlines = newv(get_class($this), array())
158 ->setViewer($this->getViewer())
159 ->setParentQuery($this)
160 ->withPHIDs($reply_phids)
161 ->execute();
162 $reply_inlines = mpull($reply_inlines, null, 'getPHID');
163 } else {
164 $reply_inlines = array();
165 }
166
167 foreach ($inlines as $key => $inline) {
168 $reply_phid = $inline->getReplyToCommentPHID();
169 if (!$reply_phid) {
170 $inline->attachReplyToComment(null);
171 continue;
172 }
173 $reply = idx($reply_inlines, $reply_phid);
174 if (!$reply) {
175 $this->didRejectResult($inline);
176 unset($inlines[$key]);
177 continue;
178 }
179 $inline->attachReplyToComment($reply);
180 }
181 }
182
183 if (!$inlines) {
184 return $inlines;
185 }
186
187 $need_drafts = $this->needAppliedDrafts;
188 $drop_void = $this->publishableComments;
189 $convert_objects = ($need_drafts || $drop_void);
190
191 if ($convert_objects) {
192 $inlines = mpull($inlines, 'newInlineCommentObject');
193
194 PhabricatorInlineComment::loadAndAttachVersionedDrafts(
195 $viewer,
196 $inlines);
197
198 if ($need_drafts) {
199 // Don't count void inlines when considering draft state.
200 foreach ($inlines as $key => $inline) {
201 if ($inline->isVoidComment($viewer)) {
202 $this->didRejectResult($inline->getStorageObject());
203 unset($inlines[$key]);
204 continue;
205 }
206
207 // For other inlines: if they have a nonempty draft state, set their
208 // content to the draft state content. We want to submit the comment
209 // as it is currently shown to the user, not as it was stored the last
210 // time they clicked "Save".
211
212 $draft_state = $inline->getContentStateForEdit($viewer);
213 if (!$draft_state->isEmptyContentState()) {
214 $inline->setContentState($draft_state);
215 }
216 }
217 }
218
219 // If we're loading publishable comments, discard any comments that are
220 // empty.
221 if ($drop_void) {
222 foreach ($inlines as $key => $inline) {
223 if ($inline->getTransactionPHID()) {
224 continue;
225 }
226
227 if ($inline->isVoidComment($viewer)) {
228 $this->didRejectResult($inline->getStorageObject());
229 unset($inlines[$key]);
230 continue;
231 }
232 }
233 }
234
235 $inlines = mpull($inlines, 'getStorageObject');
236 }
237
238 return $inlines;
239 }
240
241 protected function didFilterPage(array $inlines) {
242 $viewer = $this->getViewer();
243
244 if ($this->needHidden) {
245 $viewer_phid = $viewer->getPHID();
246
247 if ($viewer_phid) {
248 $hidden = $this->loadHiddenCommentIDs(
249 $viewer_phid,
250 $inlines);
251 } else {
252 $hidden = array();
253 }
254
255 foreach ($inlines as $inline) {
256 $inline->attachIsHidden(isset($hidden[$inline->getID()]));
257 }
258 }
259
260 if ($this->needInlineContext) {
261 $need_context = array();
262 foreach ($inlines as $inline) {
263 $object = $inline->newInlineCommentObject();
264
265 if ($object->getDocumentEngineKey() !== null) {
266 $inline->attachInlineContext(null);
267 continue;
268 }
269
270 $need_context[] = $inline;
271 }
272
273 if ($need_context) {
274 $this->loadInlineCommentContext($need_context);
275 }
276 }
277
278 return $inlines;
279 }
280
281 private function loadInlineCommentContext(array $inlines) {
282 $cache_keys = array();
283 foreach ($inlines as $key => $inline) {
284 $object = $inline->newInlineCommentObject();
285 $fragment = $object->getInlineCommentCacheFragment();
286
287 if ($fragment === null) {
288 continue;
289 }
290
291 $cache_keys[$key] = sprintf(
292 '%s.context(v%d)',
293 $fragment,
294 self::INLINE_CONTEXT_CACHE_VERSION);
295 }
296
297 $cache = PhabricatorCaches::getMutableStructureCache();
298
299 $cache_map = $cache->getKeys($cache_keys);
300
301 $context_map = array();
302 $need_construct = array();
303
304 foreach ($inlines as $key => $inline) {
305 $cache_key = idx($cache_keys, $key);
306
307 if ($cache_key !== null) {
308 if (array_key_exists($cache_key, $cache_map)) {
309 $cache_data = $cache_map[$cache_key];
310 $context_map[$key] = $this->newInlineContextFromCacheData(
311 $cache_data);
312 continue;
313 }
314 }
315
316 $need_construct[$key] = $inline;
317 }
318
319 if ($need_construct) {
320 $construct_map = $this->newInlineContextMap($need_construct);
321
322 $write_map = array();
323 foreach ($construct_map as $key => $context) {
324 if ($context === null) {
325 $cache_data = $context;
326 } else {
327 $cache_data = $this->newCacheDataFromInlineContext($context);
328 }
329
330 $cache_key = idx($cache_keys, $key);
331 if ($cache_key !== null) {
332 $write_map[$cache_key] = $cache_data;
333 }
334 }
335
336 if ($write_map) {
337 $cache->setKeys($write_map);
338 }
339
340 $context_map += $construct_map;
341 }
342
343 foreach ($inlines as $key => $inline) {
344 $inline->attachInlineContext(idx($context_map, $key));
345 }
346 }
347
348 protected function newCacheDataFromInlineContext(
349 PhabricatorInlineCommentContext $context) {
350 return $context->newCacheDataMap();
351 }
352
353 final protected function simplifyContext(array $lines, $is_head) {
354 // We want to provide the smallest amount of context we can while still
355 // being useful, since the actual code is visible nearby and showing a
356 // ton of context is silly.
357
358 // Examine each line until we find one that looks "useful" (not just
359 // whitespace or a single bracket). Once we find a useful piece of context
360 // to anchor the text, discard the rest of the lines beyond it.
361
362 if ($is_head) {
363 $lines = array_reverse($lines, true);
364 }
365
366 $saw_context = false;
367 foreach ($lines as $key => $line) {
368 if ($saw_context) {
369 unset($lines[$key]);
370 continue;
371 }
372
373 $saw_context = (strlen(trim($line)) > 3);
374 }
375
376 if ($is_head) {
377 $lines = array_reverse($lines, true);
378 }
379
380 return $lines;
381 }
382}