Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/bundle/Core/Resources/config/routing/internal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,21 @@ ibexa.content.preview.default:
ibexa.user_hash:
path: /_fos_user_context_hash

# Must be defined before ibexa.content.download, so that numeric {fieldId} takes precedence over {fieldIdentifier}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could add requirement for filename as well here and use https://github.com/symfony/routing/blob/7.4/Requirement/Requirement.php as their value source, eg. replace \d+ with Requirement::DIGITS

The same for missing requirements in ibexa.content.download. This way, I think we'll be secure about route ordering here as their expected argument types won't conflict with each other

ibexa.content.download.field_id.filename:
path: /content/download/{contentId}/{fieldId}/{filename}
defaults: { _controller: Ibexa\Core\MVC\Symfony\Controller\Content\DownloadController::downloadBinaryFileByIdAction }
requirements:
contentId: '\d+'
fieldId: '\d+'

ibexa.content.download:
path: /content/download/{contentId}/{fieldIdentifier}/{filename}
defaults: { _controller: Ibexa\Core\MVC\Symfony\Controller\Content\DownloadController::downloadBinaryFileAction }
requirements:
contentId: '\d+'

# Deprecated since 5.0, will be removed in 6.0. Use ibexa.content.download.field_id.filename instead.
ibexa.content.download.field_id:
path: /content/download/{contentId}/{fieldId}
defaults: { _controller: Ibexa\Core\MVC\Symfony\Controller\Content\DownloadController::downloadBinaryFileByIdAction }
Expand Down
4 changes: 4 additions & 0 deletions src/bundle/Core/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ services:

