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
2 changes: 2 additions & 0 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
'form_configure_email_text_instructions' => 'The view for the text version of this email.',
'form_configure_email_to_instructions' => 'Email address of the recipient(s) - comma separated.',
'form_configure_handle_instructions' => 'Used to reference this form on the frontend. This cannot be easily changed later.',
'form_configure_honeypot_behavior_instructions' => 'What happens to submissions caught by the honeypot. They can be silently discarded, or stored and marked as spam for review.',
'form_configure_honeypot_instructions' => 'Field name to use as a honeypot. Honeypots are special fields used to reduce bot spam.',
'form_configure_intro' => 'Forms collect information from visitors and can trigger events and send notifications when submissions are received.',
'form_configure_mailer_instructions' => 'Choose the mailer for sending this email. Leave blank to fall back to the default mailer.',
Expand Down Expand Up @@ -197,6 +198,7 @@
'licensing_trial_mode_alert_addons' => 'This site is using commercial addons in trial mode. Valid licenses will be required when you\'re ready to launch.',
'licensing_trial_mode_alert_statamic' => 'Thanks for trying Statamic Pro! This site is currently in trial mode — please enter a license before your site goes live.',
'licensing_utility_description' => 'View and resolve licensing details.',
'mark_as_not_spam_action_confirmation' => 'Submissions that were caught before being finalized will be finalized, triggering any configured connections.',
'max_depth_instructions' => 'Set the max page nesting level.',
'max_items_instructions' => 'Set a maximum number of selectable items.',
'navigation_configure_blueprint_instructions' => 'Choose from existing blueprints or create a new one.',
Expand Down
3 changes: 2 additions & 1 deletion resources/css/components/index-fields.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
@apply bg-green-200 text-green-900 dark:bg-green-300/6 dark:text-green-300;
}

