Skip to content
12 changes: 11 additions & 1 deletion src/Exceptions/SilentFormFailureException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

namespace Statamic\Exceptions;

use Statamic\Contracts\Forms\Submission;

class SilentFormFailureException extends \Exception
{
//
public function __construct(protected ?Submission $submission = null)
{
parent::__construct();
}

public function submission(): ?Submission
{
return $this->submission;
}
}
105 changes: 105 additions & 0 deletions src/Forms/SubmitForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace Statamic\Forms;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use Statamic\Contracts\Forms\Form;
use Statamic\Contracts\Forms\Submission;
use Statamic\Events\FormSubmitted;
use Statamic\Events\SubmissionCreated;
use Statamic\Exceptions\SilentFormFailureException;
use Statamic\Facades;
use Statamic\Facades\Asset;
use Statamic\Rules\AllowedFile;
use Statamic\Sites\Site;
use Statamic\Support\Arr;

class SubmitForm
{
public function __invoke(
Comment thread
duncanmcclean marked this conversation as resolved.
Outdated
Form $form,
array $data = [],
array $files = [],
?Site $site = null,
): Submission {
$uploadedAssets = [];

$values = array_merge($data, $files);
$fields = $form->blueprint()->fields();
$fields = $fields->addValues($values);
$site = $site ?? Facades\Site::default();

$submission = $form->makeSubmission();

try {
throw_if(Arr::get($values, $form->honeypot()), new SilentFormFailureException($submission));

$uploadedAssets = $submission->uploadFiles($files);

$values = array_merge($values, $uploadedAssets);

$submission->data(
$fields->addValues($values)->process()->values()
);

// If any event listeners return false, we'll do a silent failure.
// If they want to add validation errors, they can throw an exception.
throw_if(FormSubmitted::dispatch($submission) === false, new SilentFormFailureException($submission));
} catch (ValidationException|SilentFormFailureException $e) {
$this->removeUploadedAssets($uploadedAssets);

throw $e;
}

if ($form->store()) {
$submission->save();
} else {
// When the submission is saved, this same created event will be dispatched.
// We'll also fire it here if submissions are not configured to be stored
// so that developers may continue to listen and modify it as needed.
SubmissionCreated::dispatch($submission);
}

SendEmails::dispatch($submission, $site);

return $submission;
}

public static function validator(Form $form, array $data, array $files = []): Validator
Comment thread
duncanmcclean marked this conversation as resolved.
Outdated
{
$values = array_merge($data, $files);
$fields = $form->blueprint()->fields()->addValues($values);

return $fields
->validator()
->withRules(static::extraRules($fields))
->validator();
}

protected static function extraRules($fields): array
Comment thread
duncanmcclean marked this conversation as resolved.
Outdated
{
return $fields->all()
->filter(fn ($field) => $field->fieldtype()->handle() === 'assets')
->mapWithKeys(function ($field) {
return [$field->handle().'.*' => ['file', new AllowedFile]];
})
->all();
}

/**
* Remove any uploaded assets.
*
* Triggered by a validation exception or silent failure.
*/
protected function removeUploadedAssets(array $assets): void
Comment thread
duncanmcclean marked this conversation as resolved.
Outdated
{
collect($assets)
->flatten()
->each(function ($id) {
if ($asset = Asset::find($id)) {
$asset->delete();
}
});
}
}
101 changes: 17 additions & 84 deletions src/Http/Controllers/FormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationException;
use Statamic\Contracts\Forms\Submission;
use Statamic\Events\FormSubmitted;
use Statamic\Events\SubmissionCreated;
use Statamic\Exceptions\SilentFormFailureException;
use Statamic\Facades\Asset;
use Statamic\Facades\Form;
use Statamic\Facades\Site;
use Statamic\Forms\Exceptions\FileContentTypeRequiredException;
use Statamic\Forms\SendEmails;
use Statamic\Forms\SubmitForm;
use Statamic\Http\Requests\FrontendFormRequest;
use Statamic\Support\Arr;
use Statamic\Support\Str;
use Symfony\Component\HttpFoundation\RedirectResponse;

