PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / widgets / Form_Builder / helpers / Spam_Protection.php

Spam_Protection.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/widgets/Form_Builder/helpers/Spam_Protection.php

267 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Spam protection for Form Builder.
4 *
5 * Honeypot, Cloudflare Turnstile and hCaptcha, alongside the reCAPTCHA v3 field
6 * the widget already had.
7 *
8 * @package King_Addons
9 */
10
11 namespace King_Addons;
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 /**
18 * Renders the chosen challenge and checks it on the server.
19 */
20 class Form_Spam_Protection
21 {
22 /**
23 * Name of the honeypot input.
24 *
25 * Deliberately plausible: a bot filling in everything it recognises will
26 * fill this one too.
27 */
28 public const HONEYPOT_FIELD = 'king_addons_website_url';
29
30 /**
31 * Modes offered in the panel.
32 *
33 * @return array<string,string>
34 */
35 public static function modes(): array
36 {
37 return [
38 'none' => esc_html__('None', 'king-addons'),
39 'honeypot' => esc_html__('Honeypot (no third party)', 'king-addons'),
40 'turnstile' => esc_html__('Cloudflare Turnstile', 'king-addons'),
41 'hcaptcha' => esc_html__('hCaptcha', 'king-addons'),
42 ];
43 }
44
45 /**
46 * The mode stored for a form.
47 *
48 * @param string $form_id Elementor element id of the form.
49 *
50 * @return string
51 */
52 public static function get_mode(string $form_id): string
53 {
54 $mode = (string) get_option('king_addons_spam_protection_' . $form_id, 'none');
55
56 return array_key_exists($mode, self::modes()) ? $mode : 'none';
57 }
58
59 /**
60 * Store the mode for a form.
61 *
62 * @param string $form_id Elementor element id.
63 * @param string $mode Chosen mode.
64 *
65 * @return void
66 */
67 public static function save_mode(string $form_id, string $mode): void
68 {
69 if (!array_key_exists($mode, self::modes())) {
70 $mode = 'none';
71 }
72
73 update_option('king_addons_spam_protection_' . $form_id, $mode);
74 }
75
76 /**
77 * Markup for the chosen challenge.
78 *
79 * @param string $mode Mode.
80 *
81 * @return string
82 */
83 public static function render(string $mode): string
84 {
85 switch ($mode) {
86 case 'honeypot':
87 // Hidden from people with CSS and from screen readers, but a
88 // normal input as far as an automated filler is concerned.
89 return '<div class="king-addons-form-honeypot" aria-hidden="true">'
90 . '<label>' . esc_html__('Leave this field empty', 'king-addons')
91 . '<input type="text" name="' . esc_attr(self::HONEYPOT_FIELD) . '"'
92 . ' class="king-addons-form-honeypot__input" tabindex="-1" autocomplete="off" value=""></label>'
93 . '</div>';
94
95 case 'turnstile':
96 $site_key = (string) get_option('king_addons_turnstile_site_key', '');
97 if ('' === $site_key) {
98 return self::missing_keys_notice(esc_html__('Cloudflare Turnstile', 'king-addons'));
99 }
100
101 return '<div class="king-addons-form-captcha cf-turnstile" data-ka-captcha="turnstile"'
102 . ' data-sitekey="' . esc_attr($site_key) . '"></div>';
103
104 case 'hcaptcha':
105 $site_key = (string) get_option('king_addons_hcaptcha_site_key', '');
106 if ('' === $site_key) {
107 return self::missing_keys_notice(esc_html__('hCaptcha', 'king-addons'));
108 }
109
110 return '<div class="king-addons-form-captcha h-captcha" data-ka-captcha="hcaptcha"'
111 . ' data-sitekey="' . esc_attr($site_key) . '"></div>';
112 }
113
114 return '';
115 }
116
117 /**
118 * The script a mode needs, if any.
119 *
120 * @param string $mode Mode.
121 *
122 * @return array{handle:string,src:string}|null
123 */
124 public static function script(string $mode): ?array
125 {
126 if ('turnstile' === $mode) {
127 return [
128 'handle' => 'king-addons-turnstile',
129 'src' => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
130 ];
131 }
132
133 if ('hcaptcha' === $mode) {
134 return [
135 'handle' => 'king-addons-hcaptcha',
136 'src' => 'https://js.hcaptcha.com/1/api.js',
137 ];
138 }
139
140 return null;
141 }
142
143 /**
144 * Check the submitted challenge.
145 *
146 * Called by every submit action, because each one is its own AJAX request:
147 * checking only in the browser would leave the endpoints open.
148 *
149 * @param string $form_id Form element id.
150 * @param array<string,mixed> $request Request data ($_POST).
151 *
152 * @return true|string True when the submission may proceed, otherwise the reason.
153 */
154 public static function verify(string $form_id, array $request)
155 {
156 $mode = self::get_mode($form_id);
157
158 if ('none' === $mode) {
159 return true;
160 }
161
162 if ('honeypot' === $mode) {
163 $value = isset($request[self::HONEYPOT_FIELD])
164 ? trim((string) wp_unslash($request[self::HONEYPOT_FIELD]))
165 : '';
166
167 return '' === $value ? true : 'honeypot';
168 }
169
170 $token = isset($request['ka_captcha_token'])
171 ? sanitize_text_field(wp_unslash($request['ka_captcha_token']))
172 : '';
173
174 if ('' === $token) {
175 return 'missing-token';
176 }
177
178 if ('turnstile' === $mode) {
179 return self::verify_remote(
180 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
181 (string) get_option('king_addons_turnstile_secret_key', ''),
182 $token
183 );
184 }
185
186 return self::verify_remote(
187 'https://hcaptcha.com/siteverify',
188 (string) get_option('king_addons_hcaptcha_secret_key', ''),
189 $token
190 );
191 }
192
193 /**
194 * Ask the provider whether a token is good.
195 *
196 * @param string $endpoint Provider endpoint.
197 * @param string $secret Secret key.
198 * @param string $token Token from the browser.
199 *
200 * @return true|string
201 */
202 private static function verify_remote(string $endpoint, string $secret, string $token)
203 {
204 if ('' === $secret) {
205 return 'no-secret-key';
206 }
207
208 $body = [
209 'secret' => $secret,
210 'response' => $token,
211 ];
212
213 $ip = self::client_ip();
214 if ('' !== $ip) {
215 $body['remoteip'] = $ip;
216 }
217
218 $response = wp_remote_post($endpoint, [
219 'timeout' => 10,
220 'body' => $body,
221 ]);
222
223 if (is_wp_error($response)) {
224 return 'verification-failed';
225 }
226
227 $data = json_decode((string) wp_remote_retrieve_body($response), true);
228
229 return (is_array($data) && !empty($data['success'])) ? true : 'rejected';
230 }
231
232 /**
233 * The visitor's address, when the server reports one it is safe to trust.
234 *
235 * @return string
236 */
237 private static function client_ip(): string
238 {
239 // Only REMOTE_ADDR: forwarded headers can be set by the client.
240 $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) wp_unslash($_SERVER['REMOTE_ADDR']) : '';
241
242 return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '';
243 }
244
245 /**
246 * Shown in place of a challenge whose keys are not set up yet.
247 *
248 * @param string $provider Provider name.
249 *
250 * @return string
251 */
252 private static function missing_keys_notice(string $provider): string
253 {
254 if (!current_user_can('manage_options')) {
255 return '';
256 }
257
258 return '<p class="king-addons-form-captcha-notice">' . sprintf(
259 /* translators: 1: provider name, 2: settings URL. */
260 esc_html__('%1$s is selected but its keys are missing. Add them under %2$s.', 'king-addons'),
261 esc_html($provider),
262 '<a href="' . esc_url(admin_url('admin.php?page=king-addons-settings')) . '">'
263 . esc_html__('King Addons settings', 'king-addons') . '</a>'
264 ) . '</p>';
265 }
266 }
267