Skip to content

Commit

Permalink
Merge pull request #28 from bolt/feature/file-attachments
Browse files Browse the repository at this point in the history
[WIP] Init file attachments
  • Loading branch information
bobdenotter authored Nov 20, 2020
2 parents 3145d1e + 67f59b4 commit 50bfccc
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 14 deletions.
4 changes: 4 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
Bolt\BoltForms\EventSubscriber\FileUploadHandler:
arguments:
$projectDir: '%kernel.project_dir%'
15 changes: 15 additions & 0 deletions src/Event/PostSubmitEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ class PostSubmitEvent extends Event

private $spam = false;

/** @var Collection */
private $attachments;

public function __construct(Form $form, Collection $config, string $formName, Request $request, ExtensionRegistry $registry)
{
$this->form = $form;
$this->config = $config;
$this->formName = $formName;
$this->request = $request;
$this->registry = $registry;
$this->attachments = collect([]);
}

public function getFormName(): string
Expand Down Expand Up @@ -70,6 +74,7 @@ public function getMeta()
'timestamp' => Carbon::now(),
'path' => $this->request->getRequestUri(),
'url' => $this->request->getUri(),
'attachments' => $this->getAttachments(),
];
}

Expand All @@ -82,4 +87,14 @@ public function isSpam(): bool
{
return $this->spam;
}

public function addAttachments(array $attachments): void
{
$this->attachments = $this->attachments->merge($attachments);
}

public function getAttachments(): array
{
return $this->attachments->toArray();
}
}
131 changes: 131 additions & 0 deletions src/EventSubscriber/FileUploadHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace Bolt\BoltForms\EventSubscriber;

use Bolt\BoltForms\Event\PostSubmitEvent;
use Bolt\BoltForms\FormHelper;
use Bolt\Common\Str;
use Cocur\Slugify\Slugify;
use Sirius\Upload\Handler;
use Sirius\Upload\Result\File;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Tightenco\Collect\Support\Collection;
use Webmozart\PathUtil\Path;

class FileUploadHandler implements EventSubscriberInterface
{
/** @var string */
private $projectDir;

/** @var FormHelper */
private $helper;

public function __construct(string $projectDir = '', FormHelper $helper)
{
$this->helper = $helper;
$this->projectDir = $projectDir;
}

public function handleEvent(PostSubmitEvent $event): void
{
$form = $event->getForm();
$formConfig = $event->getFormConfig();

$fields = $form->all();
foreach ($fields as $field) {
$fieldConfig = $formConfig->get('fields')[$field->getName()] ?? null;
if ($fieldConfig && $fieldConfig['type'] === 'file') {
$this->processFileField($field, collect($fieldConfig), $event);
}
}
}

private function processFileField(Form $field, Collection $fieldConfig, PostSubmitEvent $event): void
{
$file = $field->getData();
$form = $event->getForm();
$formConfig = $event->getFormConfig();

$filename = $this->getFilename($field->getName(), $form, $formConfig);
$path = $fieldConfig['directory'] ?? '/uploads/';
Str::ensureStartsWith($path, DIRECTORY_SEPARATOR);
$files = $this->uploadFiles($filename, $file, $path);

if (isset($fieldConfig['attach']) && $fieldConfig['attach']) {
$event->addAttachments($files);
}
}

private function getFilename(string $fieldname, Form $form, Collection $formConfig): string
{
$filenameFormat = $formConfig->get('fields')[$fieldname]['file_format'] ?? 'Uploaded file'.uniqid();
$filename = $this->helper->get($filenameFormat, $form);

if (! $filename) {
$filename = uniqid();
}

return $filename;
}

/**
* @param UploadedFile|array $file
*/
private function uploadFiles(string $filename, $file, string $path = ''): array
{
$uploadPath = $this->projectDir.$path;
$uploadHandler = new Handler($uploadPath, [
Handler::OPTION_AUTOCONFIRM => true,
Handler::OPTION_OVERWRITE => false,
]);

$uploadHandler->setPrefix(mb_substr(md5(time()), 0, 8) . '_' . $filename);

$uploadHandler->setSanitizerCallback(function ($name) {
return $this->sanitiseFilename($name);
});

/** @var File $processed */
$processed = $uploadHandler->process($file);

$result = [];
if ($processed->isValid()) {
$processed->confirm();

if (is_iterable($processed)) {
foreach ($processed as $file) {
$result[] = $uploadPath . $file->__get('name');
}
} else {
$result[] = $uploadPath . $processed->__get('name');
}
}

// Very ugly. But it works when later someone uses Request::createFromGlobals();
$_FILES = [];

return $result;
}

private function sanitiseFilename(string $filename): string
{
$extensionSlug = new Slugify(['regexp' => '/([^a-z0-9]|-)+/']);
$filenameSlug = new Slugify(['lowercase' => false]);

$extension = $extensionSlug->slugify(Path::getExtension($filename));
$filename = $filenameSlug->slugify(Path::getFilenameWithoutExtension($filename));

return $filename . '.' . $extension;
}

public static function getSubscribedEvents()
{
return [
'boltforms.post_submit' => ['handleEvent', 40],
];
}
}
17 changes: 17 additions & 0 deletions src/EventSubscriber/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Bolt\BoltForms\Event\PostSubmitEvent;
use Bolt\Log\LoggerTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Tightenco\Collect\Support\Collection;

