Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/AppConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ class AppConfig {
// Default: 'no', set to 'yes' to enable
public const USE_SECURE_VIEW_ADDITIONAL_MIMES = 'use_secure_view_additional_mimes';

public const PREVIEW_CONVERSION_TIMEOUT = 'preview_conversion_timeout';
public const PREVIEW_CONVERSION_MAX_FILESIZE = 'preview_conversion_max_filesize';

private array $defaults = [
'wopi_url' => '',
'timeout' => 15,
'preview_conversion_timeout' => 5,
'preview_conversion_max_filesize' => 104857600, // 100 MB
'watermark_text' => '{userId}',
'watermark_allGroupsList' => [],
'watermark_allTagsList' => [],
Expand Down
14 changes: 12 additions & 2 deletions lib/Preview/Office.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@ public function isAvailable(FileInfo $file): bool {
}

public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
if ($file->getSize() === 0) {
$fileSize = $file->getSize();
if ($fileSize === 0) {
return null;
}

$maxFileSize = $this->appConfig->getPreviewConversionMaxFileSize();
if ($fileSize > $maxFileSize) {
$this->logger->debug('Skipping preview conversion: file size {size} exceeds limit {limit}', [
'size' => $fileSize,
'limit' => $maxFileSize,
]);
return null;
}

try {
$response = $this->remoteService->convertFileTo($file, 'png');
$response = $this->remoteService->convertFileTo($file, 'png', $this->appConfig->getPreviewConversionTimeout());
$image = new Image();
$image->loadFromData($response);

Expand Down
11 changes: 8 additions & 3 deletions lib/Service/RemoteService.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,19 @@ public function fetchTargetThumbnail(File $file, string $target): ?string {
/**
* @return resource|string
*/
public function convertFileTo(File $file, string $format) {
public function convertFileTo(File $file, string $format, int $timeout = RemoteOptionsService::REMOTE_TIMEOUT_DEFAULT) {
$fileName = $file->getStorage()->getLocalFile($file->getInternalPath());
$stream = fopen($fileName, 'rb');

if ($stream === false) {
throw new Exception('Failed to open stream');
}
return $this->convertTo($file->getName(), $stream, $format);

try {
return $this->convertTo($file->getName(), $stream, $format, [], $timeout);
} finally {
fclose($stream);
}
}

/**
Expand All @@ -73,7 +78,7 @@ public function convertFileTo(File $file, string $format) {
*/
public function convertTo(string $filename, $stream, string $format) {
$client = $this->clientService->newClient();
$options = RemoteOptionsService::getDefaultOptions();
$options = RemoteOptionsService::getDefaultOptions($timeout);
// FIXME: can be removed once https://github.com/CollaboraOnline/online/issues/6983 is fixed upstream
$options['expect'] = false;

Expand Down
36 changes: 36 additions & 0 deletions tests/lib/AppConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,40 @@ public function testGetAppValueArrayWithNoneValue() {

$this->assertSame([], $result);
}

public function testGetPreviewConversionTimeoutReturnsDefault(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('richdocuments', 'preview_conversion_timeout', 5)
->willReturn(5);

$this->assertSame(5, $this->appConfig->getPreviewConversionTimeout());
}

public function testGetPreviewConversionTimeoutReturnsConfiguredValue(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('richdocuments', 'preview_conversion_timeout', 5)
->willReturn(30);

$this->assertSame(30, $this->appConfig->getPreviewConversionTimeout());
}

public function testGetPreviewConversionMaxFileSizeReturnsDefault(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('richdocuments', 'preview_conversion_max_filesize', 104857600)
->willReturn(104857600);

$this->assertSame(104857600, $this->appConfig->getPreviewConversionMaxFileSize());
}

public function testGetPreviewConversionMaxFileSizeReturnsConfiguredValue(): void {
$this->config->expects($this->once())
->method('getAppValue')
->with('richdocuments', 'preview_conversion_max_filesize', 104857600)
->willReturn(52428800);

$this->assertSame(52428800, $this->appConfig->getPreviewConversionMaxFileSize());
}
}
103 changes: 103 additions & 0 deletions tests/lib/Preview/OfficeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace Tests\Richdocuments\Preview;

use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Capabilities;
use OCA\Richdocuments\Preview\Office;
use OCA\Richdocuments\Service\RemoteService;
use OCP\Files\File;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class OfficeTest extends TestCase {
private RemoteService&MockObject $remoteService;
private LoggerInterface&MockObject $logger;
private AppConfig&MockObject $appConfig;
private Capabilities&MockObject $capabilities;
private Office $provider;

protected function setUp(): void {
parent::setUp();

$this->remoteService = $this->createMock(RemoteService::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->appConfig = $this->createMock(AppConfig::class);
$this->capabilities = $this->createMock(Capabilities::class);
$this->capabilities->method('getCapabilities')->willReturn(['richdocuments' => []]);

$this->provider = new class($this->remoteService, $this->logger, $this->appConfig, $this->capabilities) extends Office {
#[\Override]
public function getMimeType(): string {
return '/application\\/test/';
}
};
}

public function testGetThumbnailSkipsConversionWhenFileIsTooLarge(): void {
$file = $this->createMock(File::class);
$file->expects($this->once())->method('getSize')->willReturn(101 * 1024 * 1024);

$this->appConfig->expects($this->once())
->method('getPreviewConversionMaxFileSize')
->willReturn(100 * 1024 * 1024);
$this->remoteService->expects($this->never())->method('convertFileTo');

$result = $this->provider->getThumbnail($file, 64, 64);

$this->assertNull($result);
}

public function testGetThumbnailReturnsNullForEmptyFile(): void {
$file = $this->createMock(File::class);
$file->expects($this->once())->method('getSize')->willReturn(0);

$this->appConfig->expects($this->never())->method('getPreviewConversionMaxFileSize');
$this->remoteService->expects($this->never())->method('convertFileTo');

$result = $this->provider->getThumbnail($file, 64, 64);

$this->assertNull($result);
}

public function testGetThumbnailAttemptsConversionWhenFileSizeIsExactlyAtLimit(): void {
$file = $this->createMock(File::class);
$file->expects($this->once())->method('getSize')->willReturn(100 * 1024 * 1024);

$this->appConfig->expects($this->once())
->method('getPreviewConversionMaxFileSize')
->willReturn(100 * 1024 * 1024);
// Conversion is attempted; throw to keep the test simple (image loading is not unit-tested here)
$this->remoteService->expects($this->once())
->method('convertFileTo')
->with($file, 'png')
->willThrowException(new \Exception('conversion failed'));

$result = $this->provider->getThumbnail($file, 64, 64);

$this->assertNull($result);
}

public function testGetThumbnailReturnsNullWhenConversionFails(): void {
$file = $this->createMock(File::class);
$file->expects($this->once())->method('getSize')->willReturn(1024);

$this->appConfig->expects($this->once())
->method('getPreviewConversionMaxFileSize')
->willReturn(100 * 1024 * 1024);
$this->remoteService->expects($this->once())
->method('convertFileTo')
->with($file, 'png')
->willThrowException(new \Exception('conversion failed'));

$result = $this->provider->getThumbnail($file, 64, 64);

$this->assertNull($result);
}
}