Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / files / query / PhabricatorFileQuery.php
blob80e511b1e27459e7b5f99ea96736a8b9ad54d7aa
1 <?php
3 final class PhabricatorFileQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery {
6 private $ids;
7 private $phids;
8 private $authorPHIDs;
9 private $explicitUploads;
10 private $transforms;
11 private $dateCreatedAfter;
12 private $dateCreatedBefore;
13 private $contentHashes;
14 private $minLength;
15 private $maxLength;
16 private $names;
17 private $isPartial;
18 private $isDeleted;
19 private $needTransforms;
20 private $builtinKeys;
21 private $isBuiltin;
22 private $storageEngines;
23 private $attachedObjectPHIDs;
25 public function withIDs(array $ids) {
26 $this->ids = $ids;
27 return $this;
30 public function withPHIDs(array $phids) {
31 $this->phids = $phids;
32 return $this;
35 public function withAuthorPHIDs(array $phids) {
36 $this->authorPHIDs = $phids;
37 return $this;
40 public function withDateCreatedBefore($date_created_before) {
41 $this->dateCreatedBefore = $date_created_before;
42 return $this;
45 public function withDateCreatedAfter($date_created_after) {
46 $this->dateCreatedAfter = $date_created_after;
47 return $this;
50 public function withContentHashes(array $content_hashes) {
51 $this->contentHashes = $content_hashes;
52 return $this;
55 public function withBuiltinKeys(array $keys) {
56 $this->builtinKeys = $keys;
57 return $this;
60 public function withIsBuiltin($is_builtin) {
61 $this->isBuiltin = $is_builtin;
62 return $this;
65 public function withAttachedObjectPHIDs(array $phids) {
66 $this->attachedObjectPHIDs = $phids;
67 return $this;
70 /**
71 * Select files which are transformations of some other file. For example,
72 * you can use this query to find previously generated thumbnails of an image
73 * file.
75 * As a parameter, provide a list of transformation specifications. Each
76 * specification is a dictionary with the keys `originalPHID` and `transform`.
77 * The `originalPHID` is the PHID of the original file (the file which was
78 * transformed) and the `transform` is the name of the transform to query
79 * for. If you pass `true` as the `transform`, all transformations of the
80 * file will be selected.
82 * For example:
84 * array(
85 * array(
86 * 'originalPHID' => 'PHID-FILE-aaaa',
87 * 'transform' => 'sepia',
88 * ),
89 * array(
90 * 'originalPHID' => 'PHID-FILE-bbbb',
91 * 'transform' => true,
92 * ),
93 * )
95 * This selects the `"sepia"` transformation of the file with PHID
96 * `PHID-FILE-aaaa` and all transformations of the file with PHID
97 * `PHID-FILE-bbbb`.
99 * @param list<dict> List of transform specifications, described above.
100 * @return this
102 public function withTransforms(array $specs) {
103 foreach ($specs as $spec) {
104 if (!is_array($spec) ||
105 empty($spec['originalPHID']) ||
106 empty($spec['transform'])) {
107 throw new Exception(
108 pht(
109 "Transform specification must be a dictionary with keys ".
110 "'%s' and '%s'!",
111 'originalPHID',
112 'transform'));
116 $this->transforms = $specs;
117 return $this;
120 public function withLengthBetween($min, $max) {
121 $this->minLength = $min;
122 $this->maxLength = $max;
123 return $this;
126 public function withNames(array $names) {
127 $this->names = $names;
128 return $this;
131 public function withIsPartial($partial) {
132 $this->isPartial = $partial;
133 return $this;
136 public function withIsDeleted($deleted) {
137 $this->isDeleted = $deleted;
138 return $this;
141 public function withNameNgrams($ngrams) {
142 return $this->withNgramsConstraint(
143 id(new PhabricatorFileNameNgrams()),
144 $ngrams);
147 public function withStorageEngines(array $engines) {
148 $this->storageEngines = $engines;
149 return $this;
152 public function showOnlyExplicitUploads($explicit_uploads) {
153 $this->explicitUploads = $explicit_uploads;
154 return $this;
157 public function needTransforms(array $transforms) {
158 $this->needTransforms = $transforms;
159 return $this;
162 public function newResultObject() {
163 return new PhabricatorFile();
166 protected function loadPage() {
167 $files = $this->loadStandardPage($this->newResultObject());
169 if (!$files) {
170 return $files;
173 // Figure out which files we need to load attached objects for. In most
174 // cases, we need to load attached objects to perform policy checks for
175 // files.
177 // However, in some special cases where we know files will always be
178 // visible, we skip this. See T8478 and T13106.
179 $need_objects = array();
180 $need_xforms = array();
181 foreach ($files as $file) {
182 $always_visible = false;
184 if ($file->getIsProfileImage()) {
185 $always_visible = true;
188 if ($file->isBuiltin()) {
189 $always_visible = true;
192 if ($always_visible) {
193 // We just treat these files as though they aren't attached to
194 // anything. This saves a query in common cases when we're loading
195 // profile images or builtins. We could be slightly more nuanced
196 // about this and distinguish between "not attached to anything" and
197 // "might be attached but policy checks don't need to care".
198 $file->attachObjectPHIDs(array());
199 continue;
202 $need_objects[] = $file;
203 $need_xforms[] = $file;
206 $viewer = $this->getViewer();
207 $is_omnipotent = $viewer->isOmnipotent();
209 // If we have any files left which do need objects, load the edges now.
210 $object_phids = array();
211 if ($need_objects) {
212 $attachments_map = $this->newAttachmentsMap($need_objects);
214 foreach ($need_objects as $file) {
215 $file_phid = $file->getPHID();
216 $phids = $attachments_map[$file_phid];
218 $file->attachObjectPHIDs($phids);
220 if ($is_omnipotent) {
221 // If the viewer is omnipotent, we don't need to load the associated
222 // objects either since the viewer can certainly see the object.
223 // Skipping this can improve performance and prevent cycles. This
224 // could possibly become part of the profile/builtin code above which
225 // short circuits attacment policy checks in cases where we know them
226 // to be unnecessary.
227 continue;
230 foreach ($phids as $phid) {
231 $object_phids[$phid] = true;
236 // If this file is a transform of another file, load that file too. If you
237 // can see the original file, you can see the thumbnail.
239 // TODO: It might be nice to put this directly on PhabricatorFile and
240 // remove the PhabricatorTransformedFile table, which would be a little
241 // simpler.
243 if ($need_xforms) {
244 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
245 'transformedPHID IN (%Ls)',
246 mpull($need_xforms, 'getPHID'));
247 $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID');
248 foreach ($xform_phids as $derived_phid => $original_phid) {
249 $object_phids[$original_phid] = true;
251 } else {
252 $xform_phids = array();
255 $object_phids = array_keys($object_phids);
257 // Now, load the objects.
259 $objects = array();
260 if ($object_phids) {
261 // NOTE: We're explicitly turning policy exceptions off, since the rule
262 // here is "you can see the file if you can see ANY associated object".
263 // Without this explicit flag, we'll incorrectly throw unless you can
264 // see ALL associated objects.
266 $objects = id(new PhabricatorObjectQuery())
267 ->setParentQuery($this)
268 ->setViewer($this->getViewer())
269 ->withPHIDs($object_phids)
270 ->setRaisePolicyExceptions(false)
271 ->execute();
272 $objects = mpull($objects, null, 'getPHID');
275 foreach ($files as $file) {
276 $file_objects = array_select_keys($objects, $file->getObjectPHIDs());
277 $file->attachObjects($file_objects);
280 foreach ($files as $key => $file) {
281 $original_phid = idx($xform_phids, $file->getPHID());
282 if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) {
283 // This is a special case for builtin files, which are handled
284 // oddly.
285 $original = null;
286 } else if ($original_phid) {
287 $original = idx($objects, $original_phid);
288 if (!$original) {
289 // If the viewer can't see the original file, also prevent them from
290 // seeing the transformed file.
291 $this->didRejectResult($file);
292 unset($files[$key]);
293 continue;
295 } else {
296 $original = null;
298 $file->attachOriginalFile($original);
301 return $files;
304 private function newAttachmentsMap(array $files) {
305 $file_phids = mpull($files, 'getPHID');
307 $attachments_table = new PhabricatorFileAttachment();
308 $attachments_conn = $attachments_table->establishConnection('r');
310 $attachments = queryfx_all(
311 $attachments_conn,
312 'SELECT filePHID, objectPHID FROM %R WHERE filePHID IN (%Ls)
313 AND attachmentMode IN (%Ls)',
314 $attachments_table,
315 $file_phids,
316 array(
317 PhabricatorFileAttachment::MODE_ATTACH,
320 $attachments_map = array_fill_keys($file_phids, array());
321 foreach ($attachments as $row) {
322 $file_phid = $row['filePHID'];
323 $object_phid = $row['objectPHID'];
324 $attachments_map[$file_phid][] = $object_phid;
327 return $attachments_map;
330 protected function didFilterPage(array $files) {
331 $xform_keys = $this->needTransforms;
332 if ($xform_keys !== null) {
333 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
334 'originalPHID IN (%Ls) AND transform IN (%Ls)',
335 mpull($files, 'getPHID'),
336 $xform_keys);
338 if ($xforms) {
339 $xfiles = id(new PhabricatorFile())->loadAllWhere(
340 'phid IN (%Ls)',
341 mpull($xforms, 'getTransformedPHID'));
342 $xfiles = mpull($xfiles, null, 'getPHID');
345 $xform_map = array();
346 foreach ($xforms as $xform) {
347 $xfile = idx($xfiles, $xform->getTransformedPHID());
348 if (!$xfile) {
349 continue;
351 $original_phid = $xform->getOriginalPHID();
352 $xform_key = $xform->getTransform();
353 $xform_map[$original_phid][$xform_key] = $xfile;
356 $default_xforms = array_fill_keys($xform_keys, null);
358 foreach ($files as $file) {
359 $file_xforms = idx($xform_map, $file->getPHID(), array());
360 $file_xforms += $default_xforms;
361 $file->attachTransforms($file_xforms);
365 return $files;
368 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
369 $joins = parent::buildJoinClauseParts($conn);
371 if ($this->transforms) {
372 $joins[] = qsprintf(
373 $conn,
374 'JOIN %T t ON t.transformedPHID = f.phid',
375 id(new PhabricatorTransformedFile())->getTableName());
378 if ($this->shouldJoinAttachmentsTable()) {
379 $joins[] = qsprintf(
380 $conn,
381 'JOIN %R attachments ON attachments.filePHID = f.phid
382 AND attachmentMode IN (%Ls)',
383 new PhabricatorFileAttachment(),
384 array(
385 PhabricatorFileAttachment::MODE_ATTACH,
389 return $joins;
392 private function shouldJoinAttachmentsTable() {
393 return ($this->attachedObjectPHIDs !== null);
396 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
397 $where = parent::buildWhereClauseParts($conn);
399 if ($this->ids !== null) {
400 $where[] = qsprintf(
401 $conn,
402 'f.id IN (%Ld)',
403 $this->ids);
406 if ($this->phids !== null) {
407 $where[] = qsprintf(
408 $conn,
409 'f.phid IN (%Ls)',
410 $this->phids);
413 if ($this->authorPHIDs !== null) {
414 $where[] = qsprintf(
415 $conn,
416 'f.authorPHID IN (%Ls)',
417 $this->authorPHIDs);
420 if ($this->explicitUploads !== null) {
421 $where[] = qsprintf(
422 $conn,
423 'f.isExplicitUpload = %d',
424 (int)$this->explicitUploads);
427 if ($this->transforms !== null) {
428 $clauses = array();
429 foreach ($this->transforms as $transform) {
430 if ($transform['transform'] === true) {
431 $clauses[] = qsprintf(
432 $conn,
433 '(t.originalPHID = %s)',
434 $transform['originalPHID']);
435 } else {
436 $clauses[] = qsprintf(
437 $conn,
438 '(t.originalPHID = %s AND t.transform = %s)',
439 $transform['originalPHID'],
440 $transform['transform']);
443 $where[] = qsprintf($conn, '%LO', $clauses);
446 if ($this->dateCreatedAfter !== null) {
447 $where[] = qsprintf(
448 $conn,
449 'f.dateCreated >= %d',
450 $this->dateCreatedAfter);
453 if ($this->dateCreatedBefore !== null) {
454 $where[] = qsprintf(
455 $conn,
456 'f.dateCreated <= %d',
457 $this->dateCreatedBefore);
460 if ($this->contentHashes !== null) {
461 $where[] = qsprintf(
462 $conn,
463 'f.contentHash IN (%Ls)',
464 $this->contentHashes);
467 if ($this->minLength !== null) {
468 $where[] = qsprintf(
469 $conn,
470 'byteSize >= %d',
471 $this->minLength);
474 if ($this->maxLength !== null) {
475 $where[] = qsprintf(
476 $conn,
477 'byteSize <= %d',
478 $this->maxLength);
481 if ($this->names !== null) {
482 $where[] = qsprintf(
483 $conn,
484 'name in (%Ls)',
485 $this->names);
488 if ($this->isPartial !== null) {
489 $where[] = qsprintf(
490 $conn,
491 'isPartial = %d',
492 (int)$this->isPartial);
495 if ($this->isDeleted !== null) {
496 $where[] = qsprintf(
497 $conn,
498 'isDeleted = %d',
499 (int)$this->isDeleted);
502 if ($this->builtinKeys !== null) {
503 $where[] = qsprintf(
504 $conn,
505 'builtinKey IN (%Ls)',
506 $this->builtinKeys);
509 if ($this->isBuiltin !== null) {
510 if ($this->isBuiltin) {
511 $where[] = qsprintf(
512 $conn,
513 'builtinKey IS NOT NULL');
514 } else {
515 $where[] = qsprintf(
516 $conn,
517 'builtinKey IS NULL');
521 if ($this->storageEngines !== null) {
522 $where[] = qsprintf(
523 $conn,
524 'storageEngine IN (%Ls)',
525 $this->storageEngines);
528 if ($this->attachedObjectPHIDs !== null) {
529 $where[] = qsprintf(
530 $conn,
531 'attachments.objectPHID IN (%Ls)',
532 $this->attachedObjectPHIDs);
535 return $where;
538 protected function getPrimaryTableAlias() {
539 return 'f';
542 public function getQueryApplicationClass() {
543 return 'PhabricatorFilesApplication';