mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Allow to upload files to attachments by passing a base64 encoded file
This commit is contained in:
parent
0824e11570
commit
0c33059c4e
6 changed files with 261 additions and 32 deletions
|
@ -26,6 +26,7 @@
|
|||
"florianv/swap": "^4.0",
|
||||
"florianv/swap-bundle": "dev-master",
|
||||
"gregwar/captcha-bundle": "^2.1.0",
|
||||
"hshn/base64-encoded-file": "^5.0",
|
||||
"jbtronics/2fa-webauthn": "^v2.2.0",
|
||||
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
||||
"jfcherng/php-diff": "^6.14",
|
||||
|
|
64
composer.lock
generated
64
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6c3d6e309f579d6683344fead9a86d50",
|
||||
"content-hash": "d262b7af88fd38fff57c486ce7f61cbe",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/core",
|
||||
|
@ -2904,6 +2904,68 @@
|
|||
],
|
||||
"time": "2023-12-03T20:05:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "hshn/base64-encoded-file",
|
||||
"version": "v5.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hshn/base64-encoded-file.git",
|
||||
"reference": "54fa81461ba4fbf5b67ed71d22b43ea5cc8c8748"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/hshn/base64-encoded-file/zipball/54fa81461ba4fbf5b67ed71d22b43ea5cc8c8748",
|
||||
"reference": "54fa81461ba4fbf5b67ed71d22b43ea5cc8c8748",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1.0",
|
||||
"symfony/http-foundation": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/mime": "^5.4 || ^6.0 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.0.0",
|
||||
"symfony/config": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/form": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/serializer": "^5.4 || ^6.0 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/config": "to use the bundle in a Symfony project",
|
||||
"symfony/dependency-injection": "to use the bundle in a Symfony project",
|
||||
"symfony/form": "to use base64_encoded_file type",
|
||||
"symfony/http-kernel": "to use the bundle in a Symfony project",
|
||||
"symfony/serializer": "to convert a base64 string to a Base64EncodedFile object"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Hshn\\Base64EncodedFile\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Shota Hoshino",
|
||||
"email": "sht.hshn@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Provides handling base64 encoded files, and the integration of symfony/form",
|
||||
"support": {
|
||||
"issues": "https://github.com/hshn/base64-encoded-file/issues",
|
||||
"source": "https://github.com/hshn/base64-encoded-file/tree/v5.0.1"
|
||||
},
|
||||
"time": "2023-12-24T07:23:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "imagine/imagine",
|
||||
"version": "1.3.5",
|
||||
|
|
69
src/ApiPlatform/HandleAttachmentsUploadsProcessor.php
Normal file
69
src/ApiPlatform/HandleAttachmentsUploadsProcessor.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\ApiPlatform;
|
||||
|
||||
use ApiPlatform\Metadata\DeleteOperationInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Services\Attachments\AttachmentSubmitHandler;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
/**
|
||||
* This state processor handles the upload property set on the deserialized attachment entity and
|
||||
* calls the upload handler service to handle the upload.
|
||||
*/
|
||||
final class HandleAttachmentsUploadsProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||
private readonly ProcessorInterface $persistProcessor,
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')]
|
||||
private readonly ProcessorInterface $removeProcessor,
|
||||
private readonly AttachmentSubmitHandler $attachmentSubmitHandler
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
|
||||
{
|
||||
if ($operation instanceof DeleteOperationInterface) {
|
||||
return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
//Check if the attachment has any upload data we need to handle
|
||||
//This have to happen before the persist processor is called, because the changes on the entity must be saved!
|
||||
if ($data instanceof Attachment && $data->getUpload()) {
|
||||
$upload = $data->getUpload();
|
||||
//Reset the upload data
|
||||
$data->setUpload(null);
|
||||
|
||||
$this->attachmentSubmitHandler->handleFormSubmit($data, $upload);
|
||||
}
|
||||
|
||||
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -35,6 +35,8 @@ use ApiPlatform\Metadata\Post;
|
|||
use App\ApiPlatform\DocumentedAPIProperty;
|
||||
use App\ApiPlatform\Filter\EntityFilter;
|
||||
use App\ApiPlatform\Filter\LikeFilter;
|
||||
use App\ApiPlatform\HandleAttachmentsUploadsProcessor;
|
||||
use App\EntityListeners\AttachmentUploadListener;
|
||||
use App\Repository\AttachmentRepository;
|
||||
use App\EntityListeners\AttachmentDeleteListener;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
@ -68,12 +70,13 @@ use LogicException;
|
|||
operations: [
|
||||
new Get(security: 'is_granted("read", object)'),
|
||||
new GetCollection(security: 'is_granted("@attachments.list_attachments")'),
|
||||
new Post(securityPostDenormalize: 'is_granted("create", object)'),
|
||||
new Post(securityPostDenormalize: 'is_granted("create", object)', ),
|
||||
new Patch(security: 'is_granted("edit", object)'),
|
||||
new Delete(security: 'is_granted("delete", object)'),
|
||||
],
|
||||
normalizationContext: ['groups' => ['attachment:read', 'attachment:read:standalone', 'api:basic:read'], 'openapi_definition_name' => 'Read'],
|
||||
denormalizationContext: ['groups' => ['attachment:write', 'attachment:write:standalone', 'api:basic:write'], 'openapi_definition_name' => 'Write'],
|
||||
processor: HandleAttachmentsUploadsProcessor::class,
|
||||
)]
|
||||
#[DocumentedAPIProperty(schemaName: 'Attachment-Read', property: 'media_url', type: 'string', nullable: true,
|
||||
description: 'The URL to the file, where the attachment file can be downloaded. This can be an internal or external URL.',
|
||||
|
@ -132,6 +135,14 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
*/
|
||||
protected const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class;
|
||||
|
||||
/**
|
||||
* @var AttachmentUpload|null The options used for uploading a file to this attachment or modify it.
|
||||
* This value is not persisted in the database, but is just used to pass options to the upload manager.
|
||||
* If it is null, no upload process is started.
|
||||
*/
|
||||
#[Groups(['attachment:write'])]
|
||||
protected ?AttachmentUpload $upload = null;
|
||||
|
||||
/**
|
||||
* @var string|null the original filename the file had, when the user uploaded it
|
||||
*/
|
||||
|
@ -192,6 +203,31 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the upload currently associated with this attachment.
|
||||
* This is only temporary and not persisted directly in the database.
|
||||
* @internal This function should only be used by the Attachment Submit handler service
|
||||
* @return AttachmentUpload|null
|
||||
*/
|
||||
public function getUpload(): ?AttachmentUpload
|
||||
{
|
||||
return $this->upload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current upload for this attachment.
|
||||
* It will be processed as the attachment is persisted/flushed.
|
||||
* @param AttachmentUpload|null $upload
|
||||
* @return $this
|
||||
*/
|
||||
public function setUpload(?AttachmentUpload $upload): Attachment
|
||||
{
|
||||
$this->upload = $upload;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***********************************************************
|
||||
* Various function
|
||||
***********************************************************/
|
||||
|
|
57
src/Entity/Attachments/AttachmentUpload.php
Normal file
57
src/Entity/Attachments/AttachmentUpload.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Entity\Attachments;
|
||||
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
/**
|
||||
* This is a DTO representing a file upload for an attachment and which is used to pass data to the Attachment
|
||||
* submit handler service.
|
||||
*/
|
||||
class AttachmentUpload
|
||||
{
|
||||
public function __construct(
|
||||
/** @var UploadedFile|null The file which was uploaded, or null if the file should not be changed */
|
||||
public readonly ?UploadedFile $file,
|
||||
/** @var string|null The base64 encoded data of the file which should be uploaded. */
|
||||
#[Groups(['attachment:write'])]
|
||||
public readonly ?string $data = null,
|
||||
/** @vaar string|null The original filename of the file passed in data. */
|
||||
#[Groups(['attachment:write'])]
|
||||
public readonly ?string $filename = null,
|
||||
/** @var bool True, if the URL in the attachment should be downloaded by Part-DB */
|
||||
#[Groups(['attachment:write'])]
|
||||
public readonly bool $downloadUrl = false,
|
||||
/** @var bool If true the file will be moved to private attachment storage,
|
||||
* if false it will be moved to public attachment storage. On null file is not moved
|
||||
*/
|
||||
#[Groups(['attachment:write'])]
|
||||
public readonly ?bool $private = null,
|
||||
/** @var bool If true and no preview image was set yet, the new uploaded file will become the preview image */
|
||||
#[Groups(['attachment:write'])]
|
||||
public readonly ?bool $becomePreviewIfEmpty = true,
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ use App\Entity\Attachments\Attachment;
|
|||
use App\Entity\Attachments\AttachmentContainingDBElement;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\AttachmentTypeAttachment;
|
||||
use App\Entity\Attachments\AttachmentUpload;
|
||||
use App\Entity\Attachments\CategoryAttachment;
|
||||
use App\Entity\Attachments\CurrencyAttachment;
|
||||
use App\Entity\Attachments\LabelAttachment;
|
||||
|
@ -39,6 +40,8 @@ use App\Entity\Attachments\StorageLocationAttachment;
|
|||
use App\Entity\Attachments\SupplierAttachment;
|
||||
use App\Entity\Attachments\UserAttachment;
|
||||
use App\Exceptions\AttachmentDownloadException;
|
||||
use Hshn\Base64EncodedFile\HttpFoundation\File\Base64EncodedFile;
|
||||
use Hshn\Base64EncodedFile\HttpFoundation\File\UploadedBase64EncodedFile;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function get_class;
|
||||
use InvalidArgumentException;
|
||||
|
@ -179,27 +182,39 @@ class AttachmentSubmitHandler
|
|||
* This function will move the uploaded file or download the URL file to server, if needed.
|
||||
*
|
||||
* @param Attachment $attachment the attachment that should be used for handling
|
||||
* @param UploadedFile|null $file If given, that file will be moved to the right location
|
||||
* @param array $options The options to use with the upload. Here you can specify that a URL should be downloaded,
|
||||
* or an file should be moved to a secure location.
|
||||
* @param AttachmentUpload|null $upload The upload options DTO. If it is null, it will be tried to get from the attachment option
|
||||
*
|
||||
* @return Attachment The attachment with the new filename (same instance as passed $attachment)
|
||||
*/
|
||||
public function handleFormSubmit(Attachment $attachment, ?UploadedFile $file, array $options = []): Attachment
|
||||
public function handleFormSubmit(Attachment $attachment, ?AttachmentUpload $upload): Attachment
|
||||
{
|
||||
$resolver = new OptionsResolver();
|
||||
$this->configureOptions($resolver);
|
||||
$options = $resolver->resolve($options);
|
||||
if ($upload === null) {
|
||||
$upload = $attachment->getUpload();
|
||||
if ($upload === null) {
|
||||
throw new InvalidArgumentException('No upload options given and no upload options set in attachment!');
|
||||
}
|
||||
}
|
||||
|
||||
$file = $upload->file;
|
||||
|
||||
//If no file was uploaded, but we have base64 encoded data, create a file from it
|
||||
if (!$file && $upload->data !== null) {
|
||||
$file = new UploadedBase64EncodedFile(new Base64EncodedFile($upload->data), $upload->filename ?? 'base64');
|
||||
}
|
||||
|
||||
//By default we assume a public upload
|
||||
$secure_attachment = $upload->private ?? false;
|
||||
|
||||
//When a file is given then upload it, otherwise check if we need to download the URL
|
||||
if ($file instanceof UploadedFile) {
|
||||
$this->upload($attachment, $file, $options);
|
||||
} elseif ($options['download_url'] && $attachment->isExternal()) {
|
||||
$this->downloadURL($attachment, $options);
|
||||
|
||||
$this->upload($attachment, $file, $secure_attachment);
|
||||
} elseif ($upload->downloadUrl && $attachment->isExternal()) {
|
||||
$this->downloadURL($attachment, $secure_attachment);
|
||||
}
|
||||
|
||||
//Move the attachment files to secure location (and back) if needed
|
||||
$this->moveFile($attachment, $options['secure_attachment']);
|
||||
$this->moveFile($attachment, $secure_attachment);
|
||||
|
||||
//Rename blacklisted (unsecure) files to a better extension
|
||||
$this->renameBlacklistedExtensions($attachment);
|
||||
|
@ -208,7 +223,7 @@ class AttachmentSubmitHandler
|
|||
$element = $attachment->getElement();
|
||||
if ($element instanceof AttachmentContainingDBElement) {
|
||||
//Make this attachment the master picture if needed and this was requested
|
||||
if ($options['become_preview_if_empty']
|
||||
if ($upload->becomePreviewIfEmpty
|
||||
&& $element->getMasterPictureAttachment() === null //Element must not have an preview image yet
|
||||
&& null === $attachment->getID() //Attachment must be null
|
||||
&& $attachment->isPicture() //Attachment must be a picture
|
||||
|
@ -261,17 +276,6 @@ class AttachmentSubmitHandler
|
|||
return $attachment;
|
||||
}
|
||||
|
||||
protected function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
//If no preview image was set yet, the new uploaded file will become the preview image
|
||||
'become_preview_if_empty' => true,
|
||||
//When a URL is given download the URL
|
||||
'download_url' => false,
|
||||
'secure_attachment' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given attachment to secure location (or back to public folder) if needed.
|
||||
*
|
||||
|
@ -325,11 +329,11 @@ class AttachmentSubmitHandler
|
|||
/**
|
||||
* Download the URL set in the attachment and save it on the server.
|
||||
*
|
||||
* @param array $options The options from the handleFormSubmit function
|
||||
* @param bool $secureAttachment True if the file should be moved to the secure attachment storage
|
||||
*
|
||||
* @return Attachment The attachment with the new filepath
|
||||
*/
|
||||
protected function downloadURL(Attachment $attachment, array $options): Attachment
|
||||
protected function downloadURL(Attachment $attachment, array $secureAttachment): Attachment
|
||||
{
|
||||
//Check if we are allowed to download files
|
||||
if (!$this->allow_attachments_downloads) {
|
||||
|
@ -339,7 +343,7 @@ class AttachmentSubmitHandler
|
|||
$url = $attachment->getURL();
|
||||
|
||||
$fs = new Filesystem();
|
||||
$attachment_folder = $this->generateAttachmentPath($attachment, $options['secure_attachment']);
|
||||
$attachment_folder = $this->generateAttachmentPath($attachment, $secureAttachment);
|
||||
$tmp_path = $attachment_folder.DIRECTORY_SEPARATOR.$this->generateAttachmentFilename($attachment, 'tmp');
|
||||
|
||||
try {
|
||||
|
@ -408,15 +412,15 @@ class AttachmentSubmitHandler
|
|||
*
|
||||
* @param Attachment $attachment The attachment in which the file should be saved
|
||||
* @param UploadedFile $file The file which was uploaded
|
||||
* @param array $options The options from the handleFormSubmit function
|
||||
* @param bool $secureAttachment True if the file should be moved to the secure attachment storage
|
||||
*
|
||||
* @return Attachment The attachment with the new filepath
|
||||
*/
|
||||
protected function upload(Attachment $attachment, UploadedFile $file, array $options): Attachment
|
||||
protected function upload(Attachment $attachment, UploadedFile $file, bool $secureAttachment): Attachment
|
||||
{
|
||||
//Move our temporay attachment to its final location
|
||||
//Move our temporary attachment to its final location
|
||||
$file_path = $file->move(
|
||||
$this->generateAttachmentPath($attachment, $options['secure_attachment']),
|
||||
$this->generateAttachmentPath($attachment, $secureAttachment),
|
||||
$this->generateAttachmentFilename($attachment, $file->getClientOriginalExtension())
|
||||
)->getRealPath();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue