@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
at upstream/main 382 lines 10 kB view raw
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}