3 final class PhabricatorFileQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery
{
9 private $explicitUploads;
11 private $dateCreatedAfter;
12 private $dateCreatedBefore;
13 private $contentHashes;
19 private $needTransforms;
22 private $storageEngines;
23 private $attachedObjectPHIDs;
25 public function withIDs(array $ids) {
30 public function withPHIDs(array $phids) {
31 $this->phids
= $phids;
35 public function withAuthorPHIDs(array $phids) {
36 $this->authorPHIDs
= $phids;
40 public function withDateCreatedBefore($date_created_before) {
41 $this->dateCreatedBefore
= $date_created_before;
45 public function withDateCreatedAfter($date_created_after) {
46 $this->dateCreatedAfter
= $date_created_after;
50 public function withContentHashes(array $content_hashes) {
51 $this->contentHashes
= $content_hashes;
55 public function withBuiltinKeys(array $keys) {
56 $this->builtinKeys
= $keys;
60 public function withIsBuiltin($is_builtin) {
61 $this->isBuiltin
= $is_builtin;
65 public function withAttachedObjectPHIDs(array $phids) {
66 $this->attachedObjectPHIDs
= $phids;
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
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.
86 * 'originalPHID' => 'PHID-FILE-aaaa',
87 * 'transform' => 'sepia',
90 * 'originalPHID' => 'PHID-FILE-bbbb',
91 * 'transform' => true,
95 * This selects the `"sepia"` transformation of the file with PHID
96 * `PHID-FILE-aaaa` and all transformations of the file with PHID
99 * @param list<dict> List of transform specifications, described above.
102 public function withTransforms(array $specs) {
103 foreach ($specs as $spec) {
104 if (!is_array($spec) ||
105 empty($spec['originalPHID']) ||
106 empty($spec['transform'])) {
109 "Transform specification must be a dictionary with keys ".
116 $this->transforms
= $specs;
120 public function withLengthBetween($min, $max) {
121 $this->minLength
= $min;
122 $this->maxLength
= $max;
126 public function withNames(array $names) {
127 $this->names
= $names;
131 public function withIsPartial($partial) {
132 $this->isPartial
= $partial;
136 public function withIsDeleted($deleted) {
137 $this->isDeleted
= $deleted;
141 public function withNameNgrams($ngrams) {
142 return $this->withNgramsConstraint(
143 id(new PhabricatorFileNameNgrams()),
147 public function withStorageEngines(array $engines) {
148 $this->storageEngines
= $engines;
152 public function showOnlyExplicitUploads($explicit_uploads) {
153 $this->explicitUploads
= $explicit_uploads;
157 public function needTransforms(array $transforms) {
158 $this->needTransforms
= $transforms;
162 public function newResultObject() {
163 return new PhabricatorFile();
166 protected function loadPage() {
167 $files = $this->loadStandardPage($this->newResultObject());
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
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());
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();
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.
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
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;
252 $xform_phids = array();
255 $object_phids = array_keys($object_phids);
257 // Now, load the objects.
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)
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
286 } else if ($original_phid) {
287 $original = idx($objects, $original_phid);
289 // If the viewer can't see the original file, also prevent them from
290 // seeing the transformed file.
291 $this->didRejectResult($file);
298 $file->attachOriginalFile($original);
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(
312 'SELECT filePHID, objectPHID FROM %R WHERE filePHID IN (%Ls)',
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'),
335 $xfiles = id(new PhabricatorFile())->loadAllWhere(
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());
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);
364 protected function buildJoinClauseParts(AphrontDatabaseConnection
$conn) {
365 $joins = parent
::buildJoinClauseParts($conn);
367 if ($this->transforms
) {
370 'JOIN %T t ON t.transformedPHID = f.phid',
371 id(new PhabricatorTransformedFile())->getTableName());
374 if ($this->shouldJoinAttachmentsTable()) {
377 'JOIN %R attachments ON attachments.filePHID = f.phid',
378 new PhabricatorFileAttachment());
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) {
398 if ($this->phids
!== null) {
405 if ($this->authorPHIDs
!== null) {
408 'f.authorPHID IN (%Ls)',
412 if ($this->explicitUploads
!== null) {
415 'f.isExplicitUpload = %d',
416 (int)$this->explicitUploads
);
419 if ($this->transforms
!== null) {
421 foreach ($this->transforms
as $transform) {
422 if ($transform['transform'] === true) {
423 $clauses[] = qsprintf(
425 '(t.originalPHID = %s)',
426 $transform['originalPHID']);
428 $clauses[] = qsprintf(
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) {
441 'f.dateCreated >= %d',
442 $this->dateCreatedAfter
);
445 if ($this->dateCreatedBefore
!== null) {
448 'f.dateCreated <= %d',
449 $this->dateCreatedBefore
);
452 if ($this->contentHashes
!== null) {
455 'f.contentHash IN (%Ls)',
456 $this->contentHashes
);
459 if ($this->minLength
!== null) {
466 if ($this->maxLength
!== null) {
473 if ($this->names
!== null) {
480 if ($this->isPartial
!== null) {
484 (int)$this->isPartial
);
487 if ($this->isDeleted
!== null) {
491 (int)$this->isDeleted
);
494 if ($this->builtinKeys
!== null) {
497 'builtinKey IN (%Ls)',
501 if ($this->isBuiltin
!== null) {
502 if ($this->isBuiltin
) {
505 'builtinKey IS NOT NULL');
509 'builtinKey IS NULL');
513 if ($this->storageEngines
!== null) {
516 'storageEngine IN (%Ls)',
517 $this->storageEngines
);
520 if ($this->attachedObjectPHIDs
!== null) {
523 'attachments.objectPHID IN (%Ls)',
524 $this->attachedObjectPHIDs
);
530 protected function getPrimaryTableAlias() {
534 public function getQueryApplicationClass() {
535 return 'PhabricatorFilesApplication';