diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index 071ee03991..83bf2d8dec 100644 --- a/src/applications/files/query/PhabricatorFileQuery.php +++ b/src/applications/files/query/PhabricatorFileQuery.php @@ -1,448 +1,476 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withContentHashes(array $content_hashes) { $this->contentHashes = $content_hashes; return $this; } public function withBuiltinKeys(array $keys) { $this->builtinKeys = $keys; return $this; } public function withIsBuiltin($is_builtin) { $this->isBuiltin = $is_builtin; return $this; } /** * Select files which are transformations of some other file. For example, * you can use this query to find previously generated thumbnails of an image * file. * * As a parameter, provide a list of transformation specifications. Each * specification is a dictionary with the keys `originalPHID` and `transform`. * The `originalPHID` is the PHID of the original file (the file which was * transformed) and the `transform` is the name of the transform to query * for. If you pass `true` as the `transform`, all transformations of the * file will be selected. * * For example: * * array( * array( * 'originalPHID' => 'PHID-FILE-aaaa', * 'transform' => 'sepia', * ), * array( * 'originalPHID' => 'PHID-FILE-bbbb', * 'transform' => true, * ), * ) * * This selects the `"sepia"` transformation of the file with PHID * `PHID-FILE-aaaa` and all transformations of the file with PHID * `PHID-FILE-bbbb`. * * @param list List of transform specifications, described above. * @return this */ public function withTransforms(array $specs) { foreach ($specs as $spec) { if (!is_array($spec) || empty($spec['originalPHID']) || empty($spec['transform'])) { throw new Exception( pht( "Transform specification must be a dictionary with keys ". "'%s' and '%s'!", 'originalPHID', 'transform')); } } $this->transforms = $specs; return $this; } public function withLengthBetween($min, $max) { $this->minLength = $min; $this->maxLength = $max; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withIsPartial($partial) { $this->isPartial = $partial; return $this; } public function withIsDeleted($deleted) { $this->isDeleted = $deleted; return $this; } public function withNameNgrams($ngrams) { return $this->withNgramsConstraint( id(new PhabricatorFileNameNgrams()), $ngrams); } public function showOnlyExplicitUploads($explicit_uploads) { $this->explicitUploads = $explicit_uploads; return $this; } public function needTransforms(array $transforms) { $this->needTransforms = $transforms; return $this; } public function newResultObject() { return new PhabricatorFile(); } protected function loadPage() { - $files = $this->loadStandardPage(new PhabricatorFile()); + $files = $this->loadStandardPage($this->newResultObject()); if (!$files) { return $files; } - $viewer = $this->getViewer(); - $is_omnipotent = $viewer->isOmnipotent(); - - // We need to load attached objects to perform policy checks for files. - // First, load the edges. + // Figure out which files we need to load attached objects for. In most + // cases, we need to load attached objects to perform policy checks for + // files. - $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST; - $file_phids = mpull($files, 'getPHID'); - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs($file_phids) - ->withEdgeTypes(array($edge_type)) - ->execute(); - - $object_phids = array(); + // However, in some special cases where we know files will always be + // visible, we skip this. See T8478 and T13106. + $need_objects = array(); foreach ($files as $file) { - $phids = array_keys($edges[$file->getPHID()][$edge_type]); - $file->attachObjectPHIDs($phids); + $always_visible = false; if ($file->getIsProfileImage()) { - // If this is a profile image, don't bother loading related files. - // It will always be visible, and we can get into trouble if we try - // to load objects and end up stuck in a cycle. See T8478. - continue; + $always_visible = true; } - if ($is_omnipotent) { - // If the viewer is omnipotent, we don't need to load the associated - // objects either since they can certainly see the object. Skipping - // this can improve performance and prevent cycles. + if ($file->isBuiltin()) { + $always_visible = true; + } + + if ($always_visible) { + // We just treat these files as though they aren't attached to + // anything. This saves a query in common cases when we're loading + // profile images or builtins. We could be slightly more nuanced + // about this and distinguish between "not attached to anything" and + // "might be attached but policy checks don't need to care". + $file->attachObjectPHIDs(array()); continue; } - foreach ($phids as $phid) { - $object_phids[$phid] = true; + $need_objects[] = $file; + } + + $viewer = $this->getViewer(); + $is_omnipotent = $viewer->isOmnipotent(); + + // If we have any files left which do need objects, load the edges now. + $object_phids = array(); + if ($need_objects) { + $edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST; + $file_phids = mpull($need_objects, 'getPHID'); + + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($file_phids) + ->withEdgeTypes(array($edge_type)) + ->execute(); + + foreach ($need_objects as $file) { + $phids = array_keys($edges[$file->getPHID()][$edge_type]); + $file->attachObjectPHIDs($phids); + + if ($is_omnipotent) { + // If the viewer is omnipotent, we don't need to load the associated + // objects either since the viewer can certainly see the object. + // Skipping this can improve performance and prevent cycles. This + // could possibly become part of the profile/builtin code above which + // short circuits attacment policy checks in cases where we know them + // to be unnecessary. + continue; + } + + foreach ($phids as $phid) { + $object_phids[$phid] = true; + } } } // If this file is a transform of another file, load that file too. If you // can see the original file, you can see the thumbnail. // TODO: It might be nice to put this directly on PhabricatorFile and remove // the PhabricatorTransformedFile table, which would be a little simpler. $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'transformedPHID IN (%Ls)', - $file_phids); + mpull($files, 'getPHID')); $xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID'); foreach ($xform_phids as $derived_phid => $original_phid) { $object_phids[$original_phid] = true; } $object_phids = array_keys($object_phids); // Now, load the objects. $objects = array(); if ($object_phids) { // NOTE: We're explicitly turning policy exceptions off, since the rule // here is "you can see the file if you can see ANY associated object". // Without this explicit flag, we'll incorrectly throw unless you can // see ALL associated objects. $objects = id(new PhabricatorObjectQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($object_phids) ->setRaisePolicyExceptions(false) ->execute(); $objects = mpull($objects, null, 'getPHID'); } foreach ($files as $file) { $file_objects = array_select_keys($objects, $file->getObjectPHIDs()); $file->attachObjects($file_objects); } foreach ($files as $key => $file) { $original_phid = idx($xform_phids, $file->getPHID()); if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) { // This is a special case for builtin files, which are handled // oddly. $original = null; } else if ($original_phid) { $original = idx($objects, $original_phid); if (!$original) { // If the viewer can't see the original file, also prevent them from // seeing the transformed file. $this->didRejectResult($file); unset($files[$key]); continue; } } else { $original = null; } $file->attachOriginalFile($original); } return $files; } protected function didFilterPage(array $files) { $xform_keys = $this->needTransforms; if ($xform_keys !== null) { $xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'originalPHID IN (%Ls) AND transform IN (%Ls)', mpull($files, 'getPHID'), $xform_keys); if ($xforms) { $xfiles = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', mpull($xforms, 'getTransformedPHID')); $xfiles = mpull($xfiles, null, 'getPHID'); } $xform_map = array(); foreach ($xforms as $xform) { $xfile = idx($xfiles, $xform->getTransformedPHID()); if (!$xfile) { continue; } $original_phid = $xform->getOriginalPHID(); $xform_key = $xform->getTransform(); $xform_map[$original_phid][$xform_key] = $xfile; } $default_xforms = array_fill_keys($xform_keys, null); foreach ($files as $file) { $file_xforms = idx($xform_map, $file->getPHID(), array()); $file_xforms += $default_xforms; $file->attachTransforms($file_xforms); } } return $files; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->transforms) { $joins[] = qsprintf( $conn, 'JOIN %T t ON t.transformedPHID = f.phid', id(new PhabricatorTransformedFile())->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'f.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'f.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'f.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->explicitUploads !== null) { $where[] = qsprintf( $conn, 'f.isExplicitUpload = %d', (int)$this->explicitUploads); } if ($this->transforms !== null) { $clauses = array(); foreach ($this->transforms as $transform) { if ($transform['transform'] === true) { $clauses[] = qsprintf( $conn, '(t.originalPHID = %s)', $transform['originalPHID']); } else { $clauses[] = qsprintf( $conn, '(t.originalPHID = %s AND t.transform = %s)', $transform['originalPHID'], $transform['transform']); } } $where[] = qsprintf($conn, '(%Q)', implode(') OR (', $clauses)); } if ($this->dateCreatedAfter !== null) { $where[] = qsprintf( $conn, 'f.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore !== null) { $where[] = qsprintf( $conn, 'f.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->contentHashes !== null) { $where[] = qsprintf( $conn, 'f.contentHash IN (%Ls)', $this->contentHashes); } if ($this->minLength !== null) { $where[] = qsprintf( $conn, 'byteSize >= %d', $this->minLength); } if ($this->maxLength !== null) { $where[] = qsprintf( $conn, 'byteSize <= %d', $this->maxLength); } if ($this->names !== null) { $where[] = qsprintf( $conn, 'name in (%Ls)', $this->names); } if ($this->isPartial !== null) { $where[] = qsprintf( $conn, 'isPartial = %d', (int)$this->isPartial); } if ($this->isDeleted !== null) { $where[] = qsprintf( $conn, 'isDeleted = %d', (int)$this->isDeleted); } if ($this->builtinKeys !== null) { $where[] = qsprintf( $conn, 'builtinKey IN (%Ls)', $this->builtinKeys); } if ($this->isBuiltin !== null) { if ($this->isBuiltin) { $where[] = qsprintf( $conn, 'builtinKey IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'builtinKey IS NULL'); } } return $where; } protected function getPrimaryTableAlias() { return 'f'; } public function getQueryApplicationClass() { return 'PhabricatorFilesApplication'; } }