class FormController extends Controller
{
Expand All @@ -29,60 +27,29 @@ class FormController extends Controller
public function submit(FrontendFormRequest $request, $form)
Comment thread
duncanmcclean marked this conversation as resolved.
Outdated
{
$site = Site::findByUrl(URL::previous()) ?? Site::default();
$fields = $form->blueprint()->fields();
$this->validateContentType($request, $form);
$values = $request->all();

$values = array_merge($values, $assets = $request->assets());
$params = collect($request->all())->filter(function ($value, $key) {
return Str::startsWith($key, '_');
})->all();

$fields = $fields->addValues($values);

$submission = $form->makeSubmission();
$params = collect($request->all())
->filter(fn ($value, string $key) => Str::startsWith($key, '_'))
->all();

try {
throw_if(Arr::get($values, $form->honeypot()), new SilentFormFailureException);

$uploadedAssets = $submission->uploadFiles($assets);

$values = array_merge($values, $uploadedAssets);

$submission->data(
$fields->addValues($values)->process()->values()
$submission = app(SubmitForm::class)(
Comment thread
duncanmcclean marked this conversation as resolved.
Outdated
form: $form,
data: $request->all(),
files: $request->assets(),
site: $site,
);

// If any event listeners return false, we'll do a silent failure.
// If they want to add validation errors, they can throw an exception.
throw_if(FormSubmitted::dispatch($submission) === false, new SilentFormFailureException);
return $this->formSuccess($params, $submission);
} catch (SilentFormFailureException $e) {
return $this->formSuccess($params, $e->submission(), silentFailure: true);
} catch (ValidationException $e) {
$this->removeUploadedAssets($uploadedAssets);

return $this->formFailure($params, $e->errors(), $form->handle());
} catch (SilentFormFailureException $e) {
if (isset($uploadedAssets)) {
$this->removeUploadedAssets($uploadedAssets);
}

return $this->formSuccess($params, $submission, true);
}

if ($form->store()) {
$submission->save();
} else {
// When the submission is saved, this same created event will be dispatched.
// We'll also fire it here if submissions are not configured to be stored
// so that developers may continue to listen and modify it as needed.
SubmissionCreated::dispatch($submission);
}

SendEmails::dispatch($submission, $site);

return $this->formSuccess($params, $submission);
}

private function validateContentType($request, $form)
private function validateContentType($request, $form): void
{
$type = Str::before($request->headers->get('CONTENT_TYPE'), ';');

Expand All @@ -91,15 +58,7 @@ private function validateContentType($request, $form)
}
}

/**
* The steps for a failed form submission.
*
* @param array $params
* @param array $errors
* @param string $form
* @return Response|RedirectResponse
*/
private function formFailure($params, $errors, $form)
private function formFailure(array $params, array $errors, string $form): Response|RedirectResponse
{
$request = request();

Expand All @@ -125,17 +84,7 @@ private function formFailure($params, $errors, $form)
return $response->withInput()->withErrors($errors, 'form.'.$form);
}

/**
* The steps for a successful form submission.
*
* Used for actual success and by honeypot.
*
* @param array $params
* @param Submission $submission
* @param bool $silentFailure
* @return Response
*/
private function formSuccess($params, $submission, $silentFailure = false)
private function formSuccess(array $params, Submission $submission, bool $silentFailure = false): Response|RedirectResponse
{
$redirect = $this->formSuccessRedirect($params, $submission);

Expand All @@ -159,7 +108,7 @@ private function formSuccess($params, $submission, $silentFailure = false)
return $response;
}

private function formSuccessRedirect($params, $submission)
private function formSuccessRedirect(array $params, Submission $submission): ?string
{
if ($redirect = Form::getSubmissionRedirect($submission)) {
return $redirect;
Expand All @@ -173,20 +122,4 @@ private function formSuccessRedirect($params, $submission)

return $redirect;
}

/**
* Remove any uploaded assets
*
* Triggered by a validation exception or silent failure
*/
private function removeUploadedAssets(array $assets)
{
collect($assets)
->flatten()
->each(function ($id) {
if ($asset = Asset::find($id)) {
$asset->delete();
}
});
}
}
Loading
Loading