Jake Vanderwerf
7 days ago 46d681c6b825d21b3f698d793c4e630c687d90ad
inc/managers/LoginManager.php
@@ -1,11 +1,11 @@
<?php
namespace JVBase\managers;
use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
use JVBase\meta\Form;
use JVBase\utility\Features;
use WP_Error;
use JVBase\registrar\Registrar;use WP_Error;
use WP_User;
if (!defined('ABSPATH')) {
@@ -14,7 +14,6 @@
class LoginManager
{
   protected Features $siteFeatures;
   protected ?Form $form = null;
   protected Cache $cache;
@@ -24,6 +23,7 @@
   protected array $fields = [];
   protected ?string $action = null;
   protected string $title = '';
   protected static LoginManager $instance;
   // Token handlers registry
   protected array $messageHandlers = [];
@@ -36,15 +36,14 @@
   ];
   private int $max_file_size = 5242880; // 5MB in bytes
   public function __construct()
   {
      $this->siteFeatures = Features::forSite();
      self::$instance = $this;
      $this->cache = Cache::for('login');
      $this->cache->flush();
      // Initialize magic link support if enabled
      if ($this->siteFeatures->has('magicLink')) {
      if (Site::has('magicLink')) {
         $this->initMagicLinkSupport();
      }
@@ -69,6 +68,10 @@
      add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
      add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeLoginSitemap'], 10, 1);
   }
   public static function getInstance():self
   {
      return self::$instance;
   }
   public function excludeLoginSitemap(array $ids): array
   {
@@ -129,7 +132,7 @@
               'placeholder'=> 'look@me.com'
            ]
         ];
         if (Features::forSite()->has('referrals')) {
         if (Site::has('referrals')) {
            $fields['referral_code'] = [
               'type'   => 'text',
               'required'=> false,
@@ -137,19 +140,19 @@
               'hint'   => 'Have a referral code? Paste it here!'
            ];
         }
         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'] ?? '';
         $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;
@@ -187,8 +190,13 @@
   protected function setupFields():void
   {
      $this->fields = $this->getFieldsForAction($this->action);
   }
   protected function getFieldsForAction(string $action):array
   {
      $fields = [];
      switch($this->action) {
      switch($action) {
         case 'register':
            $fields = $this->getRegistrationFormFields();
            break;
@@ -259,9 +267,10 @@
            break;
      }
      $this->fields = $fields;
      return $fields;
   }
   /**
    * Ensure login page exists
    */
@@ -293,7 +302,7 @@
         }
      }
   }
   public function loginUrl(string $login_url, string $redirect, bool $force_reauth):string
   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' );
