@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhabricatorFile>
5 */
6final class PhabricatorFileQuery
7 extends PhabricatorCursorPagedPolicyAwareQuery {
8
9 private $ids;
10 private $phids;
11 private $authorPHIDs;
12 private $explicitUploads;
13 private $transforms;
14 private $dateCreatedAfter;
15 private $dateCreatedBefore;
16 private $contentHashes;
17 private $minLength;
18 private $maxLength;
19 private $names;
20 private $isPartial;
21 private $isDeleted;
22 private $needTransforms;
23 private $builtinKeys;
24 private $isBuiltin;
25 private $storageEngines;
26 private $attachedObjectPHIDs;
27
28 public function withIDs(array $ids) {
29 $this->ids = $ids;
30 return $this;
31 }
32
33 public function withPHIDs(array $phids) {
34 $this->phids = $phids;
35 return $this;
36 }
37
38 public function withAuthorPHIDs(array $phids) {
39 $this->authorPHIDs = $phids;
40 return $this;
41 }
42
43 public function withDateCreatedBefore($date_created_before) {
44 $this->dateCreatedBefore = $date_created_before;
45 return $this;
46 }
47
48 public function withDateCreatedAfter($date_created_after) {
49 $this->dateCreatedAfter = $date_created_after;
50 return $this;
51 }
52
53 public function withContentHashes(array $content_hashes) {
54 $this->contentHashes = $content_hashes;
55 return $this;
56 }
57
58 public function withBuiltinKeys(array $keys) {
59 $this->builtinKeys = $keys;
60 return $this;
61 }
62
63 public function withIsBuiltin($is_builtin) {
64 $this->isBuiltin = $is_builtin;
65 return $this;
66 }
67
68 public function withAttachedObjectPHIDs(array $phids) {
69 $this->attachedObjectPHIDs = $phids;
70 return $this;
71 }
72
73 /**
74 * Select files which are transformations of some other file. For example,
75 * you can use this query to find previously generated thumbnails of an image
76 * file.
77 *
78 * As a parameter, provide a list of transformation specifications. Each
79 * specification is a dictionary with the keys `originalPHID` and `transform`.
80 * The `originalPHID` is the PHID of the original file (the file which was
81 * transformed) and the `transform` is the name of the transform to query
82 * for. If you pass `true` as the `transform`, all transformations of the
83 * file will be selected.
84 *
85 * For example:
86 *
87 * array(
88 * array(
89 * 'originalPHID' => 'PHID-FILE-aaaa',
90 * 'transform' => 'sepia',
91 * ),
92 * array(
93 * 'originalPHID' => 'PHID-FILE-bbbb',
94 * 'transform' => true,
95 * ),
96 * )
97 *
98 * This selects the `"sepia"` transformation of the file with PHID
99 * `PHID-FILE-aaaa` and all transformations of the file with PHID
100 * `PHID-FILE-bbbb`.
101 *
102 * @param array<int, array<string, mixed>> $specs List of transform
103 * specifications, described above.
104 * @return $this
105 */
106 public function withTransforms(array $specs) {
107 foreach ($specs as $spec) {
108 if (!is_array($spec) ||
109 empty($spec['originalPHID']) ||
110 empty($spec['transform'])) {
111 throw new Exception(
112 pht(
113 "Transform specification must be a dictionary with keys ".
114 "'%s' and '%s'!",
115 'originalPHID',
116 'transform'));
117 }
118 }
119
120 $this->transforms = $specs;
121 return $this;
122 }
123
124 public function withLengthBetween($min, $max) {
125 $this->minLength = $min;
126 $this->maxLength = $max;
127 return $this;
128 }
129
130 public function withNames(array $names) {
131 $this->names = $names;
132 return $this;
133 }
134
135 public function withIsPartial($partial) {
136 $this->isPartial = $partial;
137 return $this;
138 }
139
140 public function withIsDeleted($deleted) {
141 $this->isDeleted = $deleted;
142 return $this;
143 }
144
145 public function withNameNgrams($ngrams) {
146 return $this->withNgramsConstraint(
147 id(new PhabricatorFileNameNgrams()),
148 $ngrams);
149 }
150
151 public function withStorageEngines(array $engines) {
152 $this->storageEngines = $engines;
153 return $this;
154 }
155
156 public function showOnlyExplicitUploads($explicit_uploads) {
157 $this->explicitUploads = $explicit_uploads;
158 return $this;
159 }
160
161 public function needTransforms(array $transforms) {
162 $this->needTransforms = $transforms;
163 return $this;
164 }
165
166 public function newResultObject() {
167 return new PhabricatorFile();
168 }
169
170 protected function loadPage() {
171 $files = $this->loadStandardPage($this->newResultObject());
172
173 if (!$files) {
174 return $files;
175 }
176
177 // Figure out which files we need to load attached objects for. In most
178 // cases, we need to load attached objects to perform policy checks for
179 // files.
180
181 // However, in some special cases where we know files will always be
182 // visible, we skip this. See T8478 and T13106.
183 $need_objects = array();
184 $need_xforms = array();
185 foreach ($files as $file) {
186 $always_visible = false;
187
188 if ($file->getIsProfileImage()) {
189 $always_visible = true;
190 }
191
192 if ($file->isBuiltin()) {
193 $always_visible = true;
194 }
195
196 if ($always_visible) {
197 // We just treat these files as though they aren't attached to
198 // anything. This saves a query in common cases when we're loading
199 // profile images or builtins. We could be slightly more nuanced
200 // about this and distinguish between "not attached to anything" and
201 // "might be attached but policy checks don't need to care".
202 $file->attachObjectPHIDs(array());
203 continue;
204 }
205
206 $need_objects[] = $file;
207 $need_xforms[] = $file;
208 }
209
210 $viewer = $this->getViewer();
211 $is_omnipotent = $viewer->isOmnipotent();
212
213 // If we have any files left which do need objects, load the edges now.
214 $object_phids = array();
215 if ($need_objects) {
216 $attachments_map = $this->newAttachmentsMap($need_objects);
217
218 foreach ($need_objects as $file) {
219 $file_phid = $file->getPHID();
220 $phids = $attachments_map[$file_phid];
221
222 $file->attachObjectPHIDs($phids);
223
224 if ($is_omnipotent) {
225 // If the viewer is omnipotent, we don't need to load the associated
226 // objects either since the viewer can certainly see the object.
227 // Skipping this can improve performance and prevent cycles. This
228 // could possibly become part of the profile/builtin code above which
229 // short circuits attacment policy checks in cases where we know them
230 // to be unnecessary.
231 continue;
232 }
233
234 foreach ($phids as $phid) {
235 $object_phids[$phid] = true;
236 }
237 }
238 }
239
240 // If this file is a transform of another file, load that file too. If you
241 // can see the original file, you can see the thumbnail.
242
243 // TODO: It might be nice to put this directly on PhabricatorFile and
244 // remove the PhabricatorTransformedFile table, which would be a little
245 // simpler.
246
247 if ($need_xforms) {
248 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
249 'transformedPHID IN (%Ls)',
250 mpull($need_xforms, 'getPHID'));
251 $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID');
252 foreach ($xform_phids as $derived_phid => $original_phid) {
253 $object_phids[$original_phid] = true;
254 }
255 } else {
256 $xform_phids = array();
257 }
258
259 $object_phids = array_keys($object_phids);
260
261 // Now, load the objects.
262
263 $objects = array();
264 if ($object_phids) {
265 // NOTE: We're explicitly turning policy exceptions off, since the rule
266 // here is "you can see the file if you can see ANY associated object".
267 // Without this explicit flag, we'll incorrectly throw unless you can
268 // see ALL associated objects.
269
270 $objects = id(new PhabricatorObjectQuery())
271 ->setParentQuery($this)
272 ->setViewer($this->getViewer())
273 ->withPHIDs($object_phids)
274 ->setRaisePolicyExceptions(false)
275 ->execute();
276 $objects = mpull($objects, null, 'getPHID');
277 }
278
279 foreach ($files as $file) {
280 $file_objects = array_select_keys($objects, $file->getObjectPHIDs());
281 $file->attachObjects($file_objects);
282 }
283
284 foreach ($files as $key => $file) {
285 $original_phid = idx($xform_phids, $file->getPHID());
286 if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) {
287 // This is a special case for builtin files, which are handled
288 // oddly.
289 $original = null;
290 } else if ($original_phid) {
291 $original = idx($objects, $original_phid);
292 if (!$original) {
293 // If the viewer can't see the original file, also prevent them from
294 // seeing the transformed file.
295 $this->didRejectResult($file);
296 unset($files[$key]);
297 continue;
298 }
299 } else {
300 $original = null;
301 }
302 $file->attachOriginalFile($original);
303 }
304
305 return $files;
306 }
307
308 private function newAttachmentsMap(array $files) {
309 $file_phids = mpull($files, 'getPHID');
310
311 $attachments_table = new PhabricatorFileAttachment();
312 $attachments_conn = $attachments_table->establishConnection('r');
313
314 $attachments = queryfx_all(
315 $attachments_conn,
316 'SELECT filePHID, objectPHID FROM %R WHERE filePHID IN (%Ls)
317 AND attachmentMode IN (%Ls)',
318 $attachments_table,
319 $file_phids,
320 array(
321 PhabricatorFileAttachment::MODE_ATTACH,
322 ));
323
324 $attachments_map = array_fill_keys($file_phids, array());
325 foreach ($attachments as $row) {
326 $file_phid = $row['filePHID'];
327 $object_phid = $row['objectPHID'];
328 $attachments_map[$file_phid][] = $object_phid;
329 }
330
331 return $attachments_map;
332 }
333
334 protected function didFilterPage(array $files) {
335 $xform_keys = $this->needTransforms;
336 if ($xform_keys !== null) {
337 $xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
338 'originalPHID IN (%Ls) AND transform IN (%Ls)',
339 mpull($files, 'getPHID'),
340 $xform_keys);
341
342 if ($xforms) {
343 $xfiles = id(new PhabricatorFile())->loadAllWhere(
344 'phid IN (%Ls)',
345 mpull($xforms, 'getTransformedPHID'));
346 $xfiles = mpull($xfiles, null, 'getPHID');
347 }
348
349 $xform_map = array();
350 foreach ($xforms as $xform) {
351 $xfile = idx($xfiles, $xform->getTransformedPHID());
352 if (!$xfile) {
353 continue;
354 }
355 $original_phid = $xform->getOriginalPHID();
356 $xform_key = $xform->getTransform();
357 $xform_map[$original_phid][$xform_key] = $xfile;
358 }
359
360 $default_xforms = array_fill_keys($xform_keys, null);
361
362 foreach ($files as $file) {
363 $file_xforms = idx($xform_map, $file->getPHID(), array());
364 $file_xforms += $default_xforms;
365 $file->attachTransforms($file_xforms);
366 }
367 }
368
369 return $files;
370 }
371
372 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
373 $joins = parent::buildJoinClauseParts($conn);
374
375 if ($this->transforms) {
376 $joins[] = qsprintf(
377 $conn,
378 'JOIN %T t ON t.transformedPHID = f.phid',
379 id(new PhabricatorTransformedFile())->getTableName());
380 }
381
382 if ($this->shouldJoinAttachmentsTable()) {
383 $joins[] = qsprintf(
384 $conn,
385 'JOIN %R attachments ON attachments.filePHID = f.phid
386 AND attachmentMode IN (%Ls)',
387 new PhabricatorFileAttachment(),
388 array(
389 PhabricatorFileAttachment::MODE_ATTACH,
390 ));
391 }
392
393 return $joins;
394 }
395
396 private function shouldJoinAttachmentsTable() {
397 return ($this->attachedObjectPHIDs !== null);
398 }
399
400 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
401 $where = parent::buildWhereClauseParts($conn);
402
403 if ($this->ids !== null) {
404 $where[] = qsprintf(
405 $conn,
406 'f.id IN (%Ld)',
407 $this->ids);
408 }
409
410 if ($this->phids !== null) {
411 $where[] = qsprintf(
412 $conn,
413 'f.phid IN (%Ls)',
414 $this->phids);
415 }
416
417 if ($this->authorPHIDs !== null) {
418 $where[] = qsprintf(
419 $conn,
420 'f.authorPHID IN (%Ls)',
421 $this->authorPHIDs);
422 }
423
424 if ($this->explicitUploads !== null) {
425 $where[] = qsprintf(
426 $conn,
427 'f.isExplicitUpload = %d',
428 (int)$this->explicitUploads);
429 }
430
431 if ($this->transforms !== null) {
432 $clauses = array();
433 foreach ($this->transforms as $transform) {
434 if ($transform['transform'] === true) {
435 $clauses[] = qsprintf(
436 $conn,
437 '(t.originalPHID = %s)',
438 $transform['originalPHID']);
439 } else {
440 $clauses[] = qsprintf(
441 $conn,
442 '(t.originalPHID = %s AND t.transform = %s)',
443 $transform['originalPHID'],
444 $transform['transform']);
445 }
446 }
447 $where[] = qsprintf($conn, '%LO', $clauses);
448 }
449
450 if ($this->dateCreatedAfter !== null) {
451 $where[] = qsprintf(
452 $conn,
453 'f.dateCreated >= %d',
454 $this->dateCreatedAfter);
455 }
456
457 if ($this->dateCreatedBefore !== null) {
458 $where[] = qsprintf(
459 $conn,
460 'f.dateCreated <= %d',
461 $this->dateCreatedBefore);
462 }
463
464 if ($this->contentHashes !== null) {
465 $where[] = qsprintf(
466 $conn,
467 'f.contentHash IN (%Ls)',
468 $this->contentHashes);
469 }
470
471 if ($this->minLength !== null) {
472 $where[] = qsprintf(
473 $conn,
474 'byteSize >= %d',
475 $this->minLength);
476 }
477
478 if ($this->maxLength !== null) {
479 $where[] = qsprintf(
480 $conn,
481 'byteSize <= %d',
482 $this->maxLength);
483 }
484
485 if ($this->names !== null) {
486 $where[] = qsprintf(
487 $conn,
488 'name in (%Ls)',
489 $this->names);
490 }
491
492 if ($this->isPartial !== null) {
493 $where[] = qsprintf(
494 $conn,
495 'isPartial = %d',
496 (int)$this->isPartial);
497 }
498
499 if ($this->isDeleted !== null) {
500 $where[] = qsprintf(
501 $conn,
502 'isDeleted = %d',
503 (int)$this->isDeleted);
504 }
505
506 if ($this->builtinKeys !== null) {
507 $where[] = qsprintf(
508 $conn,
509 'builtinKey IN (%Ls)',
510 $this->builtinKeys);
511 }
512
513 if ($this->isBuiltin !== null) {
514 if ($this->isBuiltin) {
515 $where[] = qsprintf(
516 $conn,
517 'builtinKey IS NOT NULL');
518 } else {
519 $where[] = qsprintf(
520 $conn,
521 'builtinKey IS NULL');
522 }
523 }
524
525 if ($this->storageEngines !== null) {
526 $where[] = qsprintf(
527 $conn,
528 'storageEngine IN (%Ls)',
529 $this->storageEngines);
530 }
531
532 if ($this->attachedObjectPHIDs !== null) {
533 $where[] = qsprintf(
534 $conn,
535 'attachments.objectPHID IN (%Ls)',
536 $this->attachedObjectPHIDs);
537 }
538
539 return $where;
540 }
541
542 protected function getPrimaryTableAlias() {
543 return 'f';
544 }
545
546 public function getQueryApplicationClass() {
547 return PhabricatorFilesApplication::class;
548 }
549
550}