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)
313 AND attachmentMode IN (%Ls)',
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'),
339 $xfiles = id(new PhabricatorFile())->loadAllWhere(
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());
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);
368 protected function buildJoinClauseParts(AphrontDatabaseConnection
$conn) {
369 $joins = parent
::buildJoinClauseParts($conn);
371 if ($this->transforms
) {
374 'JOIN %T t ON t.transformedPHID = f.phid',
375 id(new PhabricatorTransformedFile())->getTableName());
378 if ($this->shouldJoinAttachmentsTable()) {
381 'JOIN %R attachments ON attachments.filePHID = f.phid
382 AND attachmentMode IN (%Ls)',
383 new PhabricatorFileAttachment(),
385 PhabricatorFileAttachment
::MODE_ATTACH
,
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) {
406 if ($this->phids
!== null) {
413 if ($this->authorPHIDs
!== null) {
416 'f.authorPHID IN (%Ls)',
420 if ($this->explicitUploads
!== null) {
423 'f.isExplicitUpload = %d',
424 (int)$this->explicitUploads
);
427 if ($this->transforms
!== null) {
429 foreach ($this->transforms
as $transform) {
430 if ($transform['transform'] === true) {
431 $clauses[] = qsprintf(
433 '(t.originalPHID = %s)',
434 $transform['originalPHID']);
436 $clauses[] = qsprintf(
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) {
449 'f.dateCreated >= %d',
450 $this->dateCreatedAfter
);
453 if ($this->dateCreatedBefore
!== null) {
456 'f.dateCreated <= %d',
457 $this->dateCreatedBefore
);
460 if ($this->contentHashes
!== null) {
463 'f.contentHash IN (%Ls)',
464 $this->contentHashes
);
467 if ($this->minLength
!== null) {
474 if ($this->maxLength
!== null) {
481 if ($this->names
!== null) {
488 if ($this->isPartial
!== null) {
492 (int)$this->isPartial
);
495 if ($this->isDeleted
!== null) {
499 (int)$this->isDeleted
);
502 if ($this->builtinKeys
!== null) {
505 'builtinKey IN (%Ls)',
509 if ($this->isBuiltin
!== null) {
510 if ($this->isBuiltin
) {
513 'builtinKey IS NOT NULL');
517 'builtinKey IS NULL');
521 if ($this->storageEngines
!== null) {
524 'storageEngine IN (%Ls)',
525 $this->storageEngines
);
528 if ($this->attachedObjectPHIDs
!== null) {
531 'attachments.objectPHID IN (%Ls)',
532 $this->attachedObjectPHIDs
);
538 protected function getPrimaryTableAlias() {
542 public function getQueryApplicationClass() {
543 return 'PhabricatorFilesApplication';