PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | app/Services/RecaptchaService.php +179 -118 3.0.2.7 → 3.0.16 View file →
@@ -1,10 +1,13 @@
1 1 <?php
2 2 /**
3 - * reCAPTCHA Service
4 - *
5 - * Handles Google reCAPTCHA verification
6 - *
3 + * reCAPTCHA Service (Google reCAPTCHA v3)
4 + *
5 + * Loads the v3 script (invisible, score-based), verifies tokens server-side
6 + * against a score threshold, and exposes per-form gating so operators can
7 + * choose which Yatra forms to protect. Previously this service rendered a v2
8 + * checkbox and was never wired into any form, so reCAPTCHA never actually ran.
9 + *
7 10 * @package Yatra\Services
8 11 * @since 3.0.0
9 12 */
10 13
@@ -13,173 +16,231 @@
13 16 namespace Yatra\Services;
14 17
15 18 class RecaptchaService
16 19 {
20 + /** Default v3 score threshold (0.0 = likely bot, 1.0 = likely human). */
21 + public const DEFAULT_SCORE_THRESHOLD = 0.5;
22 +
23 + /** Form key => the setting that toggles protection for that form. */
24 + private const FORM_SETTINGS = [
25 + 'enquiry' => 'recaptcha_protect_enquiry',
26 + 'booking' => 'recaptcha_protect_booking',
27 + 'registration' => 'recaptcha_protect_registration',
28 + ];
29 +
17 30 /**
18 - * Verify reCAPTCHA response
19 - *
20 - * @param string $response reCAPTCHA response token
21 - * @param string|null $remoteIp User's IP address
22 - * @return array Verification result
31 + * reCAPTCHA is usable only when enabled AND both keys are configured.
23 32 */
24 - public static function verify(string $response, ?string $remoteIp = null): array
33 + public static function isEnabled(): bool
25 34 {
26 - // Check if reCAPTCHA is enabled
35 + return SettingsService::isEnabled('recaptcha_enabled')
36 + && self::getSiteKey() !== ''
37 + && self::getSecretKey() !== '';
38 + }
39 +
40 + public static function getSiteKey(): string
41 + {
42 + return trim(SettingsService::getString('recaptcha_site_key', ''));
43 + }
44 +
45 + private static function getSecretKey(): string
46 + {
47 + return trim(SettingsService::getString('recaptcha_secret_key', ''));
48 + }
49 +
50 + /**
51 + * v3 score threshold, clamped to [0,1] and filterable.
52 + */
53 + public static function scoreThreshold(): float
54 + {
55 + $threshold = (float) SettingsService::getFloat('recaptcha_score_threshold', self::DEFAULT_SCORE_THRESHOLD);
56 + if ($threshold < 0.0 || $threshold > 1.0) {
57 + $threshold = self::DEFAULT_SCORE_THRESHOLD;
58 + }
59 +
60 + return (float) apply_filters('yatra_recaptcha_score_threshold', $threshold);
61 + }
62 +
63 + /**
64 + * Whether a given form (enquiry|booking|registration) is protected.
65 + */
66 + public static function protectsForm(string $form): bool
67 + {
68 + if (!self::isEnabled()) {
69 + return false;
70 + }
71 +
72 + $setting = self::FORM_SETTINGS[$form] ?? '';
73 + if ($setting === '') {
74 + return false;
75 + }
76 +
77 + return (bool) apply_filters(
78 + 'yatra_recaptcha_protects_form',
79 + SettingsService::isEnabled($setting),
80 + $form
81 + );
82 + }
83 +
84 + /**
85 + * Verify a token against a form. Returns success when the form is NOT
86 + * protected (no-op), so callers can always call this unconditionally.
87 + *
88 + * @return array{success:bool, message?:string, score?:float|null, action?:string|null}
89 + */
90 + public static function verifyForm(string $form, string $token, ?string $remoteIp = null): array
91 + {
92 + if (!self::protectsForm($form)) {
93 + return ['success' => true];
94 + }
95 +
96 + return self::verify($token, $form, $remoteIp);
97 + }
98 +
99 + /**
100 + * Verify a reCAPTCHA v3 token with Google (success + score + optional action).
101 + *
102 + * @param string $token The token from grecaptcha.execute().
103 + * @param string|null $expectedAction The action the token should carry.
104 + * @param string|null $remoteIp Client IP.
105 + * @return array{success:bool, message?:string, score?:float|null, action?:string|null}
106 + */
107 + public static function verify(string $token, ?string $expectedAction = null, ?string $remoteIp = null): array
108 + {
27 109 if (!SettingsService::isEnabled('recaptcha_enabled')) {
28 - return [
29 - 'success' => true,
30 - 'message' => 'reCAPTCHA is disabled'
31 - ];
110 + return ['success' => true, 'message' => 'reCAPTCHA is disabled'];
32 111 }
33 -
34 - $secret_key = SettingsService::getString('recaptcha_secret_key', '');
35 -
36 - if (empty($secret_key)) {
37 - return [
38 - 'success' => false,
39 - 'message' => __('reCAPTCHA secret key is not configured', 'yatra')
40 - ];
112 +
113 + $secret_key = self::getSecretKey();
114 + if ($secret_key === '') {
115 + return ['success' => false, 'message' => __('reCAPTCHA is not fully configured.', 'yatra')];
41 116 }
42 -
43 - if (empty($response)) {
44 - return [
45 - 'success' => false,
46 - 'message' => __('Please complete the reCAPTCHA verification', 'yatra')
47 - ];
117 +
118 + $token = trim($token);
119 + if ($token === '') {
120 + return ['success' => false, 'message' => __('reCAPTCHA verification failed. Please try again.', 'yatra')];
48 121 }
49 -
50 - // Prepare verification request
51 - $verify_url = 'https://www.google.com/recaptcha/api/siteverify';
122 +
52 123 $data = [
53 - 'secret' => $secret_key,
54 - 'response' => $response,
124 + 'secret' => $secret_key,
125 + 'response' => $token,
55 126 ];
56 -
57 127 if ($remoteIp) {
58 128 $data['remoteip'] = $remoteIp;
59 129 }
60 -
61 - // Send verification request
62 - $response = wp_remote_post($verify_url, [
63 - 'body' => $data,
130 +
131 + $http = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
132 + 'body' => $data,
64 133 'timeout' => 10,
65 134 ]);
66 -
67 - if (is_wp_error($response)) {
135 +
136 + if (is_wp_error($http)) {
137 + return ['success' => false, 'message' => __('Could not reach the reCAPTCHA service. Please try again.', 'yatra')];
138 + }
139 +
140 + $result = json_decode(wp_remote_retrieve_body($http), true);
141 + if (!is_array($result) || !isset($result['success'])) {
142 + return ['success' => false, 'message' => __('Invalid reCAPTCHA response.', 'yatra')];
143 + }
144 +
145 + if (empty($result['success'])) {
68 146 return [
69 147 'success' => false,
70 - 'message' => __('reCAPTCHA verification failed: ', 'yatra') . $response->get_error_message()
148 + 'message' => self::getErrorMessage($result['error-codes'] ?? []),
71 149 ];
72 150 }
73 -
74 - $body = wp_remote_retrieve_body($response);
75 - $result = json_decode($body, true);
76 -
77 - if (!isset($result['success'])) {
151 +
152 + $score = isset($result['score']) ? (float) $result['score'] : null;
153 + $action = $result['action'] ?? null;
154 +
155 + // v3 score gate. (A v2 token has no score; treat missing score as pass so
156 + // a mistakenly-configured v2 key still validates presence.)
157 + if ($score !== null && $score < self::scoreThreshold()) {
78 158 return [
79 159 'success' => false,
80 - 'message' => __('Invalid reCAPTCHA response', 'yatra')
160 + 'message' => __('reCAPTCHA score too low — your request looked automated. Please try again.', 'yatra'),
161 + 'score' => $score,
162 + 'action' => $action,
81 163 ];
82 164 }
83 -
84 - if (!$result['success']) {
85 - $error_codes = $result['error-codes'] ?? [];
86 - $error_message = self::getErrorMessage($error_codes);
87 -
165 +
166 + // Optional action binding (defence in depth; only enforced when both sides present).
167 + if ($expectedAction !== null && $action !== null && $action !== '' && $action !== $expectedAction) {
88 168 return [
89 169 'success' => false,
90 - 'message' => $error_message
170 + 'message' => __('reCAPTCHA action mismatch. Please try again.', 'yatra'),
171 + 'score' => $score,
172 + 'action' => $action,
91 173 ];
92 174 }
93 -
175 +
94 176 return [
95 177 'success' => true,
96 178 'message' => __('reCAPTCHA verification successful', 'yatra'),
97 - 'score' => $result['score'] ?? null,
98 - 'action' => $result['action'] ?? null,
179 + 'score' => $score,
180 + 'action' => $action,
99 181 ];
100 182 }
101 -
183 +
102 184 /**
103 - * Get user-friendly error message from error codes
104 - *
105 - * @param array $errorCodes Error codes from reCAPTCHA
106 - * @return string Error message
185 + * Map Google error codes to a friendly message.
107 186 */
108 187 private static function getErrorMessage(array $errorCodes): string
109 188 {
110 189 if (empty($errorCodes)) {
111 - return __('reCAPTCHA verification failed', 'yatra');
190 + return __('reCAPTCHA verification failed. Please try again.', 'yatra');
112 191 }
113 -
192 +
114 193 $messages = [
115 - 'missing-input-secret' => __('The secret parameter is missing', 'yatra'),
116 - 'invalid-input-secret' => __('The secret parameter is invalid or malformed', 'yatra'),
117 - 'missing-input-response' => __('The response parameter is missing', 'yatra'),
118 - 'invalid-input-response' => __('The response parameter is invalid or malformed', 'yatra'),
119 - 'bad-request' => __('The request is invalid or malformed', 'yatra'),
120 - 'timeout-or-duplicate' => __('The response is no longer valid: either is too old or has been used previously', 'yatra'),
194 + 'missing-input-secret' => __('The reCAPTCHA secret key is missing.', 'yatra'),
195 + 'invalid-input-secret' => __('The reCAPTCHA secret key is invalid or malformed.', 'yatra'),
196 + 'missing-input-response' => __('reCAPTCHA verification failed. Please try again.', 'yatra'),
197 + 'invalid-input-response' => __('reCAPTCHA verification failed. Please try again.', 'yatra'),
198 + 'bad-request' => __('The reCAPTCHA request was invalid or malformed.', 'yatra'),
199 + 'timeout-or-duplicate' => __('The reCAPTCHA response expired. Please try again.', 'yatra'),
121 200 ];
122 -
123 - $errorCode = $errorCodes[0];
124 - return $messages[$errorCode] ?? __('reCAPTCHA verification failed', 'yatra');
201 +
202 + return $messages[$errorCodes[0]] ?? __('reCAPTCHA verification failed. Please try again.', 'yatra');
125 203 }
126 -
204 +
127 205 /**
128 - * Get reCAPTCHA site key
129 - *
130 - * @return string Site key
206 + * Enqueue the reCAPTCHA v3 script + Yatra helper on the frontend.
207 + * Hooked on wp_enqueue_scripts; self-guards on isEnabled().
131 208 */
132 - public static function getSiteKey(): string
209 + public static function enqueueScript(): void
133 210 {
134 - return SettingsService::getString('recaptcha_site_key', '');
135 - }
136 -
137 - /**
138 - * Check if reCAPTCHA is enabled
139 - *
140 - * @return bool
141 - */
142 - public static function isEnabled(): bool
143 - {
144 - return SettingsService::isEnabled('recaptcha_enabled') &&
145 - !empty(self::getSiteKey()) &&
146 - !empty(SettingsService::getString('recaptcha_secret_key', ''));
147 - }
148 -
149 - /**
150 - * Render reCAPTCHA widget HTML
151 - *
152 - * @return string HTML for reCAPTCHA widget
153 - */
154 - public static function renderWidget(): string
155 - {
156 211 if (!self::isEnabled()) {
157 - return '';
212 + return;
158 213 }
159 -
160 - $site_key = self::getSiteKey();
161 -
162 - return sprintf(
163 - '<div class="g-recaptcha" data-sitekey="%s"></div>',
164 - esc_attr($site_key)
165 - );
166 - }
167 -
168 - /**
169 - * Enqueue reCAPTCHA script
170 - */
171 - public static function enqueueScript(): void
172 - {
173 - if (!self::isEnabled()) {
214 +
215 + // Skip in wp-admin (v3 badge / tokens are for public forms).
216 + if (is_admin()) {
174 217 return;
175 218 }
176 -
219 +
177 220 wp_enqueue_script(
178 221 'google-recaptcha',
179 - 'https://www.google.com/recaptcha/api.js',
222 + 'https://www.google.com/recaptcha/api.js?render=' . rawurlencode(self::getSiteKey()),
180 223 [],
181 224 null,
182 225 true
183 226 );
227 +
228 + wp_enqueue_script(
229 + 'yatra-recaptcha',
230 + YATRA_PLUGIN_URL . 'assets/js/recaptcha.js',
231 + ['google-recaptcha'],
232 + defined('YATRA_VERSION') ? YATRA_VERSION : false,
233 + true
234 + );
235 +
236 + wp_localize_script('yatra-recaptcha', 'yatraRecaptcha', [
237 + 'siteKey' => self::getSiteKey(),
238 + 'enabled' => true,
239 + 'forms' => [
240 + 'enquiry' => self::protectsForm('enquiry'),
241 + 'booking' => self::protectsForm('booking'),
242 + 'registration' => self::protectsForm('registration'),
243 + ],
244 + ]);
184 245 }
185 246 }