mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
Added an option to download a attachment via URL. Implemented secure attachments, which can not be accessed by URL and requires authentication.
This commit is contained in:
parent
d382021fee
commit
27a001b1d2
10 changed files with 266 additions and 5 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.idea/*
|
.idea/*
|
||||||
|
uploads/*
|
||||||
###> symfony/webpack-encore-bundle ###
|
###> symfony/webpack-encore-bundle ###
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/public/build/
|
/public/build/
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"symfony/flex": "^1.1",
|
"symfony/flex": "^1.1",
|
||||||
"symfony/form": "4.3.*",
|
"symfony/form": "4.3.*",
|
||||||
"symfony/framework-bundle": "4.3.*",
|
"symfony/framework-bundle": "4.3.*",
|
||||||
|
"symfony/http-client": "4.3.*",
|
||||||
"symfony/monolog-bundle": "^3.1",
|
"symfony/monolog-bundle": "^3.1",
|
||||||
"symfony/orm-pack": "*",
|
"symfony/orm-pack": "*",
|
||||||
"symfony/process": "4.3.*",
|
"symfony/process": "4.3.*",
|
||||||
|
|
121
composer.lock
generated
121
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "f4b935dcb469ba97b409fd529352d145",
|
"content-hash": "3bf8b3cc9288b991bb93c494a7d571a4",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "clue/stream-filter",
|
"name": "clue/stream-filter",
|
||||||
|
@ -4890,6 +4890,125 @@
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"time": "2019-10-04T17:45:43+00:00"
|
"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",
|
"name": "symfony/http-foundation",
|
||||||
"version": "v4.3.5",
|
"version": "v4.3.5",
|
||||||
|
|
|
@ -12,7 +12,7 @@ parameters:
|
||||||
use_gravatar: true # Set to false, if no Gravatar images should be used for user profiles.
|
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
|
default_currency: 'EUR' # The currency that should be used
|
||||||
media_directory: 'public/media/' # The folder where uploaded attachment files are saved
|
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
|
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
|
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
|
allow_attachments_downloads: true # Allow users to download attachments to server
|
||||||
|
@ -84,6 +84,7 @@ services:
|
||||||
App\Services\Attachments\AttachmentSubmitHandler:
|
App\Services\Attachments\AttachmentSubmitHandler:
|
||||||
arguments:
|
arguments:
|
||||||
$allow_attachments_downloads: '%allow_attachments_downloads%'
|
$allow_attachments_downloads: '%allow_attachments_downloads%'
|
||||||
|
$mimeTypes: '@mime_types'
|
||||||
|
|
||||||
|
|
||||||
App\EventSubscriber\TimezoneListener:
|
App\EventSubscriber\TimezoneListener:
|
||||||
|
|
|
@ -105,7 +105,11 @@ abstract class BaseAdminController extends AbstractController
|
||||||
$attachments = $form['attachments'];
|
$attachments = $form['attachments'];
|
||||||
foreach ($attachments as $attachment) {
|
foreach ($attachments as $attachment) {
|
||||||
/** @var $attachment FormInterface */
|
/** @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);
|
$em->persist($entity);
|
||||||
|
|
|
@ -291,6 +291,9 @@ abstract class Attachment extends NamedDBElement
|
||||||
*/
|
*/
|
||||||
public function setFilename(?string $new_filename): Attachment
|
public function setFilename(?string $new_filename): Attachment
|
||||||
{
|
{
|
||||||
|
if ($new_filename === "") {
|
||||||
|
$new_filename = null;
|
||||||
|
}
|
||||||
$this->original_filename = $new_filename;
|
$this->original_filename = $new_filename;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
39
src/Exceptions/AttachmentDownloadException.php
Normal file
39
src/Exceptions/AttachmentDownloadException.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* part-db version 0.1
|
||||||
|
* Copyright (C) 2005 Christoph Lechner
|
||||||
|
* http://www.cl-projects.de/
|
||||||
|
*
|
||||||
|
* part-db version 0.2+
|
||||||
|
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
|
||||||
|
* http://code.google.com/p/part-db/
|
||||||
|
*
|
||||||
|
* Part-DB Version 0.4+
|
||||||
|
* Copyright (C) 2016 - 2019 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 General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* 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 General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class AttachmentDownloadException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
|
@ -104,6 +104,12 @@ class AttachmentFormType extends AbstractType
|
||||||
'attr' => ['class' => 'form-control-sm'],
|
'attr' => ['class' => 'form-control-sm'],
|
||||||
'label_attr' => ['class' => 'checkbox-custom']]);
|
'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, [
|
$builder->add('file', FileType::class, [
|
||||||
'label' => $this->trans->trans('attachment.edit.file'),
|
'label' => $this->trans->trans('attachment.edit.file'),
|
||||||
'mapped' => false,
|
'mapped' => false,
|
||||||
|
|
|
@ -46,10 +46,19 @@ use App\Entity\Attachments\PartAttachment;
|
||||||
use App\Entity\Attachments\StorelocationAttachment;
|
use App\Entity\Attachments\StorelocationAttachment;
|
||||||
use App\Entity\Attachments\SupplierAttachment;
|
use App\Entity\Attachments\SupplierAttachment;
|
||||||
use App\Entity\Attachments\UserAttachment;
|
use App\Entity\Attachments\UserAttachment;
|
||||||
|
use App\Exceptions\AttachmentDownloadException;
|
||||||
use App\Services\AttachmentHelper;
|
use App\Services\AttachmentHelper;
|
||||||
use Doctrine\Common\Annotations\IndexedReader;
|
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\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\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.
|
* 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 $pathResolver;
|
||||||
protected $folder_mapping;
|
protected $folder_mapping;
|
||||||
protected $allow_attachments_downloads;
|
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->pathResolver = $pathResolver;
|
||||||
$this->allow_attachments_downloads = $allow_attachments_downloads;
|
$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
|
//The mapping used to determine which folder will be used for an attachment type
|
||||||
$this->folder_mapping = [PartAttachment::class => 'part', AttachmentTypeAttachment::class => 'attachment_type',
|
$this->folder_mapping = [PartAttachment::class => 'part', AttachmentTypeAttachment::class => 'attachment_type',
|
||||||
|
@ -182,6 +196,7 @@ class AttachmentSubmitHandler
|
||||||
* @param Attachment $attachment
|
* @param Attachment $attachment
|
||||||
* @param array $options The options from the handleFormSubmit function
|
* @param array $options The options from the handleFormSubmit function
|
||||||
* @return Attachment The attachment with the new filepath
|
* @return Attachment The attachment with the new filepath
|
||||||
|
* @throws AttachmentDownloadException
|
||||||
*/
|
*/
|
||||||
protected function downloadURL(Attachment $attachment, array $options) : Attachment
|
protected function downloadURL(Attachment $attachment, array $options) : Attachment
|
||||||
{
|
{
|
||||||
|
@ -189,6 +204,73 @@ class AttachmentSubmitHandler
|
||||||
if (!$this->allow_attachments_downloads) {
|
if (!$this->allow_attachments_downloads) {
|
||||||
throw new \RuntimeException('Download of attachments is not allowed!');
|
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
|
protected function upload(Attachment $attachment, UploadedFile $file, array $options) : Attachment
|
||||||
{
|
{
|
||||||
|
|
||||||
//Move our temporay attachment to its final location
|
//Move our temporay attachment to its final location
|
||||||
$file_path = $file->move(
|
$file_path = $file->move(
|
||||||
$this->generateAttachmentPath($attachment, $options['secure_attachment']),
|
$this->generateAttachmentPath($attachment, $options['secure_attachment']),
|
||||||
|
|
|
@ -414,6 +414,12 @@
|
||||||
"./src/Kernel.php"
|
"./src/Kernel.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"symfony/http-client": {
|
||||||
|
"version": "v4.3.5"
|
||||||
|
},
|
||||||
|
"symfony/http-client-contracts": {
|
||||||
|
"version": "v1.1.7"
|
||||||
|
},
|
||||||
"symfony/http-foundation": {
|
"symfony/http-foundation": {
|
||||||
"version": "v4.2.3"
|
"version": "v4.2.3"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue