diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index b7a2b2e..35707bd 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -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' diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 3ffcba2..9f26dda 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -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 diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 636096d..428276e 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.head_ref }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6475b35..839a47e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -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 diff --git a/README.md b/README.md index bec13f6..cd7d2c2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Laravel forms package -Laravel forms package +Laravel forms package with built-in spam protection and optional Cloudflare Turnstile support. ## Installation @@ -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. @@ -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,}" @@ -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,}" @@ -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') @@ -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" @@ -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"> @@ -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', ] ``` diff --git a/composer.json b/composer.json index ad08917..7a4ea3f 100644 --- a/composer.json +++ b/composer.json @@ -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", @@ -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": { diff --git a/config/forms.php b/config/forms.php index fb204cd..0815e99 100644 --- a/config/forms.php +++ b/config/forms.php @@ -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'), + ], ]; diff --git a/resources/views/components/steps/step-one.blade.php b/resources/views/components/steps/step-one.blade.php index 2491e2f..6719037 100644 --- a/resources/views/components/steps/step-one.blade.php +++ b/resources/views/components/steps/step-one.blade.php @@ -6,7 +6,7 @@
-
+
@foreach (config('forms.modal.steps.1.locations') as $location)
diff --git a/resources/views/components/steps/step-two.blade.php b/resources/views/components/steps/step-two.blade.php index ac9bd9b..84a9b25 100644 --- a/resources/views/components/steps/step-two.blade.php +++ b/resources/views/components/steps/step-two.blade.php @@ -69,6 +69,32 @@
+ + @if(config('forms.turnstile.enabled') && config('forms.turnstile.site_key')) +
+
+
+
+
+ @endif
diff --git a/resources/views/livewire/forms-modal.blade.php b/resources/views/livewire/forms-modal.blade.php index 4cbb8f0..7c42bf6 100644 --- a/resources/views/livewire/forms-modal.blade.php +++ b/resources/views/livewire/forms-modal.blade.php @@ -1,4 +1,5 @@
+ @turnstileScripts() @if($isOpen)
diff --git a/src/FormsServiceProvider.php b/src/FormsServiceProvider.php index de20dc8..92e5574 100644 --- a/src/FormsServiceProvider.php +++ b/src/FormsServiceProvider.php @@ -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; @@ -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, ]); }); @@ -58,6 +63,7 @@ public function packageBooted(): void foreach ([ HandleGclid::class, + HandleFbclid::class, HandleUtm::class, ] as $middleware) { $kernel->appendMiddlewareToGroup('web', $middleware); @@ -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, + ]); + } } diff --git a/src/Http/Controllers/FormsSubmitController.php b/src/Http/Controllers/FormsSubmitController.php index 318b1f7..ca0e332 100644 --- a/src/Http/Controllers/FormsSubmitController.php +++ b/src/Http/Controllers/FormsSubmitController.php @@ -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)); diff --git a/src/Livewire/FormsModal.php b/src/Livewire/FormsModal.php index 467ef43..29dba93 100644 --- a/src/Livewire/FormsModal.php +++ b/src/Livewire/FormsModal.php @@ -36,6 +36,7 @@ public $submitClicked; public $gclid; + public $fbclid; public $utmCampaign; @@ -61,6 +62,8 @@ public $location; + public $turnstileToken = ''; + public function boot(FormsHandlerService $formHandler, FormsProcessingService $formProcessingService, FormsValidationRuleService $validationRuleService) { $this->formHandler = $formHandler; @@ -72,6 +75,7 @@ public function mount() { $this->loadInitialData([ 'gclid', + 'fbclid', 'utmSource' => 'utm_source', 'utmMedium' => 'utm_medium', 'utmCampaign' => 'utm_campaign', @@ -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) { diff --git a/src/Middleware/HandleFbclid.php b/src/Middleware/HandleFbclid.php new file mode 100644 index 0000000..e34130e --- /dev/null +++ b/src/Middleware/HandleFbclid.php @@ -0,0 +1,24 @@ +query('fbclid')) { + $cookie = Cookie::make('fbclid', $fbclid, 60 * 24 * 30); // 30 days + $response->cookie($cookie); + + $request->session()->put('fbclid', $fbclid); + } + + return $response; + } +} diff --git a/src/Services/FormsValidationRuleService.php b/src/Services/FormsValidationRuleService.php index 2b6df0e..80782ad 100644 --- a/src/Services/FormsValidationRuleService.php +++ b/src/Services/FormsValidationRuleService.php @@ -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', @@ -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',