Skip to content

Commit 8dfcdac

Browse files
authored
Merge pull request #63347 from nextcloud/backport/63286/stable34
[stable34] fix(preview): qualify the ambiguous columns in PreviewMapper queries
2 parents b5cd336 + 2de74e3 commit 8dfcdac

2 files changed

Lines changed: 89 additions & 6 deletions

File tree

lib/private/Preview/Db/PreviewMapper.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ class PreviewMapper extends QBMapper {
3131
private const VERSION_TABLE_NAME = 'preview_versions';
3232
public const MAX_CHUNK_SIZE = 1000;
3333

34+
// Columns selected by joinLocation() that do not belong to the previews table
35+
private const JOINED_COLUMN_ALIASES = [
36+
'version' => 'v',
37+
'bucket_name' => 'l',
38+
'object_store_name' => 'l',
39+
];
40+
3441
public function __construct(
3542
IDBConnection $db,
3643
private readonly IMimeTypeLoader $mimeTypeLoader,
@@ -134,7 +141,7 @@ public function getAvailablePreviews(array $fileIds): array {
134141
public function getByFileId(int $fileId): \Generator {
135142
$selectQb = $this->db->getQueryBuilder();
136143
$this->joinLocation($selectQb)
137-
->where($selectQb->expr()->eq('file_id', $selectQb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
144+
->where($selectQb->expr()->eq('p.file_id', $selectQb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
138145
yield from $this->yieldEntities($selectQb);
139146
}
140147

@@ -254,7 +261,19 @@ public function getPreviewForSpecification(array $parameters): ?Preview {
254261
$this->joinLocation($qb);
255262

256263
foreach ($parameters as $key => $value) {
257-
$qb->andWhere($qb->expr()->eq($key, $qb->createNamedParameter($value)));
264+
// The previews table is joined with preview_versions, which shares
265+
// the file_id column name, so plain column names have to be aliased.
266+
$column = str_contains($key, '.')
267+
? $key
268+
: (self::JOINED_COLUMN_ALIASES[$key] ?? 'p') . '.' . $key;
269+
// An untyped false binds as an empty string, which PostgreSQL
270+
// rejects for a boolean column.
271+
$type = match (true) {
272+
is_bool($value) => IQueryBuilder::PARAM_BOOL,
273+
is_int($value) => IQueryBuilder::PARAM_INT,
274+
default => IQueryBuilder::PARAM_STR,
275+
};
276+
$qb->andWhere($qb->expr()->eq($column, $qb->createNamedParameter($value, $type)));
258277
}
259278

260279
try {

tests/lib/Preview/PreviewMapperTest.php

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use OC\Preview\Db\Preview;
1313
use OC\Preview\Db\PreviewMapper;
14+
use OCP\Files\IMimeTypeLoader;
1415
use OCP\IDBConnection;
1516
use OCP\Server;
1617
use OCP\Snowflake\ISnowflakeGenerator;
@@ -21,13 +22,15 @@ class PreviewMapperTest extends TestCase {
2122
private PreviewMapper $previewMapper;
2223
private IDBConnection $connection;
2324
private ISnowflakeGenerator $snowflake;
25+
private IMimeTypeLoader $mimeTypeLoader;
2426

2527
#[\Override]
2628
public function setUp(): void {
2729
parent::setUp();
2830
$this->previewMapper = Server::get(PreviewMapper::class);
2931
$this->connection = Server::get(IDBConnection::class);
3032
$this->snowflake = Server::get(ISnowflakeGenerator::class);
33+
$this->mimeTypeLoader = Server::get(IMimeTypeLoader::class);
3134

3235
$qb = $this->connection->getQueryBuilder();
3336
$qb->delete('preview_locations')->executeStatement();
@@ -66,7 +69,7 @@ public function testGetAvailablePreviews(): void {
6669
$this->assertEquals('default', $previews[43][0]->getObjectStoreName());
6770
}
6871

69-
private function createPreviewForFileId(int $fileId, ?int $bucket = null): string {
72+
private function createPreviewForFileId(int $fileId, ?int $bucket = null, int $size = 100, ?string $version = null, bool $cropped = true): string {
7073
$locationId = null;
7174
if ($bucket) {
7275
$qb = $this->connection->getQueryBuilder();
@@ -83,15 +86,16 @@ private function createPreviewForFileId(int $fileId, ?int $bucket = null): strin
8386
$preview->generateId();
8487
$preview->setFileId($fileId);
8588
$preview->setStorageId(1);
86-
$preview->setCropped(true);
89+
$preview->setCropped($cropped);
8790
$preview->setMax(true);
88-
$preview->setWidth(100);
91+
$preview->setWidth($size);
8992
$preview->setSourceMimeType('image/jpeg');
90-
$preview->setHeight(100);
93+
$preview->setHeight($size);
9194
$preview->setSize(100);
9295
$preview->setMtime(time());
9396
$preview->setMimetype('image/jpeg');
9497
$preview->setEtag('abcdefg');
98+
$preview->setVersion($version);
9599

96100
if ($locationId !== null) {
97101
$preview->setLocationId($locationId);
@@ -101,6 +105,66 @@ private function createPreviewForFileId(int $fileId, ?int $bucket = null): strin
101105
return $preview->id;
102106
}
103107

108+
/**
109+
* The previews table is joined with preview_versions, which also has a
110+
* file_id column, so the condition has to be qualified with the alias.
111+
*/
112+
public function testGetByFileId(): void {
113+
$fileId = 4242;
114+
$this->createPreviewForFileId($fileId);
115+
$this->createPreviewForFileId($fileId, size: 256);
116+
$this->createPreviewForFileId(4243);
117+
118+
$previews = iterator_to_array($this->previewMapper->getByFileId($fileId));
119+
120+
$this->assertCount(2, $previews);
121+
foreach ($previews as $preview) {
122+
$this->assertSame($fileId, $preview->getFileId());
123+
}
124+
}
125+
126+
/**
127+
* Same ambiguity, reached through the specification lookup that
128+
* Generator::savePreview() uses to recover from a unique constraint
129+
* violation. It passes the cropped flag as a PHP bool, and false is the
130+
* common case, so both values have to be covered.
131+
*/
132+
#[\PHPUnit\Framework\Attributes\TestWith([false])]
133+
#[\PHPUnit\Framework\Attributes\TestWith([true])]
134+
public function testGetPreviewForSpecification(bool $cropped): void {
135+
$fileId = 4244;
136+
$previewId = $this->createPreviewForFileId($fileId, cropped: $cropped);
137+
138+
$preview = $this->previewMapper->getPreviewForSpecification([
139+
'file_id' => $fileId,
140+
'width' => 100,
141+
'height' => 100,
142+
'mimetype_id' => $this->mimeTypeLoader->getId('image/jpeg'),
143+
'cropped' => $cropped,
144+
'version_id' => '-1',
145+
]);
146+
147+
$this->assertNotNull($preview);
148+
$this->assertEquals($previewId, $preview->getId());
149+
}
150+
151+
/**
152+
* version lives in the joined preview_versions table, so it has to keep
153+
* resolving to that alias rather than to the previews table.
154+
*/
155+
public function testGetPreviewForSpecificationOnJoinedColumn(): void {
156+
$fileId = 4245;
157+
$previewId = $this->createPreviewForFileId($fileId, version: '1000');
158+
159+
$preview = $this->previewMapper->getPreviewForSpecification([
160+
'file_id' => $fileId,
161+
'version' => '1000',
162+
]);
163+
164+
$this->assertNotNull($preview);
165+
$this->assertEquals($previewId, $preview->getId());
166+
}
167+
104168
public function testLargeIdInsertRetrieve(): void {
105169
$fileId = PHP_INT_MAX;
106170
$originalPreviewId = $this->createPreviewForFileId($fileId);

0 commit comments

Comments
 (0)