PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.8.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.8.0
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.0, at Modules/Auth/AuthHelper.php

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