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
44 changes: 43 additions & 1 deletion bundle/Controller/Resource/Upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,19 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;

use function fclose;
use function fopen;
use function fread;
use function fseek;
use function implode;
use function in_array;
use function is_array;
use function is_file;
use function is_readable;
use function strpos;
use function strtolower;

use const SEEK_END;

final class Upload extends AbstractController
{
Expand Down Expand Up @@ -88,9 +98,11 @@ public function __invoke(Request $request): Response

$md5 = $this->fileHashFactory->createHash($file->getRealPath());
$fileStruct = FileStruct::fromUploadedFile($file);

$resourceType = $this->isEncryptedPdf($file) ? 'raw' : 'auto';
$resourceStruct = new ResourceStruct(
$fileStruct,
'auto',
$resourceType,
$folder,
$visibility,
$request->request->get('filename'),
Expand All @@ -113,4 +125,34 @@ public function __invoke(Request $request): Response

return new JsonResponse($this->formatResource($resource), $httpCode);
}

private function isEncryptedPdf(UploadedFile $file): bool
{
if (strtolower($file->getClientOriginalExtension()) !== 'pdf') {
return false;
}

$path = (string) $file->getRealPath();
if ($path === '' || !is_file($path) || !is_readable($path)) {
return false;
}

$fp = @fopen($path, 'r');
if ($fp === false) {
return false;
}

$head = (string) fread($fp, 4096);

@fseek($fp, -16384, SEEK_END);
$tail = (string) fread($fp, 16384);

fclose($fp);

if (strpos($head, '%PDF-') !== 0) {
return false;
}

return strpos($head . $tail, '/Encrypt') !== false;
}
}
99 changes: 95 additions & 4 deletions tests/bundle/Controller/Resource/UploadTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;

use function file_put_contents;
use function json_encode;
use function sys_get_temp_dir;
use function tempnam;
use function unlink;

#[CoversClass(UploadController::class)]
#[CoversClass(AbstractController::class)]
Expand Down Expand Up @@ -78,7 +82,7 @@ public function testUpload(): void
->willReturn('sample_image');

$uploadedFileMock
->expects(self::exactly(2))
->expects(self::exactly(3))
->method('getClientOriginalExtension')
->willReturn('jpg');

Expand Down Expand Up @@ -179,6 +183,93 @@ public function testUpload(): void
);
}

public function testUploadEncryptedPdfIsRaw(): void
{
$tmpPdfPath = (string) tempnam(sys_get_temp_dir(), 'ngrm_encrypted_pdf_');
file_put_contents($tmpPdfPath, "%PDF-1.7\n1 0 obj\n<< /Encrypt 2 0 R >>\nendobj\n");

$request = new Request();
$request->request->add([
'folder' => 'media/document',
]);

$uploadedFileMock = $this->createMock(UploadedFile::class);

$uploadedFileMock
->expects(self::once())
->method('isFile')
->willReturn(true);

// getRealPath is used for md5 hash + FileStruct + encryption detector
$uploadedFileMock
->expects(self::exactly(4))
->method('getRealPath')
->willReturn($tmpPdfPath);

$uploadedFileMock
->expects(self::exactly(2))
->method('getClientOriginalName')
->willReturn('protected.pdf');

// getClientOriginalExtension is used for FileStruct + encryption detector
$uploadedFileMock
->expects(self::exactly(3))
->method('getClientOriginalExtension')
->willReturn('pdf');

$request->files->add([
'file' => $uploadedFileMock,
]);

$this->fileHashFactoryMock
->expects(self::once())
->method('createHash')
->with($tmpPdfPath)
->willReturn('md5hash');

$fileStruct = FileStruct::fromUploadedFile($uploadedFileMock);

$resourceStruct = new ResourceStruct(
$fileStruct,
'raw',
Folder::fromPath('media/document'),
'public',
$request->request->get('filename'),
);

$resource = new RemoteResource(
remoteId: 'upload|raw|media/document/protected.pdf',
type: 'raw',
url: 'https://cloudinary.com/test/upload/raw/media/document/protected.pdf',
md5: 'md5hash',
name: 'protected.pdf',
folder: Folder::fromPath('media/document'),
size: 123,
);

$this->providerMock
->expects(self::once())
->method('upload')
->with($resourceStruct)
->willReturn($resource);

$this->providerMock
->expects(self::exactly(0))
->method('buildVariation')
->willReturnCallback(
static fn () => new RemoteResourceVariation(
$resource,
'https://cloudinary.com/test/variation/url',
),
);

$response = $this->controller->__invoke($request);

self::assertInstanceOf(JsonResponse::class, $response);

unlink($tmpPdfPath);
}

public function testUploadProtectedWithContext(): void
{
$uploadContext = [
Expand Down Expand Up @@ -211,7 +302,7 @@ public function testUploadProtectedWithContext(): void
->willReturn('sample_image');

$uploadedFileMock
->expects(self::exactly(2))
->expects(self::exactly(3))
->method('getClientOriginalExtension')
->willReturn('jpg');

Expand Down Expand Up @@ -396,7 +487,7 @@ public function testUploadExistingFile(): void
->willReturn('sample_image');

$uploadedFileMock
->expects(self::exactly(2))
->expects(self::exactly(3))
->method('getClientOriginalExtension')
->willReturn('jpg');

Expand Down Expand Up @@ -555,7 +646,7 @@ public function testUploadExistingFileName(): void
->willReturn('sample_image');

$uploadedFileMock
->expects(self::exactly(2))
->expects(self::exactly(3))
->method('getClientOriginalExtension')
->willReturn('jpg');

Expand Down
Loading