diff --git a/resources/builtin/missing.png b/resources/builtin/missing.png new file mode 100644 index 0000000000..f301d02626 Binary files /dev/null and b/resources/builtin/missing.png differ diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php index d5731ac13b..5a110afaf9 100644 --- a/src/applications/files/query/PhabricatorFileQuery.php +++ b/src/applications/files/query/PhabricatorFileQuery.php @@ -7,6 +7,7 @@ final class PhabricatorFileQuery private $phids; private $authorPHIDs; private $explicitUploads; + private $transforms; public function withIDs(array $ids) { $this->ids = $ids; @@ -23,6 +24,21 @@ final class PhabricatorFileQuery return $this; } + public function withTransforms(array $specs) { + foreach ($specs as $spec) { + if (!is_array($spec) || + empty($spec['originalPHID']) || + empty($spec['transform'])) { + throw new Exception( + "Transform specification must be a dictionary with keys ". + "'originalPHID' and 'transform'!"); + } + } + + $this->transforms = $specs; + return $this; + } + public function showOnlyExplicitUploads($explicit_uploads) { $this->explicitUploads = $explicit_uploads; return $this; @@ -34,8 +50,9 @@ final class PhabricatorFileQuery $data = queryfx_all( $conn_r, - 'SELECT * FROM %T f %Q %Q %Q', + 'SELECT * FROM %T f %Q %Q %Q %Q', $table->getTableName(), + $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); @@ -43,6 +60,19 @@ final class PhabricatorFileQuery return $table->loadAllFromArray($data); } + private function buildJoinClause(AphrontDatabaseConnection $conn_r) { + $joins = array(); + + if ($this->transforms) { + $joins[] = qsprintf( + $conn_r, + 'JOIN %T t ON t.transformedPHID = f.phid', + id(new PhabricatorTransformedFile())->getTableName()); + } + + return implode(' ', $joins); + } + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); @@ -51,31 +81,47 @@ final class PhabricatorFileQuery if ($this->ids) { $where[] = qsprintf( $conn_r, - 'id IN (%Ld)', + 'f.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, - 'phid IN (%Ls)', + 'f.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs) { $where[] = qsprintf( $conn_r, - 'authorPHID IN (%Ls)', + 'f.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->explicitUploads) { $where[] = qsprintf( $conn_r, - 'isExplicitUpload = true'); + 'f.isExplicitUpload = true'); + } + + if ($this->transforms) { + $clauses = array(); + foreach ($this->transforms as $transform) { + $clauses[] = qsprintf( + $conn_r, + '(t.originalPHID = %s AND t.transform = %s)', + $transform['originalPHID'], + $transform['transform']); + } + $where[] = qsprintf($conn_r, '(%Q)', implode(') OR (', $clauses)); } return $this->formatWhereClause($where); } + protected function getPagingColumn() { + return 'f.id'; + } + } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 888d49234c..732ede3de2 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -682,6 +682,89 @@ final class PhabricatorFile extends PhabricatorFileDAO } + /** + * Load (or build) the {@class:PhabricatorFile} objects for builtin file + * resources. The builtin mechanism allows files shipped with Phabricator + * to be treated like normal files so that APIs do not need to special case + * things like default images or deleted files. + * + * Builtins are located in `resources/builtin/` and identified by their + * name. + * + * @param PhabricatorUser Viewing user. + * @param list List of builtin file names. + * @return dict Dictionary of named builtins. + */ + public static function loadBuiltins(PhabricatorUser $user, array $names) { + $specs = array(); + foreach ($names as $name) { + $specs[] = array( + 'originalPHID' => PhabricatorPHIDConstants::PHID_VOID, + 'transform' => 'builtin:'.$name, + ); + } + + $files = id(new PhabricatorFileQuery()) + ->setViewer($user) + ->withTransforms($specs) + ->execute(); + + $files = mpull($files, null, 'getName'); + + $root = dirname(phutil_get_library_root('phabricator')); + $root = $root.'/resources/builtin/'; + + $build = array(); + foreach ($names as $name) { + if (isset($files[$name])) { + continue; + } + + // This is just a sanity check to prevent loading arbitrary files. + if (basename($name) != $name) { + throw new Exception("Invalid builtin name '{$name}'!"); + } + + $path = $root.$name; + + if (!Filesystem::pathExists($path)) { + throw new Exception("Builtin '{$path}' does not exist!"); + } + + $data = Filesystem::readFile($path); + $params = array( + 'name' => $name, + 'ttl' => time() + (60 * 60 * 24 * 7), + ); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file = PhabricatorFile::newFromFileData($data, $params); + $xform = id(new PhabricatorTransformedFile()) + ->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID) + ->setTransform('builtin:'.$name) + ->setTransformedPHID($file->getPHID()) + ->save(); + unset($unguarded); + + $files[$name] = $file; + } + + return $files; + } + + + /** + * Convenience wrapper for @{method:loadBuiltins}. + * + * @param PhabricatorUser Viewing user. + * @param string Single builtin name to load. + * @return PhabricatorFile Corresponding builtin file. + */ + public static function loadBuiltin(PhabricatorUser $user, $name) { + return idx(self::loadBuiltins($user, array($name)), $name); + } + + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php index 6ed089f91f..5860ca1c5b 100644 --- a/src/applications/phid/PhabricatorPHIDConstants.php +++ b/src/applications/phid/PhabricatorPHIDConstants.php @@ -44,4 +44,7 @@ final class PhabricatorPHIDConstants { const PHID_TYPE_XCMT = 'XCMT'; const PHID_TYPE_XUSR = 'XUSR'; + const PHID_TYPE_VOID = 'VOID'; + const PHID_VOID = 'PHID-VOID-00000000000000000000'; + } diff --git a/src/applications/pholio/query/PholioMockQuery.php b/src/applications/pholio/query/PholioMockQuery.php index d222fed29b..d5ce886eba 100644 --- a/src/applications/pholio/query/PholioMockQuery.php +++ b/src/applications/pholio/query/PholioMockQuery.php @@ -129,7 +129,11 @@ final class PholioMockQuery } foreach ($all_images as $image) { - $image->attachFile($all_files[$image->getFilePHID()]); + $file = idx($all_files, $image->getFilePHID()); + if (!$file) { + $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png'); + } + $image->attachFile($file); if ($this->needInlineComments) { $inlines = idx($all_images, $image->getID(), array()); $image->attachInlineComments($inlines); @@ -151,7 +155,11 @@ final class PholioMockQuery $cover_file_phids), null, 'getPHID'); foreach ($mocks as $mock) { - $mock->attachCoverFile($cover_files[$mock->getCoverPHID()]); + $file = idx($cover_files, $mock->getCoverPHID()); + if (!$file) { + $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png'); + } + $mock->attachCoverFile($file); } }