@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 DifferentialInlineCommentMailView
4 extends DifferentialMailView {
5
6 private $viewer;
7 private $inlines;
8 private $changesets;
9 private $authors;
10
11 public function setViewer(PhabricatorUser $viewer) {
12 $this->viewer = $viewer;
13 return $this;
14 }
15
16 public function getViewer() {
17 return $this->viewer;
18 }
19
20 public function setInlines($inlines) {
21 $this->inlines = $inlines;
22 return $this;
23 }
24
25 public function getInlines() {
26 return $this->inlines;
27 }
28
29 public function buildMailSection() {
30 $inlines = $this->getInlines();
31
32 $comments = mpull($inlines, 'getComment');
33 $comments = mpull($comments, null, 'getPHID');
34 $parents = $this->loadParents($comments);
35 $all_comments = $comments + $parents;
36
37 $this->changesets = $this->loadChangesets($all_comments);
38 $this->authors = $this->loadAuthors($all_comments);
39 $groups = $this->groupInlines($inlines);
40
41 $hunk_parser = new DifferentialHunkParser();
42
43 $spacer_text = null;
44 $spacer_html = phutil_tag('br');
45
46 $section = new PhabricatorMetaMTAMailSection();
47
48 $last_group_key = last_key($groups);
49 foreach ($groups as $changeset_id => $group) {
50 $changeset = $this->getChangeset($changeset_id);
51 if (!$changeset) {
52 continue;
53 }
54
55 $is_last_group = ($changeset_id == $last_group_key);
56
57 $last_inline_key = last_key($group);
58 foreach ($group as $inline_key => $inline) {
59 $comment = $inline->getComment();
60 $parent_phid = $comment->getReplyToCommentPHID();
61
62 $inline_object = $comment->newInlineCommentObject();
63 $document_engine_key = $inline_object->getDocumentEngineKey();
64
65 $is_last_inline = ($inline_key == $last_inline_key);
66
67 $context_text = null;
68 $context_html = null;
69
70 if ($parent_phid) {
71 $parent = idx($parents, $parent_phid);
72 if ($parent) {
73 $context_text = $this->renderInline($parent, false, true);
74 $context_html = $this->renderInline($parent, true, true);
75 }
76 } else if ($document_engine_key !== null) {
77 // See T13513. If an inline was left on a rendered document, don't
78 // include the patch context. Document engines currently can not
79 // render to mail targets, and using the line numbers as raw source
80 // lines produces misleading context.
81
82 $patch_text = null;
83 $context_text = $this->renderPatch($comment, $patch_text, false);
84
85 $patch_html = null;
86 $context_html = $this->renderPatch($comment, $patch_html, true);
87 } else {
88 $patch_text = $this->getPatch($hunk_parser, $comment, false);
89 $context_text = $this->renderPatch($comment, $patch_text, false);
90
91 $patch_html = $this->getPatch($hunk_parser, $comment, true);
92 $context_html = $this->renderPatch($comment, $patch_html, true);
93 }
94
95 $render_text = $this->renderInline($comment, false, false);
96 $render_html = $this->renderInline($comment, true, false);
97
98 $section->addPlaintextFragment($context_text);
99 $section->addPlaintextFragment($spacer_text);
100 $section->addPlaintextFragment($render_text);
101
102 $html_fragment = $this->renderContentBox(
103 array(
104 $context_html,
105 $render_html,
106 ));
107
108 $section->addHTMLFragment($html_fragment);
109
110 if (!$is_last_group || !$is_last_inline) {
111 $section->addPlaintextFragment($spacer_text);
112 $section->addHTMLFragment($spacer_html);
113 }
114 }
115 }
116
117 return $section;
118 }
119
120 private function loadChangesets(array $comments) {
121 if (!$comments) {
122 return array();
123 }
124
125 $ids = array();
126 foreach ($comments as $comment) {
127 $ids[] = $comment->getChangesetID();
128 }
129
130 $changesets = id(new DifferentialChangesetQuery())
131 ->setViewer($this->getViewer())
132 ->withIDs($ids)
133 ->needHunks(true)
134 ->execute();
135
136 return mpull($changesets, null, 'getID');
137 }
138
139 private function loadParents(array $comments) {
140 $viewer = $this->getViewer();
141
142 $phids = array();
143 foreach ($comments as $comment) {
144 $parent_phid = $comment->getReplyToCommentPHID();
145 if (!$parent_phid) {
146 continue;
147 }
148 $phids[] = $parent_phid;
149 }
150
151 if (!$phids) {
152 return array();
153 }
154
155 $parents = id(new DifferentialDiffInlineCommentQuery())
156 ->setViewer($viewer)
157 ->withPHIDs($phids)
158 ->execute();
159
160 return mpull($parents, null, 'getPHID');
161 }
162
163 private function loadAuthors(array $comments) {
164 $viewer = $this->getViewer();
165
166 $phids = array();
167 foreach ($comments as $comment) {
168 $author_phid = $comment->getAuthorPHID();
169 if (!$author_phid) {
170 continue;
171 }
172 $phids[] = $author_phid;
173 }
174
175 if (!$phids) {
176 return array();
177 }
178
179 return $viewer->loadHandles($phids);
180 }
181
182 private function groupInlines(array $inlines) {
183 return DifferentialTransactionComment::sortAndGroupInlines(
184 $inlines,
185 $this->changesets);
186 }
187
188 private function renderInline(
189 DifferentialTransactionComment $comment,
190 $is_html,
191 $is_quote) {
192
193 $changeset = $this->getChangeset($comment->getChangesetID());
194 if (!$changeset) {
195 return null;
196 }
197
198 $content = $comment->getContent();
199 $content = $this->renderRemarkupContent($content, $is_html);
200
201 if ($is_quote) {
202 $header = $this->renderHeader($comment, $is_html, true);
203 } else {
204 $header = null;
205 }
206
207 if ($is_html) {
208 $style = array(
209 'margin: 8px 0;',
210 'padding: 0 12px;',
211 );
212
213 if ($is_quote) {
214 $style[] = 'color: #74777D;';
215 }
216
217 $content = phutil_tag(
218 'div',
219 array(
220 'style' => implode(' ', $style),
221 ),
222 $content);
223 }
224
225 $parts = array(
226 $header,
227 "\n",
228 $content,
229 );
230
231 if (!$is_html) {
232 $parts = implode('', $parts);
233 $parts = trim($parts);
234 }
235
236 if ($is_quote) {
237 if ($is_html) {
238 $parts = $this->quoteHTML($parts);
239 } else {
240 $parts = $this->quoteText($parts);
241 }
242 }
243
244 return $parts;
245 }
246
247 private function renderRemarkupContent($content, $is_html) {
248 $viewer = $this->getViewer();
249 $production_uri = PhabricatorEnv::getProductionURI('/');
250
251 if ($is_html) {
252 $mode = PhutilRemarkupEngine::MODE_HTML_MAIL;
253 } else {
254 $mode = PhutilRemarkupEngine::MODE_TEXT;
255 }
256
257 $attributes = array(
258 'style' => 'padding: 0; margin: 8px;',
259 );
260
261 $engine = PhabricatorMarkupEngine::newMarkupEngine(array())
262 ->setConfig('viewer', $viewer)
263 ->setConfig('uri.base', $production_uri)
264 ->setConfig('default.p.attributes', $attributes)
265 ->setMode($mode);
266
267 try {
268 return $engine->markupText($content);
269 } catch (Exception $ex) {
270 return $content;
271 }
272 }
273
274 private function getChangeset($id) {
275 return idx($this->changesets, $id);
276 }
277
278 private function getAuthor($phid) {
279 if (isset($this->authors[$phid])) {
280 return $this->authors[$phid];
281 }
282 return null;
283 }
284
285 private function quoteText($block) {
286 $block = phutil_split_lines($block);
287 foreach ($block as $key => $line) {
288 $block[$key] = '> '.$line;
289 }
290
291 return implode('', $block);
292 }
293
294 private function quoteHTML($block) {
295 $styles = array(
296 'padding: 0;',
297 'background: #F7F7F7;',
298 'border-color: #e3e4e8;',
299 'border-style: solid;',
300 'border-width: 0 0 1px 0;',
301 'margin: 0;',
302 );
303
304 $styles = implode(' ', $styles);
305
306 return phutil_tag(
307 'div',
308 array(
309 'style' => $styles,
310 ),
311 $block);
312 }
313
314 private function getPatch(
315 DifferentialHunkParser $parser,
316 DifferentialTransactionComment $comment,
317 $is_html) {
318
319 $changeset = $this->getChangeset($comment->getChangesetID());
320 $is_new = $comment->getIsNewFile();
321 $start = $comment->getLineNumber();
322 $length = $comment->getLineLength();
323
324 // By default, show one line of context around the target inline.
325 $context = 1;
326
327 // If the inline is at least 3 lines long, don't show any extra context.
328 if ($length >= 2) {
329 $context = 0;
330 }
331
332 // If the inline is more than 7 lines long, only show the first 7 lines.
333 if ($length >= 6) {
334 $length = 6;
335 }
336
337 if (!$is_html) {
338 $hunks = $changeset->getHunks();
339 $patch = $parser->makeContextDiff(
340 $hunks,
341 $is_new,
342 $start,
343 $length,
344 $context);
345 $patch = phutil_split_lines($patch);
346
347 // Remove the "@@ -x,y +u,v @@" line.
348 array_shift($patch);
349
350 return implode('', $patch);
351 }
352
353 $viewer = $this->getViewer();
354 $engine = new PhabricatorMarkupEngine();
355
356 if ($is_new) {
357 $offset_mode = 'new';
358 } else {
359 $offset_mode = 'old';
360 }
361
362 // See PHI894. Use the parse cache since we can end up with a large
363 // rendering cost otherwise when users or bots leave hundreds of inline
364 // comments on diffs with long recipient lists.
365 $cache_key = $changeset->getID();
366
367 $viewstate = new PhabricatorChangesetViewState();
368
369 $parser = id(new DifferentialChangesetParser())
370 ->setRenderCacheKey($cache_key)
371 ->setViewer($viewer)
372 ->setViewstate($viewstate)
373 ->setChangeset($changeset)
374 ->setOffsetMode($offset_mode)
375 ->setMarkupEngine($engine);
376
377 $parser->setRenderer(new DifferentialChangesetOneUpMailRenderer());
378
379 return $parser->render(
380 $start - $context,
381 $length + (2 * $context),
382 array());
383 }
384
385 private function renderPatch(
386 DifferentialTransactionComment $comment,
387 $patch,
388 $is_html) {
389
390 if ($is_html) {
391 if ($patch !== null) {
392 $patch = $this->renderCodeBlock($patch);
393 }
394 }
395
396 $header = $this->renderHeader($comment, $is_html, false);
397
398 if ($patch === null) {
399 $patch = array(
400 $header,
401 );
402 } else {
403 $patch = array(
404 $header,
405 "\n",
406 $patch,
407 );
408 }
409
410 if (!$is_html) {
411 $patch = implode('', $patch);
412 $patch = $this->quoteText($patch);
413 } else {
414 $patch = $this->quoteHTML($patch);
415 }
416
417 return $patch;
418 }
419
420 private function renderHeader(
421 DifferentialTransactionComment $comment,
422 $is_html,
423 $with_author) {
424
425 $changeset = $this->getChangeset($comment->getChangesetID());
426 $path = $changeset->getFilename();
427
428 // Only show the filename.
429 $path = basename($path);
430
431 $start = $comment->getLineNumber();
432 $length = $comment->getLineLength();
433 if ($length) {
434 $range = pht('%s-%s', $start, $start + $length);
435 } else {
436 $range = $start;
437 }
438
439 $header = "{$path}:{$range}";
440 if ($is_html) {
441 $header = $this->renderHeaderBold($header);
442 }
443
444 if ($with_author) {
445 $author = $this->getAuthor($comment->getAuthorPHID());
446 } else {
447 $author = null;
448 }
449
450 if ($author) {
451 $byline = $author->getName();
452
453 if ($is_html) {
454 $byline = $this->renderHeaderBold($byline);
455 }
456
457 $header = pht('%s wrote in %s', $byline, $header);
458 }
459
460 if ($is_html) {
461 $link_href = $this->getInlineURI($comment);
462 if ($link_href) {
463 $link_style = array(
464 'float: right;',
465 'text-decoration: none;',
466 );
467
468 $link = phutil_tag(
469 'a',
470 array(
471 'style' => implode(' ', $link_style),
472 'href' => $link_href,
473 ),
474 array(
475 pht('View Inline'),
476
477 // See PHI920. Add a space after the link so we render this into
478 // the document:
479 //
480 // View Inline filename.txt
481 //
482 // Otherwise, we render "Inlinefilename.txt" and double-clicking
483 // the file name selects the word "Inline" as well.
484 ' ',
485 ));
486 } else {
487 $link = null;
488 }
489
490 $header = $this->renderHeaderBlock(array($link, $header));
491 }
492
493 return $header;
494 }
495
496 private function getInlineURI(DifferentialTransactionComment $comment) {
497 $changeset = $this->getChangeset($comment->getChangesetID());
498 if (!$changeset) {
499 return null;
500 }
501
502 $diff = $changeset->getDiff();
503 if (!$diff) {
504 return null;
505 }
506
507 $revision = $diff->getRevision();
508 if (!$revision) {
509 return null;
510 }
511
512 $link_href = '/'.$revision->getMonogram().'#inline-'.$comment->getID();
513 $link_href = PhabricatorEnv::getProductionURI($link_href);
514
515 return $link_href;
516 }
517
518
519}