Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple file upload widgets in form summary step #83

Merged
merged 1 commit into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"require": {
"php": "^8.1",
"contao/core-bundle": "^4.13 || ^5.0.8",
"codefog/contao-haste": "^5.0",
"codefog/contao-haste": "^5.2",
"symfony/filesystem": "^5.4 || ^6.0",
"symfony/var-dumper": "^5.4 || ^6.0"
},
Expand Down
2 changes: 2 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Codefog\HasteBundle\FileUploadNormalizer;
use Codefog\HasteBundle\UrlParser;
use Terminal42\MultipageFormsBundle\Controller\FrontendModule\StepsController;
use Terminal42\MultipageFormsBundle\EventListener\CompileFormFieldsListener;
Expand Down Expand Up @@ -57,6 +58,7 @@
->args([
service(FormManagerFactoryInterface::class),
service('request_stack'),
service(FileUploadNormalizer::class),
])
;

Expand Down
4 changes: 2 additions & 2 deletions src/EventListener/CompileFormFieldsListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ public function __invoke(array $formFields, string $formId, Form $form): array
// here! The problem with storing $_FILES across requests is that we would need to move
// it from its tmp_name as PHP deletes files automatically after the request has
// finished. We could indeed move them here but if we did at this stage the form fields
// themselves would later not be able to move them to their own desired place. So
// we cannot store any file information at this stage.
// themselves would later not be able to move them to their own desired place. So we
// cannot store any file information at this stage.
if ($_POST) {
$stepData = $stepData->withOriginalPostData(new ParameterBag($_POST));
$manager->storeStepData($stepData);
Expand Down
10 changes: 6 additions & 4 deletions src/EventListener/PrepareFomDataListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Terminal42\MultipageFormsBundle\EventListener;

use Codefog\HasteBundle\FileUploadNormalizer;
use Contao\CoreBundle\ContaoCoreBundle;
use Contao\CoreBundle\DependencyInjection\Attribute\AsHook;
use Contao\Form;
Expand All @@ -18,7 +19,8 @@ class PrepareFomDataListener
{
public function __construct(
private readonly FormManagerFactoryInterface $formManagerFactory,
private readonly RequestStack $reqestStack,
private readonly RequestStack $requestStack,
private readonly FileUploadNormalizer $fileUploadNormalizer,
) {
}

Expand Down Expand Up @@ -83,12 +85,12 @@ public function __invoke(array &$submitted, array &$labels, array $fields, Form
private function getUploadedFiles($hook = []): FileParameterBag
{
// Contao 5
if (0 !== (is_countable($hook) ? \count($hook) : 0)) {
return new FileParameterBag($hook);
if (\is_array($hook) && [] !== $hook) {
return new FileParameterBag($this->fileUploadNormalizer->normalize($hook));
}

// Contao 4.13
$request = $this->reqestStack->getCurrentRequest();
$request = $this->requestStack->getCurrentRequest();

if (null === $request) {
return new FileParameterBag();
Expand Down
32 changes: 23 additions & 9 deletions src/Step/FileParameterBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

use Symfony\Component\Filesystem\Filesystem;

/**
* This class expects $parameters to be in the format of.
*
* array<string, array<array{name: string, type: string, tmp_name: string, error:
* int, size: int, uploaded: bool, uuid: ?string, stream: ?resource}>>
*
* as provided by the FileUploadNormalizer service. Meaning that every file upload
* can contain multiple files.
*/
class FileParameterBag extends ParameterBag
{
/**
Expand All @@ -16,15 +25,20 @@ class FileParameterBag extends ParameterBag
*/
public function set(string $name, mixed $value): self
{
if (
\is_array($value)
&& \array_key_exists('tmp_name', $value)
&& \is_string($value['tmp_name'])
&& is_uploaded_file($value['tmp_name'])
) {
$target = (new Filesystem())->tempnam(sys_get_temp_dir(), 'nc');
move_uploaded_file($value['tmp_name'], $target);
$value['tmp_name'] = $target;
if (!\is_array($value)) {
throw new \InvalidArgumentException('$value must be an array normalized by the FileUploadNormalizer service.');
}

foreach ($value as $k => $upload) {
if (!\is_array($upload) && !\array_key_exists('tmp_name', $upload)) {
throw new \InvalidArgumentException('$value must be an array normalized by the FileUploadNormalizer service.');
}

if (is_uploaded_file($upload['tmp_name'])) {
$target = (new Filesystem())->tempnam(sys_get_temp_dir(), 'nc');
move_uploaded_file($upload['tmp_name'], $target);
$value[$k]['tmp_name'] = $target;
}
}

return parent::set($name, $value);
Expand Down
8 changes: 4 additions & 4 deletions src/Step/StepData.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ private function __construct(
private readonly int $step,
private ParameterBag $submitted,
private ParameterBag $labels,
private ParameterBag $files,
private FileParameterBag $files,
private ParameterBag $originalPostData,
) {
}
Expand Down Expand Up @@ -39,7 +39,7 @@ public function getLabels(): ParameterBag
return $this->labels;
}

public function getFiles(): ParameterBag
public function getFiles(): FileParameterBag
{
return $this->files;
}
Expand Down Expand Up @@ -73,7 +73,7 @@ public function withSubmitted(ParameterBag $submitted): self
return $clone;
}

public function withFiles(ParameterBag $files): self
public function withFiles(FileParameterBag $files): self
{
$clone = clone $this;
$clone->files = $files;
Expand All @@ -87,7 +87,7 @@ public static function create(int $step): self
$step,
new ParameterBag(),
new ParameterBag(),
new ParameterBag(),
new FileParameterBag(),
new ParameterBag(),
);
}
Expand Down
120 changes: 74 additions & 46 deletions src/Widget/Placeholder.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ public function generate(): void
private function generateTokens(): array
{
$tokens = [];
$fileTokens = [];
$summaryTokens = [];

/** @var FormManagerFactoryInterface $factory */
Expand All @@ -91,60 +90,47 @@ private function generateTokens(): array
/** @var StringParser $stringParser */
$stringParser = System::getContainer()->get(StringParser::class);

/** @var UrlParser $urlParser */
$urlParser = System::getContainer()->get(UrlParser::class);

$manager = $factory->forFormId((int) $this->pid);

$stepsCollection = $manager->getDataOfAllSteps();

foreach ($stepsCollection->getAllSubmitted() as $k => $v) {
$stringParser->flatten($v, 'form_'.$k, $tokens);
$summaryTokens[$k]['value'] = $tokens['form_'.$k];
foreach ($stepsCollection->getAllSubmitted() as $formFieldName => $formFieldValue) {
$stringParser->flatten($formFieldValue, 'form_'.$formFieldName, $tokens);
$summaryTokens[$formFieldName]['value'] = $tokens['form_'.$formFieldName];
}

foreach ($stepsCollection->getAllLabels() as $k => $v) {
$stringParser->flatten($v, 'formlabel_'.$k, $tokens);
$summaryTokens[$k]['label'] = $tokens['formlabel_'.$k];
foreach ($stepsCollection->getAllLabels() as $formFieldName => $formFieldValue) {
$stringParser->flatten($formFieldValue, 'formlabel_'.$formFieldName, $tokens);
$summaryTokens[$formFieldName]['label'] = $tokens['formlabel_'.$formFieldName];
}

foreach ($stepsCollection->getAllFiles() as $k => $v) {
try {
$file = new File($v['tmp_name']);
} catch (FileNotFoundException $e) {
continue;
}

if (isset($_GET['summary_download']) && $k === $_GET['summary_download']) {
$binaryFileResponse = new BinaryFileResponse($file);
$binaryFileResponse->setContentDisposition(
$this->mp_forms_downloadInline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$file->getBasename(),
);

throw new ResponseException($binaryFileResponse);
foreach ($stepsCollection->getAllFiles() as $formFieldName => $normalizedFiles) {
$html = [];

foreach ($normalizedFiles as $k => $normalizedFile) {
try {
$file = new File($normalizedFile['tmp_name']);
} catch (FileNotFoundException $e) {
return [];
}

// Generate the tokens for the index (file 0, 1, 2, ...) and store the HTML per
// download for later
$tokens = array_merge($tokens, $this->generateFileTokens($file, 'file_'.$formFieldName.'_'.$k));
$html[] = $this->generateFileDownloadHtml($file, $this->generateAndHandleDownloadUrl($file, 'file_'.$formFieldName.'_'.$k));

// If we are at key 0 we also generate one non-indexed token for BC reasons and
// easier usage for single upload fields.
if (0 === $k) {
$tokens = array_merge($tokens, $this->generateFileTokens($file, 'file_'.$formFieldName));
}
}

$fileTokens['download_url'] = $urlParser->addQueryString('summary_download='.$k);
$fileTokens['extension'] = $file->getExtension();
$fileTokens['mime'] = $file->getMimeType();
$fileTokens['size'] = $file->getSize();

foreach ($fileTokens as $kk => $vv) {
$stringParser->flatten($vv, 'file_'.$k.'_'.$kk, $tokens);
}

// Generate a general HTML output using the download template
$tpl = new FrontendTemplate(empty($this->mp_forms_downloadTemplate) ? 'ce_download' : $this->mp_forms_downloadTemplate);
$tpl->link = $file->getBasename($file->getExtension());
$tpl->title = StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['download'], $file->getBasename($file->getExtension())));
$tpl->href = $fileTokens['download_url'];
$tpl->filesize = System::getReadableSize($file->getSize());
$tpl->mime = $file->getMimeType();
$tpl->extension = $file->getExtension();

$stringParser->flatten($tpl->parse(), 'file_'.$k, $tokens);
$summaryTokens[$k]['value'] = $tokens['file_'.$k];
// Generate an HTML token (can contain multiple downloads) and add that as the
// default value for the "file_<formfield>" token and our summary for later
$htmlToken = implode(' ', $html);
$tokens['file_'.$formFieldName] = $htmlToken;
$summaryTokens[$formFieldName]['value'] = $htmlToken;
}

// Add a simple summary token that outputs label plus value for everything that
Expand All @@ -162,7 +148,7 @@ private function generateTokens(): array
}

$summaryToken[] = sprintf('<div data-ff-name="%s" class="label">%s</div>', htmlspecialchars($k), $v['label'] ?? '');
$summaryToken[] = sprintf('<div data-ff-name="%s" class="value">%s</div>', htmlspecialchars($k), $v['value'] ?? '');
$summaryToken[] = sprintf('<div data-ff-name="%s" class="value">%s</div>', htmlspecialchars($k), $v['value']);
}

$tokens['mp_forms_summary'] = implode("\n", $summaryToken);
Expand All @@ -184,4 +170,46 @@ private function generateTokens(): array

return $tokens;
}

private function generateFileTokens(File $file, string $tokenKey): array
{
$fileTokens = [];
$fileTokens[$tokenKey.'_download_url'] = $this->generateAndHandleDownloadUrl($file, $tokenKey);
$fileTokens[$tokenKey.'_extension'] = $file->getExtension();
$fileTokens[$tokenKey.'_mime'] = $file->getMimeType();
$fileTokens[$tokenKey.'_size'] = $file->getSize();

return $fileTokens;
}

private function generateAndHandleDownloadUrl(File $file, string $key): string
{
if (isset($_GET['summary_download']) && $key === $_GET['summary_download']) {
$binaryFileResponse = new BinaryFileResponse($file);
$binaryFileResponse->setContentDisposition(
$this->mp_forms_downloadInline ? ResponseHeaderBag::DISPOSITION_INLINE : ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$file->getBasename(),
);

throw new ResponseException($binaryFileResponse);
}

$urlParser = System::getContainer()->get(UrlParser::class);

return $urlParser->addQueryString('summary_download='.$key);
}

private function generateFileDownloadHtml(File $file, string $downloadUrl): string
{
// Generate a general HTML output using the download template
$tpl = new FrontendTemplate(empty($this->mp_forms_downloadTemplate) ? 'ce_download' : $this->mp_forms_downloadTemplate);
$tpl->link = $file->getBasename($file->getExtension());
$tpl->title = StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['download'], $file->getBasename($file->getExtension())));
$tpl->href = $downloadUrl;
$tpl->filesize = System::getReadableSize($file->getSize());
$tpl->mime = $file->getMimeType();
$tpl->extension = $file->getExtension();

return $tpl->parse();
}
}
5 changes: 3 additions & 2 deletions tests/EventListener/PrepareFormDataListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Terminal42\MultipageFormsBundle\Test\EventListener;

use Codefog\HasteBundle\FileUploadNormalizer;
use Contao\CoreBundle\Exception\RedirectResponseException;
use Contao\FormModel;
use Symfony\Component\HttpFoundation\RequestStack;
Expand Down Expand Up @@ -33,7 +34,7 @@ public function testDataIsStoredProperlyAndDoesNotAdjustHookParametersIfNotOnLas
1, // This mocks step=1 (page 2)
);

$listener = new PrepareFomDataListener($factory, $this->createMock(RequestStack::class));
$listener = new PrepareFomDataListener($factory, $this->createMock(RequestStack::class), $this->createMock(FileUploadNormalizer::class));

$submitted = ['submitted2' => 'foobar', 'mp_form_pageswitch' => 'continue'];
$labels = [];
Expand Down Expand Up @@ -77,7 +78,7 @@ public function testDataIsStoredProperlyAndDoesAdjustHookParametersOnLastStep():
2, // This mocks step=2 (page 3 - last page)
);

$listener = new PrepareFomDataListener($factory, $this->createMock(RequestStack::class));
$listener = new PrepareFomDataListener($factory, $this->createMock(RequestStack::class), $this->createMock(FileUploadNormalizer::class));

$submitted = ['submitted3' => 'foobar', 'mp_form_pageswitch' => 'continue'];
$labels = [];
Expand Down
2 changes: 0 additions & 2 deletions tests/Step/StepDataCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@ public function testGetAll(): void

$this->assertSame($expected, $stepCollection->getAllLabels());
$this->assertSame($expected, $stepCollection->getAllSubmitted());
$this->assertSame($expected, $stepCollection->getAllFiles());
}

private function createStepData(int $step, ParameterBag $parameters): StepData
{
$step = StepData::create($step);
$step = $step->withSubmitted($parameters);
$step = $step->withFiles($parameters);

return $step->withLabels($parameters);
}
Expand Down
14 changes: 9 additions & 5 deletions tests/Step/StepDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Terminal42\MultipageFormsBundle\Test\Step;

use PHPUnit\Framework\TestCase;
use Terminal42\MultipageFormsBundle\Step\FileParameterBag;
use Terminal42\MultipageFormsBundle\Step\ParameterBag;
use Terminal42\MultipageFormsBundle\Step\StepData;

Expand All @@ -25,14 +26,17 @@ public function testSubmitted(array $data): void
$this->assertTrue($parameters->equals($stepData->getSubmitted()));
}

/**
* @dataProvider parametersDataProvider
*/
public function testFiles(array $data): void
public function testFiles(): void
{
$stepData = StepData::create(1);

$parameters = new ParameterBag($data);
$parameters = new FileParameterBag([
'file_upload' => [
[
'tmp_name' => '/tmp/file.tmp',
],
],
]);

$this->assertTrue($stepData->getFiles()->empty());

Expand Down
Loading