PluginProbe
HTML Forms – Simple WordPress Forms Plugin / trunk
HTML Forms – Simple WordPress Forms Plugin vtrunk
trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0 1.3.0 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 All 66 releases
html-forms / src / admin / class-recaptcha.php

class-recaptcha.php in HTML Forms – Simple WordPress Forms Plugin trunk, at src/admin/class-recaptcha.php

303 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace HTML_Forms\Admin;
4
5 class Recaptcha {
6 private $settings;
7 private static $scripts_enqueued = false;
8
9 public function __construct() {
10 if ( function_exists( 'get_option' ) ) {
11 $this->settings = hf_get_settings();
12 } else {
13 $this->settings = array(
14 'google_recaptcha' => array(
15 'site_key' => '',
16 'secret_key' => '',
17 )
18 );
19 }
20 }
21
22 public function hook() {
23 if ( is_admin() ) {
24 add_action( 'hf_admin_output_form_messages', array( $this, 'output_recaptcha_message_fields' ) );
25 }
26
27 if ( $this->is_configured() ) {
28 add_filter( 'hf_ignored_field_names', array( $this, 'ignored_fields' ) );
29 add_filter( 'hf_validate_form', array( $this, 'validate_recaptcha' ), 10, 3 );
30 add_filter( 'hf_form_markup', array( $this, 'add_recaptcha_to_form' ), 10, 2 );
31 add_filter( 'hf_form_html', array( $this, 'enqueue_recaptcha_on_form_render' ), 10, 2 );
32
33 add_action( 'admin_notices', array( $this, 'show_admin_notice' ) );
34 }
35 }
36
37 /**
38 * Check if reCAPTCHA is properly configured
39 *
40 * @return bool
41 */
42 private function is_configured() {
43 $site_key = ! empty( $this->settings['google_recaptcha']['site_key'] );
44 $secret_key = ! empty( $this->settings['google_recaptcha']['secret_key'] );
45 $configured = $site_key && $secret_key;
46
47 return $configured;
48 }
49
50 /**
51 * Add reCAPTCHA response fields to ignored fields list
52 *
53 * @param array $ignored_fields
54 * @return array
55 */
56 public function ignored_fields( $ignored_fields ) {
57 $ignored_fields[] = 'g-recaptcha-response';
58 $ignored_fields[] = 'g-recaptcha-failed';
59
60 return $ignored_fields;
61 }
62
63 /**
64 * Enqueue reCAPTCHA scripts when a form is being rendered
65 *
66 * @param string $html
67 * @param \HTML_Forms\Form $form
68 * @return string
69 */
70 public function enqueue_recaptcha_on_form_render( $html, $form ) {
71 // Only enqueue scripts once per page load
72 if ( self::$scripts_enqueued ) {
73 return $html;
74 }
75
76 // Skip if we're in admin area
77 if ( is_admin() ) {
78 return $html;
79 }
80
81 $this->enqueue_recaptcha_script();
82 self::$scripts_enqueued = true;
83
84 return $html;
85 }
86
87 /**
88 * Enqueue Google reCAPTCHA v3 script
89 */
90 public function enqueue_recaptcha_script() {
91 $site_key = $this->settings['google_recaptcha']['site_key'];
92
93 // Enqueue Google reCAPTCHA v3 API
94 wp_enqueue_script(
95 'google-recaptcha-v3',
96 "https://www.google.com/recaptcha/api.js?render={$site_key}",
97 array(),
98 null,
99 true
100 );
101
102 // Enqueue reCAPTCHA integration script
103 wp_enqueue_script(
104 'html-forms-recaptcha',
105 plugins_url( 'assets/js/recaptcha.js', $this->get_plugin_file() ),
106 array( 'google-recaptcha-v3', 'html-forms' ),
107 HTML_FORMS_VERSION,
108 true
109 );
110
111 wp_localize_script( 'html-forms-recaptcha', 'hf_recaptcha', array(
112 'site_key' => $site_key,
113 ) );
114 }
115
116 /**
117 * Get the plugin file path
118 *
119 * @return string
120 */
121 private function get_plugin_file() {
122 return dirname( dirname( __DIR__ ) ) . '/html-forms.php';
123 }
124
125 /**
126 * Add reCAPTCHA comment to form markup
127 *
128 * @param string $markup
129 * @param \HTML_Forms\Form $form
130 * @return string
131 */
132 public function add_recaptcha_to_form( $markup, $form ) {
133 $recaptcha_notice = "\n<!-- Google reCAPTCHA v3 active on this form -->\n";
134
135 return $recaptcha_notice . $markup;
136 }
137
138 /**
139 * Validate reCAPTCHA response
140 *
141 * @param string $error_code
142 * @param \HTML_Forms\Form $form
143 * @param array $data
144 * @return string
145 */
146 public function validate_recaptcha( $error_code, $form, $data ) {
147 // If there's already an error, don't proceed
148 if ( ! empty( $error_code ) ) {
149 return $error_code;
150 }
151
152 // Get reCAPTCHA response from form data
153 $recaptcha_response = isset( $data['g-recaptcha-response'] ) ? $data['g-recaptcha-response'] : '';
154 $recaptcha_failed = isset( $data['g-recaptcha-failed'] ) ? $data['g-recaptcha-failed'] : '';
155
156 // Check if reCAPTCHA execution failed on the client side
157 if ( ! empty( $recaptcha_failed ) ) {
158 $this->log_debug( 'reCAPTCHA execution failed on client side', $form );
159 return 'recaptcha_failed';
160 }
161
162 if ( empty( $recaptcha_response ) ) {
163 $this->log_debug( 'reCAPTCHA token missing from form submission', $form );
164 return 'recaptcha_failed';
165 }
166
167 // Basic token format validation (should be a long string)
168 if ( strlen( $recaptcha_response ) < 20 ) {
169 $this->log_debug( 'reCAPTCHA token appears to be invalid (too short): ' . $recaptcha_response, $form );
170 return 'recaptcha_failed';
171 }
172
173 // Verify reCAPTCHA with Google
174 $verification_result = $this->verify_recaptcha( $recaptcha_response );
175
176 if ( ! $verification_result['success'] ) {
177 $error_codes = isset( $verification_result['error-codes'] ) ? implode( ', ', $verification_result['error-codes'] ) : 'unknown';
178 $this->log_debug( 'reCAPTCHA verification failed with error codes: ' . $error_codes, $form );
179
180 // Check for specific error codes that indicate token reuse or timeout
181 if ( isset( $verification_result['error-codes'] ) && is_array( $verification_result['error-codes'] ) ) {
182 $error_codes_array = $verification_result['error-codes'];
183 if ( in_array( 'timeout-or-duplicate', $error_codes_array ) || in_array( 'invalid-input-response', $error_codes_array ) ) {
184 $this->log_debug( 'reCAPTCHA token was reused or expired', $form );
185 }
186 }
187
188 return 'recaptcha_failed';
189 }
190
191 // Check score
192 $min_score = apply_filters( 'hf_recaptcha_min_score', 0.5, $form );
193
194 if ( isset( $verification_result['score'] ) && $verification_result['score'] < $min_score ) {
195 $this->log_debug( sprintf( 'reCAPTCHA score %.2f below minimum %.2f', $verification_result['score'], $min_score ), $form );
196 return 'recaptcha_low_score';
197 }
198
199 return $error_code;
200 }
201
202 /**
203 * Verify reCAPTCHA response with Google's API
204 *
205 * @param string $response
206 * @return array
207 */
208 private function verify_recaptcha( $response ) {
209 $secret_key = $this->settings['google_recaptcha']['secret_key'];
210 $remote_ip = $_SERVER['REMOTE_ADDR'] ?? '';
211
212 $url = 'https://www.google.com/recaptcha/api/siteverify';
213 $data = array(
214 'secret' => $secret_key,
215 'response' => $response,
216 'remoteip' => $remote_ip,
217 );
218
219 $response = wp_remote_post( $url, array(
220 'body' => $data,
221 'timeout' => 10,
222 ) );
223
224 if ( is_wp_error( $response ) ) {
225 return array( 'success' => false, 'error' => 'network_error' );
226 }
227
228 $body = wp_remote_retrieve_body( $response );
229 $result = json_decode( $body, true );
230
231 if ( ! $result ) {
232 return array( 'success' => false, 'error' => 'invalid_response' );
233 }
234
235 return $result;
236 }
237
238 /**
239 * Log debug information if WP_DEBUG is enabled
240 *
241 * @param string $message
242 * @param \HTML_Forms\Form $form
243 */
244 private function log_debug( $message, $form = null ) {
245 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
246 $log_message = '[HTML Forms reCAPTCHA] ' . $message;
247 if ( $form ) {
248 $log_message .= sprintf( ' (Form: "%s", ID: %d)', $form->title, $form->ID );
249 }
250 error_log( $log_message );
251 }
252 }
253
254 /**
255 * Output reCAPTCHA message fields in the Messages tab
256 *
257 * @param \HTML_Forms\Form $form
258 */
259 public function output_recaptcha_message_fields( $form ) {
260 // Only show these fields if reCAPTCHA is configured
261 if ( ! $this->is_configured() ) {
262 return;
263 }
264 ?>
265 <tr valign="top">
266 <th scope="row" colspan="2" class="hf-settings-header"><?php echo __( 'Google reCAPTCHA v3', 'html-forms' ); ?></th>
267 </tr>
268
269 <tr valign="top">
270 <th scope="row"><label for="hf_form_recaptcha_failed"><?php _e( 'reCAPTCHA Failed', 'html-forms' ); ?></label></th>
271 <td>
272 <input type="text" class="widefat" id="hf_form_recaptcha_failed" name="form[messages][recaptcha_failed]" value="<?php echo esc_attr( $form->messages['recaptcha_failed'] ); ?>" required />
273 <p class="description"><?php _e( 'The text that shows when reCAPTCHA verification fails.', 'html-forms' ); ?></p>
274 </td>
275 </tr>
276
277 <tr valign="top">
278 <th scope="row"><label for="hf_form_recaptcha_low_score"><?php _e( 'reCAPTCHA Low Score', 'html-forms' ); ?></label></th>
279 <td>
280 <input type="text" class="widefat" id="hf_form_recaptcha_low_score" name="form[messages][recaptcha_low_score]" value="<?php echo esc_attr( $form->messages['recaptcha_low_score'] ); ?>" required />
281 <p class="description"><?php _e( 'The text that shows when a submission appears to be spam based on reCAPTCHA score.', 'html-forms' ); ?></p>
282 </td>
283 </tr>
284 <?php
285 }
286
287 /**
288 * Show admin notice when reCAPTCHA is active
289 */
290 public function show_admin_notice() {
291 // Only show on HTML Forms pages, not every admin page
292 if ( empty( $_GET['page'] ) || $_GET['page'] !== 'html-forms' || empty( $_GET['form_id'] ) ) {
293 return;
294 }
295
296 echo '<div class="notice notice-success" data-notice="hf-recaptcha">';
297 echo '<p><span class="dashicons dashicons-shield" style="color:#46b450;"></span> <strong>' . __( 'Google reCAPTCHA v3 is enabled on this form.', 'html-forms' ) . '</strong> ';
298 echo __( 'Submissions will be automatically protected from spam and abuse.', 'html-forms' );
299 echo ' <a href="' . admin_url( 'admin.php?page=html-forms-settings' ) . '">' . __( 'View settings', 'html-forms' ) . '</a>.</p>';
300 echo '</div>';
301 }
302 }
303