class Logger implements EventSubscriberInterface
Expand Down Expand Up @@ -42,6 +43,22 @@ public function log(): void

$data = $this->event->getForm()->getData();

// We cannot serialize Uploaded file. See https://github.com/symfony/symfony/issues/19572.
// So instead, let's get the filename. ¯\_(ツ)_/¯
// todo: Can we fix this?
foreach ($data as $key => $value) {
if ($value instanceof UploadedFile) {
$data[$key] = $value->getClientOriginalName();
} elseif (is_iterable($value)) {
// Multiple files
foreach ($value as $k => $v) {
if ($v instanceof UploadedFile) {
$data[$key][$k] = $v->getClientOriginalName();
}
}
}
}

$data['formname'] = $this->event->getFormName();

$this->logger->info('[Boltforms] Form {formname} - submitted Form data (see \'Context\')', $data);
Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function buildEmail(): TemplatedEmail
public static function getSubscribedEvents()
{
return [
'boltforms.post_submit' => ['handleEvent', 20],
'boltforms.post_submit' => ['handleEvent', 40],
];
}
}
8 changes: 8 additions & 0 deletions src/Factory/EmailFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public function create(Collection $formConfig, Collection $config, Form $form, a

$debug = (bool) $this->config->get('debug')['enabled'];

$attachments = $meta['attachments'] ?? [];
unset($meta['attachments']);

$email = (new TemplatedEmail())
->from($this->getFrom())
->to($this->getTo())
Expand Down Expand Up @@ -63,6 +66,11 @@ public function create(Collection $formConfig, Collection $config, Form $form, a
$email->replyTo($this->getReplyTo());
}

/** @var File $attachment */
foreach ($attachments as $attachment) {
$email->attachFromPath($attachment);
}

// Override the "to"
if ($debug) {
$email->to($this->config->get('debug')['address']);
Expand Down
6 changes: 3 additions & 3 deletions src/Factory/FieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\DateIntervalType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\DateIntervalType;

class FieldType
{
Expand Down
33 changes: 33 additions & 0 deletions src/FormHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Bolt\BoltForms;

use Symfony\Component\Form\Form;

class FormHelper
{
public function get(?string $format, Form $form, $values = []): ?string
{
if (! $format || ! $form->isSubmitted()) {
return null;
}

return preg_replace_callback(
'/{([\w]+)}/i',
function ($match) use ($form, $values) {
if (array_key_exists($match[1], $form->all())) {
return (string) $form->get($match[1])->getData();
}

if (array_key_exists($match[1], $values)) {
return (string) $values[$match[1]];
}

return '(unknown)';
},
$format
);
}
}
21 changes: 11 additions & 10 deletions templates/email_blocks.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
{%- set field = attribute(config.fields, fieldname) %}
{%- set label = field.options.label|default(fieldname) %}

{%- if values is iterable %}
<li>
{{ block('field_label') }}
{{ block('value_array') }}
</li>
{%- else %}
{%- set value = values %}
<li>{{ block('field_label') }}{{ block('field_value') }}</li>
{%- endif %}

{% if not (field.type == 'file' and field.attach|default) %}
{%- if values is iterable %}
<li>
{{ block('field_label') }}
{{ block('value_array') }}
</li>
{%- else %}
{%- set value = values %}
<li>{{ block('field_label') }}{{ block('field_value') }}</li>
{%- endif %}
{% endif %}
{%- endfor %}
</ul>
<ul>
Expand Down

0 comments on commit 50bfccc

Please sign in to comment.