├── .php-cs-fixer.php ├── README.md ├── SECURITY.md ├── composer.json ├── phpunit.xml ├── resources └── lang │ └── en │ └── recaptcha.php └── src ├── Exceptions └── LivewireRecaptchaException.php ├── LivewireRecaptcha.php ├── LivewireRecaptchaServiceProvider.php ├── ValidatesRecaptcha.php ├── directive.recaptcha.v2.blade.php └── directive.recaptcha.v3.blade.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | true, 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'binary_operator_spaces' => [ 10 | 'default' => 'single_space', 11 | // 'operators' => ['=>' => null], // single space makes code look more coherent in style. But sometimes it is not beter, in that case, manually override. 12 | ], 13 | 'blank_line_after_namespace' => true, 14 | 'blank_line_after_opening_tag' => true, 15 | 'blank_line_before_statement' => [ 16 | 'statements' => ['return'], 17 | ], 18 | 'braces' => true, 19 | 'cast_spaces' => true, 20 | 'class_attributes_separation' => [ 21 | 'elements' => [ 22 | 'const' => 'only_if_meta', 23 | 'method' => 'one', 24 | 'property' => 'one', 25 | 'trait_import' => 'none', 26 | ], 27 | ], 28 | 'class_definition' => [ 29 | 'multi_line_extends_each_single_line' => true, 30 | 'single_item_single_line' => true, 31 | 'single_line' => true, 32 | ], 33 | 'concat_space' => [ 34 | 'spacing' => 'none', 35 | ], 36 | 'constant_case' => ['case' => 'lower'], 37 | 'declare_equal_normalize' => true, 38 | 'elseif' => true, 39 | 'encoding' => true, 40 | 'full_opening_tag' => true, 41 | 'fully_qualified_strict_types' => false, 42 | // added by Shift 43 | 'function_declaration' => true, 44 | 'function_typehint_space' => true, 45 | 'general_phpdoc_tag_rename' => true, 46 | 'heredoc_to_nowdoc' => true, 47 | 'include' => true, 48 | 'increment_style' => ['style' => 'post'], 49 | 'indentation_type' => true, 50 | 'linebreak_after_opening_tag' => true, 51 | 'line_ending' => true, 52 | 'lowercase_cast' => true, 53 | 'lowercase_keywords' => true, 54 | 'lowercase_static_reference' => true, 55 | // added from Symfony 56 | 'magic_method_casing' => true, 57 | // added from Symfony 58 | 'magic_constant_casing' => true, 59 | 'method_argument_space' => [ 60 | 'on_multiline' => 'ignore', 61 | ], 62 | 'multiline_whitespace_before_semicolons' => [ 63 | 'strategy' => 'no_multi_line', 64 | ], 65 | 'native_function_casing' => true, 66 | 'no_alias_functions' => true, 67 | 'no_extra_blank_lines' => [ 68 | 'tokens' => [ 69 | 'extra', 70 | 'throw', 71 | 'use', 72 | 'switch', 73 | 'case', 74 | 'default', 75 | ], 76 | ], 77 | 'no_blank_lines_after_class_opening' => true, 78 | 'no_blank_lines_after_phpdoc' => true, 79 | 'no_closing_tag' => true, 80 | 'no_empty_phpdoc' => true, 81 | 'no_empty_statement' => true, 82 | 'no_leading_import_slash' => true, 83 | 'no_leading_namespace_whitespace' => true, 84 | 'no_mixed_echo_print' => [ 85 | 'use' => 'echo', 86 | ], 87 | 'no_multiline_whitespace_around_double_arrow' => true, 88 | 'no_short_bool_cast' => true, 89 | 'no_singleline_whitespace_before_semicolons' => true, 90 | 'no_spaces_after_function_name' => true, 91 | 'no_spaces_around_offset' => [ 92 | 'positions' => [ 93 | 'inside', 94 | 'outside', 95 | ], 96 | ], 97 | 'no_spaces_inside_parenthesis' => true, 98 | 'no_trailing_comma_in_list_call' => true, 99 | 'no_trailing_comma_in_singleline_array' => true, 100 | 'no_trailing_whitespace' => true, 101 | 'no_trailing_whitespace_in_comment' => true, 102 | 'no_unneeded_control_parentheses' => [ 103 | 'statements' => [ 104 | 'break', 105 | 'clone', 106 | 'continue', 107 | 'echo_print', 108 | 'return', 109 | 'switch_case', 110 | 'yield', 111 | ], 112 | ], 113 | 'no_unreachable_default_argument_value' => true, 114 | 'no_useless_return' => true, 115 | 'no_whitespace_before_comma_in_array' => true, 116 | 'no_whitespace_in_blank_line' => true, 117 | 'normalize_index_brace' => true, 118 | 'not_operator_with_successor_space' => true, 119 | 'object_operator_without_whitespace' => true, 120 | 'ordered_imports' => [ 121 | 'sort_algorithm' => 'alpha', 122 | 'imports_order' => [ 123 | 'class', 124 | 'function', 125 | 'const', 126 | ], 127 | ], 128 | 'psr_autoloading' => true, 129 | 'phpdoc_indent' => true, 130 | 'phpdoc_inline_tag_normalizer' => true, 131 | 'phpdoc_no_access' => true, 132 | 'phpdoc_no_package' => true, 133 | 'phpdoc_no_useless_inheritdoc' => true, 134 | 'phpdoc_scalar' => true, 135 | 'phpdoc_single_line_var_spacing' => true, 136 | 'phpdoc_summary' => false, 137 | 'phpdoc_to_comment' => false, 138 | // override to preserve user preference 139 | 'phpdoc_tag_type' => true, 140 | 'phpdoc_trim' => true, 141 | 'phpdoc_types' => true, 142 | 'phpdoc_var_without_name' => true, 143 | 'self_accessor' => true, 144 | 'short_scalar_cast' => true, 145 | 'simplified_null_return' => false, 146 | // disabled as "risky" 147 | 'single_blank_line_at_eof' => true, 148 | 'single_blank_line_before_namespace' => true, 149 | 'single_class_element_per_statement' => [ 150 | 'elements' => [ 151 | 'const', 152 | 'property', 153 | ], 154 | ], 155 | 'single_import_per_statement' => true, 156 | 'single_line_after_imports' => true, 157 | 'single_line_comment_style' => [ 158 | 'comment_types' => ['hash'], 159 | ], 160 | 'single_quote' => true, 161 | 'space_after_semicolon' => true, 162 | 'standardize_not_equals' => true, 163 | 'switch_case_semicolon_to_colon' => true, 164 | 'switch_case_space' => true, 165 | 'ternary_operator_spaces' => true, 166 | 'trailing_comma_in_multiline' => [ 167 | 'elements' => [ 168 | 'arrays', 169 | 'parameters', 170 | ], 171 | ], 172 | 'trim_array_spaces' => true, 173 | 'types_spaces' => [ 174 | 'space' => 'single', 175 | ], 176 | 'unary_operator_spaces' => true, 177 | 'visibility_required' => [ 178 | 'elements' => [ 179 | 'method', 180 | 'property', 181 | 'const', 182 | ], 183 | ], 184 | 'whitespace_after_comma_in_array' => true, 185 | 186 | // DCC 187 | 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], 188 | 'simplified_if_return' => true, 189 | 'method_chaining_indentation' => true, 190 | ]; 191 | 192 | $finder = Finder::create() 193 | ->in([ 194 | __DIR__ . '/src', 195 | __DIR__ . '/tests', 196 | ]) 197 | ->name('*.php') 198 | ->notName('*.blade.php') 199 | ->ignoreDotFiles(true) 200 | ->ignoreVCS(true); 201 | 202 | return (new Config) 203 | ->setFinder($finder) 204 | ->setRules($rules) 205 | ->setRiskyAllowed(true) 206 | ->setUsingCache(true); 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livewire ReCAPTCHA v3/v2/v2-invisible 2 | 3 | [](https://packagist.org/packages/dutchcodingcompany/livewire-recaptcha) 4 | [](https://github.com/dutchcodingcompany/livewire-recaptcha/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [](https://github.com/dutchcodingcompany/livewire-recaptcha/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 6 | [](https://github.com/DutchCodingCompany/livewire-recaptcha/actions?query=workflow%3APHPStan++branch%3Amain) 7 | [](https://packagist.org/packages/dutchcodingcompany/livewire-recaptcha) 8 | 9 | This package provides a custom Livewire directive to protect your Livewire functions with a _Google reCAPTCHA (v2 + v2 10 | invisible + v3)_ check. 11 | 12 | ## Installation 13 | 14 | ```shell 15 | composer require dutchcodingcompany/livewire-recaptcha 16 | ``` 17 | 18 | ## Configuration 19 | 20 | Read https://developers.google.com/recaptcha/intro on how to create your own key pair for the specific ReCaptcha 21 | version you are going to implement. 22 | 23 | This package supports the following versions. Note that each version requires a different sitekey/secretkey pair: 24 | 25 | | **Version** | **Docs** | **Notes** | 26 | |----------------------|-------------------------------------------------------------------|-----------------------------| 27 | | **v3** (recommended) | [V3 Docs](https://developers.google.com/recaptcha/docs/v3) | | 28 | | **v2** | [V2 Docs](https://developers.google.com/recaptcha/docs/display) | | 29 | | **v2 invisible** | [V2 Docs](https://developers.google.com/recaptcha/docs/invisible) | Use `'size' => 'invisible'` | 30 | 31 | Your options should reside in the `config/services.php` file: 32 | 33 | ```php 34 | // V3 config: 35 | 'google' => [ 36 | 'recaptcha' => [ 37 | 'site_key' => env('GOOGLE_RECAPTCHA_SITE_KEY'), 38 | 'secret_key' => env('GOOGLE_RECAPTCHA_SECRET_KEY'), 39 | 'version' => 'v3', 40 | 'score' => 0.5, // An integer between 0 and 1, that indicates the minimum score to pass the Captcha challenge. 41 | ], 42 | ], 43 | 44 | // V2 config: 45 | 'google' => [ 46 | 'recaptcha' => [ 47 | 'site_key' => env('GOOGLE_RECAPTCHA_SITE_KEY'), 48 | 'secret_key' => env('GOOGLE_RECAPTCHA_SECRET_KEY'), 49 | 'version' => 'v2', 50 | 'size' => 'normal', // 'normal', 'compact' or 'invisible'. 51 | 'theme' => 'light', // 'light' or 'dark'. 52 | ], 53 | ], 54 | ``` 55 | 56 | #### Component 57 | 58 | In your Livewire component, at your form submission method, add the `#[ValidatesRecaptcha]` attribute: 59 | 60 | ```php 61 | use Livewire\Component; 62 | use DutchCodingCompany\LivewireRecaptcha\ValidatesRecaptcha; 63 | 64 | class SomeComponent extends Component 65 | { 66 | // (optional) Set a response property on your component. 67 | // If not given, the `gRecaptchaResponse` property is dynamically assigned. 68 | public string $gRecaptchaResponse; 69 | 70 | #[ValidatesRecaptcha] 71 | public function save(): mixed 72 | { 73 | // Your logic here will only be called if the captcha passes... 74 | } 75 | } 76 | ``` 77 | 78 | For fine-grained control, you can pass a custom secret key and minimum score (applies only to V3) using: 79 | 80 | ```php 81 | #[ValidatesRecaptcha(secretKey: 'mysecretkey', score: 0.9)] 82 | ``` 83 | 84 | #### View 85 | 86 | On the view side, you have to include the Blade directive `@livewireRecaptcha`. This adds two scripts to the page, 87 | one for the reCAPTCHA script and one for the custom Livewire directive to hook into the form submission. 88 | 89 | Preferrably these scripts are only added to the page that has the Captcha-protected form (alternatively, you can add 90 | the `@livewireRecaptcha` directive on a higher level, lets say your layout). 91 | 92 | Secondly, add the new directive `wire:recaptcha` to the form element that you want to protect. 93 | 94 | ```html 95 | 96 | 97 | 98 | @if($errors->has('gRecaptchaResponse')) 99 |