Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(files): more conversion tests and translate error messages #50240

Merged
merged 2 commits into from
Jan 18, 2025
Merged
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
12 changes: 10 additions & 2 deletions apps/files/lib/Controller/ConversionApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@
namespace OCA\Files\Controller;

use OC\Files\Utils\PathHelper;
use OC\ForbiddenException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\Files\Conversion\IConversionManager;
use OCP\Files\File;
use OCP\Files\GenericFileException;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\IRequest;
Expand Down Expand Up @@ -59,7 +62,8 @@ public function convert(int $fileId, string $targetMimeType, ?string $destinatio
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$file = $userFolder->getFirstNodeById($fileId);

if (!($file instanceof File)) {
// Also throw a 404 if the file is not readable to not leak information
if (!($file instanceof File) || $file->isReadable() === false) {
throw new OCSNotFoundException($this->l10n->t('The file cannot be found'));
}

Expand All @@ -72,14 +76,18 @@ public function convert(int $fileId, string $targetMimeType, ?string $destinatio
}

if (!$userFolder->get($parentDir)->isCreatable()) {
throw new OCSForbiddenException();
throw new OCSForbiddenException($this->l10n->t('You do not have permission to create a file at the specified location'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this shown on the frontend? If not then it makes no sense to translate if it is only for developers (also makes debugging harder)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is shown

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly not a HintException 😔

}

$destination = $userFolder->getFullPath($destination);
}

try {
$convertedFile = $this->fileConversionManager->convert($file, $targetMimeType, $destination);
} catch (ForbiddenException $e) {
throw new OCSForbiddenException($e->getMessage());
} catch (GenericFileException $e) {
throw new OCSBadRequestException($e->getMessage());
} catch (\Exception $e) {
logger('files')->error($e->getMessage(), ['exception' => $e]);
throw new OCSException($this->l10n->t('The file could not be converted.'));
Expand Down
21 changes: 21 additions & 0 deletions build/integration/file_conversions/file_conversions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ Feature: conversions
Then as "user0" the file "/image.jpg" exists
Then as "user0" the file "/image.png" does not exist

Scenario: Converting a file to a given path without extension fails
Given user "user0" uploads file "data/clouds.jpg" to "/image.jpg"
And User "user0" created a folder "/folder"
Then as "user0" the file "/image.jpg" exists
Then as "user0" the folder "/folder" exists
When user "user0" converts file "/image.jpg" to "image/png" and saves it to "/folder/image"
Then the HTTP status code should be "400"
Then the OCS status code should be "400"
Then as "user0" the file "/folder/image.png" does not exist
Then as "user0" the file "/image.png" does not exist

@local_storage
Scenario: Converting a file bigger than 100 MiB fails
Given file "/image.jpg" of size 108003328 is created in local storage
Then as "user0" the folder "/local_storage" exists
Then as "user0" the file "/local_storage/image.jpg" exists
When user "user0" converts file "/local_storage/image.jpg" to "image/png" and saves it to "/image.png"
Then the HTTP status code should be "400"
Then the OCS status code should be "400"
Then as "user0" the file "/image.png" does not exist

Scenario: Forbid conversion to a destination without create permission
Given user "user1" exists
# Share the folder with user1
Expand Down
43 changes: 34 additions & 9 deletions lib/private/Files/Conversion/ConversionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
namespace OC\Files\Conversion;

use OC\AppFramework\Bootstrap\Coordinator;
use OC\ForbiddenException;
use OC\SystemConfig;
use OCP\Files\Conversion\IConversionManager;
use OCP\Files\Conversion\IConversionProvider;
use OCP\Files\File;
use OCP\Files\GenericFileException;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\ITempManager;
use OCP\L10N\IFactory;
use OCP\PreConditionNotMetException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
Expand All @@ -37,14 +40,17 @@ class ConversionManager implements IConversionManager {
/** @var list<IConversionProvider> */
private array $providers = [];

private IL10N $l10n;
public function __construct(
private Coordinator $coordinator,
private ContainerInterface $serverContainer,
private IRootFolder $rootFolder,
private ITempManager $tempManager,
private LoggerInterface $logger,
private SystemConfig $config,
IFactory $l10nFactory,
) {
$this->l10n = $l10nFactory->get('files');
}

public function hasProviders(): bool {
Expand All @@ -62,30 +68,28 @@ public function getProviders(): array {

public function convert(File $file, string $targetMimeType, ?string $destination = null): string {
if (!$this->hasProviders()) {
throw new PreConditionNotMetException('No file conversion providers available');
throw new PreConditionNotMetException($this->l10n->t('No file conversion providers available'));
}

// Operate in mebibytes
$fileSize = $file->getSize() / (1024 * 1024);
$threshold = $this->config->getValue('max_file_conversion_filesize', 100);
if ($fileSize > $threshold) {
throw new GenericFileException('File is too large to convert');
throw new GenericFileException($this->l10n->t('File is too large to convert'));
}

$fileMimeType = $file->getMimetype();
$validProvider = $this->getValidProvider($fileMimeType, $targetMimeType);

if ($validProvider !== null) {
$convertedFile = $validProvider->convertFile($file, $targetMimeType);

// Get the target extension given by the provider
$targetExtension = '';
foreach ($validProvider->getSupportedMimeTypes() as $mimeProvider) {
if ($mimeProvider->getTo() === $targetMimeType) {
$targetExtension = $mimeProvider->getExtension();
break;
}
}

// If destination not provided, we use the same path
// as the original file, but with the new extension
if ($destination === null) {
Expand All @@ -94,11 +98,21 @@ public function convert(File $file, string $targetMimeType, ?string $destination
$destination = $parent->getFullPath($basename . '.' . $targetExtension);
}

// If destination doesn't match the target extension, we throw an error
if (pathinfo($destination, PATHINFO_EXTENSION) !== $targetExtension) {
throw new GenericFileException($this->l10n->t('Destination does not match conversion extension'));
}

// Check destination before converting
$this->checkDestination($destination);

// Convert the file and write it to the destination
$convertedFile = $validProvider->convertFile($file, $targetMimeType);
$convertedFile = $this->writeToDestination($destination, $convertedFile);
return $convertedFile->getPath();
}

throw new RuntimeException('Could not convert file');
throw new RuntimeException($this->l10n->t('Could not convert file'));
}

/**
Expand Down Expand Up @@ -127,14 +141,25 @@ private function getRegisteredProviders(): array {
return array_values(array_merge([], $this->preferredProviders, $this->providers));
}

private function checkDestination(string $destination): void {
if (!$this->rootFolder->nodeExists(dirname($destination))) {
throw new ForbiddenException($this->l10n->t('Destination does not exist'));
}

$folder = $this->rootFolder->get(dirname($destination));
if (!$folder->isCreatable()) {
throw new ForbiddenException($this->l10n->t('Destination is not creatable'));
}
}

private function writeToDestination(string $destination, mixed $content): File {
$this->checkDestination($destination);

if ($this->rootFolder->nodeExists($destination)) {
$file = $this->rootFolder->get($destination);
$parent = $file->getParent();
if (!$parent->isCreatable()) {
throw new GenericFileException('Destination is not creatable');
}

// Folder permissions is already checked in checkDestination method
$newName = $parent->getNonExistingName(basename($destination));
$destination = $parent->getFullPath($newName);
}
Expand Down
Loading