PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.2
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.2
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
← All changes | app/Http/Controllers/AuthController.php +57 -110 trunk2.1.2 View file →
@@ -56,17 +56,11 @@
56 56 * @param array $formData
57 57 */
58 58 do_action('fluent_support/before_signup_validation', $formData);
59 59
60 - // the verification code is only submitted on the second step, after the customer has
61 - // already passed the captcha and received the email; that step validates the hash
62 - // against an issued record below, so the captcha can be skipped there. The first
63 - // submit (which sends the verification email) always has to pass the captcha.
64 - $isVerificationStep = !empty($formData['_email_verification_token']);
65 -
66 - $checkRecaptchaAvailability = ReCaptchaHandler::isRecaptchaApplicable('signup_form');
67 - if ($checkRecaptchaAvailability && !$isVerificationStep) {
68 - $validateCaptcha = ReCaptchaHandler::validateRecaptcha($formData['g-recaptcha-response'] ?? '', null, null, 'signup');
60 + $checkRecaptchaAvailability = $this->isRecaptchaApplicable('signup_form');
61 + if ($checkRecaptchaAvailability && !$formData['_email_verification_hash']) {
62 + $validateCaptcha = ReCaptchaHandler::validateRecaptcha($formData['g-recaptcha-response']);
69 63 if (!$validateCaptcha) {
70 64 return $this->response([
71 65 'message' => __('Your recaptcha is not verified', 'fluent-support')
72 66 ], 422);
@@ -74,9 +68,9 @@
74 68 }
75 69
76 70 $this->validate($formData, $rules, $messages);
77 71
78 - if (!$isVerificationStep) {
72 + if (empty($formData['_email_verification_token'])) {
79 73 $tokenHtml = EmailVerificationHandler::sendSignupEmailVerificationHtml($formData);
80 74
81 75 return $this->response([
82 76 'verification_html' => $tokenHtml
@@ -82,9 +76,9 @@
82 76 'verification_html' => $tokenHtml
83 77 ]);
84 78 } else {
85 79 $token = $formData['_email_verification_token'];
86 - $verificationHash = $formData['_email_verification_hash'] ?? '';
80 + $verificationHash = $formData['_email_verification_hash'];
87 81
88 82 $logHashMeta = Meta::where('object_type', 'fs_login_hashes',)
89 83 ->where('key', $verificationHash)
90 84 ->first();
@@ -102,31 +96,11 @@
102 96 'message' => __('Please provide a valid verification code that was sent to your email address', 'fluent-support')
103 97 ], 422);
104 98 }
105 99
106 - // the code must still be unused and must not have been consumed by a prior request
107 - if (($logHash['status'] ?? '') !== 'issued') {
108 - wp_send_json([
109 - 'message' => __('Your verification code has already been used. Please try again', 'fluent-support')
110 - ], 422);
111 - }
112 -
113 - // records created before the email-binding fix (or any other legacy/malformed record)
114 - // have no bound email; treat them as invalid rather than proceeding with a null email
115 - if (empty($logHash['email'])) {
116 - wp_send_json([
117 - 'message' => __('Your verification code has expired. Please request a new one', 'fluent-support')
118 - ], 422);
119 - }
120 -
121 100 // check if it got expired or not
122 - // valid_till is written via gmdate() on a current_time('timestamp') basis (always
123 - // UTC-equivalent, since WP resets the runtime timezone to UTC on every request), so
124 - // it must be parsed back as UTC here - otherwise strtotime() would silently reinterpret
125 - // it under whatever timezone a plugin/theme may have switched to via
126 - // date_default_timezone_set() without restoring it, causing false expiry (or non-expiry)
127 101 $validTill = $logHash['valid_till'] ?? '';
128 - if (($logHash['used_count'] ?? 0) > 5 || ($validTill && strtotime($validTill . ' UTC') < current_time('timestamp'))) {
102 + if (($logHash['used_count'] ?? 0) > 5 || ($validTill && strtotime($validTill) < current_time('timestamp'))) {
129 103 wp_send_json([
130 104 'message' => __('Your verification code has been expired. Please try again', 'fluent-support')
131 105 ], 422);
132 106 }
@@ -142,30 +116,14 @@
142 116 'message' => __('Please provide a valid verification code that was sent to your email address', 'fluent-support')
143 117 ], 422);
144 118 }
145 119
146 - // atomically consume the code: only succeeds if the record is still in the exact
147 - // state we just read, closing the race where two requests both pass the checks above
148 - $consumed = Meta::where('key', $logHash['login_hash'])
149 - ->where('object_type', 'fs_login_hashes')
150 - ->where('value', $logHashMeta->value)
151 - ->update([
152 - 'value' => maybe_serialize(array_merge($logHash, [
153 - 'used_count' => ($logHash['used_count'] ?? 0) + 1,
154 - 'status' => 'used',
155 - ]))
156 - ]);
120 + $logHash['used_count'] += 1;
121 + $logHash['status'] = 'used';
157 122
158 - if (!$consumed) {
159 - wp_send_json([
160 - 'message' => __('Your verification code has already been used. Please try again', 'fluent-support')
161 - ], 422);
162 - }
163 -
164 - // the email is now server-verified for this code; ignore whatever the client
165 - // submitted and use the address the code was actually issued to, so the signup
166 - // can never be completed against a different (e.g. victim's) email address
167 - $formData['email'] = $logHash['email'];
123 + Meta::where('key', $logHash['login_hash'])->update([
124 + 'value' => maybe_serialize($logHash)
125 + ]);
168 126 }
169 127
170 128 /*
171 129 * Action After validate user signup validation success
@@ -232,11 +190,11 @@
232 190 }
233 191
234 192 $data = $request->all();
235 193
236 - $checkRecaptchaAvailability = ReCaptchaHandler::isRecaptchaApplicable('login_form');
194 + $checkRecaptchaAvailability = $this->isRecaptchaApplicable('login_form');
237 195 if ($checkRecaptchaAvailability) {
238 - $validateCaptcha = ReCaptchaHandler::validateRecaptcha($data['g-recaptcha-response'] ?? '', null, null, 'login');
196 + $validateCaptcha = ReCaptchaHandler::validateRecaptcha($data['g-recaptcha-response']);
239 197
240 198 if (!$validateCaptcha) {
241 199 return $this->response([
242 200 'message' => __('Your recaptcha is not verified', 'fluent-support')
@@ -287,9 +245,9 @@
287 245 ], 429);
288 246 }
289 247
290 248 if (!$user) {
291 - $user = new \WP_Error('authentication_failed', __('Invalid username, email address or incorrect password.', 'fluent-support'));
249 + $user = new \WP_Error('authentication_failed', __('<strong>Error</strong>: Invalid username, email address or incorrect password.', 'fluent-support'));
292 250
293 251 do_action('wp_login_failed', $email, $user);
294 252 $this->incrementLoginAttempts($ipKey);
295 253 $this->incrementLoginAttempts($accountKey);
@@ -301,17 +259,8 @@
301 259 }
302 260
303 261 $twoFactorEnabled = Helper::getBusinessSettings('enable_two_fa');
304 262 if ('yes' === $twoFactorEnabled) {
305 - if (!wp_check_password($password, $user->user_pass, $user->ID)) {
306 - $this->incrementLoginAttempts($ipKey);
307 - $this->incrementLoginAttempts($accountKey);
308 -
309 - return $this->response([
310 - 'message' => __('Invalid username, email address or incorrect password.', 'fluent-support')
311 - ], 403);
312 - }
313 -
314 263 (new TwoFaHandler)->maybe2FaRedirect($user);
315 264 }
316 265
317 266 if (apply_filters('fluent_support_use_native_login', true)) {
@@ -346,9 +295,9 @@
346 295 $this->incrementLoginAttempts($ipKey);
347 296 $this->incrementLoginAttempts($accountKey);
348 297
349 298 return $this->response([
350 - 'message' => __('Invalid username, email address or incorrect password.', 'fluent-support')
299 + 'message' => __('<strong>Error</strong>: Invalid username, email address or incorrect password.', 'fluent-support')
351 300 ], 403);
352 301 }
353 302
354 303 private function incrementLoginAttempts($rateLimitKey)
@@ -360,8 +309,26 @@
360 309 set_transient($rateLimitKey, $attempts + 1, 15 * MINUTE_IN_SECONDS);
361 310 }
362 311 }
363 312
313 + public function isRecaptchaApplicable($formName)
314 + {
315 + $reCaptchaSettingsData = Meta::where('object_type', '_fs_recaptcha_settings')->first();
316 + if(!isset($reCaptchaSettingsData->value)){
317 + return false;
318 + }
319 + $reCaptchaData = Helper::safeUnserialize($reCaptchaSettingsData->value, []);
320 + if(!isset($reCaptchaData['is_enabled']) || !isset($reCaptchaData['formContainingReCaptcha'])){
321 + return false;
322 + }
323 + $isEnabled = filter_var($reCaptchaData['is_enabled'], FILTER_VALIDATE_BOOLEAN);
324 + if (!$isEnabled) {
325 + return false;
326 + }
327 + $formContainingReCaptcha = $reCaptchaData['formContainingReCaptcha'];
328 + return 'yes' === $formContainingReCaptcha[$formName];
329 + }
330 +
364 331 private function nativeLoginHandler($user, $info, $redirectUrl = '')
365 332 {
366 333 if (!$redirectUrl) {
367 334 $redirectUrl = Helper::getPortalBaseUrl();
@@ -483,18 +450,8 @@
483 450 'message' => 'Username or email is required'
484 451 ]);
485 452 }
486 453
487 - // IP bucket is a generous volumetric backstop (shared office/NAT IPs can have many
488 - // unrelated users). It runs before the account lookup so that probes for accounts
489 - // that don't exist are throttled too. Keyed on the IP only, so a 429 here reveals
490 - // nothing about whether any given account exists.
491 - if (Helper::hitRateLimit('fs_reset_pass_ip_' . wp_hash(Helper::getIp()), 20)) {
492 - return $this->sendError([
493 - 'message' => __('Too many password reset requests. Please try again after 15 minutes.', 'fluent-support')
494 - ], 429);
495 - }
496 -
497 454 $user_data = get_user_by('email', $usernameOrEmail);
498 455
499 456 if (!$user_data) {
500 457 $user_data = get_user_by('login', $usernameOrEmail);
@@ -500,9 +457,11 @@
500 457 $user_data = get_user_by('login', $usernameOrEmail);
501 458 }
502 459
503 460 if (!$user_data) {
504 - return $this->sendResetPassResponse();
461 + return $this->sendError([
462 + 'message' => __('Invalid username or email', 'fluent-support')
463 + ]);
505 464 }
506 465
507 466 $user_data = apply_filters('lostpassword_user_data', $user_data, $errors);
508 467
@@ -510,17 +469,24 @@
510 469
511 470 $errors = apply_filters('lostpassword_errors', $errors, $user_data);
512 471
513 472 if ($errors->has_errors()) {
514 - return $this->sendResetPassResponse();
473 + return $this->sendError([
474 + 'message' => $errors->get_error_message()
475 + ]);
515 476 }
516 477
517 478 if (!$user_data) {
518 - return $this->sendResetPassResponse();
479 + return $this->sendError([
480 + 'message' => __('<strong>Error</strong>: There is no account with that username or email address.', 'fluent-support')
481 + ]);
519 482 }
520 483
521 484 if (is_multisite() && !is_user_member_of_blog($user_data->ID, get_current_blog_id())) {
522 - return $this->sendResetPassResponse();
485 +
486 + return $this->sendError([
487 + 'message' => __('<strong>Error</strong>: Invalid username or email', 'fluent-support')
488 + ]);
523 489 }
524 490
525 491 // Redefining user_login ensures we return the right case in the email.
526 492 $user_login = $user_data->user_login;
@@ -528,13 +494,21 @@
528 494 do_action('retrieve_password', $user_login);
529 495
530 496 $allow = apply_filters('allow_password_reset', true, $user_data->ID);
531 497
532 - if (!$allow || is_wp_error($allow)) {
533 - return $this->sendResetPassResponse();
498 + if (!$allow) {
499 + return $this->sendError([
500 + 'message' => __('Password reset is not allowed for this user', 'fluent-support')
501 + ]);
534 502 }
535 503
504 + if (is_wp_error($allow)) {
505 + return $this->sendError([
506 + 'message' => $allow->get_error_message()
507 + ]);
508 + }
536 509
510 +
537 511 /*
538 512 * Filter reset password link text
539 513 *
540 514 * @since v1.5.7
@@ -542,21 +516,8 @@
542 516 */
543 517 // translators: %s is the site name
544 518 $linkText = apply_filters("fluent_support/reset_password_link", sprintf(__('Reset your password for %s', 'fluent-support'), get_bloginfo('name')));
545 519
546 - // Issuance cooldown. get_password_reset_key() rotates the stored key, invalidating
547 - // any link already sitting in the account owner's inbox, so an unthrottled caller
548 - // could deny password recovery indefinitely. Suppressing the duplicate issuance is
549 - // safe: reset mail only ever goes to the account owner, so whoever triggered the
550 - // first send has already put a working link in that inbox.
551 - $cooldownKey = 'fs_reset_pass_sent_' . wp_hash($user_data->ID);
552 -
553 - if (get_transient($cooldownKey)) {
554 - return $this->sendResetPassResponse();
555 - }
556 -
557 - set_transient($cooldownKey, 1, 5 * MINUTE_IN_SECONDS);
558 -
559 520 $resetUrl = add_query_arg([
560 521 'action' => 'rp',
561 522 'key' => get_password_reset_key($user_data),
562 523 'login' => rawurlencode($user_data->user_login)
@@ -594,24 +555,10 @@
594 555 $headers = array('Content-Type: text/html; charset=UTF-8');
595 556
596 557 wp_mail($user_data->user_email, $mailSubject, $message, $headers);
597 558
598 - return $this->sendResetPassResponse();
599 - }
600 -
601 - /**
602 - * Single response for every password reset outcome.
603 - *
604 - * Whether the account exists, is disallowed, is not a member of this site or is
605 - * inside the resend cooldown, the caller sees the same thing — otherwise the form
606 - * confirms which usernames and email addresses are real.
607 - *
608 - * @return mixed
609 - */
610 - protected function sendResetPassResponse()
611 - {
612 559 return $this->sendSuccess([
613 - 'message' => __('If an account matches that username or email, a password reset link has been sent. Please check your inbox, including the spam folder.', 'fluent-support')
560 + 'message' => __('Please check your email for the reset link', 'fluent-support')
614 561 ]);
615 562 }
616 563
617 564 /**