diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 49f1ac28..4c4f9840 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -284,29 +284,34 @@ final class InvoiceController extends AbstractController */ public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository) { - $dir = $documentRepository->getCustomInvoiceDirectory(); - $invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir; + $dir = $documentRepository->getUploadDirectory(); + $invoiceDir = $dir; + + // do not execute realpath, as it will return an empty string if the invoice directory is NOT existing! + if ($invoiceDir[0] !== '/') { + $invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir; + } + $canUpload = true; - $form = null; if (!file_exists($invoiceDir)) { - @mkdir($invoiceDir); + @mkdir($invoiceDir, 0777); } - if (!file_exists($invoiceDir)) { - $this->flashError(sprintf('Invoice directory is not existing and could not be created: %s', $dir)); - $canUpload = false; - } - if (!is_writable($invoiceDir)) { - $this->flashError(sprintf('Invoice directory cannot be written: %s', $dir)); + + if (!is_dir($invoiceDir)) { + $this->flashError(sprintf('Invoice directory "%s" is not existing and could not be created.', $dir)); + $canUpload = false; + } elseif (!is_writable($invoiceDir)) { + $this->flashError(sprintf('Invoice directory "%s" cannot be written.', $dir)); $canUpload = false; } + $form = $this->createForm(InvoiceDocumentUploadForm::class, null, [ + 'action' => $this->generateUrl('admin_invoice_document_upload', []), + 'method' => 'POST' + ]); + if ($canUpload) { - $form = $this->createForm(InvoiceDocumentUploadForm::class, null, [ - 'action' => $this->generateUrl('admin_invoice_document_upload', []), - 'method' => 'POST' - ]); - $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { @@ -334,8 +339,8 @@ final class InvoiceController extends AbstractController } return $this->render('invoice/document_upload.html.twig', [ - 'form' => (null !== $form) ? $form->createView() : null, - 'documents' => $this->service->getDocuments(), + 'form' => $form->createView(), + 'documents' => $this->service->getDocuments(true), 'baseDirectory' => $projectDirectory . DIRECTORY_SEPARATOR, ]); } diff --git a/src/DataFixtures/CustomerFixtures.php b/src/DataFixtures/CustomerFixtures.php index 291ffab2..b9fab31a 100644 --- a/src/DataFixtures/CustomerFixtures.php +++ b/src/DataFixtures/CustomerFixtures.php @@ -13,7 +13,7 @@ use App\Entity\Activity; use App\Entity\Customer; use App\Entity\Project; use Doctrine\Bundle\FixturesBundle\Fixture; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Faker\Factory; use Faker\Generator; diff --git a/src/DataFixtures/InvoiceFixtures.php b/src/DataFixtures/InvoiceFixtures.php index dee25b10..9c662e92 100644 --- a/src/DataFixtures/InvoiceFixtures.php +++ b/src/DataFixtures/InvoiceFixtures.php @@ -11,7 +11,7 @@ namespace App\DataFixtures; use App\Entity\InvoiceTemplate; use Doctrine\Bundle\FixturesBundle\Fixture; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Faker\Factory; use Faker\Generator; @@ -86,6 +86,7 @@ class InvoiceFixtures extends Fixture // name, title, renderer, calculator, numberGenerator, company, vat, dueDays, address, paymentTerms return [ + ['Invoice (PDF)', 'Invoice', 'default-pdf', 'default', 'default', $faker->company, 16, 10, $paymentTerms], ['Invoice (HTML)', 'Company name', 'default', 'default', 'default', $faker->company, 19, 30, $paymentTerms], ['Freelancer (HTML, short)', 'Invoice', 'freelancer', 'short', 'default', $faker->company, 19, 14, $paymentTerms_de], ['Timesheet (HTML)', 'Timesheet', 'timesheet', 'default', 'default', $faker->company, 19, 7, $paymentTerms_alt], diff --git a/src/DataFixtures/TagFixtures.php b/src/DataFixtures/TagFixtures.php index ad281ab6..49c342ad 100644 --- a/src/DataFixtures/TagFixtures.php +++ b/src/DataFixtures/TagFixtures.php @@ -11,7 +11,7 @@ namespace App\DataFixtures; use App\Entity\Tag; use Doctrine\Bundle\FixturesBundle\Fixture; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** diff --git a/src/DataFixtures/TeamFixtures.php b/src/DataFixtures/TeamFixtures.php index 05bbe7c5..77486345 100644 --- a/src/DataFixtures/TeamFixtures.php +++ b/src/DataFixtures/TeamFixtures.php @@ -14,7 +14,7 @@ use App\Entity\Team; use App\Entity\User; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\DependentFixtureInterface; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** diff --git a/src/DataFixtures/TimesheetFixtures.php b/src/DataFixtures/TimesheetFixtures.php index 215cdd68..4d348801 100644 --- a/src/DataFixtures/TimesheetFixtures.php +++ b/src/DataFixtures/TimesheetFixtures.php @@ -18,7 +18,7 @@ use App\Entity\UserPreference; use App\Timesheet\Util; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\DataFixtures\DependentFixtureInterface; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Faker\Factory; /** diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index cdc92e78..a1a83652 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -12,7 +12,7 @@ namespace App\DataFixtures; use App\Entity\User; use App\Entity\UserPreference; use Doctrine\Bundle\FixturesBundle\Fixture; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Faker\Factory; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 1705a6eb..9f7aa021 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -11,6 +11,7 @@ namespace App\DependencyInjection; use App\Entity\Customer; use App\Entity\User; +use App\Repository\InvoiceDocumentRepository; use App\Timesheet\Rounding\RoundingInterface; use App\Widget\Type\CompoundRow; use App\Widget\Type\Counter; @@ -215,7 +216,7 @@ class Configuration implements ConfigurationInterface ->scalarPrototype()->end() ->defaultValue([ 'var/invoices/', - 'templates/invoice/renderer/' + InvoiceDocumentRepository::DEFAULT_DIRECTORY ]) ->end() ->arrayNode('documents') diff --git a/src/Form/InvoiceDocumentUploadForm.php b/src/Form/InvoiceDocumentUploadForm.php index e3b65f52..001f0c03 100644 --- a/src/Form/InvoiceDocumentUploadForm.php +++ b/src/Form/InvoiceDocumentUploadForm.php @@ -9,14 +9,28 @@ namespace App\Form; +use App\Repository\InvoiceDocumentRepository; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\File; +use Symfony\Component\Validator\Context\ExecutionContextInterface; class InvoiceDocumentUploadForm extends AbstractType { + /** + * @var InvoiceDocumentRepository + */ + private $repository; + + public function __construct(InvoiceDocumentRepository $repository) + { + $this->repository = $repository; + } + /** * {@inheritdoc} */ @@ -37,12 +51,33 @@ class InvoiceDocumentUploadForm extends AbstractType 'application/vnd.oasis.opendocument.spreadsheet', ], 'mimeTypesMessage' => 'This file type is not allowed', - ]) + ]), + new Callback([$this, 'validateDocument']) ], ]) ; } + public function validateDocument($value, ExecutionContextInterface $context) + { + if (!($value instanceof UploadedFile)) { + return; + } + + $name = $value->getClientOriginalName(); + + foreach ($this->repository->findBuiltIn() as $document) { + if ($document->getName() !== $name) { + continue; + } + + $context->buildViolation('This invoice document cannot be used, please rename the file and upload it again.') + ->setTranslationDomain('validators') + ->setCode('kimai-invoice-document-upload-01') + ->addViolation(); + } + } + /** * {@inheritdoc} */ diff --git a/src/Invoice/ServiceInvoice.php b/src/Invoice/ServiceInvoice.php index cbc32cca..346eb120 100644 --- a/src/Invoice/ServiceInvoice.php +++ b/src/Invoice/ServiceInvoice.php @@ -134,10 +134,15 @@ final class ServiceInvoice /** * Returns an array of invoice renderer, which will consist of a unique name and a controller action. * + * @param bool $customOnly * @return InvoiceDocument[] */ - public function getDocuments(): array + public function getDocuments(bool $customOnly = false): array { + if ($customOnly) { + return $this->documents->findCustom(); + } + return $this->documents->findAll(); } diff --git a/src/Repository/InvoiceDocumentRepository.php b/src/Repository/InvoiceDocumentRepository.php index f72fdd41..638b3bb9 100644 --- a/src/Repository/InvoiceDocumentRepository.php +++ b/src/Repository/InvoiceDocumentRepository.php @@ -14,6 +14,8 @@ use Symfony\Component\Finder\Finder; final class InvoiceDocumentRepository { + public const DEFAULT_DIRECTORY = 'templates/invoice/renderer/'; + /** * @var array */ @@ -21,12 +23,48 @@ final class InvoiceDocumentRepository public function __construct(array $directories) { - $this->documentDirs = $directories; + foreach ($directories as $directory) { + $this->addDirectory($directory); + } } + public function addDirectory(string $directory) + { + $this->documentDirs[] = $directory; + + return $this; + } + + public function removeDirectory(string $directory) + { + if (($key = array_search($directory, $this->documentDirs)) !== false) { + unset($this->documentDirs[$key]); + } + + return $this; + } + + /** + * @deprecated since 1.10 - will be removed with 2.0 - use getCustomInvoiceDirectory() instead + */ public function getCustomInvoiceDirectory(): string { - return $this->documentDirs[0]; + return $this->getUploadDirectory(); + } + + public function getUploadDirectory(): string + { + // reverse the array, as bundles can register invoice directories a well (as prepend extensions) + // and then the first entries are the directories from the bundles and not the default ones registered in Kimai + foreach (array_reverse($this->documentDirs) as $dir) { + if ($dir === self::DEFAULT_DIRECTORY) { + continue; + } + + return $dir; + } + + throw new \Exception('Unknown upload directory'); } public function findByName(string $name): ?InvoiceDocument @@ -41,21 +79,71 @@ final class InvoiceDocumentRepository } /** - * Returns an array of invoice renderer, which will consist of a unique name and a controller action. + * Returns an array of all custom invoice documents. + * + * @return InvoiceDocument[] + */ + public function findCustom() + { + $paths = []; + foreach ($this->documentDirs as $dir) { + if ($dir === self::DEFAULT_DIRECTORY) { + continue; + } + $paths[] = $dir; + } + + return $this->findByPaths($paths); + } + + /** + * Returns an array of all original Kimai documents. + * + * @return InvoiceDocument[] + */ + public function findBuiltIn() + { + foreach ($this->documentDirs as $dir) { + if ($dir === self::DEFAULT_DIRECTORY) { + return $this->findByPaths([$dir]); + } + } + + return []; + } + + /** + * Returns an array of invoice documents. * * @return InvoiceDocument[] */ public function findAll() + { + return $this->findByPaths($this->documentDirs); + } + + /** + * Returns an array of invoice documents. + * + * @return InvoiceDocument[] + */ + private function findByPaths(array $paths) { $base = \dirname(\dirname(__DIR__)) . DIRECTORY_SEPARATOR; $documents = []; - foreach ($this->documentDirs as $searchPath) { - if (!is_dir($base . $searchPath)) { + foreach ($paths as $searchPath) { + $searchDir = $searchPath; + if ($searchDir[0] !== '/') { + $searchDir = $base . $searchPath; + } + + if (!is_dir($searchDir)) { continue; } - $finder = Finder::create()->ignoreDotFiles(true)->files()->in($base . $searchPath)->name('*.*'); + + $finder = Finder::create()->ignoreDotFiles(true)->files()->in($searchDir)->name('*.*'); foreach ($finder->getIterator() as $file) { $doc = new InvoiceDocument($file); // the first found invoice document wins diff --git a/templates/invoice/document_upload.html.twig b/templates/invoice/document_upload.html.twig index 60154100..1358ce25 100644 --- a/templates/invoice/document_upload.html.twig +++ b/templates/invoice/document_upload.html.twig @@ -36,9 +36,9 @@
{% for document in documents %}