Skip to content
Open
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
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;
}
}
132 changes: 132 additions & 0 deletions src/Forms/SubmitForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace Statamic\Forms;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Traits\Localizable;
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
{
use Localizable;

public function submit(
Form $form,
array $data = [],
array $files = [],
?Site $site = null,
): Submission {
$values = array_merge($data, $files);
$fields = $form->blueprint()->fields();
$fields = $fields->addValues($values);
$site = $site ?? Facades\Site::default();

$uploadedAssets = [];
$files = $this->normalizeFiles($form, $files);

$this->withLocale($site->lang(), fn () => $this->validator($form, $data, $files)->validate());

$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 function validator(Form $form, array $data, array $files = []): Validator
{
$values = array_merge($data, $files);
$fields = $form->blueprint()->fields()->addValues($values);

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

private function extraRules($fields): array
{
return $fields->all()
->filter(fn ($field) => $field->fieldtype()->handle() === 'assets')
->mapWithKeys(function ($field) {
return [$field->handle().'.*' => ['file', new AllowedFile]];
})
->all();
}

/**
* Normalize uploaded files to arrays.
*
* The assets fieldtype expects arrays, even for `max_files: 1`,
* but we don't want to force that on the front end.
*/
private function normalizeFiles(Form $form, array $files): array
{
$assetFields = $form->blueprint()->fields()->all()
->filter(fn ($field) => in_array($field->fieldtype()->handle(), ['assets', 'files']))
->keys();

foreach ($assetFields as $handle) {
if (isset($files[$handle])) {
$files[$handle] = Arr::wrap($files[$handle]);
}
}

return $files;
}

/**
* Remove any uploaded assets.
*
* Triggered by a validation exception or silent failure.
*/
private function removeUploadedAssets(array $assets): void
{
collect($assets)
->flatten()
->each(function ($id) {
if ($asset = Asset::find($id)) {
$asset->delete();
}
});
}
}
107 changes: 20 additions & 87 deletions src/Http/Controllers/FormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@

namespace Statamic\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\URL;
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\Http\Requests\FrontendFormRequest;
use Statamic\Forms\SubmitForm;
use Statamic\Support\Arr;
use Statamic\Support\Str;
use Symfony\Component\HttpFoundation\RedirectResponse;

class FormController extends Controller
{
Expand All @@ -26,80 +24,41 @@ class FormController extends Controller
*
* @return mixed
*/
public function submit(FrontendFormRequest $request, $form)
public function submit(Request $request, $form, SubmitForm $submitForm)
{
$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 = $submitForm->submit(
form: $form,
data: $request->all(),
files: $request->allFiles(),
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 $request, $form): void
{
$type = Str::before($request->headers->get('CONTENT_TYPE'), ';');

if ($type !== 'multipart/form-data' && $form->hasFiles() && $request->assets()) {
if ($type !== 'multipart/form-data' && $form->hasFiles() && $request->allFiles()) {
throw new FileContentTypeRequiredException;
}
}

/**
* 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