Remove all "FileHasObject" edge reads and writes
[phabricator.git] / src / applications / files / query / PhabricatorFileQuery.php
blob292507fdd1a8600f769825a9883273a3c838e11a
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 $attachments_table,
314 $file_phids);
316 $attachments_map = array_fill_keys($file_phids, array());
317 foreach ($attachments as $row) {
318 $file_phid = $row['filePHID'];
319 $object_phid = $row['objectPHID'];
320 $attachments_map[$file_phid][] = $object_phid;
323 return $attachments_map;
326 protected function didFilterPage(array $files) {
327 $xform_keys = $this->needTransforms;
328 if ($xform_keys !== null) {
329 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
330 'originalPHID IN (%Ls) AND transform IN (%Ls)',
331 mpull($files, 'getPHID'),
332 $xform_keys);
334 if ($xforms) {
335 $xfiles = id(new PhabricatorFile())->loadAllWhere(
336 'phid IN (%Ls)',
337 mpull($xforms, 'getTransformedPHID'));
338 $xfiles = mpull($xfiles, null, 'getPHID');
341 $xform_map = array();
342 foreach ($xforms as $xform) {
343 $xfile = idx($xfiles, $xform->getTransformedPHID());
344 if (!$xfile) {
345 continue;
347 $original_phid = $xform->getOriginalPHID();
348 $xform_key = $xform->getTransform();
349 $xform_map[$original_phid][$xform_key] = $xfile;
352 $default_xforms = array_fill_keys($xform_keys, null);
354 foreach ($files as $file) {
355 $file_xforms = idx($xform_map, $file->getPHID(), array());
356 $file_xforms += $default_xforms;
357 $file->attachTransforms($file_xforms);
361 return $files;
364 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
365 $joins = parent::buildJoinClauseParts($conn);
367 if ($this->transforms) {
368 $joins[] = qsprintf(
369 $conn,
370 'JOIN %T t ON t.transformedPHID = f.phid',
371 id(new PhabricatorTransformedFile())->getTableName());
374 if ($this->shouldJoinAttachmentsTable()) {
375 $joins[] = qsprintf(
376 $conn,
377 'JOIN %R attachments ON attachments.filePHID = f.phid',
378 new PhabricatorFileAttachment());
381 return $joins;
384 private function shouldJoinAttachmentsTable() {
385 return ($this->attachedObjectPHIDs !== null);
388 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
389 $where = parent::buildWhereClauseParts($conn);
391 if ($this->ids !== null) {
392 $where[] = qsprintf(
393 $conn,
394 'f.id IN (%Ld)',
395 $this->ids);
398 if ($this->phids !== null) {
399 $where[] = qsprintf(
400 $conn,
401 'f.phid IN (%Ls)',
402 $this->phids);
405 if ($this->authorPHIDs !== null) {
406 $where[] = qsprintf(
407 $conn,
408 'f.authorPHID IN (%Ls)',
409 $this->authorPHIDs);
412 if ($this->explicitUploads !== null) {
413 $where[] = qsprintf(
414 $conn,
415 'f.isExplicitUpload = %d',
416 (int)$this->explicitUploads);
419 if ($this->transforms !== null) {
420 $clauses = array();
421 foreach ($this->transforms as $transform) {
422 if ($transform['transform'] === true) {
423 $clauses[] = qsprintf(
424 $conn,
425 '(t.originalPHID = %s)',
426 $transform['originalPHID']);
427 } else {
428 $clauses[] = qsprintf(
429 $conn,
430 '(t.originalPHID = %s AND t.transform = %s)',
431 $transform['originalPHID'],
432 $transform['transform']);
435 $where[] = qsprintf($conn, '%LO', $clauses);
438 if ($this->dateCreatedAfter !== null) {
439 $where[] = qsprintf(
440 $conn,
441 'f.dateCreated >= %d',
442 $this->dateCreatedAfter);
445 if ($this->dateCreatedBefore !== null) {
446 $where[] = qsprintf(
447 $conn,
448 'f.dateCreated <= %d',
449 $this->dateCreatedBefore);
452 if ($this->contentHashes !== null) {
453 $where[] = qsprintf(
454 $conn,
455 'f.contentHash IN (%Ls)',
456 $this->contentHashes);
459 if ($this->minLength !== null) {
460 $where[] = qsprintf(
461 $conn,
462 'byteSize >= %d',
463 $this->minLength);
466 if ($this->maxLength !== null) {
467 $where[] = qsprintf(
468 $conn,
469 'byteSize <= %d',
470 $this->maxLength);
473 if ($this->names !== null) {
474 $where[] = qsprintf(
475 $conn,
476 'name in (%Ls)',
477 $this->names);
480 if ($this->isPartial !== null) {
481 $where[] = qsprintf(
482 $conn,
483 'isPartial = %d',
484 (int)$this->isPartial);
487 if ($this->isDeleted !== null) {
488 $where[] = qsprintf(
489 $conn,
490 'isDeleted = %d',
491 (int)$this->isDeleted);
494 if ($this->builtinKeys !== null) {
495 $where[] = qsprintf(
496 $conn,
497 'builtinKey IN (%Ls)',
498 $this->builtinKeys);
501 if ($this->isBuiltin !== null) {
502 if ($this->isBuiltin) {
503 $where[] = qsprintf(
504 $conn,
505 'builtinKey IS NOT NULL');
506 } else {
507 $where[] = qsprintf(
508 $conn,
509 'builtinKey IS NULL');
513 if ($this->storageEngines !== null) {
514 $where[] = qsprintf(
515 $conn,
516 'storageEngine IN (%Ls)',
517 $this->storageEngines);
520 if ($this->attachedObjectPHIDs !== null) {
521 $where[] = qsprintf(
522 $conn,
523 'attachments.objectPHID IN (%Ls)',
524 $this->attachedObjectPHIDs);
527 return $where;
530 protected function getPrimaryTableAlias() {
531 return 'f';
534 public function getQueryApplicationClass() {
535 return 'PhabricatorFilesApplication';