Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / files / query / PhabricatorFileQuery.php
blobc19574acaa0e75c026b10cfe34719d258ea02ef5
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;
24 public function withIDs(array $ids) {
25 $this->ids = $ids;
26 return $this;
29 public function withPHIDs(array $phids) {
30 $this->phids = $phids;
31 return $this;
34 public function withAuthorPHIDs(array $phids) {
35 $this->authorPHIDs = $phids;
36 return $this;
39 public function withDateCreatedBefore($date_created_before) {
40 $this->dateCreatedBefore = $date_created_before;
41 return $this;
44 public function withDateCreatedAfter($date_created_after) {
45 $this->dateCreatedAfter = $date_created_after;
46 return $this;
49 public function withContentHashes(array $content_hashes) {
50 $this->contentHashes = $content_hashes;
51 return $this;
54 public function withBuiltinKeys(array $keys) {
55 $this->builtinKeys = $keys;
56 return $this;
59 public function withIsBuiltin($is_builtin) {
60 $this->isBuiltin = $is_builtin;
61 return $this;
64 /**
65 * Select files which are transformations of some other file. For example,
66 * you can use this query to find previously generated thumbnails of an image
67 * file.
69 * As a parameter, provide a list of transformation specifications. Each
70 * specification is a dictionary with the keys `originalPHID` and `transform`.
71 * The `originalPHID` is the PHID of the original file (the file which was
72 * transformed) and the `transform` is the name of the transform to query
73 * for. If you pass `true` as the `transform`, all transformations of the
74 * file will be selected.
76 * For example:
78 * array(
79 * array(
80 * 'originalPHID' => 'PHID-FILE-aaaa',
81 * 'transform' => 'sepia',
82 * ),
83 * array(
84 * 'originalPHID' => 'PHID-FILE-bbbb',
85 * 'transform' => true,
86 * ),
87 * )
89 * This selects the `"sepia"` transformation of the file with PHID
90 * `PHID-FILE-aaaa` and all transformations of the file with PHID
91 * `PHID-FILE-bbbb`.
93 * @param list<dict> List of transform specifications, described above.
94 * @return this
96 public function withTransforms(array $specs) {
97 foreach ($specs as $spec) {
98 if (!is_array($spec) ||
99 empty($spec['originalPHID']) ||
100 empty($spec['transform'])) {
101 throw new Exception(
102 pht(
103 "Transform specification must be a dictionary with keys ".
104 "'%s' and '%s'!",
105 'originalPHID',
106 'transform'));
110 $this->transforms = $specs;
111 return $this;
114 public function withLengthBetween($min, $max) {
115 $this->minLength = $min;
116 $this->maxLength = $max;
117 return $this;
120 public function withNames(array $names) {
121 $this->names = $names;
122 return $this;
125 public function withIsPartial($partial) {
126 $this->isPartial = $partial;
127 return $this;
130 public function withIsDeleted($deleted) {
131 $this->isDeleted = $deleted;
132 return $this;
135 public function withNameNgrams($ngrams) {
136 return $this->withNgramsConstraint(
137 id(new PhabricatorFileNameNgrams()),
138 $ngrams);
141 public function withStorageEngines(array $engines) {
142 $this->storageEngines = $engines;
143 return $this;
146 public function showOnlyExplicitUploads($explicit_uploads) {
147 $this->explicitUploads = $explicit_uploads;
148 return $this;
151 public function needTransforms(array $transforms) {
152 $this->needTransforms = $transforms;
153 return $this;
156 public function newResultObject() {
157 return new PhabricatorFile();
160 protected function loadPage() {
161 $files = $this->loadStandardPage($this->newResultObject());
163 if (!$files) {
164 return $files;
167 // Figure out which files we need to load attached objects for. In most
168 // cases, we need to load attached objects to perform policy checks for
169 // files.
171 // However, in some special cases where we know files will always be
172 // visible, we skip this. See T8478 and T13106.
173 $need_objects = array();
174 $need_xforms = array();
175 foreach ($files as $file) {
176 $always_visible = false;
178 if ($file->getIsProfileImage()) {
179 $always_visible = true;
182 if ($file->isBuiltin()) {
183 $always_visible = true;
186 if ($always_visible) {
187 // We just treat these files as though they aren't attached to
188 // anything. This saves a query in common cases when we're loading
189 // profile images or builtins. We could be slightly more nuanced
190 // about this and distinguish between "not attached to anything" and
191 // "might be attached but policy checks don't need to care".
192 $file->attachObjectPHIDs(array());
193 continue;
196 $need_objects[] = $file;
197 $need_xforms[] = $file;
200 $viewer = $this->getViewer();
201 $is_omnipotent = $viewer->isOmnipotent();
203 // If we have any files left which do need objects, load the edges now.
204 $object_phids = array();
205 if ($need_objects) {
206 $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST;
207 $file_phids = mpull($need_objects, 'getPHID');
209 $edges = id(new PhabricatorEdgeQuery())
210 ->withSourcePHIDs($file_phids)
211 ->withEdgeTypes(array($edge_type))
212 ->execute();
214 foreach ($need_objects as $file) {
215 $phids = array_keys($edges[$file->getPHID()][$edge_type]);
216 $file->attachObjectPHIDs($phids);
218 if ($is_omnipotent) {
219 // If the viewer is omnipotent, we don't need to load the associated
220 // objects either since the viewer can certainly see the object.
221 // Skipping this can improve performance and prevent cycles. This
222 // could possibly become part of the profile/builtin code above which
223 // short circuits attacment policy checks in cases where we know them
224 // to be unnecessary.
225 continue;
228 foreach ($phids as $phid) {
229 $object_phids[$phid] = true;
234 // If this file is a transform of another file, load that file too. If you
235 // can see the original file, you can see the thumbnail.
237 // TODO: It might be nice to put this directly on PhabricatorFile and
238 // remove the PhabricatorTransformedFile table, which would be a little
239 // simpler.
241 if ($need_xforms) {
242 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
243 'transformedPHID IN (%Ls)',
244 mpull($need_xforms, 'getPHID'));
245 $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID');
246 foreach ($xform_phids as $derived_phid => $original_phid) {
247 $object_phids[$original_phid] = true;
249 } else {
250 $xform_phids = array();
253 $object_phids = array_keys($object_phids);
255 // Now, load the objects.
257 $objects = array();
258 if ($object_phids) {
259 // NOTE: We're explicitly turning policy exceptions off, since the rule
260 // here is "you can see the file if you can see ANY associated object".
261 // Without this explicit flag, we'll incorrectly throw unless you can
262 // see ALL associated objects.
264 $objects = id(new PhabricatorObjectQuery())
265 ->setParentQuery($this)
266 ->setViewer($this->getViewer())
267 ->withPHIDs($object_phids)
268 ->setRaisePolicyExceptions(false)
269 ->execute();
270 $objects = mpull($objects, null, 'getPHID');
273 foreach ($files as $file) {
274 $file_objects = array_select_keys($objects, $file->getObjectPHIDs());
275 $file->attachObjects($file_objects);
278 foreach ($files as $key => $file) {
279 $original_phid = idx($xform_phids, $file->getPHID());
280 if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) {
281 // This is a special case for builtin files, which are handled
282 // oddly.
283 $original = null;
284 } else if ($original_phid) {
285 $original = idx($objects, $original_phid);
286 if (!$original) {
287 // If the viewer can't see the original file, also prevent them from
288 // seeing the transformed file.
289 $this->didRejectResult($file);
290 unset($files[$key]);
291 continue;
293 } else {
294 $original = null;
296 $file->attachOriginalFile($original);
299 return $files;
302 protected function didFilterPage(array $files) {
303 $xform_keys = $this->needTransforms;
304 if ($xform_keys !== null) {
305 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
306 'originalPHID IN (%Ls) AND transform IN (%Ls)',
307 mpull($files, 'getPHID'),
308 $xform_keys);
310 if ($xforms) {
311 $xfiles = id(new PhabricatorFile())->loadAllWhere(
312 'phid IN (%Ls)',
313 mpull($xforms, 'getTransformedPHID'));
314 $xfiles = mpull($xfiles, null, 'getPHID');
317 $xform_map = array();
318 foreach ($xforms as $xform) {
319 $xfile = idx($xfiles, $xform->getTransformedPHID());
320 if (!$xfile) {
321 continue;
323 $original_phid = $xform->getOriginalPHID();
324 $xform_key = $xform->getTransform();
325 $xform_map[$original_phid][$xform_key] = $xfile;
328 $default_xforms = array_fill_keys($xform_keys, null);
330 foreach ($files as $file) {
331 $file_xforms = idx($xform_map, $file->getPHID(), array());
332 $file_xforms += $default_xforms;
333 $file->attachTransforms($file_xforms);
337 return $files;
340 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
341 $joins = parent::buildJoinClauseParts($conn);
343 if ($this->transforms) {
344 $joins[] = qsprintf(
345 $conn,
346 'JOIN %T t ON t.transformedPHID = f.phid',
347 id(new PhabricatorTransformedFile())->getTableName());
350 return $joins;
353 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
354 $where = parent::buildWhereClauseParts($conn);
356 if ($this->ids !== null) {
357 $where[] = qsprintf(
358 $conn,
359 'f.id IN (%Ld)',
360 $this->ids);
363 if ($this->phids !== null) {
364 $where[] = qsprintf(
365 $conn,
366 'f.phid IN (%Ls)',
367 $this->phids);
370 if ($this->authorPHIDs !== null) {
371 $where[] = qsprintf(
372 $conn,
373 'f.authorPHID IN (%Ls)',
374 $this->authorPHIDs);
377 if ($this->explicitUploads !== null) {
378 $where[] = qsprintf(
379 $conn,
380 'f.isExplicitUpload = %d',
381 (int)$this->explicitUploads);
384 if ($this->transforms !== null) {
385 $clauses = array();
386 foreach ($this->transforms as $transform) {
387 if ($transform['transform'] === true) {
388 $clauses[] = qsprintf(
389 $conn,
390 '(t.originalPHID = %s)',
391 $transform['originalPHID']);
392 } else {
393 $clauses[] = qsprintf(
394 $conn,
395 '(t.originalPHID = %s AND t.transform = %s)',
396 $transform['originalPHID'],
397 $transform['transform']);
400 $where[] = qsprintf($conn, '%LO', $clauses);
403 if ($this->dateCreatedAfter !== null) {
404 $where[] = qsprintf(
405 $conn,
406 'f.dateCreated >= %d',
407 $this->dateCreatedAfter);
410 if ($this->dateCreatedBefore !== null) {
411 $where[] = qsprintf(
412 $conn,
413 'f.dateCreated <= %d',
414 $this->dateCreatedBefore);
417 if ($this->contentHashes !== null) {
418 $where[] = qsprintf(
419 $conn,
420 'f.contentHash IN (%Ls)',
421 $this->contentHashes);
424 if ($this->minLength !== null) {
425 $where[] = qsprintf(
426 $conn,
427 'byteSize >= %d',
428 $this->minLength);
431 if ($this->maxLength !== null) {
432 $where[] = qsprintf(
433 $conn,
434 'byteSize <= %d',
435 $this->maxLength);
438 if ($this->names !== null) {
439 $where[] = qsprintf(
440 $conn,
441 'name in (%Ls)',
442 $this->names);
445 if ($this->isPartial !== null) {
446 $where[] = qsprintf(
447 $conn,
448 'isPartial = %d',
449 (int)$this->isPartial);
452 if ($this->isDeleted !== null) {
453 $where[] = qsprintf(
454 $conn,
455 'isDeleted = %d',
456 (int)$this->isDeleted);
459 if ($this->builtinKeys !== null) {
460 $where[] = qsprintf(
461 $conn,
462 'builtinKey IN (%Ls)',
463 $this->builtinKeys);
466 if ($this->isBuiltin !== null) {
467 if ($this->isBuiltin) {
468 $where[] = qsprintf(
469 $conn,
470 'builtinKey IS NOT NULL');
471 } else {
472 $where[] = qsprintf(
473 $conn,
474 'builtinKey IS NULL');
478 if ($this->storageEngines !== null) {
479 $where[] = qsprintf(
480 $conn,
481 'storageEngine IN (%Ls)',
482 $this->storageEngines);
485 return $where;
488 protected function getPrimaryTableAlias() {
489 return 'f';
492 public function getQueryApplicationClass() {
493 return 'PhabricatorFilesApplication';