@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 401 lines 10 kB view raw
1<?php 2 3/** 4 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhrictionDocument> 5 */ 6final class PhrictionDocumentQuery 7 extends PhabricatorCursorPagedPolicyAwareQuery { 8 9 private $ids; 10 private $phids; 11 private $slugs; 12 private $depths; 13 private $slugPrefix; 14 private $statuses; 15 16 private $parentPaths; 17 private $ancestorPaths; 18 19 private $needContent; 20 21 const ORDER_HIERARCHY = 'hierarchy'; 22 23 public function withIDs(array $ids) { 24 $this->ids = $ids; 25 return $this; 26 } 27 28 public function withPHIDs(array $phids) { 29 $this->phids = $phids; 30 return $this; 31 } 32 33 public function withSlugs(array $slugs) { 34 $this->slugs = $slugs; 35 return $this; 36 } 37 38 public function withDepths(array $depths) { 39 $this->depths = $depths; 40 return $this; 41 } 42 43 public function withSlugPrefix($slug_prefix) { 44 $this->slugPrefix = $slug_prefix; 45 return $this; 46 } 47 48 public function withStatuses(array $statuses) { 49 $this->statuses = $statuses; 50 return $this; 51 } 52 53 public function withParentPaths(array $paths) { 54 $this->parentPaths = $paths; 55 return $this; 56 } 57 58 public function withAncestorPaths(array $paths) { 59 $this->ancestorPaths = $paths; 60 return $this; 61 } 62 63 public function needContent($need_content) { 64 $this->needContent = $need_content; 65 return $this; 66 } 67 68 public function newResultObject() { 69 return new PhrictionDocument(); 70 } 71 72 protected function willFilterPage(array $documents) { 73 74 if ($documents) { 75 $ancestor_slugs = array(); 76 foreach ($documents as $key => $document) { 77 $document_slug = $document->getSlug(); 78 foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) { 79 $ancestor_slugs[$ancestor][] = $key; 80 } 81 } 82 83 if ($ancestor_slugs) { 84 $table = new PhrictionDocument(); 85 $conn_r = $table->establishConnection('r'); 86 $ancestors = queryfx_all( 87 $conn_r, 88 'SELECT * FROM %T WHERE slug IN (%Ls)', 89 $document->getTableName(), 90 array_keys($ancestor_slugs)); 91 $ancestors = $table->loadAllFromArray($ancestors); 92 $ancestors = mpull($ancestors, null, 'getSlug'); 93 94 foreach ($ancestor_slugs as $ancestor_slug => $document_keys) { 95 $ancestor = idx($ancestors, $ancestor_slug); 96 foreach ($document_keys as $document_key) { 97 $documents[$document_key]->attachAncestor( 98 $ancestor_slug, 99 $ancestor); 100 } 101 } 102 } 103 } 104 // To view a Phriction document, you must also be able to view all of the 105 // ancestor documents. Filter out documents which have ancestors that are 106 // not visible. 107 108 $document_map = array(); 109 foreach ($documents as $document) { 110 $document_map[$document->getSlug()] = $document; 111 foreach ($document->getAncestors() as $key => $ancestor) { 112 if ($ancestor) { 113 $document_map[$key] = $ancestor; 114 } 115 } 116 } 117 118 $filtered_map = $this->applyPolicyFilter( 119 $document_map, 120 array(PhabricatorPolicyCapability::CAN_VIEW)); 121 122 // Filter all of the documents where a parent is not visible. 123 foreach ($documents as $document_key => $document) { 124 // If the document itself is not visible, filter it. 125 if (!isset($filtered_map[$document->getSlug()])) { 126 $this->didRejectResult($documents[$document_key]); 127 unset($documents[$document_key]); 128 continue; 129 } 130 131 // If an ancestor exists but is not visible, filter the document. 132 foreach ($document->getAncestors() as $ancestor_key => $ancestor) { 133 if (!$ancestor) { 134 continue; 135 } 136 137 if (!isset($filtered_map[$ancestor_key])) { 138 $this->didRejectResult($documents[$document_key]); 139 unset($documents[$document_key]); 140 break; 141 } 142 } 143 } 144 145 if (!$documents) { 146 return $documents; 147 } 148 149 if ($this->needContent) { 150 $contents = id(new PhrictionContentQuery()) 151 ->setViewer($this->getViewer()) 152 ->setParentQuery($this) 153 ->withPHIDs(mpull($documents, 'getContentPHID')) 154 ->execute(); 155 $contents = mpull($contents, null, 'getPHID'); 156 157 foreach ($documents as $key => $document) { 158 $content_phid = $document->getContentPHID(); 159 if (empty($contents[$content_phid])) { 160 unset($documents[$key]); 161 continue; 162 } 163 $document->attachContent($contents[$content_phid]); 164 } 165 } 166 167 return $documents; 168 } 169 170 protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { 171 $select = parent::buildSelectClauseParts($conn); 172 173 if ($this->shouldJoinContentTable()) { 174 $select[] = qsprintf($conn, 'c.title'); 175 } 176 177 return $select; 178 } 179 180 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { 181 $joins = parent::buildJoinClauseParts($conn); 182 183 if ($this->shouldJoinContentTable()) { 184 $content_dao = new PhrictionContent(); 185 $joins[] = qsprintf( 186 $conn, 187 'JOIN %T c ON d.contentPHID = c.phid', 188 $content_dao->getTableName()); 189 } 190 191 return $joins; 192 } 193 194 private function shouldJoinContentTable() { 195 if ($this->getOrderVector()->containsKey('title')) { 196 return true; 197 } 198 199 return false; 200 } 201 202 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 203 $where = parent::buildWhereClauseParts($conn); 204 205 if ($this->ids !== null) { 206 $where[] = qsprintf( 207 $conn, 208 'd.id IN (%Ld)', 209 $this->ids); 210 } 211 212 if ($this->phids !== null) { 213 $where[] = qsprintf( 214 $conn, 215 'd.phid IN (%Ls)', 216 $this->phids); 217 } 218 219 if ($this->slugs !== null) { 220 $where[] = qsprintf( 221 $conn, 222 'd.slug IN (%Ls)', 223 $this->slugs); 224 } 225 226 if ($this->statuses !== null) { 227 $where[] = qsprintf( 228 $conn, 229 'd.status IN (%Ls)', 230 $this->statuses); 231 } 232 233 if ($this->slugPrefix !== null) { 234 $where[] = qsprintf( 235 $conn, 236 'd.slug LIKE %>', 237 $this->slugPrefix); 238 } 239 240 if ($this->depths !== null) { 241 $where[] = qsprintf( 242 $conn, 243 'd.depth IN (%Ld)', 244 $this->depths); 245 } 246 247 if ($this->parentPaths !== null || $this->ancestorPaths !== null) { 248 $sets = array( 249 array( 250 'paths' => $this->parentPaths, 251 'parents' => true, 252 ), 253 array( 254 'paths' => $this->ancestorPaths, 255 'parents' => false, 256 ), 257 ); 258 259 $paths = array(); 260 foreach ($sets as $set) { 261 $set_paths = $set['paths']; 262 if ($set_paths === null) { 263 continue; 264 } 265 266 if (!$set_paths) { 267 throw new PhabricatorEmptyQueryException( 268 pht('No parent/ancestor paths specified.')); 269 } 270 271 $is_parents = $set['parents']; 272 foreach ($set_paths as $path) { 273 $path_normal = PhabricatorSlug::normalize($path); 274 if ($path !== $path_normal) { 275 throw new Exception( 276 pht( 277 'Document path "%s" is not a valid path. The normalized '. 278 'form of this path is "%s".', 279 $path, 280 $path_normal)); 281 } 282 283 $depth = PhabricatorSlug::getDepth($path_normal); 284 if ($is_parents) { 285 $min_depth = $depth + 1; 286 $max_depth = $depth + 1; 287 } else { 288 $min_depth = $depth + 1; 289 $max_depth = null; 290 } 291 292 $paths[] = array( 293 $path_normal, 294 $min_depth, 295 $max_depth, 296 ); 297 } 298 } 299 300 $path_clauses = array(); 301 foreach ($paths as $path) { 302 $parts = array(); 303 list($prefix, $min, $max) = $path; 304 305 // If we're getting children or ancestors of the root document, they 306 // aren't actually stored with the leading "/" in the database, so 307 // just skip this part of the clause. 308 if ($prefix !== '/') { 309 $parts[] = qsprintf( 310 $conn, 311 'd.slug LIKE %>', 312 $prefix); 313 } 314 315 if ($min !== null) { 316 $parts[] = qsprintf( 317 $conn, 318 'd.depth >= %d', 319 $min); 320 } 321 322 if ($max !== null) { 323 $parts[] = qsprintf( 324 $conn, 325 'd.depth <= %d', 326 $max); 327 } 328 329 if ($parts) { 330 $path_clauses[] = qsprintf($conn, '%LA', $parts); 331 } 332 } 333 334 if ($path_clauses) { 335 $where[] = qsprintf($conn, '%LO', $path_clauses); 336 } 337 } 338 339 return $where; 340 } 341 342 public function getBuiltinOrders() { 343 return parent::getBuiltinOrders() + array( 344 self::ORDER_HIERARCHY => array( 345 'vector' => array('depth', 'title', 'updated', 'id'), 346 'name' => pht('Hierarchy'), 347 ), 348 ); 349 } 350 351 public function getOrderableColumns() { 352 return parent::getOrderableColumns() + array( 353 'depth' => array( 354 'table' => 'd', 355 'column' => 'depth', 356 'reverse' => true, 357 'type' => 'int', 358 ), 359 'title' => array( 360 'table' => 'c', 361 'column' => 'title', 362 'reverse' => true, 363 'type' => 'string', 364 ), 365 'updated' => array( 366 'table' => 'd', 367 'column' => 'editedEpoch', 368 'type' => 'int', 369 'unique' => false, 370 ), 371 ); 372 } 373 374 protected function newPagingMapFromCursorObject( 375 PhabricatorQueryCursor $cursor, 376 array $keys) { 377 378 $document = $cursor->getObject(); 379 380 $map = array( 381 'id' => (int)$document->getID(), 382 'depth' => $document->getDepth(), 383 'updated' => (int)$document->getEditedEpoch(), 384 ); 385 386 if (isset($keys['title'])) { 387 $map['title'] = $cursor->getRawRowProperty('title'); 388 } 389 390 return $map; 391 } 392 393 protected function getPrimaryTableAlias() { 394 return 'd'; 395 } 396 397 public function getQueryApplicationClass() { 398 return PhabricatorPhrictionApplication::class; 399 } 400 401}