Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
e1cbfb1
[Composer] Bumped symfony-related packages requirements to ^6.4
alongosz Oct 8, 2024
b762f70
[Composer] Bumped 3rd party requirements for Symfony 6
alongosz Oct 9, 2024
7f0f2cf
[PHPDoc] Improved StorageConnectionFactory annotations for Symfony 6
alongosz Oct 10, 2024
9b5ace9
[Tests] Upgraded ExceptionMessageTemplateFileVisitorTest for nikic/ph…
alongosz Oct 10, 2024
fb2feb7
[Tests] Refactored BaseRenderStrategyTest for Symfony 6
alongosz Oct 10, 2024
19ce4ad
Refactored fragment renderers for Symfony 6
alongosz Oct 21, 2024
5dd4cfb
[Tests] Refactored fragment renderer tests for Symfony 6
alongosz Oct 21, 2024
7014ebf
[Tests] Aligned SerializerStub methods with Symfony 6
alongosz Oct 21, 2024
cb2eb09
Aligned Entity Manager factory with Symfony 6
alongosz Oct 21, 2024
5439234
[Tests] Aligned Entity Manager factory tests with Symfony 6
alongosz Oct 21, 2024
feac688
Aligned SiteAccess-aware Common config parser with Symfony 6
alongosz Oct 21, 2024
84c385a
Aligned JsRouting ExposedRoutesExtractor with Symfony 6
alongosz Oct 21, 2024
85cfffa
Aligned HttpUtils with Symfony 6
alongosz Oct 21, 2024
4a8bb1f
Aligned FieldTypeRegistryPass with Symfony 6
alongosz Oct 21, 2024
ef3bf60
Refactored JMS PHP files visitors to align with php-parser v5
alongosz Oct 30, 2024
b24cd2e
[Tests] Added missing test coverage for JMS PHP file visitors
alongosz Oct 30, 2024
9c260a8
Aligned HiddenLocationException with Symfony 6
alongosz Oct 30, 2024
37cde04
Aligned RepositoryUserAuthenticationSubscriber with Symfony 6
alongosz Oct 30, 2024
53e394c
[Tests] Aligned RepositoryUserAuthenticationSubscriberTest with Symfo…
alongosz Oct 30, 2024
ac082b2
[PHPStan] Added missing extends annotation to APIUserProviderInterface
alongosz Oct 30, 2024
f6f0218
[Security] Dropped obsolete InteractiveLoginToken
alongosz Oct 31, 2024
2539874
[Tests] Aligned StreamFileListenerTest with Symfony 6
alongosz Oct 31, 2024
7d819a3
Extracted duplicated code fragment to RequestContextFactory
alongosz Oct 31, 2024
108df2e
[Tests] Added coverage for RequestContextFactory
alongosz Oct 31, 2024
f9e3f57
Aligned UrlWildcardRouter with Symfony 6
alongosz Oct 31, 2024
425dc57
Aligned DefaultRouter with Symfony 6
alongosz Oct 31, 2024
3b9e881
Aligned URLAlias router with Symfony 6 and improved code quality
alongosz Oct 31, 2024
f1aff42
Improved quality of BinaryStreamResponse and aligned with Symfony 6
alongosz Oct 31, 2024
f848b96
[Security] Aligned UserWrapped with Symfony 6
alongosz Nov 4, 2024
18cf2f9
[Tests] Aligned UserWrappedTest with Symfony 6
alongosz Nov 4, 2024
2098e46
[Cache] Aligned TransactionalInMemoryCacheAdapter with Symfony 6
alongosz Nov 4, 2024
a4a3864
[PHPStan] Added to baseline classes extending final PropertyNormalizer
alongosz Nov 4, 2024
e19a3ab
[Tests] Aligned IOService tests with strict types changes and Symfony 6
alongosz Nov 5, 2024
ad48f8d
Fixed BinaryFile Value members' strict types to reflect usage
alongosz Nov 5, 2024
9848a1c
Changed IOService::loadBinaryFile to throw an exception for abs. paths
alongosz Nov 5, 2024
b5998d4
[Tests] Aligned AliasGeneratorTest with BinaryFile strict requirements
alongosz Nov 5, 2024
c068b13
Aligned Symfony DI Extension implementations with Symfony 6 deprecations
alongosz Nov 6, 2024
85a90b5
[Tests] Improved quality of IbexaRepositoryInstallerBundleTest class
alongosz Nov 6, 2024
c76adde
[PHPUnit] Updated new deprecation counts coming from Symfony 6
alongosz Nov 6, 2024
e2403e9
Aligned Symfony Bundle implementations with Symfony 6 deprecations
alongosz Nov 6, 2024
698da65
[Tests] Dropped obsolete semantic config options from tests setup
alongosz Nov 6, 2024
733d3f4
[Tests] Aligned BinaryBaseStorageTest with BinaryFile strict types
alongosz Nov 6, 2024
44a9b91
Fixed octal values in semantic config for Symfony 6
alongosz Nov 6, 2024
0f35380
[Tests] Improved SiteAccessAwareVisibilityConverterTest strict octal …
alongosz Nov 6, 2024
a59234e
[Tests] Replaced deprecated function utf8_decode with mb_convert_enco…
alongosz Nov 6, 2024
e71ca90
Improved code quality
alongosz Nov 13, 2024
2338ed6
[PHPStan] Removed resolved issues from the baseline
alongosz Nov 8, 2024
c279a67
Aligned StructWrapperValidator with Symfony 6
alongosz Dec 9, 2024
af0e094
Fixed property-read type of Values\Content\Content::@$fields
alongosz Dec 10, 2024
8da2d30
[Tests] Adjusted Symfony deprecation helper config after the changes
alongosz Dec 11, 2024
c299dfa
IBX-8470: Upgraded Pagerfanta to v3 for Symfony 6 (#462)
alongosz Dec 16, 2024
edd7107
[Pagerfanta] Defined int range for Notification total count
alongosz Dec 27, 2024
fe3dd6d
[PHPStan] Aligned baseline after the changes
alongosz Dec 27, 2024
832479c
[Pagerfanta] Defined int range for SearchResult total count
alongosz Dec 27, 2024
280036c
Replaced Param Converters with Value Resolvers
ViniTou Jan 3, 2025
6f2431b
Dropped sensio/framework-extra-bundle dependency
adamwojs Jan 5, 2025
b7395b1
Fixed controller - route syntax to match with new convention
ViniTou Jan 16, 2025
cca6b32
Fixed DirectFragmentRenderer inheritance bug
ViniTou Jan 21, 2025
476f8b1
Updated phpstan baseline
ViniTou Jan 21, 2025
5449499
Moved lazy from service to factory definition - otherwise, it caused …
ViniTou Jan 31, 2025
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
Prev Previous commit
Next Next commit
Improved quality of BinaryStreamResponse and aligned with Symfony 6
  • Loading branch information
alongosz committed Dec 27, 2024
commit f1aff42ed522c847299f72d8b43c6e16d98ac56c
188 changes: 95 additions & 93 deletions src/bundle/IO/BinaryStreamResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@

namespace Ibexa\Bundle\IO;

use DateTime;
use Ibexa\Core\IO\IOServiceInterface;
use Ibexa\Core\IO\Values\BinaryFile;
use LogicException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

Expand All @@ -20,32 +18,31 @@
*/
class BinaryStreamResponse extends Response
{
protected static $trustXSendfileTypeHeader = false;
protected BinaryFile $file;

/** @var \Ibexa\Core\IO\Values\BinaryFile */
protected $file;
protected IOServiceInterface $ioService;

/** @var \Ibexa\Core\IO\IOServiceInterface */
protected $ioService;
protected int $offset;

protected $offset;

protected $maxlen;
protected int $maxlen;

/**
* Constructor.
* @param array<string, array<string>> $headers An array of response headers
* @param bool $public Files are public by default
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @param bool $public Files are public by default
* @param bool $public files are public by default

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, why do we even bother with adding info that is already part of the declaration?

Suggested change
* @param bool $public Files are public by default

*
* @phpstan-param \Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_*|null $contentDisposition The type of Content-Disposition to set automatically with the filename
*
* @param \Ibexa\Core\IO\Values\BinaryFile $binaryFile The name of the file to stream
* @param \Ibexa\Core\IO\IOServiceInterface $ioService The name of the file to stream
* @param int $status The response status code
* @param array $headers An array of response headers
* @param bool $public Files are public by default
* @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param bool $autoEtag Whether the ETag header should be automatically set
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
*/
public function __construct(BinaryFile $binaryFile, IOServiceInterface $ioService, $status = 200, $headers = [], $public = true, $contentDisposition = null, $autoLastModified = true)
{
public function __construct(
BinaryFile $binaryFile,
IOServiceInterface $ioService,
int $status = 200,
array $headers = [],
bool $public = true,
?string $contentDisposition = null,
bool $autoLastModified = true
) {
$this->ioService = $ioService;

parent::__construct(null, $status, $headers);
Expand All @@ -58,60 +55,47 @@ public function __construct(BinaryFile $binaryFile, IOServiceInterface $ioServic
}

/**
* Sets the file to stream.
*
* @param \SplFileInfo|string $file The file to stream
* @param string $contentDisposition
* @param bool $autoEtag
* @param bool $autoLastModified
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* @phpstan-param \Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_*|null $contentDisposition
*/
public function setFile($file, $contentDisposition = null, $autoLastModified = true)
public function setFile(BinaryFile $file, ?string $contentDisposition = null, bool $autoLastModified = true): static
{
$this->file = $file;

if ($autoLastModified) {
$this->setAutoLastModified();
}

if ($contentDisposition) {
if (!empty($contentDisposition)) {
$this->setContentDisposition($contentDisposition);
}

return $this;
}

/**
* Gets the file.
*
* @return \Ibexa\Core\IO\Values\BinaryFile The file to stream
*/
public function getFile()
public function getFile(): BinaryFile
{
return $this->file;
}

/**
* Automatically sets the Last-Modified header according the file modification date.
*/
public function setAutoLastModified()
public function setAutoLastModified(): static
{
$this->setLastModified(DateTime::createFromFormat('U', $this->file->mtime->getTimestamp()));
$this->setLastModified($this->file->mtime);

return $this;
}

/**
* Sets the Content-Disposition header with the given filename.
*
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT
* @phpstan-param \Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_* $disposition
*
* @param string $filename Optionally use this filename instead of the real name of the file
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename
*
* @return BinaryStreamResponse
*/
public function setContentDisposition($disposition, $filename = '', $filenameFallback = '')
public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = ''): BinaryStreamResponse
{
if ($filename === '') {
$filename = $this->file->id;
Expand All @@ -127,12 +111,9 @@ public function setContentDisposition($disposition, $filename = '', $filenameFal
return $this;
}

/**
* {@inheritdoc}
*/
public function prepare(Request $request)
public function prepare(Request $request): static
{
$this->headers->set('Content-Length', $this->file->size);
$this->headers->set('Content-Length', (string)$this->file->size);
$this->headers->set('Accept-Ranges', 'bytes');
$this->headers->set('Content-Transfer-Encoding', 'binary');

Expand All @@ -143,7 +124,7 @@ public function prepare(Request $request)
);
}

if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
if ('HTTP/1.0' !== $request->server->get('SERVER_PROTOCOL')) {
$this->setProtocolVersion('1.1');
}

Expand All @@ -152,77 +133,98 @@ public function prepare(Request $request)
$this->offset = 0;
$this->maxlen = -1;

if ($request->headers->has('Range')) {
// Process the range headers.
if (!$request->headers->has('If-Range') || $this->getEtag() == $request->headers->get('If-Range')) {
$range = $request->headers->get('Range');
$fileSize = $this->file->size;

list($start, $end) = explode('-', substr($range, 6), 2) + [0];

$end = ('' === $end) ? $fileSize - 1 : (int)$end;

if ('' === $start) {
$start = $fileSize - $end;
$end = $fileSize - 1;
} else {
$start = (int)$start;
}

if ($start <= $end) {
if ($start < 0 || $end > $fileSize - 1) {
$this->setStatusCode(Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE); // HTTP_REQUESTED_RANGE_NOT_SATISFIABLE
} elseif ($start !== 0 || $end !== $fileSize - 1) {
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
$this->offset = $start;

$this->setStatusCode(Response::HTTP_PARTIAL_CONTENT); // HTTP_PARTIAL_CONTENT
$this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize));
$this->headers->set('Content-Length', $end - $start + 1);
}
}
}
if ($this->isRangeRequest($request)) {
$this->processRangeRequest($request);
}

return $this;
}

/**
* Sends the file.
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
*/
public function sendContent()
public function sendContent(): static
{
if (!$this->isSuccessful()) {
parent::sendContent();

return;
return $this;
}

if (0 === $this->maxlen) {
return;
}
if ($this->maxlen > 0) {
$out = fopen('php://output', 'wb');
if ($out === false) {
throw new LogicException('Failed to create binary output stream');
}

$in = $this->ioService->getFileInputStream($this->file);
stream_copy_to_stream($in, $out, $this->maxlen, $this->offset);

$out = fopen('php://output', 'wb');
$in = $this->ioService->getFileInputStream($this->file);
stream_copy_to_stream($in, $out, $this->maxlen, $this->offset);
fclose($out);
}

fclose($out);
return $this;
}

/**
* {@inheritdoc}
*
* @throws \LogicException when the content is not null
*/
public function setContent($content)
public function setContent(?string $content): static
{
if (null !== $content) {
throw new LogicException('The content cannot be set on a BinaryStreamResponse instance.');
}

return $this;
}

public function getContent(): false
{
return false;
}

public function getContent()
/**
* @phpstan-assert-if-true !null $request->headers->get('Range')
*/
private function isRangeRequest(Request $request): bool
{
return null;
return $request->headers->has('Range')
&& (
!$request->headers->has('If-Range') || $this->getEtag() === $request->headers->get('If-Range')
);
}

private function processRangeRequest(Request $request): void
{
$range = $request->headers->get('Range');
$fileSize = $this->file->size;

[$start, $end] = explode('-', substr($range, 6), 2) + [0];

$end = ('' === $end) ? $fileSize - 1 : (int)$end;

if ('' === $start) {
$start = $fileSize - $end;
$end = $fileSize - 1;
} else {
$start = (int)$start;
}

if ($start <= $end) {
if ($start < 0 || $end > $fileSize - 1) {
$this->setStatusCode(
Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE
); // HTTP_REQUESTED_RANGE_NOT_SATISFIABLE
} elseif ($start !== 0 || $end !== $fileSize - 1) {
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
$this->offset = $start;

$this->setStatusCode(Response::HTTP_PARTIAL_CONTENT); // HTTP_PARTIAL_CONTENT
$this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize));
$this->headers->set('Content-Length', $end - $start + 1);
}
}
}
}
21 changes: 7 additions & 14 deletions src/lib/IO/Values/BinaryFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@

namespace Ibexa\Core\IO\Values;

use DateTime;
use Ibexa\Contracts\Core\Repository\Values\ValueObject;

/**
* This class provides an abstract access to binary files.
* This class provides abstract access to binary files.
*
* It allows reading & writing of files in a unified way
*
* @property-read string $id The id of the binary file
* @property-read int $mtime File modification time
* @property-read \DateTime $mtime File modification time
* @property-read string $uri HTTP URI to the binary file
* @property-read int $size File size
*/
Expand All @@ -24,29 +25,21 @@ class BinaryFile extends ValueObject
/**
* Unique ID
* Ex: media/images/ibexa-logo/209-1-eng-GB/Ibexa-Logo.gif, or application/2b042138835bb5f48beb9c9df6e86de4.pdf.
*
* @var mixed
*/
protected $id;
protected string $id;

/**
* File size, in bytes.
*
* @var int
*/
protected $size;
protected int $size;

/**
* File modification time.
*
* @var \DateTime
*/
protected $mtime;
protected DateTime $mtime;

/**
* URI to the binary file.
*
* @var string
*/
protected $uri;
protected string $uri;
}