Ibexa\Core\MVC\Symfony\Controller\Content\DownloadRedirectionController:
class: Ibexa\Core\MVC\Symfony\Controller\Content\DownloadRedirectionController
deprecated:
package: 'ibexa/core'
version: '5.0'
message: 'Since ibexa/core 5.0: The "%service_id%" service is deprecated and will be removed in 6.0. No route references it, use Ibexa\Core\MVC\Symfony\Controller\Content\DownloadController::downloadBinaryFileByIdAction instead'
arguments:
$contentService: '@ibexa.api.service.content'
$router: "@router"
Expand Down
23 changes: 21 additions & 2 deletions src/lib/MVC/Symfony/Controller/Content/DownloadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,23 @@ public function __construct(
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException If the content is trashed, or can't be found.
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException If the user has no access to read content and in case of un-published content: read versions.
*/
public function downloadBinaryFileByIdAction(Request $request, int $contentId, int $fieldId): BinaryStreamResponse
{
public function downloadBinaryFileByIdAction(
Request $request,
int $contentId,
int $fieldId,
?string $filename = null
): BinaryStreamResponse {
if ($filename === null) {
trigger_deprecation(
'ibexa/core',
'5.0',
'The "ibexa.content.download.field_id" route (/content/download/{contentId}/{fieldId}) is deprecated'
. ' and will be removed in 6.0.'
. ' Use the "ibexa.content.download.field_id.filename" route'
. ' (/content/download/{contentId}/{fieldId}/{filename}) instead.'
);
}

$versionNo = $request->query->has('version') ? $request->query->getInt('version') : null;
$language = $request->query->has('inLanguage') ? $request->query->get('inLanguage') : null;

Expand All @@ -65,6 +80,10 @@ public function downloadBinaryFileByIdAction(Request $request, int $contentId, i
throw new NotFoundException('File', $fieldId);
}

if ($filename !== null && $field->value->fileName !== $filename) {
throw new NotFoundException('File', $filename);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it worth to add test for that exception?

}

return $this->downloadBinaryFileAction($contentId, $field->fieldDefIdentifier, $field->value->fileName, $request);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;

/**
* @deprecated since ibexa/core 5.0, will be removed in 6.0. No route has referenced this controller since the
* "ibexa.content.download.field_id" route started serving files directly.
* Use {@see \Ibexa\Core\MVC\Symfony\Controller\Content\DownloadController::downloadBinaryFileByIdAction} instead.
*/
class DownloadRedirectionController extends Controller
{
private ContentService $contentService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@

class ContentDownloadUrlGenerator implements RouteAwarePathGenerator
{
/** @var \Symfony\Component\Routing\RouterInterface */
private $router;
private const string ROUTE = 'ibexa.content.download.field_id.filename';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need that const if use only in one place in this class?


/** @var string */
private $route = 'ibexa.content.download.field_id';
private RouterInterface $router;

public function __construct(RouterInterface $router)
{
Expand All @@ -27,7 +25,10 @@ public function __construct(RouterInterface $router)

public function getStoragePathForField(Field $field, VersionInfo $versionInfo): string
{
return $this->generate($this->route, $this->getParameters($field, $versionInfo));
return $this->generate(
$this->getRoute($field, $versionInfo),
$this->getParameters($field, $versionInfo)
);
}

public function generate(string $route, ?array $parameters = []): string
Expand All @@ -37,7 +38,7 @@ public function generate(string $route, ?array $parameters = []): string

public function getRoute(Field $field, VersionInfo $versionInfo): string
{
return $this->route;
return self::ROUTE;
}

public function getParameters(Field $field, VersionInfo $versionInfo): array
Expand All @@ -46,6 +47,7 @@ public function getParameters(Field $field, VersionInfo $versionInfo): array
'contentId' => $versionInfo->contentInfo->id,
'fieldId' => $field->id,
'version' => $versionInfo->versionNo,
'filename' => $field->value->externalData['fileName'] ?? '',
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Core\MVC\Symfony\FieldType\BinaryBase;

use Ibexa\Contracts\Core\Persistence\Content\ContentInfo;
use Ibexa\Contracts\Core\Persistence\Content\Field;
use Ibexa\Contracts\Core\Persistence\Content\FieldValue;
use Ibexa\Contracts\Core\Persistence\Content\VersionInfo;
use Ibexa\Core\MVC\Symfony\FieldType\BinaryBase\ContentDownloadUrlGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\RouterInterface;

/**
* @covers \Ibexa\Core\MVC\Symfony\FieldType\BinaryBase\ContentDownloadUrlGenerator
*/
final class ContentDownloadUrlGeneratorTest extends TestCase
{
private const string ROUTE = 'ibexa.content.download.field_id.filename';

private RouterInterface & MockObject $router;

private ContentDownloadUrlGenerator $generator;

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

$this->router = $this->createMock(RouterInterface::class);
$this->generator = new ContentDownloadUrlGenerator($this->router);
}

public function testGetRouteReturnsFilenameAwareRoute(): void
{
self::assertSame(
self::ROUTE,
$this->generator->getRoute($this->createField(), $this->createVersionInfo())
);
}

public function testGetParametersContainFilename(): void
{
self::assertSame(
[
'contentId' => 42,
'fieldId' => 7,
'version' => 2,
'filename' => 'Test-file.pdf',
],
$this->generator->getParameters($this->createField(), $this->createVersionInfo())
);
}

public function testGetStoragePathForFieldGeneratesFilenameAwareUrl(): void
{
$this->router
->expects(self::once())
->method('generate')
->with(
self::ROUTE,
[
'contentId' => 42,
'fieldId' => 7,
'version' => 2,
'filename' => 'Test-file.pdf',
]
)
->willReturn('/content/download/42/7/Test-file.pdf?version=2');

self::assertSame(
'/content/download/42/7/Test-file.pdf?version=2',
$this->generator->getStoragePathForField($this->createField(), $this->createVersionInfo())
);
}

private function createField(): Field
{
return new Field([
'id' => 7,
'value' => new FieldValue([
'externalData' => [
'fileName' => 'Test-file.pdf',
],
]),
]);
}

private function createVersionInfo(): VersionInfo
{
return new VersionInfo([
'versionNo' => 2,
'contentInfo' => new ContentInfo([
'id' => 42,
]),
]);
}
}
Loading