@@ -345,7 +354,7 @@
   protected function initMagicLinkSupport(): void
   {
      if (!Features::forSite()->has('magicLink')) {
      if (!Site::has('magicLink')) {
         return;
      }
   }
@@ -358,6 +367,11 @@
      if (!$this->isLoginPage()) {
         return $template;
      }
      global $_GET;
      if (is_user_logged_in() && (!array_key_exists('action', $_GET) || $_GET['action']!=='logout')) {
         wp_redirect(get_home_url(null, '/dash'));
         exit;
      }
      $this->setup();
      $page = $this->cache->remember(
            $this->getAction(),
@@ -474,8 +488,8 @@
         .login main .login-box {
            --gap: .75rem;
            padding: 1rem;
            background-color:rgba(var(--base-rgb),var(--op-6));
            border-radius: var(--outerRadius);
            background-color: var(--overlay-heavy);
            box-shadow: var(--shadow-right), var(--shadow-down);
            margin: 15vh auto 0!important;
         }
@@ -505,7 +519,8 @@
            .login main .navigation,
            .login main .login-box {
               max-width: 60vw!important;
               margin: 0 2rem 0 auto!important;
               padding-right: 4rem!important;
               margin: 0 0 0 auto!important;
            }
            .login main .login-box {
               padding: 2rem;
@@ -534,32 +549,12 @@
   {
      $form = $this->action.'form';
      ?>
      <section class="login-box col btw">
      <section class="login-box col y-btw">
         <h1><?=$this->labels['title']?></h1>
         <?= $this->labels['description'] ?>
         <?= $this->renderLoginForm($this->action); ?>
         <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
            do_action('jvb_add_token_inputs', $this->action);
            foreach ($this->fields as $name => $config) {
               echo Form::render($name, '', $config);
            }
            $this->maybeTurnstile();
             ?>
             <div class="row btw nowrap">
               <button type="submit" class="button button-primary button-large"><?=$this->labels['submit']?></button>
               <?php $this->maybeMagicLink(); ?>
            </div>
         </form>
         <?php
         if (is_array($this->labels['extra'])) {
@@ -573,7 +568,7 @@
         }
         ?>
         <div class="options row btw">
         <div class="options row x-btw">
            <?php
            switch ($this->action) {
               case 'login': ?>
@@ -598,7 +593,7 @@
         </div>
      </section>
      <div class="navigation row btw">
      <div class="navigation row x-btw">
         <a href="<?= get_home_url() ?>">Home</a>
         <?php
         $privacy = get_privacy_policy_url();
@@ -608,6 +603,59 @@
      </div>
      <?php
   }
   public function renderLoginForm(string $action = 'login', string $redirect = '', string $title = ''):string
   {
      ob_start();
      do_action('jvb_add_token_inputs', $this->action);
      $additionalInputs = ob_get_clean();
      $fields = '';
      $theFields = $this->getFieldsForAction($action);
      foreach ($theFields as $name => $config) {
         $fields .= Form::render($name, '', $config);
      }
      ob_start();
      $this->maybeTurnstile();
      $turnstile = ob_get_clean();
      ob_start();
      $this->maybeMagicLink();
      $magicLink = ob_get_clean();
      $redirect = !empty($redirect) ? $redirect : esc_attr($_GET['redirect_to'] ?? '');
      return sprintf(
         '<form name="%sform" method="post" data-action="jvb_%s">
            %s%s%s
            <input type="hidden" name="action" value="jvb_%s">
            <input type="hidden" name="redirect_to" value="%s">
            <input type="hidden" name="request_id" value="%s">
            %s
            %s
            %s
            %s
             <div class="row x-btw nowrap">
               <button type="submit" class="button button-primary button-large">%s</button>
               %s
            </div>
         </form>',
         $action,
         $action,
         jvbFormStatus(),
         $title,
         wp_nonce_field('jvb_'.$action),
         $action,
         $redirect,
         wp_generate_password(16, false),
         ($action === 'magic') ? '<input type="hidden" name="type" value="login">' : '',
         $additionalInputs,
         $fields,
         $turnstile,
         $this->labels['submit'],
         $magicLink
      );
   }
   protected function renderHeader():void
    {
    ?>
@@ -626,7 +674,7 @@
            <?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">
            echo '<label title="'.$title.'" id="theme-switch" class="switch" for="theme-switcher">
               <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']).
@@ -715,7 +763,7 @@
   protected function maybeTurnstile(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->renderTurnstile();
@@ -723,7 +771,7 @@
   protected function maybeTurnstileScripts(): void
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return;
      }
      JVB()->connect('cloudflare')->enqueueTurnstileScripts();
@@ -731,7 +779,7 @@
   protected function verifyTurnstile(): bool
   {
      if (!Features::hasIntegration('cloudflare')) {
      if (!Site::hasIntegration('cloudflare')) {
         return true; // Not enabled, pass verification
      }
@@ -784,61 +832,19 @@
   {
      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':
         case 'rp':
            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',
            ];
            return Site::login()->getLabels('resetPassword');
         case 'logout':
            return [
               'title' => JVB_LOGIN['logout']['title'] ?? 'Logged Out!',
               'description' => JVB_LOGIN['logout']['description'] ?? [],
               'extra' => JVB_LOGIN['logout']['extra'] ?? [],
               'footer' => JVB_LOGIN['logout']['footer'] ?? '',
               'submit' => JVB_LOGIN['logout']['submit'] ?? '',
            ];
            return Site::login()->getLabels('logout');
         case 'magic':
            return [
               'title' => JVB_LOGIN['magic']['title'] ?? 'Log in with Magic Link',
               'description' => JVB_LOGIN['magic']['description'] ?? ['Enter your email.','You\'ll get an email with a magic link.','Click it, and you\'re logged in!'],
               'extra' => JVB_LOGIN['magic']['extra'] ?? [],
               'footer' => JVB_LOGIN['magic']['footer'] ?? '',
               'submit' => JVB_LOGIN['magic']['submit'] ?? jvbIcon('magic-wand').'Send Magic Link',
            ];
            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');
      }
   }
@@ -870,7 +876,7 @@
    $action = $this->getAction();
    $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : '';
    $has_turnstile = Features::hasIntegration('cloudflare');
    $has_turnstile = Site::hasIntegration('cloudflare');
    ob_start();
@@ -886,9 +892,8 @@
            if (!form || !window.jvbForm) return;
            window.jvbForm.registerForm(form, {
               autosave: false,
               endpoint: '<?= $action ?>',
               formStatus: false,
               showStatus: false,
               cache: false,
            });
@@ -945,13 +950,13 @@
                           if (result.redirect) {
                              setTimeout(() => {
                                 window.location.href = result.redirect;
                              }, 100);
                              }, 20);
                           }
                        });
                     } else if (result.redirect) {
                        setTimeout(() => {
                           window.location.href = result.redirect;
                        }, 100);
                        }, 20);
                     }
                  })
                  .catch(error => {
@@ -1009,6 +1014,11 @@
   {
   }
   public function setAction(string $action = 'login'):void
   {
      $this->action = $action;
      $this->setup();
   }
}
// Initialize the login manager