diff --git a/.gitignore b/.gitignore index 00b9cd31..098c0d4f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ .idea/ .idea/* +uploads/* ###> symfony/webpack-encore-bundle ### /node_modules/ /public/build/ diff --git a/composer.json b/composer.json index 94ad1985..0993c383 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "symfony/flex": "^1.1", "symfony/form": "4.3.*", "symfony/framework-bundle": "4.3.*", + "symfony/http-client": "4.3.*", "symfony/monolog-bundle": "^3.1", "symfony/orm-pack": "*", "symfony/process": "4.3.*", diff --git a/composer.lock b/composer.lock index 4a0a5d29..f1e86c2c 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "f4b935dcb469ba97b409fd529352d145", + "content-hash": "3bf8b3cc9288b991bb93c494a7d571a4", "packages": [ { "name": "clue/stream-filter", @@ -4890,6 +4890,125 @@ "homepage": "https://symfony.com", "time": "2019-10-04T17:45:43+00:00" }, + { + "name": "symfony/http-client", + "version": "v4.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "69d438274718121e1166e7f65c290f891a4c8ddb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/69d438274718121e1166e7f65c290f891a4c8ddb", + "reference": "69d438274718121e1166e7f65c290f891a4c8ddb", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "psr/log": "^1.0", + "symfony/http-client-contracts": "^1.1.7", + "symfony/polyfill-php73": "^1.11" + }, + "provide": { + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "1.1" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "psr/http-client": "^1.0", + "symfony/http-kernel": "^4.3", + "symfony/process": "^4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony HttpClient component", + "homepage": "https://symfony.com", + "time": "2019-10-07T10:52:41+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v1.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "353b2a3e907e5c34cf8f74827a4b21eb745aab1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/353b2a3e907e5c34cf8f74827a4b21eb745aab1d", + "reference": "353b2a3e907e5c34cf8f74827a4b21eb745aab1d", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-09-26T22:09:58+00:00" + }, { "name": "symfony/http-foundation", "version": "v4.3.5", diff --git a/config/services.yaml b/config/services.yaml index ca36313f..f8ae3d54 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -12,7 +12,7 @@ parameters: use_gravatar: true # Set to false, if no Gravatar images should be used for user profiles. default_currency: 'EUR' # The currency that should be used media_directory: 'public/media/' # The folder where uploaded attachment files are saved - secure_media_directory: 'media/' # The folder where secured attachment files are saved (must not be in public/) + secure_media_directory: 'uploads/' # The folder where secured attachment files are saved (must not be in public/) db_version_fallback: '5.6' # Be sure to override this, in your .env with your real DB version global_theme: '' # The theme to use globally (see public/build/themes/ for choices). Set to '' for default bootstrap theme allow_attachments_downloads: true # Allow users to download attachments to server @@ -84,6 +84,7 @@ services: App\Services\Attachments\AttachmentSubmitHandler: arguments: $allow_attachments_downloads: '%allow_attachments_downloads%' + $mimeTypes: '@mime_types' App\EventSubscriber\TimezoneListener: diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index abb19091..8d247b62 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -105,7 +105,11 @@ abstract class BaseAdminController extends AbstractController $attachments = $form['attachments']; foreach ($attachments as $attachment) { /** @var $attachment FormInterface */ - $this->attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData()); + $options = [ + 'secure_attachment' => $attachment['secureFile']->getData(), + 'download_url' => $attachment['downloadURL']->getData() + ]; + $this->attachmentSubmitHandler->handleFormSubmit($attachment->getData(), $attachment['file']->getData(), $options); } $em->persist($entity); diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 2897382f..faa01f3b 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -291,6 +291,9 @@ abstract class Attachment extends NamedDBElement */ public function setFilename(?string $new_filename): Attachment { + if ($new_filename === "") { + $new_filename = null; + } $this->original_filename = $new_filename; return $this; } diff --git a/src/Exceptions/AttachmentDownloadException.php b/src/Exceptions/AttachmentDownloadException.php new file mode 100644 index 00000000..9f7a55be --- /dev/null +++ b/src/Exceptions/AttachmentDownloadException.php @@ -0,0 +1,39 @@ + ['class' => 'form-control-sm'], 'label_attr' => ['class' => 'checkbox-custom']]); + $builder->add('secureFile', CheckboxType::class, ['required' => false, + 'label' => $this->trans->trans('attachment.edit.secure_file'), + 'mapped' => false, + 'attr' => ['class' => 'form-control-sm'], + 'label_attr' => ['class' => 'checkbox-custom']]); + $builder->add('file', FileType::class, [ 'label' => $this->trans->trans('attachment.edit.file'), 'mapped' => false, diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index 4ebeb265..b9ec2d03 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -46,10 +46,19 @@ use App\Entity\Attachments\PartAttachment; use App\Entity\Attachments\StorelocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; +use App\Exceptions\AttachmentDownloadException; use App\Services\AttachmentHelper; use Doctrine\Common\Annotations\IndexedReader; +use Nyholm\Psr7\Request; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\Mime\MimeTypeGuesserInterface; +use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Mime\MimeTypesInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; /** * This service handles the form submitting of an attachment and handles things like file uploading and downloading. @@ -60,11 +69,16 @@ class AttachmentSubmitHandler protected $pathResolver; protected $folder_mapping; protected $allow_attachments_downloads; + protected $httpClient; + protected $mimeTypes; - public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads) + public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads, + HttpClientInterface $httpClient, MimeTypesInterface $mimeTypes) { $this->pathResolver = $pathResolver; $this->allow_attachments_downloads = $allow_attachments_downloads; + $this->httpClient = $httpClient; + $this->mimeTypes = $mimeTypes; //The mapping used to determine which folder will be used for an attachment type $this->folder_mapping = [PartAttachment::class => 'part', AttachmentTypeAttachment::class => 'attachment_type', @@ -182,6 +196,7 @@ class AttachmentSubmitHandler * @param Attachment $attachment * @param array $options The options from the handleFormSubmit function * @return Attachment The attachment with the new filepath + * @throws AttachmentDownloadException */ protected function downloadURL(Attachment $attachment, array $options) : Attachment { @@ -189,6 +204,73 @@ class AttachmentSubmitHandler if (!$this->allow_attachments_downloads) { throw new \RuntimeException('Download of attachments is not allowed!'); } + + $url = $attachment->getURL(); + + $fs = new Filesystem(); + $attachment_folder = $this->generateAttachmentPath($attachment, $options['secure_attachment']); + $tmp_path = $attachment_folder . DIRECTORY_SEPARATOR . $this->generateAttachmentFilename($attachment, 'tmp'); + + try { + $response = $this->httpClient->request('GET', $url, [ + 'buffer' => false, + ]); + + if (200 !== $response->getStatusCode()) { + throw new AttachmentDownloadException('Statuscode:' . $response->getStatusCode()); + } + + //Open a temporary file in the attachment folder + $fs->mkdir($attachment_folder); + $fileHandler = fopen($tmp_path, 'wb'); + //Write the downloaded data to file + foreach ($this->httpClient->stream($response) as $chunk) { + fwrite($fileHandler, $chunk->getContent()); + } + fclose($fileHandler); + + //File download should be finished here, so determine the new filename and extension + $headers = $response->getHeaders(); + //Try to determine an filename + $filename = ""; + + //If an content disposition header was set try to extract the filename out of it + if (isset($headers['content-disposition'])) { + $tmp = []; + preg_match('/[^;\\n=]*=([\'\"])*(.*)(?(1)\1|)/', $headers['content-disposition'][0], $tmp); + $filename = $tmp[2]; + } + + //If we dont know filename yet, try to determine it out of url + if ($filename === "") { + $filename = basename(parse_url($url, PHP_URL_PATH)); + } + + //Set original file + $attachment->setFilename($filename); + + //Check if we have a extension given + $pathinfo = pathinfo($filename); + if (!empty($pathinfo['extension'])) { + $new_ext = $pathinfo['extension']; + } else { //Otherwise we have to guess the extension for the new file, based on its content + $new_ext = $this->mimeTypes->getExtensions($this->mimeTypes->guessMimeType($tmp_path))[0] ?? 'tmp'; + } + + //Rename the file to its new name and save path to attachment entity + $new_path = $attachment_folder . DIRECTORY_SEPARATOR . $this->generateAttachmentFilename($attachment, $new_ext); + $fs->rename($tmp_path, $new_path); + + //Make our file path relative to %BASE% + $new_path = $this->pathResolver->realPathToPlaceholder($new_path); + //Save the path to the attachment + $attachment->setPath($new_path); + + } catch (TransportExceptionInterface $exception) { + throw new AttachmentDownloadException('Transport error!'); + } + + return $attachment; } /** @@ -200,7 +282,6 @@ class AttachmentSubmitHandler */ protected function upload(Attachment $attachment, UploadedFile $file, array $options) : Attachment { - //Move our temporay attachment to its final location $file_path = $file->move( $this->generateAttachmentPath($attachment, $options['secure_attachment']), diff --git a/symfony.lock b/symfony.lock index b4d8c321..9f0659f8 100644 --- a/symfony.lock +++ b/symfony.lock @@ -414,6 +414,12 @@ "./src/Kernel.php" ] }, + "symfony/http-client": { + "version": "v4.3.5" + }, + "symfony/http-client-contracts": { + "version": "v1.1.7" + }, "symfony/http-foundation": { "version": "v4.2.3" },