@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 recaptime-dev/main 550 lines 15 kB view raw
1<?php 2 3/** 4 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhabricatorFile> 5 */ 6final class PhabricatorFileQuery 7 extends PhabricatorCursorPagedPolicyAwareQuery { 8 9 private $ids; 10 private $phids; 11 private $authorPHIDs; 12 private $explicitUploads; 13 private $transforms; 14 private $dateCreatedAfter; 15 private $dateCreatedBefore; 16 private $contentHashes; 17 private $minLength; 18 private $maxLength; 19 private $names; 20 private $isPartial; 21 private $isDeleted; 22 private $needTransforms; 23 private $builtinKeys; 24 private $isBuiltin; 25 private $storageEngines; 26 private $attachedObjectPHIDs; 27 28 public function withIDs(array $ids) { 29 $this->ids = $ids; 30 return $this; 31 } 32 33 public function withPHIDs(array $phids) { 34 $this->phids = $phids; 35 return $this; 36 } 37 38 public function withAuthorPHIDs(array $phids) { 39 $this->authorPHIDs = $phids; 40 return $this; 41 } 42 43 public function withDateCreatedBefore($date_created_before) { 44 $this->dateCreatedBefore = $date_created_before; 45 return $this; 46 } 47 48 public function withDateCreatedAfter($date_created_after) { 49 $this->dateCreatedAfter = $date_created_after; 50 return $this; 51 } 52 53 public function withContentHashes(array $content_hashes) { 54 $this->contentHashes = $content_hashes; 55 return $this; 56 } 57 58 public function withBuiltinKeys(array $keys) { 59 $this->builtinKeys = $keys; 60 return $this; 61 } 62 63 public function withIsBuiltin($is_builtin) { 64 $this->isBuiltin = $is_builtin; 65 return $this; 66 } 67 68 public function withAttachedObjectPHIDs(array $phids) { 69 $this->attachedObjectPHIDs = $phids; 70 return $this; 71 } 72 73 /** 74 * Select files which are transformations of some other file. For example, 75 * you can use this query to find previously generated thumbnails of an image 76 * file. 77 * 78 * As a parameter, provide a list of transformation specifications. Each 79 * specification is a dictionary with the keys `originalPHID` and `transform`. 80 * The `originalPHID` is the PHID of the original file (the file which was 81 * transformed) and the `transform` is the name of the transform to query 82 * for. If you pass `true` as the `transform`, all transformations of the 83 * file will be selected. 84 * 85 * For example: 86 * 87 * array( 88 * array( 89 * 'originalPHID' => 'PHID-FILE-aaaa', 90 * 'transform' => 'sepia', 91 * ), 92 * array( 93 * 'originalPHID' => 'PHID-FILE-bbbb', 94 * 'transform' => true, 95 * ), 96 * ) 97 * 98 * This selects the `"sepia"` transformation of the file with PHID 99 * `PHID-FILE-aaaa` and all transformations of the file with PHID 100 * `PHID-FILE-bbbb`. 101 * 102 * @param array<int, array<string, mixed>> $specs List of transform 103 * specifications, described above. 104 * @return $this 105 */ 106 public function withTransforms(array $specs) { 107 foreach ($specs as $spec) { 108 if (!is_array($spec) || 109 empty($spec['originalPHID']) || 110 empty($spec['transform'])) { 111 throw new Exception( 112 pht( 113 "Transform specification must be a dictionary with keys ". 114 "'%s' and '%s'!", 115 'originalPHID', 116 'transform')); 117 } 118 } 119 120 $this->transforms = $specs; 121 return $this; 122 } 123 124 public function withLengthBetween($min, $max) { 125 $this->minLength = $min; 126 $this->maxLength = $max; 127 return $this; 128 } 129 130 public function withNames(array $names) { 131 $this->names = $names; 132 return $this; 133 } 134 135 public function withIsPartial($partial) { 136 $this->isPartial = $partial; 137 return $this; 138 } 139 140 public function withIsDeleted($deleted) { 141 $this->isDeleted = $deleted; 142 return $this; 143 } 144 145 public function withNameNgrams($ngrams) { 146 return $this->withNgramsConstraint( 147 id(new PhabricatorFileNameNgrams()), 148 $ngrams); 149 } 150 151 public function withStorageEngines(array $engines) { 152 $this->storageEngines = $engines; 153 return $this; 154 } 155 156 public function showOnlyExplicitUploads($explicit_uploads) { 157 $this->explicitUploads = $explicit_uploads; 158 return $this; 159 } 160 161 public function needTransforms(array $transforms) { 162 $this->needTransforms = $transforms; 163 return $this; 164 } 165 166 public function newResultObject() { 167 return new PhabricatorFile(); 168 } 169 170 protected function loadPage() { 171 $files = $this->loadStandardPage($this->newResultObject()); 172 173 if (!$files) { 174 return $files; 175 } 176 177 // Figure out which files we need to load attached objects for. In most 178 // cases, we need to load attached objects to perform policy checks for 179 // files. 180 181 // However, in some special cases where we know files will always be 182 // visible, we skip this. See T8478 and T13106. 183 $need_objects = array(); 184 $need_xforms = array(); 185 foreach ($files as $file) { 186 $always_visible = false; 187 188 if ($file->getIsProfileImage()) { 189 $always_visible = true; 190 } 191 192 if ($file->isBuiltin()) { 193 $always_visible = true; 194 } 195 196 if ($always_visible) { 197 // We just treat these files as though they aren't attached to 198 // anything. This saves a query in common cases when we're loading 199 // profile images or builtins. We could be slightly more nuanced 200 // about this and distinguish between "not attached to anything" and 201 // "might be attached but policy checks don't need to care". 202 $file->attachObjectPHIDs(array()); 203 continue; 204 } 205 206 $need_objects[] = $file; 207 $need_xforms[] = $file; 208 } 209 210 $viewer = $this->getViewer(); 211 $is_omnipotent = $viewer->isOmnipotent(); 212 213 // If we have any files left which do need objects, load the edges now. 214 $object_phids = array(); 215 if ($need_objects) { 216 $attachments_map = $this->newAttachmentsMap($need_objects); 217 218 foreach ($need_objects as $file) { 219 $file_phid = $file->getPHID(); 220 $phids = $attachments_map[$file_phid]; 221 222 $file->attachObjectPHIDs($phids); 223 224 if ($is_omnipotent) { 225 // If the viewer is omnipotent, we don't need to load the associated 226 // objects either since the viewer can certainly see the object. 227 // Skipping this can improve performance and prevent cycles. This 228 // could possibly become part of the profile/builtin code above which 229 // short circuits attacment policy checks in cases where we know them 230 // to be unnecessary. 231 continue; 232 } 233 234 foreach ($phids as $phid) { 235 $object_phids[$phid] = true; 236 } 237 } 238 } 239 240 // If this file is a transform of another file, load that file too. If you 241 // can see the original file, you can see the thumbnail. 242 243 // TODO: It might be nice to put this directly on PhabricatorFile and 244 // remove the PhabricatorTransformedFile table, which would be a little 245 // simpler. 246 247 if ($need_xforms) { 248 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 249 'transformedPHID IN (%Ls)', 250 mpull($need_xforms, 'getPHID')); 251 $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID'); 252 foreach ($xform_phids as $derived_phid => $original_phid) { 253 $object_phids[$original_phid] = true; 254 } 255 } else { 256 $xform_phids = array(); 257 } 258 259 $object_phids = array_keys($object_phids); 260 261 // Now, load the objects. 262 263 $objects = array(); 264 if ($object_phids) { 265 // NOTE: We're explicitly turning policy exceptions off, since the rule 266 // here is "you can see the file if you can see ANY associated object". 267 // Without this explicit flag, we'll incorrectly throw unless you can 268 // see ALL associated objects. 269 270 $objects = id(new PhabricatorObjectQuery()) 271 ->setParentQuery($this) 272 ->setViewer($this->getViewer()) 273 ->withPHIDs($object_phids) 274 ->setRaisePolicyExceptions(false) 275 ->execute(); 276 $objects = mpull($objects, null, 'getPHID'); 277 } 278 279 foreach ($files as $file) { 280 $file_objects = array_select_keys($objects, $file->getObjectPHIDs()); 281 $file->attachObjects($file_objects); 282 } 283 284 foreach ($files as $key => $file) { 285 $original_phid = idx($xform_phids, $file->getPHID()); 286 if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) { 287 // This is a special case for builtin files, which are handled 288 // oddly. 289 $original = null; 290 } else if ($original_phid) { 291 $original = idx($objects, $original_phid); 292 if (!$original) { 293 // If the viewer can't see the original file, also prevent them from 294 // seeing the transformed file. 295 $this->didRejectResult($file); 296 unset($files[$key]); 297 continue; 298 } 299 } else { 300 $original = null; 301 } 302 $file->attachOriginalFile($original); 303 } 304 305 return $files; 306 } 307 308 private function newAttachmentsMap(array $files) { 309 $file_phids = mpull($files, 'getPHID'); 310 311 $attachments_table = new PhabricatorFileAttachment(); 312 $attachments_conn = $attachments_table->establishConnection('r'); 313 314 $attachments = queryfx_all( 315 $attachments_conn, 316 'SELECT filePHID, objectPHID FROM %R WHERE filePHID IN (%Ls) 317 AND attachmentMode IN (%Ls)', 318 $attachments_table, 319 $file_phids, 320 array( 321 PhabricatorFileAttachment::MODE_ATTACH, 322 )); 323 324 $attachments_map = array_fill_keys($file_phids, array()); 325 foreach ($attachments as $row) { 326 $file_phid = $row['filePHID']; 327 $object_phid = $row['objectPHID']; 328 $attachments_map[$file_phid][] = $object_phid; 329 } 330 331 return $attachments_map; 332 } 333 334 protected function didFilterPage(array $files) { 335 $xform_keys = $this->needTransforms; 336 if ($xform_keys !== null) { 337 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 338 'originalPHID IN (%Ls) AND transform IN (%Ls)', 339 mpull($files, 'getPHID'), 340 $xform_keys); 341 342 if ($xforms) { 343 $xfiles = id(new PhabricatorFile())->loadAllWhere( 344 'phid IN (%Ls)', 345 mpull($xforms, 'getTransformedPHID')); 346 $xfiles = mpull($xfiles, null, 'getPHID'); 347 } 348 349 $xform_map = array(); 350 foreach ($xforms as $xform) { 351 $xfile = idx($xfiles, $xform->getTransformedPHID()); 352 if (!$xfile) { 353 continue; 354 } 355 $original_phid = $xform->getOriginalPHID(); 356 $xform_key = $xform->getTransform(); 357 $xform_map[$original_phid][$xform_key] = $xfile; 358 } 359 360 $default_xforms = array_fill_keys($xform_keys, null); 361 362 foreach ($files as $file) { 363 $file_xforms = idx($xform_map, $file->getPHID(), array()); 364 $file_xforms += $default_xforms; 365 $file->attachTransforms($file_xforms); 366 } 367 } 368 369 return $files; 370 } 371 372 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { 373 $joins = parent::buildJoinClauseParts($conn); 374 375 if ($this->transforms) { 376 $joins[] = qsprintf( 377 $conn, 378 'JOIN %T t ON t.transformedPHID = f.phid', 379 id(new PhabricatorTransformedFile())->getTableName()); 380 } 381 382 if ($this->shouldJoinAttachmentsTable()) { 383 $joins[] = qsprintf( 384 $conn, 385 'JOIN %R attachments ON attachments.filePHID = f.phid 386 AND attachmentMode IN (%Ls)', 387 new PhabricatorFileAttachment(), 388 array( 389 PhabricatorFileAttachment::MODE_ATTACH, 390 )); 391 } 392 393 return $joins; 394 } 395 396 private function shouldJoinAttachmentsTable() { 397 return ($this->attachedObjectPHIDs !== null); 398 } 399 400 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 401 $where = parent::buildWhereClauseParts($conn); 402 403 if ($this->ids !== null) { 404 $where[] = qsprintf( 405 $conn, 406 'f.id IN (%Ld)', 407 $this->ids); 408 } 409 410 if ($this->phids !== null) { 411 $where[] = qsprintf( 412 $conn, 413 'f.phid IN (%Ls)', 414 $this->phids); 415 } 416 417 if ($this->authorPHIDs !== null) { 418 $where[] = qsprintf( 419 $conn, 420 'f.authorPHID IN (%Ls)', 421 $this->authorPHIDs); 422 } 423 424 if ($this->explicitUploads !== null) { 425 $where[] = qsprintf( 426 $conn, 427 'f.isExplicitUpload = %d', 428 (int)$this->explicitUploads); 429 } 430 431 if ($this->transforms !== null) { 432 $clauses = array(); 433 foreach ($this->transforms as $transform) { 434 if ($transform['transform'] === true) { 435 $clauses[] = qsprintf( 436 $conn, 437 '(t.originalPHID = %s)', 438 $transform['originalPHID']); 439 } else { 440 $clauses[] = qsprintf( 441 $conn, 442 '(t.originalPHID = %s AND t.transform = %s)', 443 $transform['originalPHID'], 444 $transform['transform']); 445 } 446 } 447 $where[] = qsprintf($conn, '%LO', $clauses); 448 } 449 450 if ($this->dateCreatedAfter !== null) { 451 $where[] = qsprintf( 452 $conn, 453 'f.dateCreated >= %d', 454 $this->dateCreatedAfter); 455 } 456 457 if ($this->dateCreatedBefore !== null) { 458 $where[] = qsprintf( 459 $conn, 460 'f.dateCreated <= %d', 461 $this->dateCreatedBefore); 462 } 463 464 if ($this->contentHashes !== null) { 465 $where[] = qsprintf( 466 $conn, 467 'f.contentHash IN (%Ls)', 468 $this->contentHashes); 469 } 470 471 if ($this->minLength !== null) { 472 $where[] = qsprintf( 473 $conn, 474 'byteSize >= %d', 475 $this->minLength); 476 } 477 478 if ($this->maxLength !== null) { 479 $where[] = qsprintf( 480 $conn, 481 'byteSize <= %d', 482 $this->maxLength); 483 } 484 485 if ($this->names !== null) { 486 $where[] = qsprintf( 487 $conn, 488 'name in (%Ls)', 489 $this->names); 490 } 491 492 if ($this->isPartial !== null) { 493 $where[] = qsprintf( 494 $conn, 495 'isPartial = %d', 496 (int)$this->isPartial); 497 } 498 499 if ($this->isDeleted !== null) { 500 $where[] = qsprintf( 501 $conn, 502 'isDeleted = %d', 503 (int)$this->isDeleted); 504 } 505 506 if ($this->builtinKeys !== null) { 507 $where[] = qsprintf( 508 $conn, 509 'builtinKey IN (%Ls)', 510 $this->builtinKeys); 511 } 512 513 if ($this->isBuiltin !== null) { 514 if ($this->isBuiltin) { 515 $where[] = qsprintf( 516 $conn, 517 'builtinKey IS NOT NULL'); 518 } else { 519 $where[] = qsprintf( 520 $conn, 521 'builtinKey IS NULL'); 522 } 523 } 524 525 if ($this->storageEngines !== null) { 526 $where[] = qsprintf( 527 $conn, 528 'storageEngine IN (%Ls)', 529 $this->storageEngines); 530 } 531 532 if ($this->attachedObjectPHIDs !== null) { 533 $where[] = qsprintf( 534 $conn, 535 'attachments.objectPHID IN (%Ls)', 536 $this->attachedObjectPHIDs); 537 } 538 539 return $where; 540 } 541 542 protected function getPrimaryTableAlias() { 543 return 'f'; 544 } 545 546 public function getQueryApplicationClass() { 547 return PhabricatorFilesApplication::class; 548 } 549 550}