&.status-scheduled {
&.status-scheduled,
&.status-spam {
@apply bg-amber-200 text-amber-900 dark:bg-amber-300/6 dark:text-amber-300;
}
}
Expand Down
4 changes: 3 additions & 1 deletion resources/js/components/forms/SubmissionStatusIndicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const props = defineProps({
type: String,
required: false,
default: 'finalized',
validator: (value) => ['finalized', 'partial'].includes(value),
validator: (value) => ['finalized', 'partial', 'spam'].includes(value),
},
showDot: { type: Boolean, default: true },
showLabel: { type: Boolean, default: false },
Expand All @@ -16,13 +16,15 @@ const statusClass = computed(() => {
return {
finalized: 'bg-green-400',
partial: 'bg-gray-300 dark:bg-gray-200',
spam: 'bg-amber-400',
}[props.status];
});

const label = computed(() => {
return {
finalized: __('Finalized'),
partial: __('Partial'),
spam: __('Spam'),
}[props.status];
});
</script>
Expand Down
53 changes: 53 additions & 0 deletions src/Actions/MarkAsNotSpam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Statamic\Actions;

use Statamic\Contracts\Forms\Submission;

use function Statamic\trans as __;
use function Statamic\trans_choice;

class MarkAsNotSpam extends Action
{
protected $icon = 'checkmark-circle';

public static function title()
{
return __('Mark as Not Spam');
}

public function visibleTo($item)
{
return $item instanceof Submission && $item->isSpam();
}

public function authorize($user, $item)
{
return $user->can('markAsNotSpam', $item);
}

public function buttonText()
{
/** @translation */
return 'Mark as Not Spam|Mark :count Submissions as Not Spam';
}

public function confirmationText()
{
/** @translation */
return 'statamic::messages.mark_as_not_spam_action_confirmation';
}

public function run($items, $values)
{
$items->each(function ($submission) {
$submission->markAsNotSpam();

$submission->isPartial() ? $submission->finalize() : $submission->save();
});

return [
'message' => trans_choice('Submission marked as not spam|Submissions marked as not spam', $items->count()),
];
}
}
45 changes: 45 additions & 0 deletions src/Actions/MarkAsSpam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Statamic\Actions;

use Statamic\Contracts\Forms\Submission;

use function Statamic\trans as __;
use function Statamic\trans_choice;

class MarkAsSpam extends Action
{
protected $confirm = false;

protected $icon = 'alert-warning-exclamation-mark';

public static function title()
{
return __('Mark as Spam');
}

public function visibleTo($item)
{
return $item instanceof Submission && ! $item->isSpam();
}

public function authorize($user, $item)
{
return $user->can('markAsSpam', $item);
}

public function buttonText()
{
/** @translation */
return 'Mark as Spam|Mark :count Submissions as Spam';
}

public function run($items, $values)
{
$items->each(fn ($submission) => $submission->markAsSpam()->save());

return [
'message' => trans_choice('Submission marked as spam|Submissions marked as spam', $items->count()),
];
}
}
2 changes: 1 addition & 1 deletion src/Forms/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ private function submissionLimitReached(): bool

private function submissionCount(): int
{
$query = $this->querySubmissions()->whereNull('partial');
$query = $this->querySubmissions()->whereNull('partial')->whereNull('spam');

if ($start = $this->submissionLimitPeriodStart()) {
$query->where('date', '>=', $start);
Expand Down
31 changes: 27 additions & 4 deletions src/Forms/Submission.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ public function data($data = null)
$data = collect($data);

// A full data replacement would otherwise drop the internal lifecycle
// keys, so carry over the existing partial and site values unless the
// incoming payload provides its own.
foreach (['partial', 'site'] as $key) {
// keys, so carry over the existing partial, spam, and site values
// unless the incoming payload provides its own.
foreach (['partial', 'spam', 'site'] as $key) {
if ($this->has($key) && ! $data->has($key)) {
$data[$key] = $this->get($key);
}
Expand Down Expand Up @@ -164,12 +164,35 @@ public function asPartial(): self

public function isPartial(): bool
{
return (bool) $this->get('partial');
// Spam submissions aren't in progress, so they shouldn't be resumed.
// The "partial" key sticks around to indicate that the submission was
// never finalized, so marking it as not spam can finalize it as normal.
return $this->get('partial') && ! $this->isSpam();
}

public function markAsSpam(): self
{
$this->set('spam', true);

return $this;
}

public function markAsNotSpam(): self
{
$this->remove('spam');

return $this;
}

public function isSpam(): bool
{
return (bool) $this->get('spam');
}

public function status(): string
{
return match (true) {
$this->isSpam() => 'spam',
$this->isPartial() => 'partial',
default => 'finalized',
};
Expand Down
21 changes: 19 additions & 2 deletions src/Forms/SubmitForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,16 @@ public function submit(array $data, array $files = []): SubmissionResult
}

if ($this->shouldFinalize($nextPage)) {
throw_if(Arr::get($values, $this->form->honeypot()), new SilentFormFailureException);
if (Arr::get($values, $this->form->honeypot())) {
$this->rejectSpamSubmission();
}

throw_if(FormSubmitted::dispatch($this->submission) === false, new SilentFormFailureException);
}
} catch (ValidationException|SilentFormFailureException $e) {
$this->removeUploadedAssets($uploadedAssets);
if (! $this->submission?->isSpam()) {
$this->removeUploadedAssets($uploadedAssets);
}

throw $e;
}
Expand Down Expand Up @@ -175,6 +180,18 @@ private function fieldHandles(string $page): array
->all();
}

private function rejectSpamSubmission(): void
{
if (
$this->form->get('honeypot_behavior', 'ignore') === 'mark_as_spam'
&& $this->form->store()
) {
$this->submission->markAsSpam()->save();
}

throw new SilentFormFailureException;
}

/**
* Remove any uploaded assets.
*
Expand Down
16 changes: 13 additions & 3 deletions src/Http/Controllers/CP/Forms/FormsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function index(Request $request)
'id' => $form->handle(),
'title' => __($form->title()),
'status' => $form->status(),
'submissions' => $canViewSubmissions ? $form->querySubmissions()->whereNull('partial')->count() : null,
'submissions' => $canViewSubmissions ? $form->querySubmissions()->whereNull('partial')->whereNull('spam')->count() : null,
'show_url' => $form->showUrl(),
'submissions_url' => $form->submissionsUrl(),
'edit_url' => $form->editUrl(),
Expand Down Expand Up @@ -195,13 +195,23 @@ protected function editFormBlueprint($form)
],
],
],
'fields' => [
'display' => __('Fields'),
'honeypot' => [
'display' => __('Honeypot'),
'fields' => [
'honeypot' => [
'type' => 'text',
'instructions' => __('statamic::messages.form_configure_honeypot_instructions'),
],
'honeypot_behavior' => [
'display' => __('Honeypot Behavior'),
'type' => 'button_group',
'default' => 'ignore',
'options' => [
'ignore' => __('Ignore'),
'mark_as_spam' => __('Save as Spam'),
],
'instructions' => __('statamic::messages.form_configure_honeypot_behavior_instructions'),
],
],
],
'submissions' => [
Expand Down
4 changes: 4 additions & 0 deletions src/Http/Controllers/FormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ public function submit(Request $request, $form, SubmitForm $action)
} catch (SilentFormFailureException $e) {
$result = new SubmissionResult(submission: $action->submission());

if ($result->submission->isSpam()) {
$this->forgetPartialSubmission($form);
}

return $this->formSuccess($params, $result, silentFailure: true);
} catch (ValidationException $e) {
return $this->formFailure($params, $e->errors(), $form->handle());
Expand Down
1 change: 1 addition & 0 deletions src/Jobs/DeletePartialFormSubmissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function handle(): void

FormSubmission::query()
->where('partial', true)
->whereNull('spam')
->where('date', '<', $threshold)
->get()
->each->delete();
Expand Down
10 changes: 10 additions & 0 deletions src/Policies/FormSubmissionPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,14 @@ public function delete($user, $submission)
{
return User::fromUser($user)->can('deleteSubmissions', $submission->form());
}

public function markAsSpam($user, $submission)
{
return User::fromUser($user)->can('viewSubmissions', $submission->form());
}

public function markAsNotSpam($user, $submission)
{
return User::fromUser($user)->can('viewSubmissions', $submission->form());
}
}
2 changes: 2 additions & 0 deletions src/Providers/ExtensionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class ExtensionServiceProvider extends ServiceProvider
Actions\DuplicateEntry::class,
Actions\DuplicateForm::class,
Actions\DuplicateTerm::class,
Actions\MarkAsSpam::class,
Actions\MarkAsNotSpam::class,
Actions\Publish::class,
Actions\Unpublish::class,
Actions\SendPasswordReset::class,
Expand Down
6 changes: 4 additions & 2 deletions src/Query/Scopes/Filters/SubmissionStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public function autoApply()
public function apply($query, $values)
{
match ($values['status']) {
'partial' => $query->where('partial', true),
default => $query->where('partial', '!=', true),
'partial' => $query->where('partial', true)->where('spam', '!=', true),
'spam' => $query->where('spam', true),
default => $query->where('partial', '!=', true)->where('spam', '!=', true),
};
}

Expand All @@ -53,6 +54,7 @@ protected function options()
return collect([
'finalized' => __('Finalized'),
'partial' => __('Partial'),
'spam' => __('Spam'),
]);
}
}
Loading
Loading