Fixed file extension validation for attachments.

This fixes issue #63.
This commit is contained in:
Jan Böhmer 2020-06-01 15:55:34 +02:00
parent f0d0a78f65
commit 1b06203ca6
5 changed files with 61 additions and 154 deletions

View file

@ -46,6 +46,7 @@ use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Form\Type\StructuralEntityType; use App\Form\Type\StructuralEntityType;
use App\Services\Attachments\AttachmentManager; use App\Services\Attachments\AttachmentManager;
use App\Services\Attachments\AttachmentSubmitHandler;
use App\Validator\Constraints\AllowedFileExtension; use App\Validator\Constraints\AllowedFileExtension;
use App\Validator\Constraints\UrlOrBuiltin; use App\Validator\Constraints\UrlOrBuiltin;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@ -53,13 +54,16 @@ use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\Url; use Symfony\Component\Validator\Constraints\Url;
use Symfony\Contracts\Translation\TranslatorInterface;
class AttachmentFormType extends AbstractType class AttachmentFormType extends AbstractType
{ {
@ -67,14 +71,19 @@ class AttachmentFormType extends AbstractType
protected $urlGenerator; protected $urlGenerator;
protected $allow_attachments_download; protected $allow_attachments_download;
protected $security; protected $security;
protected $submitHandler;
protected $translator;
public function __construct(AttachmentManager $attachmentHelper, public function __construct(AttachmentManager $attachmentHelper,
UrlGeneratorInterface $urlGenerator, Security $security, bool $allow_attachments_downloads) UrlGeneratorInterface $urlGenerator, Security $security,
bool $allow_attachments_downloads, AttachmentSubmitHandler $submitHandler, TranslatorInterface $translator)
{ {
$this->attachment_helper = $attachmentHelper; $this->attachment_helper = $attachmentHelper;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->allow_attachments_download = $allow_attachments_downloads; $this->allow_attachments_download = $allow_attachments_downloads;
$this->security = $security; $this->security = $security;
$this->submitHandler = $submitHandler;
$this->translator = $translator;
} }
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
@ -153,13 +162,29 @@ class AttachmentFormType extends AbstractType
'data-show-upload' => 'false', 'data-show-upload' => 'false',
], ],
'constraints' => [ 'constraints' => [
new AllowedFileExtension(), //new AllowedFileExtension(),
new File([ new File([
'maxSize' => $options['max_file_size'], 'maxSize' => $options['max_file_size'],
]), ]),
], ],
]); ]);
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
$form = $event->getForm();
$attachment = $form->getData();
$file_form = $form->get('file');
$file = $file_form->getData();
if ($attachment instanceof Attachment && $file instanceof UploadedFile && $attachment->getAttachmentType()) {
if (!$this->submitHandler->isValidFileExtension($attachment->getAttachmentType(), $file)) {
$event->getForm()->get('file')->addError(
new FormError($this->translator->trans("validator.file_ext_not_allowed"))
);
}
}
});
//Check the secure file checkbox, if file is in securefile location //Check the secure file checkbox, if file is in securefile location
$builder->get('secureFile')->addEventListener( $builder->get('secureFile')->addEventListener(
FormEvents::PRE_SET_DATA, FormEvents::PRE_SET_DATA,

View file

@ -44,6 +44,7 @@ namespace App\Services\Attachments;
use App\Entity\Attachments\Attachment; use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CategoryAttachment;
use App\Entity\Attachments\CurrencyAttachment; use App\Entity\Attachments\CurrencyAttachment;
@ -57,6 +58,7 @@ 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\Exceptions\AttachmentDownloadException;
use Symfony\Component\Form\FormInterface;
use const DIRECTORY_SEPARATOR; use const DIRECTORY_SEPARATOR;
use function get_class; use function get_class;
use InvalidArgumentException; use InvalidArgumentException;
@ -78,15 +80,20 @@ class AttachmentSubmitHandler
protected $allow_attachments_downloads; protected $allow_attachments_downloads;
protected $httpClient; protected $httpClient;
protected $mimeTypes; protected $mimeTypes;
protected $filterTools;
public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads, public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads,
HttpClientInterface $httpClient, MimeTypesInterface $mimeTypes) HttpClientInterface $httpClient, MimeTypesInterface $mimeTypes,
FileTypeFilterTools $filterTools)
{ {
$this->pathResolver = $pathResolver; $this->pathResolver = $pathResolver;
$this->allow_attachments_downloads = $allow_attachments_downloads; $this->allow_attachments_downloads = $allow_attachments_downloads;
$this->httpClient = $httpClient; $this->httpClient = $httpClient;
$this->mimeTypes = $mimeTypes; $this->mimeTypes = $mimeTypes;
$this->filterTools = $filterTools;
//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 = [ $this->folder_mapping = [
PartAttachment::class => 'part', PartAttachment::class => 'part',
@ -104,6 +111,26 @@ class AttachmentSubmitHandler
]; ];
} }
/**
* Check if the extension of the uploaded file is allowed for the given attachment type.
* Returns true, if the file is allowed, false if not.
* @param Attachment $attachment
* @param UploadedFile $uploadedFile
* @return bool
*/
public function isValidFileExtension(AttachmentType $attachment_type, UploadedFile $uploadedFile): bool
{
//Only validate if the attachment type has specified an filetype filter:
if (empty($attachment_type->getFiletypeFilter())) {
return true;
}
return $this->filterTools->isExtensionAllowed(
$attachment_type->getFiletypeFilter(),
$uploadedFile->getClientOriginalExtension()
);
}
/** /**
* Generates a filename for the given attachment and extension. * Generates a filename for the given attachment and extension.
* The filename contains a random id, so every time this function is called you get an unique name. * The filename contains a random id, so every time this function is called you get an unique name.

View file

@ -1,52 +0,0 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 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);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 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\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class AllowedFileExtension extends Constraint
{
}

View file

@ -1,99 +0,0 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 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);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 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\Validator\Constraints;
use App\Entity\Attachments\Attachment;
use App\Services\Attachments\FileTypeFilterTools;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class AllowedFileExtensionValidator extends ConstraintValidator
{
protected $filterTools;
public function __construct(FileTypeFilterTools $filterTools)
{
$this->filterTools = $filterTools;
}
/**
* Checks if the passed value is valid.
*
* @param mixed $value The value that should be validated
* @param Constraint $constraint The constraint for the validation
*/
public function validate($value, Constraint $constraint): void
{
if (! $constraint instanceof AllowedFileExtension) {
throw new UnexpectedTypeException($constraint, AllowedFileExtension::class);
}
if ($value instanceof UploadedFile) {
if ($this->context->getObject() instanceof Attachment) {
/** @var Attachment $attachment */
$attachment = $this->context->getObject();
} elseif ($this->context->getObject() instanceof FormInterface) {
$attachment = $this->context->getObject()->getParent()->getData();
} else {
return;
}
$attachment_type = $attachment->getAttachmentType();
//Only validate if the attachment type has specified an filetype filter:
if (null === $attachment_type || empty($attachment_type->getFiletypeFilter())) {
return;
}
if (! $this->filterTools->isExtensionAllowed(
$attachment_type->getFiletypeFilter(),
$value->getClientOriginalExtension()
)) {
$this->context->buildViolation('validator.file_ext_not_allowed')->addViolation();
}
}
}
}

View file

@ -9219,5 +9219,11 @@ Element 3</target>
<target>Save and clone</target> <target>Save and clone</target>
</segment> </segment>
</unit> </unit>
<unit id="zpqGCO8" name="validator.file_ext_not_allowed">
<segment>
<source>validator.file_ext_not_allowed</source>
<target>File extension not allowed for this attachment type.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>