Skip to content
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 .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
application_id: ${{ vars.FUELVIEWS_BOT_APP_ID }}
application_private_key: ${{ secrets.FUELVIEWS_BOT_APP_PRIVATE_KEY }}

- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: 'main'
fetch-depth: '0'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Checkout the code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Dependabot metadata
id: metadata
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/php-cs-fixer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Laravel forms package

Laravel forms package
Laravel forms package with built-in spam protection and optional Cloudflare Turnstile support.

## Installation

Expand Down Expand Up @@ -28,6 +28,38 @@ You can publish the view files with:
php artisan vendor:publish --tag="forms-views"
```

## Cloudflare Turnstile Setup (Optional)

This package includes built-in support for Cloudflare Turnstile CAPTCHA to protect your forms from spam.

### 1. Get Your Turnstile Keys

1. Sign up for a free [Cloudflare account](https://dash.cloudflare.com/sign-up) if you don't have one
2. Go to the [Turnstile dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)
3. Create a new site and get your Site Key and Secret Key

### 2. Configure Your Environment

Add these to your `.env` file:

```env
FORMS_TURNSTILE_ENABLED=true
TURNSTILE_SITE_KEY=your_site_key_here
TURNSTILE_SECRET_KEY=your_secret_key_here
```

### Configuration Options

The package configuration in `config/forms.php` supports flexible environment variables:

```php
'turnstile' => [
'enabled' => env('FORMS_TURNSTILE_ENABLED', false),
'site_key' => env('TURNSTILE_SITE_KEY','1x00000000000000000000AA'),
'secret_key' => env('TURNSTILE_SECRET_KEY','1x0000000000000000000000000000000AA'),
],
```

## Form Usage (basic)

Include form method type, form method route, spam strap in the start and end of the form, form key, fake submit button, and a real submit button.
Expand Down Expand Up @@ -63,6 +95,7 @@ Include form method type, form method route, spam strap in the start and end of
name="firstName"
id="firstName"
wire:model="firstName"
value="{{ old('firstName') }}"
autocomplete="given-name"
class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
pattern="[A-Za-z]{2,}"
Expand All @@ -82,6 +115,7 @@ Include form method type, form method route, spam strap in the start and end of
name="first-name"
id="lastName"
wire:model="lastName"
value="{{ old('lastName') }}"
autocomplete="family-name"
class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
pattern=".{2,}"
Expand All @@ -101,6 +135,7 @@ Include form method type, form method route, spam strap in the start and end of
name="email"
type="email"
wire:model="email"
value="{{ old('email') }}"
autocomplete="email"
class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" />
@error('email')
Expand All @@ -118,6 +153,7 @@ Include form method type, form method route, spam strap in the start and end of
name="phone"
id="phone"
wire:model="phone"
value="{{ old('phone') }}"
autocomplete="tel"
aria-describedby="phone-description"
class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
Expand All @@ -141,6 +177,7 @@ Include form method type, form method route, spam strap in the start and end of
id="message"
name="message"
wire:model="message"
value="{{ old('messsage') }}"
rows="4"
aria-describedby="message-description"
class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"></textarea>
Expand Down Expand Up @@ -178,7 +215,7 @@ Add laravel-forms to your tailwind.config.js file.

```javascript
content: [
'./vendor/fuelviews/laravel-forms/resources/**/*.php'
'./vendor/fuelviews/laravel-forms/resources/**/*.blade.php',
]
```

Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"require": {
"php": "^8.3",
"spatie/laravel-package-tools": "^1.16",
"illuminate/contracts": "^10.0||^11.0||^12.0"
"illuminate/contracts": "^10.0||^11.0||^12.0",
"ryangjchandler/laravel-cloudflare-turnstile": "^2.0||^1.1"
},
"require-dev": {
"fuelviews/laravel-sabhero-wrapper": ">=0.0",
Expand All @@ -30,7 +31,7 @@
"pestphp/pest-plugin-arch": "^3.0||^2.7",
"pestphp/pest-plugin-laravel": "^3.2||^2.3",
"rector/rector": "^2.0",
"driftingly/rector-laravel": "^2.0"
"driftingly/rector-laravel": "^2.0||^1.0"
},
"autoload": {
"psr-4": {
Expand Down
6 changes: 6 additions & 0 deletions config/forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,10 @@
'yelp' => 'https://yelp.com',
'bbb' => 'https://bbb.org',
],

'turnstile' => [
'enabled' => env('FORMS_TURNSTILE_ENABLED', false),
'site_key' => env('TURNSTILE_SITE_KEY','1x00000000000000000000AA'),
'secret_key' => env('TURNSTILE_SECRET_KEY','1x0000000000000000000000000000000AA'),
],
];
2 changes: 1 addition & 1 deletion resources/views/components/steps/step-one.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<input type="text" name="gotcha" style="display:none" />

<div class="col-span-2">
<div class="flex space-x-2 pt-4">
<div class="flex flex-wrap gap-x-2 gap-y-6 pt-4">
@foreach (config('forms.modal.steps.1.locations') as $location)
<div>
<x-forms::buttons.location-button :location="$location" />
Expand Down
26 changes: 26 additions & 0 deletions resources/views/components/steps/step-two.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@
<div class="sm:col-span-2">
<x-forms::error :errorKey="'form.submit.limit'" />
</div>

@if(config('forms.turnstile.enabled') && config('forms.turnstile.site_key'))
<div class="sm:col-span-2">
<div
x-data="{
initTurnstile() {
if (typeof turnstile !== 'undefined' && this.$el.querySelector('.cf-turnstile')) {
const container = this.$el.querySelector('.cf-turnstile');
if (!container.hasChildNodes() || container.children.length === 0) {
turnstile.render(container, {
sitekey: '{{ config('forms.turnstile.site_key') }}',
callback: function(token) {
@this.set('turnstileToken', token);
}
});
}
}
}
}"
x-init="$nextTick(() => { setTimeout(() => initTurnstile(), 500); })"
wire:ignore
>
<div class="cf-turnstile" data-sitekey="{{ config('forms.turnstile.site_key') }}"></div>
</div>
</div>
@endif
</div>

<input type="text" name="gotcha" class="hidden" />
Expand Down
1 change: 1 addition & 0 deletions resources/views/livewire/forms-modal.blade.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<div>
@turnstileScripts()
@if($isOpen)
<div class="fixed inset-0 flex justify-center items-center z-50 bg-black bg-opacity-50">
<div wire:click="closeModal" class="relative p-4 flex justify-center items-center flex-col w-full">
Expand Down
23 changes: 23 additions & 0 deletions src/FormsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Fuelviews\Forms\Contracts\FormsHandlerService;
use Fuelviews\Forms\Http\Controllers\FormsSubmitController;
use Fuelviews\Forms\Livewire\FormsModal;
use Fuelviews\Forms\Middleware\HandleFbclid;
use Fuelviews\Forms\Middleware\HandleGclid;
use Fuelviews\Forms\Middleware\HandleUtm;
use Fuelviews\Forms\Services\FormsSubmitService;
Expand Down Expand Up @@ -44,11 +45,15 @@ public function packageBooted(): void
Livewire::component('forms-modal', FormsModal::class);
}

// Merge Turnstile configuration to services.turnstile for compatibility with ryangjchandler/laravel-cloudflare-turnstile
$this->mergeTurnstileConfig();

$this->app->extend(Application::class, function (Application $app) {
if (method_exists($app, 'configureMiddleware')) {
$app->configureMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('web', [
HandleGclid::class,
HandleFbclid::class,
HandleUtm::class,
]);
});
Expand All @@ -58,6 +63,7 @@ public function packageBooted(): void

foreach ([
HandleGclid::class,
HandleFbclid::class,
HandleUtm::class,
] as $middleware) {
$kernel->appendMiddlewareToGroup('web', $middleware);
Expand Down Expand Up @@ -87,4 +93,21 @@ private function providerIsLoaded($app, $providerClass): bool
{
return collect($app->getLoadedProviders())->has($providerClass);
}

private function mergeTurnstileConfig(): void
{
// Merge Turnstile configuration from forms.turnstile to services.turnstile
// This ensures compatibility with the ryangjchandler/laravel-cloudflare-turnstile package

// Use the TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY env vars directly
// with fallback to the test keys if not set
$siteKey = env('TURNSTILE_SITE_KEY', '1x00000000000000000000AA');
$secretKey = env('TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA');

// Set the services.turnstile config that the Turnstile package expects
config([
'services.turnstile.key' => $siteKey,
'services.turnstile.secret' => $secretKey,
]);
}
}
10 changes: 10 additions & 0 deletions src/Http/Controllers/FormsSubmitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public function __construct(

public function handleSubmit(Request $request)
{
// Validate Turnstile if it's enabled
if (config('forms.turnstile.enabled') && config('forms.turnstile.site_key')) {
$request->validate([
'cf-turnstile-response' => ['required', 'turnstile'],
], [
'cf-turnstile-response.required' => 'Please complete the security challenge.',
'cf-turnstile-response.turnstile' => 'Security challenge validation failed. Please try again.',
]);
}

$formKey = request()->input('form_key', 'default');
$rules = FormsValidationRuleService::getRulesForDefault($formKey);
$result = $this->formProcessingService->processForm($request, $request->validate($rules));
Expand Down
18 changes: 18 additions & 0 deletions src/Livewire/FormsModal.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
public $submitClicked;

public $gclid;
public $fbclid;

public $utmCampaign;

Expand All @@ -61,6 +62,8 @@

public $location;

public $turnstileToken = '';

public function boot(FormsHandlerService $formHandler, FormsProcessingService $formProcessingService, FormsValidationRuleService $validationRuleService)
{
$this->formHandler = $formHandler;
Expand All @@ -72,6 +75,7 @@ public function mount()
{
$this->loadInitialData([
'gclid',
'fbclid',
'utmSource' => 'utm_source',
'utmMedium' => 'utm_medium',
'utmCampaign' => 'utm_campaign',
Expand All @@ -92,11 +96,25 @@ public function openModal()
*/
public function nextStep()
{
// Validate Turnstile on step 2 if enabled
if ($this->step === 2 && config('forms.turnstile.enabled') && config('forms.turnstile.site_key')) {
$this->validate([
'turnstileToken' => ['required', 'turnstile'],
], [
'turnstileToken.required' => 'Please complete the security challenge.',
'turnstileToken.turnstile' => 'Security challenge validation failed. Please try again.',
]);
}

if ($this->isLastStep($this->step)) {
$this->isLoading = true;
}

$validatedData = $this->validateStepData();

// Remove turnstileToken from validated data so it's not stored
unset($validatedData['turnstileToken']);

$this->formData = array_merge($validatedData, $this->formData);

if ($this->step < $this->totalSteps) {
Expand Down
24 changes: 24 additions & 0 deletions src/Middleware/HandleFbclid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Fuelviews\Forms\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;

class HandleFbclid
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);

if ($fbclid = $request->query('fbclid')) {
$cookie = Cookie::make('fbclid', $fbclid, 60 * 24 * 30); // 30 days
$response->cookie($cookie);

$request->session()->put('fbclid', $fbclid);
}

return $response;
}
}
2 changes: 2 additions & 0 deletions src/Services/FormsValidationRuleService.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public static function getRulesForDefault($formKey): array
'gotcha' => 'nullable|string',
'submitClicked' => 'nullable',
'gclid' => 'nullable|string',
'fbclid' => 'nullable|string',
'utmSource' => 'nullable|string',
'utmMedium' => 'nullable|string',
'utmCampaign' => 'nullable|string',
Expand All @@ -42,6 +43,7 @@ public function getRulesForStep($step): array
'gotcha' => 'nullable|string',
'submitClicked' => 'nullable',
'gclid' => 'nullable',
'fbclid' => 'nullable',
'utmSource' => 'nullable|string',
'utmMedium' => 'nullable|string',
'utmCampaign' => 'nullable|string',
Expand Down
Loading