PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.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 All 78 releases
fluent-community / Modules / Auth / AuthHelper.php

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

613 lines 26.9 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 /**
143 * The slug this plugin is known by inside FluentAuth.
144 */
145 const FLUENT_AUTH_HOST = 'fluent-community';
146
147 /**
148 * Whether FluentAuth's login stack is usable on this screen.
149 *
150 * Note the order this has to be asked in: adoptFluentAuth() is what makes the answer
151 * yes on a site whose shortcode setting is off, so the auth screen adopts first and
152 * asks second. Everywhere else - a plugin wondering whether the portal is running
153 * FluentAuth's forms - the question stands on its own.
154 */
155 public static function isFluentAuthAvailable()
156 {
157 if (!defined('FLUENT_AUTH_VERSION') || !FLUENT_AUTH_VERSION) {
158 return false;
159 }
160
161 return (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled();
162 }
163
164 /**
165 * Tells FluentAuth this plugin exists, so it recognises the admin-ajax posts our
166 * auth screen makes later. Cheap enough to run on every request, which is what it
167 * has to do - the form post is a request of its own.
168 *
169 * @return void
170 */
171 public static function registerWithFluentAuth()
172 {
173 if (!self::hasFluentAuthBridge()) {
174 return;
175 }
176
177 \FluentAuth\App\Services\LoginBridge::register(
178 self::FLUENT_AUTH_HOST,
179 'is_fcom_auth',
180 /*
181 * Our own login endpoint. Unreachable while the portal renders FluentAuth's
182 * form, but a site that filters the adoption back off falls through to it,
183 * and this is what keeps the second factor arriving as a form there rather
184 * than as the error message handleUserLogin() would otherwise print.
185 */
186 ['fcom_user_login_form']
187 );
188 }
189
190 /**
191 * Hands the screen being rendered to FluentAuth: its shortcodes render here even
192 * where the site has the front end forms switched off, its assets load, and its
193 * endpoints answer the posts this screen's forms make.
194 *
195 * @return bool whether FluentAuth took it
196 */
197 public static function adoptFluentAuth()
198 {
199 if (!self::hasFluentAuthBridge()) {
200 return false;
201 }
202
203 \FluentAuth\App\Services\LoginBridge::adopt([
204 'host' => self::FLUENT_AUTH_HOST
205 ]);
206
207 return true;
208 }
209
210 /**
211 * @return bool whether the installed FluentAuth is new enough to be adopted
212 */
213 private static function hasFluentAuthBridge()
214 {
215 // FluentAuth's autoloader requires the mapped file unconditionally, so probing
216 // for a class an older release does not ship is a fatal error, not a false.
217 return defined('FLUENT_AUTH_VERSION')
218 && FLUENT_AUTH_VERSION
219 && defined('FLUENT_AUTH_PLUGIN_PATH')
220 && file_exists(FLUENT_AUTH_PLUGIN_PATH . 'app/Services/LoginBridge.php')
221 && class_exists('\FluentAuth\App\Services\LoginBridge');
222 }
223
224 public static function getTermsText()
225 {
226 $policyUrl = apply_filters('fluent_community/terms_policy_url', get_privacy_policy_url());
227
228 $termsText = __('I agree to the terms and conditions', 'fluent-community');
229 if ($policyUrl) {
230 /* translators: %1$s is replaced by the text "terms and conditions", %2$s is replaced by the text "to the terms and conditions" */
231 $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>');
232 }
233
234 return $termsText;
235 }
236
237 public static function getFormFields($invitation = null)
238 {
239 $fields = apply_filters('fluent_community/auth/signup_fields', [
240 'full_name' => [
241 'label' => __('Full name', 'fluent-community'),
242 'placeholder' => __('Your first & last name', 'fluent-community'),
243 'type' => 'text',
244 'required' => true,
245 'value' => $invitation ? Arr::get($invitation->meta, 'invitee_name') : '',
246 'sanitize_callback' => 'sanitize_text_field'
247 ],
248 'email' => [
249 'type' => 'email',
250 'placeholder' => __('Your email address', 'fluent-community'),
251 'label' => __('Email Address', 'fluent-community'),
252 'required' => true,
253 'value' => $invitation ? $invitation->message : '',
254 'readonly' => $invitation && $invitation->message,
255 'sanitize_callback' => 'sanitize_email'
256 ],
257 'username' => [
258 'type' => 'text',
259 'placeholder' => __('No space or special characters', 'fluent-community'),
260 'label' => __('Username', 'fluent-community'),
261 'required' => true,
262 'sanitize_callback' => 'sanitize_user'
263 ],
264 'password' => [
265 'type' => 'password',
266 'placeholder' => __('Password', 'fluent-community'),
267 'label' => __('Account Password', 'fluent-community'),
268 'required' => true,
269 'sanitize_callback' => 'sanitize_text_field'
270 ],
271 'conf_password' => [
272 'type' => 'password',
273 'placeholder' => __('Password Confirmation', 'fluent-community'),
274 'label' => __('Re-type Account Password', 'fluent-community'),
275 'required' => true,
276 'sanitize_callback' => 'sanitize_text_field'
277 ],
278 'terms' => [
279 'type' => 'inline_checkbox',
280 'inline_label' => self::getTermsText(),
281 'required' => true
282 ]
283 ], $invitation);
284
285 if (!self::isPasswordConfRequired()) {
286 unset($fields['conf_password']);
287 }
288
289 return $fields;
290 }
291
292 public static function getLostPasswordUrl($redirectUrl = '')
293 {
294 if (self::isFluentAuthAvailable()) {
295 $url = add_query_arg([
296 'form' => 'reset_password'
297 ], Helper::getAuthUrl());
298 } else {
299 $url = wp_lostpassword_url($redirectUrl);;
300 }
301
302 return apply_filters('fluent_community/auth/lost_password_url', $url);
303 }
304
305 public static function getLoginFormFields()
306 {
307 return apply_filters('fluent_community/auth/login_fields', [
308 'username' => [
309 'type' => 'text',
310 'placeholder' => __('Your account email address', 'fluent-community'),
311 'label' => __('Email Address', 'fluent-community'),
312 'required' => true,
313 'sanitize_callback' => 'sanitize_user'
314 ],
315 'password' => [
316 'type' => 'password',
317 'placeholder' => __('Your account password', 'fluent-community'),
318 'label' => __('Password', 'fluent-community'),
319 'required' => true,
320 'sanitize_callback' => 'sanitize_text_field'
321 ]
322 ]);
323 }
324
325 public static function isPasswordConfRequired()
326 {
327 $isRequired = apply_filters_deprecated('fluent_community/autg/password_confirmation', [true], '2.7.8', 'fluent_community/auth/password_confirmation');
328
329 return apply_filters('fluent_community/auth/password_confirmation', $isRequired);
330 }
331
332 public static function isRegistrationEnabled()
333 {
334
335 $enabled = !!get_option('users_can_register');
336
337 if (!$enabled) {
338 $generalSettinsg = Helper::generalSettings();
339 $enabled = $generalSettinsg['explicit_registration'] !== 'no';
340 }
341
342 return apply_filters('fluent_community/auth/registration_enabled', $enabled);
343 }
344
345 public static function isTwoFactorEnabled()
346 {
347 // fluent_auth/verify_signup_email is kept for backward compatibility with FluentAuth-targeted snippets
348 $enabled = apply_filters('fluent_auth/verify_signup_email', true);
349
350 return apply_filters('fluent_community/auth/two_factor_enabled', $enabled);
351 }
352
353 public static function get2FaRegistrationCodeForm($formData)
354 {
355 $generalSettings = Helper::generalSettings();
356 try {
357 $verifcationCode = str_pad((string) random_int(100123, 900987), 6, '0', STR_PAD_LEFT);
358 } catch (\Exception $e) {
359 $verifcationCode = str_pad((string) wp_rand(100123, 900987), 6, '0', STR_PAD_LEFT);
360 }
361
362 // Keep the code hash server-side, keyed by an opaque challenge id. The client only ever
363 // receives the id, never the password verifier, so the code cannot be recovered offline.
364 $codeHash = wp_hash_password($verifcationCode);
365 $signedToken = 'fcs_' . wp_generate_password(40, false);
366 set_transient('fcom_signup_2fa_' . $signedToken, [
367 'email' => $formData['email'],
368 'code_hash' => $codeHash,
369 'expires' => time() + 600, // 10 minutes expiry
370 'attempts' => 0,
371 ], 600);
372
373 /* translators: %s is replaced by the title of the site */
374 $mailSubject = apply_filters("fluent_community/auth/signup_verification_mail_subject", sprintf(__('Your registration verification code for %s', 'fluent-community'), Arr::get($generalSettings, 'site_title')));
375
376 $pStart = '<p style="font-family: Arial, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">';
377
378 /* translators: %s is replaced by the name of the user */
379 $message = $pStart . sprintf(__('Hello %s,', 'fluent-community'), Arr::get($formData, 'first_name')) . '</p>' .
380 $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>' .
381 /* translators: %s is replaced by the verification code */
382 $pStart . '<b>' . sprintf(__('Verification Code: %s', 'fluent-community'), $verifcationCode) . '</b></p>' .
383 '<br />' .
384 $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>';
385
386 $message = apply_filters('fluent_community/auth/signup_verification_email_body', $message, $verifcationCode, $formData);
387
388 $generalSettings = Helper::generalSettings();
389 $message = (string)App::make('view')->make('email.template', [
390 'logo' => [
391 'url' => $generalSettings['logo'],
392 'alt' => $generalSettings['site_title']
393 ],
394 'bodyContent' => $message,
395 'pre_header' => __('Activate your account', 'fluent-community'),
396 'footerLines' => [
397 __('If you did not initiate this request, please ignore this email.', 'fluent-community'),
398 /* translators: %1$s is replaced by the title of the site, %2$s is replaced by the home URL */
399 sprintf(__('This email has been sent from %1$s. Site: %2$s', 'fluent-community'), Arr::get($generalSettings, 'site_title'), home_url())
400 ]
401 ]);
402
403 $mailer = new Mailer($formData['email'], $mailSubject, $message);
404
405 if ($formData['first_name']) {
406 $toName = trim(Arr::get($formData, 'first_name') . ' ' . Arr::get($formData, 'last_name'));
407 $mailer = $mailer->to($formData['email'], $toName);
408 }
409
410 $mailer->send();
411
412 ob_start();
413 ?>
414 <div class="fls_signup_verification">
415 <input type="hidden" name="__two_fa_signed_token" value="<?php echo esc_attr($signedToken); ?>"/>
416 <?php /* translators: %s is replaced by the email address */ ?>
417 <p><?php echo esc_html(\sprintf(__('A verification code has been sent to %s. Please provide the code below: ', 'fluent-community'), $formData['email'])) ?></p>
418 <div class="fcom_form-group fcom_field_verification">
419 <div class="fcom_form_label">
420 <label for="fcom_field_verification"><?php esc_html_e('Verification Code', 'fluent-community'); ?></label>
421 </div>
422 <div class="fs_input_wrap">
423 <input type="text" id="fcom_field_verification"
424 placeholder="<?php esc_html_e('2FA Code', 'fluent-community'); ?>" name="_email_verification_code"
425 required/>
426 </div>
427 </div>
428 <div class="fcom_form-group">
429 <div class="fcom_form_input">
430 <button type="submit" class="fcom_btn has_svg_loader fcom_btn_primary">
431 <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">
432 <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">
433 <animateTransform attributeType="xml"
434 attributeName="transform"
435 type="rotate"
436 from="0 25 25"
437 to="360 25 25"
438 dur="0.6s"
439 repeatCount="indefinite"/>
440 </path>
441 </svg>
442 <span> <?php esc_html_e('Complete Signup', 'fluent-community'); ?></span>
443 </button>
444 </div>
445 </div>
446 </div>
447
448 <?php
449 return ob_get_clean();
450 }
451
452 public static function validateVerificationCode($code, $verificationToken, $formData)
453 {
454 if (!is_string($verificationToken) || $verificationToken === '') {
455 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
456 }
457
458 $transientKey = 'fcom_signup_2fa_' . $verificationToken;
459 $data = get_transient($transientKey);
460
461 if (!is_array($data) || empty($data['expires']) || empty($data['email']) || empty($data['code_hash'])) {
462 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
463 }
464
465 if ((int)$data['expires'] < time()) {
466 delete_transient($transientKey);
467 return new \WP_Error('expired_token', __('Verification token has expired. Please try again.', 'fluent-community'));
468 }
469
470 if (!isset($formData['email']) || $data['email'] !== $formData['email']) {
471 return new \WP_Error('invalid_email', __('Invalid email address. Please try again', 'fluent-community'));
472 }
473
474 // Cap online guesses per challenge: after too many wrong codes the challenge is burned.
475 if ((int) Arr::get($data, 'attempts', 0) >= 10) {
476 delete_transient($transientKey);
477 return new \WP_Error('too_many_attempts', __('Too many invalid attempts. Please try again', 'fluent-community'));
478 }
479
480 if (!wp_check_password($code, $data['code_hash'])) {
481 $data['attempts'] = (int) Arr::get($data, 'attempts', 0) + 1;
482 set_transient($transientKey, $data, max(1, (int) $data['expires'] - time()));
483 return new \WP_Error('invalid_code', __('Invalid verification code. Please try again', 'fluent-community'));
484 }
485
486 // Single-use: consume the challenge on success.
487 delete_transient($transientKey);
488
489 return true;
490 }
491
492 public static function isAuthRateLimit()
493 {
494 if (apply_filters('fluent_community/auth/disable_rate_limit', false)) {
495 return true;
496 }
497
498 $transientKey = 'fluent_com_rate_limit_' . md5(Helper::getIp());
499 $rateLimit = get_transient($transientKey);
500
501 if (!$rateLimit) {
502 $rateLimit = 0;
503 }
504
505 if ($rateLimit >= 10) {
506 return new \WP_Error('rate_limit', __('Too many requests. Please try again later', 'fluent-community'));
507 }
508
509 $rateLimit = $rateLimit + 1;
510 set_transient($transientKey, $rateLimit, 300); // per 5 minutes
511 return true;
512 }
513
514
515 public static function nativeLoginForm($args = array(), $hiddenFields = [])
516 {
517 $defaults = array(
518 'echo' => true,
519 'redirect' => (is_ssl() ? 'https://' : 'http://')
520 . (isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '')
521 . (isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''),
522 'form_id' => 'loginform',
523 'label_username' => __('Email Address', 'fluent-community'),
524 'label_password' => __('Password', 'fluent-community'),
525 'label_remember' => __('Remember Me', 'fluent-community'),
526 'label_log_in' => __('Log In', 'fluent-community'),
527 'id_username' => 'user_login',
528 'id_password' => 'user_pass',
529 'id_remember' => 'rememberme',
530 'id_submit' => 'wp-submit',
531 'remember' => true,
532 'value_username' => '',
533 'username_placeholder' => __('Your account email address', 'fluent-community'),
534 'password_placeholder' => __('Your account password', 'fluent-community'),
535 'value_remember' => false,
536 );
537
538 $args = wp_parse_args($args, apply_filters('login_form_defaults', $defaults)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
539
540 $login_form_top = apply_filters('login_form_top', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
541
542 $login_form_middle = apply_filters('login_form_middle', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
543
544 $login_form_bottom = apply_filters('login_form_bottom', '', $args); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
545
546 $actionUrl = esc_url(site_url('wp-login.php', 'login_post'));
547
548 if (isset($args['action_url'])) {
549 $actionUrl = esc_url($args['action_url']);
550 }
551
552 foreach ($hiddenFields as $key => $value) {
553 $login_form_top .= \sprintf(
554 '<input type="hidden" name="%1$s" value="%2$s" />',
555 esc_attr($key),
556 esc_attr($value)
557 );
558 }
559
560 $form = \sprintf(
561 '<form name="%1$s" id="%1$s" action="%2$s" method="post">',
562 esc_attr($args['form_id']),
563 $actionUrl
564 ) .
565 $login_form_top .
566 \sprintf(
567 '<p class="login-username fcom_form-group">
568 <label for="%1$s">%2$s</label>
569 <input type="text" name="log" id="%1$s" autocomplete="username" class="input" value="%3$s" placeholder="%4$s" size="20" />
570 </p>',
571 esc_attr($args['id_username']),
572 esc_html($args['label_username']),
573 esc_attr($args['value_username']),
574 esc_attr($args['username_placeholder']),
575 ) .
576 \sprintf(
577 '<p class="login-password fcom_form-group">
578 <label for="%1$s">%2$s</label>
579 <input type="password" name="pwd" id="%1$s" autocomplete="current-password" placeholder="%3$s" class="input" value="" size="20" />
580 </p>',
581 esc_attr($args['id_password']),
582 esc_html($args['label_password']),
583 esc_attr($args['password_placeholder'])
584 ) .
585 $login_form_middle .
586 ($args['remember'] ?
587 \sprintf(
588 '<p class="login-remember fcom_form-group"><label><input name="rememberme" type="checkbox" id="%1$s" value="forever"%2$s /> %3$s</label></p>',
589 esc_attr($args['id_remember']),
590 ($args['value_remember'] ? ' checked="checked"' : ''),
591 esc_html($args['label_remember'])
592 ) : ''
593 ) .
594 \sprintf(
595 '<p class="login-submit">
596 <input type="submit" name="wp-submit" id="%1$s" class="button button-primary" value="%2$s" />
597 <input type="hidden" name="redirect_to" value="%3$s" />
598 </p>',
599 esc_attr($args['id_submit']),
600 esc_attr($args['label_log_in']),
601 esc_url($args['redirect'])
602 ) .
603 $login_form_bottom .
604 '</form>';
605
606 if ($args['echo']) {
607 echo $form; // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
608 } else {
609 return $form;
610 }
611 }
612 }
613