| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\blocks\CustomBlocks; |
| | | use JVBase\base\Site; |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\meta\MetaManager; |
| | | use JVBase\meta\MetaForm; |
| | | use JVBase\managers\AjaxRateLimiter; |
| | | use JVBase\utility\Features; |
| | | use WP_Error; |
| | | use JVBase\meta\Form; |
| | | |
| | | use JVBase\registrar\Registrar;use WP_Error; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | |
| | | class LoginManager |
| | | { |
| | | protected Features $siteFeatures; |
| | | protected ?MagicLinkManager $magicLink = null; |
| | | protected ?MetaForm $metaForm = null; |
| | | protected EmailManager $emailManager; |
| | | protected AjaxRateLimiter $rateLimiter; |
| | | protected ?Form $form = null; |
| | | protected Cache $cache; |
| | | |
| | | |
| | | protected array $forms =[]; |
| | |
| | | protected string $title = ''; |
| | | |
| | | // Token handlers registry |
| | | protected array $tokenHandlers = []; |
| | | protected array $messageHandlers = []; |
| | | |
| | | private array $allowed_file_types = [ |
| | |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->siteFeatures = Features::forSite(); |
| | | $this->metaForm = new MetaForm(); |
| | | $this->emailManager = new EmailManager(); |
| | | $this->rateLimiter = new AjaxRateLimiter(); |
| | | |
| | | // Register default token handlers |
| | | $this->registerDefaultHandlers(); |
| | | $this->cache = Cache::for('login'); |
| | | |
| | | // Initialize magic link support if enabled |
| | | if ($this->siteFeatures->has('magicLink')) { |
| | | if (Site::has('magicLink')) { |
| | | $this->initMagicLinkSupport(); |
| | | } |
| | | |
| | |
| | | |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15); |
| | | |
| | | // Handle form submissions via AJAX |
| | | add_action('wp_ajax_nopriv_jvb_login', [$this, 'handleAjaxLogin']); |
| | | add_action('wp_ajax_nopriv_jvb_register', [$this, 'handleAjaxRegister']); |
| | | add_action('wp_ajax_nopriv_jvb_lostpassword', [$this, 'handleAjaxLostPassword']); |
| | | add_action('wp_ajax_nopriv_jvb_resetpass', [$this, 'handleAjaxResetPassword']); |
| | | |
| | | // Login success handling |
| | | add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2); |
| | | |
| | | add_filter('lostpassword_url', [$this, 'resetPasswordUrl'], 10, 2); |
| | | add_filter( 'login_url', [$this, 'loginUrl'], 10, 3 ); |
| | | add_filter( 'logout_url', [$this, 'logoutUrl'], 10, 2 ); |
| | | // Allow other features to register handlers |
| | | do_action('jvbLoginManagerInit', $this); |
| | | add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2); |
| | | add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1); |
| | | } |
| | | |
| | | public function excludeLoginSitemap(array $ids): array |
| | | { |
| | | $ids[] = $this->getLoginPage(); |
| | | return $ids; |
| | | } |
| | | /************************************************************************** |
| | | * SETUP & CONFIGURATION |
| | | **************************************************************************/ |
| | |
| | | */ |
| | | public function redirectToCustomLogin(): void |
| | | { |
| | | // Handle interim login |
| | | if (isset($_GET['interim-login'])) { |
| | | // Don't redirect - let WP handle it |
| | | return; |
| | | } |
| | | // Don't redirect if AJAX or REST |
| | | if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) { |
| | | return; |
| | | } |
| | | // Build custom login URL with all query args |
| | | $custom_login_page = home_url('/login'); |
| | | $custom_login_page = home_url('/login/'); |
| | | $query_args = $_GET; |
| | | |
| | | // Remove WordPress internal args |
| | |
| | | $select = []; |
| | | //Basic fields, for any |
| | | $fields = [ |
| | | 'name' => [ |
| | | 'user_name' => [ |
| | | 'type' => 'text', |
| | | 'required' => true, |
| | | 'label' => 'Your Name', |
| | | 'placeholder'=> 'Mister Meseeks' |
| | | ], |
| | | 'email' => [ |
| | | 'user_email' => [ |
| | | 'type' => 'email', |
| | | 'required' => true, |
| | | 'label' => 'Your Email', |
| | | 'placeholder'=> 'look@me.com' |
| | | ] |
| | | ]; |
| | | if (count(JVB_USER) > 1) { |
| | | foreach (JVB_USER as $slug => $config) { |
| | | if (!array_key_exists('can_register', $config) || !$config['can_register']) { |
| | | continue; |
| | | } |
| | | $icon = $config['icon'] ?? ''; |
| | | if (Site::has('referrals')) { |
| | | $fields['referral_code'] = [ |
| | | 'type' => 'text', |
| | | 'required'=> false, |
| | | 'label' => 'Referral Code', |
| | | 'hint' => 'Have a referral code? Paste it here!' |
| | | ]; |
| | | } |
| | | $canRegister = Registrar::getFeatured('can_register', 'user'); |
| | | if (!empty($canRegister)) { |
| | | foreach ($canRegister as $role) { |
| | | $registrar = Registrar::getInstance($role); |
| | | $config = $registrar->getConfig('register'); |
| | | $icon = $registrar->getIcon('user'); |
| | | $icon = ($icon !== '') ? jvbIcon($icon) : ''; |
| | | $select[$slug] = '<span class="label">'.$icon.$config['label'].'</span><span class="text">'.$config['register']['text']??''.'</span>'; |
| | | if (!empty($config['register']['fields']??[])){ |
| | | foreach ($config['register']['fields'] as $field) { |
| | | $select[$role] = '<span class="label">'.$icon.$registrar->getSingular().'</span><span class="text">'.$config['description']??Site::login()->getDescription('register')??''.'</span>'; |
| | | if (!empty($config['fields'])){ |
| | | foreach ($config['fields'] as $field) { |
| | | $field['condition'] = [ |
| | | 'field' => 'user_select', |
| | | 'value' => $slug, |
| | | 'value' => $role, |
| | | 'operator' => '==' |
| | | ]; |
| | | $fields[] = $field; |
| | |
| | | $fields = $this->getRegistrationFormFields(); |
| | | break; |
| | | case 'lostpassword': |
| | | case 'magic': |
| | | $fields = [ |
| | | 'user_email' => [ |
| | | 'type' => 'email', |
| | |
| | | 'type' => 'email', |
| | | 'label' => __('Email Address', 'jvb'), |
| | | 'required' => true, |
| | | 'autocomplete' => 'email', |
| | | 'placeholder' => 'look@me.com', |
| | | ], |
| | | 'user_password' => [ |
| | | 'type' => 'text', |
| | | 'subtype'=> 'password', |
| | | 'label' => __('Password', 'jvb'), |
| | | 'autocomplete' => 'current-password', |
| | | 'required' => true, |
| | | ], |
| | | 'remember_me' => [ |
| | |
| | | } |
| | | } |
| | | } |
| | | public function loginUrl(string $login_url, ?string $redirect, bool $force_reauth):string |
| | | { |
| | | // This will append /custom-login/ to you main site URL as configured in general settings (ie https://domain.com/custom-login/) |
| | | $login_url = site_url( '/login/', 'login' ); |
| | | if ( ! empty( $redirect ) ) { |
| | | $login_url = add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url ); |
| | | } |
| | | if ( $force_reauth ) { |
| | | $login_url = add_query_arg( 'reauth', '1', $login_url ); |
| | | } |
| | | return $login_url; |
| | | } |
| | | |
| | | public function logoutUrl(string $logout_url, string $redirect): string |
| | | { |
| | | // Build custom logout URL |
| | | $logout_url = site_url('/login/', 'login'); |
| | | $logout_url = add_query_arg('action', 'logout', $logout_url); |
| | | |
| | | if (!empty($redirect)) { |
| | | $logout_url = add_query_arg('redirect_to', urlencode($redirect), $logout_url); |
| | | } |
| | | |
| | | // Add nonce for security |
| | | return wp_nonce_url($logout_url, 'log-out'); |
| | | } |
| | | public function resetPasswordUrl(string $url, string $redirect):string |
| | | { |
| | | error_log('reset Password Url:'.print_r($url, true)); |
| | | error_log('reset password redirect: '.print_r($redirect, true)); |
| | | |
| | | return str_replace('wp_login.php', 'login/', $url); |
| | | |
| | | } |
| | | public function getLoginPage():int|false |
| | | { |
| | | return (int)get_option(BASE.'login_page'); |
| | |
| | | return $self->isLoginPage(); |
| | | } |
| | | |
| | | /************************************************************************** |
| | | TOKEN & MESSAGE HANDLERS |
| | | Extensible by other classes |
| | | **************************************************************************/ |
| | | public function registerTokenHandler(string $token_key, callable $handler, int $priority = 10): void |
| | | { |
| | | if (!isset($this->tokenHandlers[$priority])) { |
| | | $this->tokenHandlers[$priority] = []; |
| | | } |
| | | |
| | | $this->tokenHandlers[$priority][$token_key] = $handler; |
| | | ksort($this->tokenHandlers); |
| | | } |
| | | |
| | | public function registerMessageHandler(string $type, callable $handler, ?callable $condition = null): void |
| | | { |
| | | $this->messageHandlers[$type] = [ |
| | | 'handler' => $handler, |
| | | 'condition' => $condition |
| | | ]; |
| | | } |
| | | |
| | | protected function registerDefaultHandlers(): void |
| | | { |
| | | // Invitation handler |
| | | if ($this->siteFeatures->has('invitations')) { |
| | | $this->registerTokenHandler('invite', function($token, $email, $user_id) { |
| | | if (isset($_POST['invite_token'])) { |
| | | JVB()->routes('invites')->acceptInvitation( |
| | | sanitize_text_field($_POST['invite_token']), |
| | | sanitize_email($_POST['invite_email']), |
| | | $user_id |
| | | ); |
| | | } |
| | | }); |
| | | |
| | | $this->registerMessageHandler('invitation', |
| | | function() { |
| | | $data = JVB()->routes('invites')->verifyInvitation( |
| | | sanitize_text_field($_GET['invite']), |
| | | sanitize_email($_GET['email']) |
| | | ); |
| | | $name = $data->name; |
| | | $inviters = json_decode($data->inviters, true); |
| | | $names = []; |
| | | |
| | | foreach ($inviters as $inviter) { |
| | | $artist = jvbContentFromUser((int)$inviter['user_id']); |
| | | $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name']; |
| | | } |
| | | |
| | | $message = (count($names) > 1) |
| | | ? 'are already here, and have invited you to join in!' |
| | | : ' is already here, and invited you to join in!'; |
| | | |
| | | return '<h2>Join the Scene, '.$name.'</h2> |
| | | <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>'; |
| | | }, |
| | | function() { |
| | | return isset($_GET['invite']) && isset($_GET['email']); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | // List sharing handler (Favourites) |
| | | if ($this->siteFeatures->has('favourites')) { |
| | | $this->registerTokenHandler('list_token', function($token, $email, $user_id) { |
| | | if (!empty($_GET['list_token']) && !empty($_GET['email'])) { |
| | | JVB()->routes('favourites')->acceptListInvitation( |
| | | sanitize_text_field($_GET['list_token']), |
| | | sanitize_email($_GET['email']), |
| | | $user_id |
| | | ); |
| | | } |
| | | }); |
| | | |
| | | $this->registerMessageHandler('favourites', |
| | | function() { |
| | | return '<h2>'.(JVB_LOGIN['login_from_favourite_header'] ?? 'Save your Favourites').'</h2>'; |
| | | }, |
| | | function() { |
| | | return isset($_GET['type']) && $_GET['type'] === 'favourites'; |
| | | } |
| | | ); |
| | | } |
| | | |
| | | // Referral handler - FIXED VERSION |
| | | $this->registerTokenHandler('referral_code', function($code, $email, $user_id) { |
| | | // $code is already sanitized from processTokenHandlers |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | } |
| | | $_SESSION[BASE . 'referral_code'] = $code; |
| | | setcookie( |
| | | BASE . 'referral_code', |
| | | $code, |
| | | time() + (86400 * 30), |
| | | '/' |
| | | ); |
| | | }, 5); |
| | | } |
| | | |
| | | protected function initMagicLinkSupport(): void |
| | | { |
| | | if (!Features::forSite()->has('magicLink')) { |
| | | if (!Site::has('magicLink')) { |
| | | return; |
| | | } |
| | | $this->magicLink = new MagicLinkManager(); |
| | | } |
| | | |
| | | |
| | | |
| | | /********************************************************************* |
| | | RENDERING |
| | | *********************************************************************/ |
| | |
| | | return $template; |
| | | } |
| | | $this->setup(); |
| | | $page = $this->cache->remember( |
| | | $this->getAction(), |
| | | function() { |
| | | return $this->renderPage(); |
| | | }, |
| | | 5 |
| | | ); |
| | | |
| | | echo $page; |
| | | return ''; |
| | | } |
| | | protected function renderPage() { |
| | | ob_start(); |
| | | jvbInlineStyles('nav'); |
| | | jvbInlineStyles('dash'); |
| | |
| | | $this->renderForms(); |
| | | $this->renderFooter(); |
| | | |
| | | echo ob_get_clean(); |
| | | return ''; |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function setup():void |
| | | protected function getAction():string |
| | | { |
| | | if (array_key_exists('action', $_GET)) { |
| | | switch ($_GET['action']){ |
| | |
| | | } else { |
| | | $action = 'login'; |
| | | } |
| | | return $action; |
| | | } |
| | | |
| | | $this->action = $action; |
| | | protected function setup():void |
| | | { |
| | | $this->action = $this->getAction(); |
| | | if ($this->action == 'logout' || array_key_exists('loggedout', $_GET)) { |
| | | wp_logout(); |
| | | wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url())); |
| | | exit; |
| | | } |
| | | $this->setupLabels(); |
| | | $this->setupFields(); |
| | | $this->setupTitle(); |
| | |
| | | protected function customStyles():void |
| | | { |
| | | $logo = get_theme_mod('custom_logo'); |
| | | $small = $large = ''; |
| | | if ($logo) { |
| | | $small = wp_get_attachment_image_src($logo, 'medium')[0]; |
| | | $large = wp_get_attachment_image_src($logo, 'large')[0]; |
| | | |
| | | $small = wp_get_attachment_image_src($logo, 'medium')[0]??''; |
| | | $large = wp_get_attachment_image_src($logo, 'large')[0]??''; |
| | | } |
| | | echo '<style> |
| | | .login header, |
| | |
| | | justify-content: center; |
| | | position: relative; |
| | | } |
| | | .login .fstatus.fstatus { |
| | | --wrap: nowrap; |
| | | top:0; |
| | | bottom:unset; |
| | | right: 0; |
| | | } |
| | | .login main::before { |
| | | background-size: 20vw; |
| | | inset: 0; |
| | |
| | | |
| | | protected function renderForms():void |
| | | { |
| | | |
| | | $form = $this->action.'form'; |
| | | |
| | | ?> |
| | | <section class="login-box col btw"> |
| | | <h1><?=$this->labels['title']?></h1> |
| | | <?= $this->labels['description'] ?> |
| | | |
| | | |
| | | <form name="<?=$form?>" method="post" data-action="jvb_<?=$this->action?>"> |
| | | <?= jvbFormStatus() ?> |
| | | <?php wp_nonce_field('jvb_'.$this->action, '_wpnonce'); ?> |
| | | <input type="hidden" name="action" value="jvb_<?=$this->action?>"> |
| | | <input type="hidden" name="redirect_to" value="<?= esc_attr($_GET['redirect_to'] ?? '') ?>"> |
| | | <input type="hidden" name="request_id" value="<?= wp_generate_password(16, false) ?>"> |
| | | |
| | | <?= ($this->action === 'magic') ? '<input type="hidden" name="type" value="login">' : '' ?> |
| | | <?php |
| | | $this->addHiddenTokenFields(); |
| | | do_action('jvb_add_token_inputs', $this->action); |
| | | |
| | | foreach ($this->fields as $name => $config) { |
| | | $this->metaForm->render($name, '', $config); |
| | | echo Form::render($name, '', $config); |
| | | } |
| | | |
| | | $this->maybeTurnstile(); |
| | | ?> |
| | | <div class="row btw nowrap"> |
| | | <button type="submit" class="button button-primary button-large">Log In</button> |
| | | <button type="submit" class="button button-primary button-large"><?=$this->labels['submit']?></button> |
| | | <?php $this->maybeMagicLink(); ?> |
| | | </div> |
| | | </form> |
| | |
| | | <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a> |
| | | <?php |
| | | break; |
| | | case 'lostpassword': ?> |
| | | case 'lostpassword': |
| | | case 'magic': ?> |
| | | <a href="<?= get_the_permalink() ?>">Login Instead</a> |
| | | <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a> |
| | | <?php |
| | |
| | | $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : ''; |
| | | $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode'; |
| | | echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher"> |
| | | <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'. |
| | | jvbIcon('light', ['title'=> 'Light Mode']). |
| | | jvbIcon('dark', ['title'=>'Dark Mode']). |
| | | <span class="screen-reader-text">Toggle dark mode</span> |
| | | <input class="theme-switch row" id="theme-switcher" name="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme name="dark-mode" aria-label="Toggle dark mode"><span class="slider">'. |
| | | jvbIcon('sun-dim', ['title'=> 'Light Mode']). |
| | | jvbIcon('moon', ['title'=>'Dark Mode']). |
| | | '</span></label>'; |
| | | ?> |
| | | <p class="title"> |
| | |
| | | } |
| | | } |
| | | if ($out == '') { |
| | | $out =jvbIcon('home'); |
| | | $out =jvbIcon('house'); |
| | | } |
| | | ?><?= $out ?> |
| | | </a> |
| | |
| | | <?php |
| | | } |
| | | |
| | | protected function addHiddenTokenFields(): void |
| | | { |
| | | foreach ($this->tokenHandlers as $priority => $handlers) { |
| | | foreach ($handlers as $token_key => $handler) { |
| | | if (isset($_GET[$token_key])) { |
| | | $value = sanitize_text_field($_GET[$token_key]); |
| | | echo '<input type="hidden" name="' . esc_attr($token_key) . '" value="' . esc_attr($value) . '">'; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (isset($_GET['email'])) { |
| | | echo '<input type="hidden" name="token_email" value="' . esc_attr(sanitize_email($_GET['email'])) . '">'; |
| | | } |
| | | } |
| | | |
| | | /************************************************************************* |
| | | AJAX HANDLERS |
| | | *************************************************************************/ |
| | | public function handleAjaxLogin(): void |
| | | { |
| | | check_ajax_referer('jvb_login', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('login')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | // Duplicate submission check |
| | | if (!$this->checkRequestId()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Duplicate request detected', |
| | | 'code' => 'duplicate_request' |
| | | ], 409); |
| | | } |
| | | |
| | | $email = sanitize_email($_POST['user_email'] ?? ''); |
| | | $password = $_POST['user_password'] ?? ''; |
| | | $remember = !empty($_POST['remember_me']); |
| | | |
| | | if (empty($email) || empty($password)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Please fill in all fields', |
| | | 'field' => empty($email) ? 'user_email' : 'user_password', |
| | | 'code' => 'missing_fields' |
| | | ]); |
| | | } |
| | | |
| | | // Verify Turnstile if enabled |
| | | if (!$this->verifyTurnstile()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Security verification failed', |
| | | 'code' => 'turnstile_failed' |
| | | ]); |
| | | } |
| | | |
| | | $user = get_user_by('email', $email); |
| | | if (!$user) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Unknown email address', |
| | | 'field' => 'user_email', |
| | | 'code' => 'invalid_email' |
| | | ]); |
| | | } |
| | | |
| | | $user = wp_authenticate($user->user_login, $password); |
| | | |
| | | if (is_wp_error($user)) { |
| | | wp_send_json_error([ |
| | | 'message' => $user->get_error_message(), |
| | | 'field' => 'user_password', |
| | | 'code' => $user->get_error_code() |
| | | ]); |
| | | } |
| | | |
| | | wp_clear_auth_cookie(); |
| | | wp_set_current_user($user->ID); |
| | | wp_set_auth_cookie($user->ID, $remember); |
| | | |
| | | do_action('wp_login', $user->user_login, $user); |
| | | |
| | | $redirect = $_POST['redirect_to'] ?? home_url('/dash'); |
| | | wp_send_json_success(['redirect' => $redirect]); |
| | | } |
| | | |
| | | public function handleAjaxRegister(): void |
| | | { |
| | | check_ajax_referer('jvb_register', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('register')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | // Duplicate submission check |
| | | if (!$this->checkRequestId()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Duplicate request detected', |
| | | 'code' => 'duplicate_request' |
| | | ], 409); |
| | | } |
| | | |
| | | // Verify Turnstile |
| | | if (!$this->verifyTurnstile()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Security verification failed', |
| | | 'code' => 'turnstile_failed' |
| | | ]); |
| | | } |
| | | |
| | | $name = sanitize_text_field($_POST['name'] ?? ''); |
| | | $email = sanitize_email($_POST['email'] ?? ''); |
| | | $user_type = sanitize_text_field($_POST['user_select'] ?? 'subscriber'); |
| | | |
| | | // Spam prevention - if subscriber is selected and there are other options |
| | | if ($user_type === 'subscriber' && count(JVB_USER) > 0) { |
| | | $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false); |
| | | if (!empty($registerable)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Please select a valid account type', |
| | | 'field' => 'user_select', |
| | | 'code' => 'invalid_user_type' |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | // Validate fields |
| | | if (empty($name)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Name is required', |
| | | 'field' => 'name', |
| | | 'code' => 'missing_name' |
| | | ]); |
| | | } |
| | | |
| | | if (empty($email)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Email is required', |
| | | 'field' => 'email', |
| | | 'code' => 'missing_email' |
| | | ]); |
| | | } |
| | | |
| | | // Check if role can register |
| | | if ($user_type !== 'subscriber') { |
| | | if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Invalid account type', |
| | | 'field' => 'user_select', |
| | | 'code' => 'invalid_user_type' |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | // Check if email exists |
| | | if (email_exists($email)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Email already registered', |
| | | 'field' => 'email', |
| | | 'code' => 'duplicate_email' |
| | | ]); |
| | | } |
| | | |
| | | // Create user |
| | | $user_id = wp_create_user($email, wp_generate_password(), $email); |
| | | |
| | | if (is_wp_error($user_id)) { |
| | | wp_send_json_error([ |
| | | 'message' => $user_id->get_error_message(), |
| | | 'code' => 'user_creation_failed' |
| | | ]); |
| | | } |
| | | |
| | | // Update user data |
| | | wp_update_user([ |
| | | 'ID' => $user_id, |
| | | 'display_name' => $name, |
| | | 'first_name' => $name |
| | | ]); |
| | | |
| | | // Set role |
| | | $user = new WP_User($user_id); |
| | | if ($user_type === 'subscriber') { |
| | | $user->set_role('subscriber'); |
| | | } else { |
| | | $role = JVB_USER[$user_type]['role'] ?? 'subscriber'; |
| | | $user->set_role($role); |
| | | |
| | | // Check if needs approval |
| | | if (Features::forMembership()->has('memberVerified') && |
| | | in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) { |
| | | $user->add_cap('skip_moderation', false); |
| | | update_user_meta($user_id, BASE . 'pending_approval', true); |
| | | } |
| | | } |
| | | |
| | | // Save additional fields |
| | | update_user_meta($user_id, BASE . 'user_type', $user_type); |
| | | |
| | | // Process additional fields from form |
| | | foreach ($_POST as $key => $value) { |
| | | if (in_array($key, ['name', 'email', 'action', '_wpnonce', 'request_id', 'user_select'])) { |
| | | continue; |
| | | } |
| | | update_user_meta($user_id, BASE . $key, sanitize_text_field($value)); |
| | | } |
| | | |
| | | // Handle token handlers |
| | | $this->processTokenHandlers($user_id, $email); |
| | | |
| | | // Send welcome email with password setup link |
| | | $this->sendWelcomeEmail($user_id); |
| | | |
| | | // Trigger registration action for other systems |
| | | do_action('jvbAfterUserRegistration', $user_id, $user_type, $_POST); |
| | | |
| | | wp_send_json_success([ |
| | | 'message' => 'Registration successful! Check your email.', |
| | | 'title' => $this->labels['successTitle'] ?? 'Success!', |
| | | 'description' => $this->labels['successDescription'] ?? 'Check your email for next steps', |
| | | 'user_id' => $user_id // Important for file upload dependencies! |
| | | ]); |
| | | } |
| | | |
| | | public function handleAjaxLostPassword(): void |
| | | { |
| | | check_ajax_referer('jvb_lostpassword', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('lostpassword')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | $email = sanitize_email($_POST['user_email'] ?? ''); |
| | | |
| | | if (empty($email)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Email required', |
| | | 'field' => 'user_email', |
| | | 'code' => 'missing_email' |
| | | ]); |
| | | } |
| | | |
| | | // Verify Turnstile |
| | | if (!$this->verifyTurnstile()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Security verification failed', |
| | | 'code' => 'turnstile_failed' |
| | | ]); |
| | | } |
| | | |
| | | // Use WordPress's built-in function |
| | | $result = retrieve_password($email); |
| | | |
| | | if (is_wp_error($result)) { |
| | | wp_send_json_error([ |
| | | 'message' => $result->get_error_message(), |
| | | 'code' => $result->get_error_code() |
| | | ]); |
| | | } |
| | | |
| | | wp_send_json_success(['message' => 'Check your email for reset link']); |
| | | } |
| | | |
| | | public function handleAjaxResetPassword(): void |
| | | { |
| | | check_ajax_referer('jvb_resetpass', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('resetpass')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | $key = sanitize_text_field($_POST['key'] ?? $_GET['key'] ?? ''); |
| | | $login = sanitize_text_field($_POST['login'] ?? $_GET['login'] ?? ''); |
| | | $pass1 = $_POST['pass1'] ?? ''; |
| | | $pass2 = $_POST['pass2'] ?? ''; |
| | | |
| | | if (empty($key) || empty($login)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Invalid reset link', |
| | | 'code' => 'invalid_key' |
| | | ]); |
| | | } |
| | | |
| | | if (empty($pass1) || empty($pass2)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Please enter a password', |
| | | 'field' => empty($pass1) ? 'pass1' : 'pass2', |
| | | 'code' => 'missing_password' |
| | | ]); |
| | | } |
| | | |
| | | if ($pass1 !== $pass2) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Passwords do not match', |
| | | 'field' => 'pass2', |
| | | 'code' => 'password_mismatch' |
| | | ]); |
| | | } |
| | | |
| | | // Verify reset key |
| | | $user = check_password_reset_key($key, $login); |
| | | |
| | | if (is_wp_error($user)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Invalid or expired reset link', |
| | | 'code' => 'invalid_key' |
| | | ]); |
| | | } |
| | | |
| | | // Reset password |
| | | reset_password($user, $pass1); |
| | | |
| | | wp_send_json_success([ |
| | | 'message' => 'Password reset successfully', |
| | | 'redirect' => home_url('/login') |
| | | ]); |
| | | } |
| | | |
| | | |
| | | /********************************************************************** |
| | | TOKEN PROCESSING |
| | | **********************************************************************/ |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | /*********************************************************************** |
| | | EMAIL SENDING |
| | | ***********************************************************************/ |
| | | protected function sendWelcomeEmail(int $user_id): void |
| | | { |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | // Generate password reset key |
| | | $key = get_password_reset_key($user); |
| | | if (is_wp_error($key)) { |
| | | error_log('Failed to generate password reset key: ' . $key->get_error_message()); |
| | | return; |
| | | } |
| | | |
| | | $reset_url = add_query_arg([ |
| | | 'action' => 'rp', |
| | | 'key' => $key, |
| | | 'login' => rawurlencode($user->user_login) |
| | | ], home_url('/login')); |
| | | |
| | | $subject = $this->labels['email'] ?? 'Welcome to ' . get_bloginfo('name'); |
| | | |
| | | $message = '<h2>Welcome, ' . esc_html($user->display_name) . '!</h2>'; |
| | | $message .= '<p>Your account has been created. Click the button below to set your password and get started:</p>'; |
| | | $message .= jvbMailButton($reset_url, 'Set Your Password'); |
| | | $message .= '<p>This link expires in 24 hours.</p>'; |
| | | |
| | | $this->emailManager->sendEmail($user->user_email, $subject, $message); |
| | | } |
| | | |
| | | /************************************************************************* |
| | | * SECURITY & VALIDATION |
| | | *************************************************************************/ |
| | | protected function checkAjaxRateLimit(string $action): bool |
| | | { |
| | | return $this->rateLimiter->checkLimit($action); |
| | | } |
| | | |
| | | protected function checkRequestId(): bool |
| | | { |
| | | $request_id = $_POST['request_id'] ?? ''; |
| | |
| | | |
| | | protected function maybeTurnstile(): void |
| | | { |
| | | if (!Features::hasIntegration('cloudflare')) { |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | return; |
| | | } |
| | | JVB()->connect('cloudflare')->renderTurnstile(); |
| | |
| | | |
| | | protected function maybeTurnstileScripts(): void |
| | | { |
| | | if (!Features::hasIntegration('cloudflare')) { |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | return; |
| | | } |
| | | JVB()->connect('cloudflare')->enqueueTurnstileScripts(); |
| | |
| | | |
| | | protected function verifyTurnstile(): bool |
| | | { |
| | | if (!Features::hasIntegration('cloudflare')) { |
| | | if (!Site::hasIntegration('cloudflare')) { |
| | | return true; // Not enabled, pass verification |
| | | } |
| | | |
| | |
| | | protected function setupLabels(): void |
| | | { |
| | | $default = $this->getDefaultLabels(); |
| | | $this->labels = apply_filters('jvbLoginLabels', $default, $_GET); |
| | | $default = apply_filters('jvbLoginLabels', $default, $_GET); |
| | | |
| | | foreach (['description', 'footer', 'extra'] as $location) { |
| | | $text = (!is_array($this->labels[$location])) ? [$this->labels[$location]] : $this->labels[$location]; |
| | | if (!empty($text)) { |
| | | $this->labels[$location] = '<div class="'.$location.'">'; |
| | | foreach ($text as $d) { |
| | | $this->labels[$location] .= '<p>'.$d.'</p>'; |
| | | if(array_key_exists('type', $_GET) && $_GET['type'] === 'favourites') { |
| | | if (array_key_exists('favourites', JVB_LOGIN)) { |
| | | foreach (JVB_LOGIN['favourites'] as $key => $value) { |
| | | $default[$key] = $value; |
| | | } |
| | | $this->labels[$location] .= '</div>'; |
| | | } |
| | | } |
| | | |
| | | foreach (['description', 'footer', 'extra'] as $location) { |
| | | if ($default[$location] === '') { |
| | | continue; |
| | | } |
| | | if (empty($default[$location])) { |
| | | $default[$location] = ''; |
| | | continue; |
| | | } |
| | | $text = (!is_array($default[$location])) ? [$default[$location]] : $default[$location]; |
| | | |
| | | if (!empty($text)) { |
| | | $default[$location] = '<div class="'.$location.'">'; |
| | | foreach ($text as $d) { |
| | | $default[$location] .= '<p>'.$d.'</p>'; |
| | | } |
| | | $default[$location] .= '</div>'; |
| | | } |
| | | } |
| | | $this->labels = $default; |
| | | } |
| | | |
| | | protected function getDefaultLabels(): array |
| | | { |
| | | switch ($this->action) { |
| | | case 'register': |
| | | return [ |
| | | 'title' => JVB_LOGIN['register']['title'] ?? 'Create Your Account', |
| | | 'description' => JVB_LOGIN['register']['description'] ?? [], |
| | | 'extra' => JVB_LOGIN['register']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['register']['footer'] ?? '', |
| | | 'email' => JVB_LOGIN['register']['email']['subject'] ?? '['.get_bloginfo('name').'] Finish Creating Your Account', |
| | | 'submit' => JVB_LOGIN['register']['submit'] ?? 'Create Account', |
| | | 'successTitle' => JVB_LOGIN['register']['success']['title'] ?? 'Success!', |
| | | 'successDescription' => JVB_LOGIN['register']['success']['description'] ?? ['See your email for next steps','(Check your spam folder if you cannot find it after a couple minutes.)'], |
| | | ]; |
| | | return Site::login()->getLabels('register'); |
| | | case 'lostpassword': |
| | | return [ |
| | | 'title' => JVB_LOGIN['forgot_password']['title'] ?? 'Reset Password', |
| | | 'description' => JVB_LOGIN['forgot_password']['description'] ?? [], |
| | | 'extra' => JVB_LOGIN['forgot_password']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['forgot_password']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['forgot_password']['submit'] ?? 'Send Reset Link', |
| | | 'successTitle' => JVB_LOGIN['forgot_password']['success']['title'] ?? 'Success!', |
| | | 'successDescription' => JVB_LOGIN['forgot_password']['success']['description'] ?? ['Check your email for reset instructions'], |
| | | ]; |
| | | return Site::login()->getLabels('lostPassword'); |
| | | case 'resetpass': |
| | | return [ |
| | | 'title' => JVB_LOGIN['reset_pass']['title'] ?? 'Reset Your Password', |
| | | 'description' => JVB_LOGIN['reset_pass']['description'] ?? [], |
| | | 'extra' => JVB_LOGIN['reset_pass']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['reset_pass']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['reset_pass']['submit'] ?? 'Reset Password', |
| | | ]; |
| | | case 'rp': |
| | | return Site::login()->getLabels('resetPassword'); |
| | | case 'logout': |
| | | return Site::login()->getLabels('logout'); |
| | | case 'magic': |
| | | return Site::login()->getLabels('magic'); |
| | | case 'login': |
| | | default: |
| | | return [ |
| | | 'title' => JVB_LOGIN['login']['title'] ?? 'Sign in', |
| | | 'description' => JVB_LOGIN['login']['description'] ?? [], |
| | | 'extra' => JVB_LOGIN['login']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['login']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['login']['submit'] ?? 'Sign In', |
| | | ]; |
| | | return Site::login()->getLabels('login'); |
| | | } |
| | | } |
| | | |
| | | protected function maybeMagicLink(): void |
| | | { |
| | | if (!$this->magicLink || !in_array($this->action, ['login', 'lostpassword'])) { |
| | | if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) { |
| | | return; |
| | | } |
| | | ?> |
| | | <button type="button" id="magic-link-btn" class="button button-secondary button-large"> |
| | | <?= jvbIcon('email', ['size' => 20]); ?> |
| | | Get Login Link |
| | | </button> |
| | | <script type="text/javascript"> |
| | | document.getElementById('magic-link-btn')?.addEventListener('click', function(e) { |
| | | e.preventDefault(); |
| | | const email = document.querySelector('input[name="user_email"]')?.value; |
| | | if (!email) { |
| | | alert('Please enter your email address first'); |
| | | return; |
| | | } |
| | | |
| | | fetch('<?= rest_url('jvb/v1/magic-link'); ?>', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': '<?= wp_create_nonce('wp_rest') ?>' |
| | | }, |
| | | body: JSON.stringify({ email: email, type: 'login' }) |
| | | }) |
| | | .then(r => r.json()) |
| | | .then(data => { |
| | | alert(data.success ? 'Check your email!' : (data.message || 'Failed to send link')); |
| | | }); |
| | | }); |
| | | </script> |
| | | <a class="button" href="<?= add_query_arg('action', 'magic', wp_login_url()) ?>" title="Email yourself a link to log you in auto-magically!"> |
| | | <?= jvbIcon('magic-wand'); ?> |
| | | Magic Link |
| | | </a> |
| | | <?php |
| | | } |
| | | |
| | |
| | | SCRIPTS |
| | | ************************************************************************/ |
| | | public function enqueueScripts(): void |
| | | { |
| | | if (!$this->isLoginPage()) { |
| | | return; |
| | | } |
| | | { |
| | | if (!$this->isLoginPage()) { |
| | | return; |
| | | } |
| | | |
| | | $this->maybeTurnstileScripts(); |
| | | wp_enqueue_script('jvb-form'); |
| | | $this->maybeTurnstileScripts(); |
| | | wp_enqueue_script('jvb-form'); |
| | | $action = $this->getAction(); |
| | | |
| | | $script = " |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | const form = document.querySelector('.login form'); |
| | | if (form && window.jvbForm) { |
| | | let controller = new window.jvbForm(); |
| | | controller.registerForm(form, { |
| | | autosave: false, |
| | | endpoint: false |
| | | $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : ''; |
| | | $has_turnstile = Site::hasIntegration('cloudflare'); |
| | | |
| | | ob_start(); |
| | | |
| | | ?> |
| | | |
| | | document.addEventListener('DOMContentLoaded', async function () { |
| | | const hasTurnstile = <?= json_encode($has_turnstile) ?>; |
| | | const redirectTo = <?= json_encode($redirect_to) ?>; |
| | | |
| | | window.auth.subscribe(event => { |
| | | if (event === 'auth-loaded') { |
| | | const form = document.querySelector('.login form'); |
| | | if (!form || !window.jvbForm) return; |
| | | |
| | | window.jvbForm.registerForm(form, { |
| | | endpoint: '<?= $action ?>', |
| | | showStatus: false, |
| | | cache: false, |
| | | }); |
| | | } else if (form && !window.jvbForm) { |
| | | console.error('jvbForm not loaded'); |
| | | } |
| | | });"; |
| | | |
| | | window.jvbForm.subscribe((event, data) => { |
| | | if (event === 'form-submit') { |
| | | const { config } = data; |
| | | const formElement = config.element; |
| | | |
| | | // Collect current form data |
| | | const formData = new FormData(formElement); |
| | | const formObject = Object.fromEntries(formData.entries()); |
| | | |
| | | let params = new URLSearchParams(window.location.search); |
| | | if (params.has('key')) { |
| | | formObject['key'] = params.get('key'); |
| | | } |
| | | if (params.has('login')) { |
| | | formObject['login'] = params.get('login'); |
| | | } |
| | | |
| | | // Add redirect_to from URL |
| | | if (redirectTo) { |
| | | formObject.redirect_to = redirectTo; |
| | | } |
| | | |
| | | const submit = formElement.querySelector('[type=submit]'); |
| | | const oldText = submit.textContent; |
| | | |
| | | window.jvbForm.showFormStatus(config.id, 'uploading'); |
| | | |
| | | submit.disabled = true; |
| | | submit.textContent = 'Loading...'; |
| | | |
| | | window.auth.fetch(`${jvbSettings.api}auth/<?= $action ?>`, { |
| | | method: 'POST', |
| | | body: JSON.stringify(formObject) |
| | | }) |
| | | .then(response => response.json().then(result => ({ response, result }))) |
| | | .then(({ response, result }) => { |
| | | if (!response.ok) { |
| | | window.jvbForm.showFormStatus(config.id, 'error'); |
| | | window.jvbForm.handleFormError(formElement, result); |
| | | return; |
| | | } |
| | | |
| | | window.jvbForm.showFormStatus(config.id, 'submitted'); |
| | | |
| | | if (result.message) { |
| | | window.jvbForm.handleFormSuccess(formElement, result); |
| | | } |
| | | |
| | | if (window.auth?.handleLogin && result.auth) { |
| | | return window.auth.handleLogin(result.auth).then(() => { |
| | | if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 100); |
| | | } |
| | | }); |
| | | } else if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 100); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error('Form submission error:', error); |
| | | window.jvbForm.showFormStatus(config.id, 'error'); |
| | | window.jvbForm.handleFormError(formElement, { |
| | | message: 'Network error. Please check your connection and try again.', |
| | | code: 'network_error' |
| | | }); |
| | | }) |
| | | .finally(() => { |
| | | submit.textContent = oldText; |
| | | submit.disabled = false; |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | <?php |
| | | $script = ob_get_clean(); |
| | | wp_add_inline_script('jvb-form', $script); |
| | | } |
| | | |
| | |
| | | wp_safe_redirect($login_url); |
| | | exit; |
| | | } |
| | | |
| | | public function saveRegistrationFields(int $user_id, array $userdata):void |
| | | { |
| | | |
| | | } |
| | | } |
| | | |
| | | // Initialize the login manager |