PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.8.1
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.8.1
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / Modules / Auth / AuthHelper.php

AuthHelper.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.8.1, at Modules/Auth/AuthHelper.php

540 lines 24.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCommunity\Modules\Auth;
4
5 use FluentCommunity\App\App;
6 use FluentCommunity\App\Services\Helper;
7 use FluentCommunity\App\Services\Libs\Mailer;
8 use FluentCommunity\Framework\Support\Arr;
9
10 class AuthHelper
11 {
12 public static function registerNewUser($user_login, $user_email, $user_pass = '', $extraData = [])
13 {
14 $errors = new \WP_Error();
15
16 $sanitized_user_login = sanitize_user($user_login);
17
18 $user_email = apply_filters('user_registration_email', $user_email); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
19
20 // Check the username.
21 if ('' === $sanitized_user_login) {
22 $errors->add('empty_username', __('<strong>Error</strong>: Please enter a username.', 'fluent-community'));
23 } elseif (!validate_username($user_login)) {
24 $errors->add('invalid_username', __('<strong>Error</strong>: This username is invalid because it uses illegal characters. Please enter a valid username.', 'fluent-community'));
25 $sanitized_user_login = '';
26 } elseif (username_exists($sanitized_user_login)) {
27 $errors->add('username_exists', __('<strong>Error</strong>: This username is already registered. Please choose another one.', 'fluent-community'));
28 } else {
29 /** This filter is documented in wp-includes/user.php */
30 $illegal_user_logins = (array)apply_filters('illegal_user_logins', array()); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
31 if (in_array(strtolower($sanitized_user_login), array_map('strtolower', $illegal_user_logins), true)) {
32 $errors->add('invalid_username', __('<strong>Error</strong>: Sorry, that username is not allowed.', 'fluent-community'));
33 }
34 }
35
36 // Check the email address.
37 if ('' === $user_email) {
38 $errors->add('empty_email', __('<strong>Error</strong>: Please type your email address.', 'fluent-community'));
39 } elseif (!is_email($user_email)) {
40 $errors->add('invalid_email', __('<strong>Error</strong>: The email address is not correct.', 'fluent-community'));
41 $user_email = '';
42 } elseif (email_exists($user_email)) {
43 $errors->add(
44 'email_exists',
45 __('<strong>Error:</strong> This email address is already registered. Please login or try resetting your password.', 'fluent-community')
46 );
47 }
48
49 /**
50 * MemberPress rejects every `register_post` while its "Disable WordPress registration form"
51 * option is on (default on). That option targets wp-login.php, not the community portal, which has its own registration gate.
52 */
53 $hadMeprBlocker = remove_action('register_post', 'MeprUsersCtrl::maybe_disable_wp_registration_form', 10);
54
55 do_action('register_post', $sanitized_user_login, $user_email, $errors); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
56
57 if ($hadMeprBlocker) {
58 add_action('register_post', 'MeprUsersCtrl::maybe_disable_wp_registration_form', 10, 3);
59 }
60
61 if ($errors->has_errors()) {
62 return $errors;
63 }
64
65 if (!$user_pass) {
66 $user_pass = wp_generate_password(8, false);
67 }
68
69 $data = [
70 'user_login' => wp_slash($sanitized_user_login),
71 'user_email' => wp_slash($user_email),
72 'user_pass' => $user_pass
73 ];
74
75 if (!empty($extraData['first_name'])) {
76 $data['first_name'] = sanitize_text_field($extraData['first_name']);
77 }
78
79 if (!empty($extraData['last_name'])) {
80 $data['last_name'] = sanitize_text_field($extraData['last_name']);
81 }
82
83 if (!empty($extraData['full_name']) && empty($extraData['first_name']) && empty($extraData['last_name'])) {
84 $extraData['full_name'] = sanitize_text_field($extraData['full_name']);
85 // extract the names
86 $fullNameArray = explode(' ', $extraData['full_name']);
87 $data['first_name'] = array_shift($fullNameArray);
88 if ($fullNameArray) {
89 $data['last_name'] = implode(' ', $fullNameArray);
90 } else {
91 $data['last_name'] = '';
92 }
93 }
94
95 if (!empty($extraData['description'])) {
96 $data['description'] = sanitize_textarea_field($extraData['description']);
97 }
98
99 if (!empty($extraData['user_url']) && filter_var($extraData['user_url'], FILTER_VALIDATE_URL)) {
100 $data['user_url'] = sanitize_url($extraData['user_url']);
101 }
102
103 if (!empty($extraData['role'])) {
104 $data['role'] = $extraData['role'];
105 }
106
107 $user_id = wp_insert_user($data);
108
109 if (!$user_id || is_wp_error($user_id)) {
110 $errors->add('registerfail', __('<strong>Error</strong>: Could not register you. Please contact the site admin!', 'fluent-community')
111 );
112 return $errors;
113 }
114
115 if (!empty($_COOKIE['wp_lang'])) {
116 $wp_lang = sanitize_text_field(wp_unslash($_COOKIE['wp_lang']));
117 if (in_array($wp_lang, get_available_languages(), true)) {
118 update_user_meta($user_id, 'locale', $wp_lang); // Set user locale if defined on registration.
119 }
120 }
121
122 do_action('register_new_user', $user_id); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
123
124 return $user_id;
125 }
126
127 public static function makeLogin($user)
128 {
129 wp_clear_auth_cookie();
130 wp_set_current_user($user->ID, $user->user_login);
131 wp_set_auth_cookie($user->ID, true, is_ssl());
132
133 $user = get_user_by('ID', $user->ID);
134
135 if ($user) {
136 do_action('wp_login', $user->user_login, $user); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
137 }
138
139 return $user;
140 }
141
142 public static function isFluentAuthAvailable()
143 {
144 if (defined('FLUENT_AUTH_VERSION') && FLUENT_AUTH_VERSION) {
145 return (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled();
146 }
147
148 return false;
149 }
150
151 public static function getTermsText()
152 {
153 $policyUrl = apply_filters('fluent_community/terms_policy_url', get_privacy_policy_url());
154
155 $termsText = __('I agree to the terms and conditions', 'fluent-community');
156 if ($policyUrl) {
157 /* translators: %1$s is replaced by the text "terms and conditions", %2$s is replaced by the text "to the terms and conditions" */
158 $termsText = sprintf(__('I agree to the %1$s terms and conditions %2$s', 'fluent-community'), '<a rel="noopener" href="' . esc_url($policyUrl) . '" target="_blank">', '</a>');
159 }
160
161 return $termsText;
162 }
163
164 public static function getFormFields($invitation = null)
165 {
166 $fields = apply_filters('fluent_community/auth/signup_fields', [
167 'full_name' => [
168 'label' => __('Full name', 'fluent-community'),
169 'placeholder' => __('Your first & last name', 'fluent-community'),
170 'type' => 'text',
171 'required' => true,
172 'value' => $invitation ? Arr::get($invitation->meta, 'invitee_name') : '',
173 'sanitize_callback' => 'sanitize_text_field'
174 ],
175 'email' => [
176 'type' => 'email',
177 'placeholder' => __('Your email address', 'fluent-community'),
178 'label' => __('Email Address', 'fluent-community'),
179 'required' => true,
180 'value' => $invitation ? $invitation->message : '',
181 'readonly' => $invitation && $invitation->message,
182 'sanitize_callback' => 'sanitize_email'
183 ],
184 'username' => [
185 'type' => 'text',
186 'placeholder' => __('No space or special characters', 'fluent-community'),
187 'label' => __('Username', 'fluent-community'),
188 'required' => true,
189 'sanitize_callback' => 'sanitize_user'
190 ],
191 'password' => [
192 'type' => 'password',
193 'placeholder' => __('Password', 'fluent-community'),
194 'label' => __('Account Password', 'fluent-community'),
195 'required' => true,
196 'sanitize_callback' => 'sanitize_text_field'
197 ],
198 'conf_password' => [
199 'type' => 'password',
200 'placeholder' => __('Password Confirmation', 'fluent-community'),
201 'label' => __('Re-type Account Password', 'fluent-community'),
202 'required' => true,
203 'sanitize_callback' => 'sanitize_text_field'
204 ],
205 'terms' => [
206 'type' => 'inline_checkbox',
207 'inline_label' => self::getTermsText(),
208 'required' => true
209 ]
210 ], $invitation);
211
212 if (!self::isPasswordConfRequired()) {
213 unset($fields['conf_password']);
214 }
215
216 return $fields;
217 }
218
219 public static function getLostPasswordUrl($redirectUrl = '')
220 {
221 if (self::isFluentAuthAvailable()) {
222 $url = add_query_arg([
223 'form' => 'reset_password'
224 ], Helper::getAuthUrl());
225 } else {
226 $url = wp_lostpassword_url($redirectUrl);;
227 }
228
229 return apply_filters('fluent_community/auth/lost_password_url', $url);
230 }
231
232 public static function getLoginFormFields()
233 {
234 return apply_filters('fluent_community/auth/login_fields', [
235 'username' => [
236 'type' => 'text',
237 'placeholder' => __('Your account email address', 'fluent-community'),
238 'label' => __('Email Address', 'fluent-community'),
239 'required' => true,
240 'sanitize_callback' => 'sanitize_user'
241 ],
242 'password' => [
243 'type' => 'password',
244 'placeholder' => __('Your account password', 'fluent-community'),
245 'label' => __('Password', 'fluent-community'),
246 'required' => true,
247 'sanitize_callback' => 'sanitize_text_field'
248 ]
249 ]);
250 }
251
252 public static function isPasswordConfRequired()
253 {
254 $isRequired = apply_filters_deprecated('fluent_community/autg/password_confirmation', [true], '2.7.8', 'fluent_community/auth/password_confirmation');
255
256 return apply_filters('fluent_community/auth/password_confirmation', $isRequired);
257 }
258
259 public static function isRegistrationEnabled()
260 {
261
262 $enabled = !!get_option('users_can_register');
263
264 if (!$enabled) {
265 $generalSettinsg = Helper::generalSettings();
266 $enabled = $generalSettinsg['explicit_registration'] !== 'no';
267 }
268
269 return apply_filters('fluent_community/auth/registration_enabled', $enabled);
270 }
271
272 public static function isTwoFactorEnabled()
273 {
274 // fluent_auth/verify_signup_email is kept for backward compatibility with FluentAuth-targeted snippets
275 $enabled = apply_filters('fluent_auth/verify_signup_email', true);
276
277 return apply_filters('fluent_community/auth/two_factor_enabled', $enabled);
278 }
279
280 public static function get2FaRegistrationCodeForm($formData)
281 {
282 $generalSettings = Helper::generalSettings();
283 try {
284 $verifcationCode = str_pad((string) random_int(100123, 900987), 6, '0', STR_PAD_LEFT);
285 } catch (\Exception $e) {
286 $verifcationCode = str_pad((string) wp_rand(100123, 900987), 6, '0', STR_PAD_LEFT);
287 }
288
289 // Keep the code hash server-side, keyed by an opaque challenge id. The client only ever
290 // receives the id, never the password verifier, so the code cannot be recovered offline.
291 $codeHash = wp_hash_password($verifcationCode);
292 $signedToken = 'fcs_' . wp_generate_password(40, false);
293 set_transient('fcom_signup_2fa_' . $signedToken, [
294 'email' => $formData['email'],
295 'code_hash' => $codeHash,
296 'expires' => time() + 600, // 10 minutes expiry
297 'attempts' => 0,
298 ], 600);
299
300 /* translators: %s is replaced by the title of the site */
301 $mailSubject = apply_filters("fluent_community/auth/signup_verification_mail_subject", sprintf(__('Your registration verification code for %s', 'fluent-community'), Arr::get($generalSettings, 'site_title')));
302
303 $pStart = '<p style="font-family: Arial, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">';
304
305 /* translators: %s is replaced by the name of the user */
306 $message = $pStart . sprintf(__('Hello %s,', 'fluent-community'), Arr::get($formData, 'first_name')) . '</p>' .
307 $pStart . __('Thank you for registering with us! To complete the setup of your account, please enter the verification code below on the registration page.', 'fluent-community') . '</p>' .
308 /* translators: %s is replaced by the verification code */
309 $pStart . '<b>' . sprintf(__('Verification Code: %s', 'fluent-community'), $verifcationCode) . '</b></p>' .
310 '<br />' .
311 $pStart . __('This code is valid for 10 minutes and is meant to ensure the security of your account. If you did not initiate this request, please ignore this email.', 'fluent-community') . '</p>';
312
313 $message = apply_filters('fluent_community/auth/signup_verification_email_body', $message, $verifcationCode, $formData);
314
315 $generalSettings = Helper::generalSettings();
316 $message = (string)App::make('view')->make('email.template', [
317 'logo' => [
318 'url' => $generalSettings['logo'],
319 'alt' => $generalSettings['site_title']
320 ],
321 'bodyContent' => $message,
322 'pre_header' => __('Activate your account', 'fluent-community'),
323 'footerLines' => [
324 __('If you did not initiate this request, please ignore this email.', 'fluent-community'),
325 /* translators: %1$s is replaced by the title of the site, %2$s is replaced by the home URL */
326 sprintf(__('This email has been sent from %1$s. Site: %2$s', 'fluent-community'), Arr::get($generalSettings, 'site_title'), home_url())
327 ]
328 ]);
329
330 $mailer = new Mailer($formData['email'], $mailSubject, $message);
331
332 if ($formData['first_name']) {
333 $toName = trim(Arr::get($formData, 'first_name') . ' ' . Arr::get($formData, 'last_name'));
334 $mailer = $mailer->to($formData['email'], $toName);
335 }
336
337 $mailer->send();
338
339 ob_start();
340 ?>
341 <div class="fls_signup_verification">
342 <input type="hidden" name="__two_fa_signed_token" value="<?php echo esc_attr($signedToken); ?>"/>
343 <?php /* translators: %s is replaced by the email address */ ?>
344 <p><?php echo esc_html(\sprintf(__('A verification code has been sent to %s. Please provide the code below: ', 'fluent-community'), $formData['email'])) ?></p>
345 <div class="fcom_form-group fcom_field_verification">
346 <div class="fcom_form_label">
347 <label for="fcom_field_verification"><?php esc_html_e('Verification Code', 'fluent-community'); ?></label>
348 </div>
349 <div class="fs_input_wrap">
350 <input type="text" id="fcom_field_verification"
351 placeholder="<?php esc_html_e('2FA Code', 'fluent-community'); ?>" name="_email_verification_code"
352 required/>
353 </div>
354 </div>
355 <div class="fcom_form-group">
356 <div class="fcom_form_input">
357 <button type="submit" class="fcom_btn has_svg_loader fcom_btn_primary">
358 <svg version="1.1" class="fls_loading_svg" x="0px" y="0px" width="40px" height="20px" viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
359 <path fill="currentColor" d="M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z">
360 <animateTransform attributeType="xml"
361 attributeName="transform"
362 type="rotate"
363 from="0 25 25"
364 to="360 25 25"
365 dur="0.6s"
366 repeatCount="indefinite"/>
367 </path>
368 </svg>
369 <span> <?php esc_html_e('Complete Signup', 'fluent-community'); ?></span>
370 </button>
371 </div>
372 </div>
373 </div>
374
375 <?php
376 return ob_get_clean();
377 }
378
379 public static function validateVerificationCode($code, $verificationToken, $formData)
380 {
381 if (!is_string($verificationToken) || $verificationToken === '') {
382 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
383 }
384
385 $transientKey = 'fcom_signup_2fa_' . $verificationToken;
386 $data = get_transient($transientKey);
387
388 if (!is_array($data) || empty($data['expires']) || empty($data['email']) || empty($data['code_hash'])) {
389 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
390 }
391
392 if ((int)$data['expires'] < time()) {
393 delete_transient($transientKey);
394 return new \WP_Error('expired_token', __('Verification token has expired. Please try again.', 'fluent-community'));
395 }
396
397 if (!isset($formData['email']) || $data['email'] !== $formData['email']) {
398 return new \WP_Error('invalid_email', __('Invalid email address. Please try again', 'fluent-community'));
399 }
400
401 // Cap online guesses per challenge: after too many wrong codes the challenge is burned.
402 if ((int) Arr::get($data, 'attempts', 0) >= 10) {
403 delete_transient($transientKey);
404 return new \WP_Error('too_many_attempts', __('Too many invalid attempts. Please try again', 'fluent-community'));
405 }
406
407 if (!wp_check_password($code, $data['code_hash'])) {
408 $data['attempts'] = (int) Arr::get($data, 'attempts', 0) + 1;
409 set_transient($transientKey, $data, max(1, (int) $data['expires'] - time()));
410 return new \WP_Error('invalid_code', __('Invalid verification code. Please try again', 'fluent-community'));
411 }
412
413 // Single-use: consume the challenge on success.
414 delete_transient($transientKey);
415
416 return true;
417 }
418
419 public static function isAuthRateLimit()
420 {
421 if (apply_filters('fluent_community/auth/disable_rate_limit', false)) {
422 return true;
423 }
424
425 $transientKey = 'fluent_com_rate_limit_' . md5(Helper::getIp());
426 $rateLimit = get_transient($transientKey);
427
428 if (!$rateLimit) {
429 $rateLimit = 0;
430 }
431
432 if ($rateLimit >= 10) {
433 return new \WP_Error('rate_limit', __('Too many requests. Please try again later', 'fluent-community'));
434 }
435
436 $rateLimit = $rateLimit + 1;
437 set_transient($transientKey, $rateLimit, 300); // per 5 minutes
438 return true;
439 }
440
441
442 public static function nativeLoginForm($args = array(), $hiddenFields = [])
443 {
444 $defaults = array(
445 'echo' => true,
446 'redirect' => (is_ssl() ? 'https://' : 'http://')
447 . (isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '')
448 . (isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''),
449 'form_id' => 'loginform',
450 'label_username' => __('Email Address', 'fluent-community'),
451 'label_password' => __('Password', 'fluent-community'),
452 'label_remember' => __('Remember Me', 'fluent-community'),
453 'label_log_in' => __('Log In', 'fluent-community'),
454 'id_username' => 'user_login',
455 'id_password' => 'user_pass',
456 'id_remember' => 'rememberme',
457 'id_submit' => 'wp-submit',
458 'remember' => true,
459 'value_username' => '',
460 'username_placeholder' => __('Your account email address', 'fluent-community'),
461 'password_placeholder' => __('Your account password', 'fluent-community'),
462 'value_remember' => false,
463 );
464
465 $args = wp_parse_args($args, apply_filters('login_form_defaults', $defaults)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
466
467 $login_form_top = apply_filters('login_form_top', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
468
469 $login_form_middle = apply_filters('login_form_middle', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
470
471 $login_form_bottom = apply_filters('login_form_bottom', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
472
473 $actionUrl = esc_url(site_url('wp-login.php', 'login_post'));
474
475 if (isset($args['action_url'])) {
476 $actionUrl = esc_url($args['action_url']);
477 }
478
479 foreach ($hiddenFields as $key => $value) {
480 $login_form_top .= \sprintf(
481 '<input type="hidden" name="%1$s" value="%2$s" />',
482 esc_attr($key),
483 esc_attr($value)
484 );
485 }
486
487 $form = \sprintf(
488 '<form name="%1$s" id="%1$s" action="%2$s" method="post">',
489 esc_attr($args['form_id']),
490 $actionUrl
491 ) .
492 $login_form_top .
493 \sprintf(
494 '<p class="login-username fcom_form-group">
495 <label for="%1$s">%2$s</label>
496 <input type="text" name="log" id="%1$s" autocomplete="username" class="input" value="%3$s" placeholder="%4$s" size="20" />
497 </p>',
498 esc_attr($args['id_username']),
499 esc_html($args['label_username']),
500 esc_attr($args['value_username']),
501 esc_attr($args['username_placeholder']),
502 ) .
503 \sprintf(
504 '<p class="login-password fcom_form-group">
505 <label for="%1$s">%2$s</label>
506 <input type="password" name="pwd" id="%1$s" autocomplete="current-password" placeholder="%3$s" class="input" value="" size="20" />
507 </p>',
508 esc_attr($args['id_password']),
509 esc_html($args['label_password']),
510 esc_attr($args['password_placeholder'])
511 ) .
512 $login_form_middle .
513 ($args['remember'] ?
514 \sprintf(
515 '<p class="login-remember fcom_form-group"><label><input name="rememberme" type="checkbox" id="%1$s" value="forever"%2$s /> %3$s</label></p>',
516 esc_attr($args['id_remember']),
517 ($args['value_remember'] ? ' checked="checked"' : ''),
518 esc_html($args['label_remember'])
519 ) : ''
520 ) .
521 \sprintf(
522 '<p class="login-submit">
523 <input type="submit" name="wp-submit" id="%1$s" class="button button-primary" value="%2$s" />
524 <input type="hidden" name="redirect_to" value="%3$s" />
525 </p>',
526 esc_attr($args['id_submit']),
527 esc_attr($args['label_log_in']),
528 esc_url($args['redirect'])
529 ) .
530 $login_form_bottom .
531 '</form>';
532
533 if ($args['echo']) {
534 echo $form; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
535 } else {
536 return $form;
537 }
538 }
539 }
540