From 46d681c6b825d21b3f698d793c4e630c687d90ad Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 21 May 2026 21:41:53 +0000
Subject: [PATCH] =Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start
---
inc/managers/LoginManager.php | 1917 +++++++++++++++++++++++++++++------------------------------
1 files changed, 944 insertions(+), 973 deletions(-)
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 7b04aef..68db551 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -1,1054 +1,1025 @@
<?php
namespace JVBase\managers;
-use JVBase\meta\MetaManager;
-use WP_Error;
+use JVBase\base\Site;
+use JVBase\forms\TaxonomySelector;
+use JVBase\meta\Form;
+
+use JVBase\registrar\Registrar;use WP_Error;
use WP_User;
if (!defined('ABSPATH')) {
- exit; // Exit if accessed directly
+ exit;
}
class LoginManager
{
- private array|null $invitation_data = null;
- protected array $inviteData = [];
- private array $allowed_file_types = [
- 'image/jpeg',
- 'image/png',
- 'image/gif',
- 'application/pdf'
- ];
- private int $max_file_size = 5242880; // 5MB in bytes
+ protected ?Form $form = null;
+ protected Cache $cache;
- public function __construct()
- {
- // Common login page customization
- add_action('login_enqueue_scripts', array($this, 'loginStyles'));
- add_action('login_header', array($this, 'loginHeader'), 0);
- add_action('login_footer', array($this, 'loginFooter'));
- // Login page filters
- add_filter('login_headerurl', array($this, 'logoUrl'));
- add_filter('login_headertext', array($this, 'logoTitle'));
- add_filter('login_message', array($this, 'loginMessage'));
- add_filter('login_errors', array($this, 'loginErrors'));
+ protected array $forms =[];
+ protected array $labels = [];
+ protected array $fields = [];
+ protected ?string $action = null;
+ protected string $title = '';
+ protected static LoginManager $instance;
- // Login success handling
- add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
+ // Token handlers registry
+ protected array $messageHandlers = [];
- // Registration-specific hooks
- if ($this->isRegistrationPage()) {
- $this->initRegistrationHooks();
- }
- }
+ private array $allowed_file_types = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif',
+ 'application/pdf'
+ ];
+ private int $max_file_size = 5242880; // 5MB in bytes
- /**
- * Check if we're on the registration page
- */
- private function isRegistrationPage(): bool
- {
- return isset($_GET['action']) && $_GET['action'] === 'register';
- }
- /**
- * Initialize registration-specific hooks
- */
- private function initRegistrationHooks(): void
- {
- add_action('register_form', array($this, 'addRegistrationFields'));
- add_action('login_header', array($this, 'addRegistrationScript'));
- add_filter('registration_errors', array($this, 'registrationErrorsFilter'), 10, 3);
- add_action('user_register', array($this, 'saveRegistrationFields'), 999, 2);
- add_action('login_head', array($this, 'modifyRegistrationForm'));
- add_action('register_form', array($this, 'addUploadSupport'));
- add_filter('pre_user_login', array($this, 'setUserLogin'), 1);
- add_filter('pre_user_email', array($this, 'setUserEmail'), 1);
- add_filter('register_message', array($this, 'customRegisterMessage'));
- add_filter('wp_login_errors', array($this, 'registrationSuccessMessage'), 10, 2);
- add_filter('login_form_top', array($this, 'loginFormTop'));
- add_filter('login_form_bottom', array($this, 'loginFormBottom'));
- add_filter('login_form_middle', array($this, 'loginFormMiddle'));
+ public function __construct()
+ {
+ self::$instance = $this;
+ $this->cache = Cache::for('login');
+ $this->cache->flush();
+ // Initialize magic link support if enabled
+ if (Site::has('magicLink')) {
+ $this->initMagicLinkSupport();
+ }
- // Remove default username requirement for registration
- remove_filter('registration_errors', 'registration_auth_pass_filter', 10);
- }
+ // Create login page if it doesn't exist
+ $this->ensureLoginPageExists();
- /**
- * Combined login styles for both login and registration
- */
- public function loginStyles(): void
- {
- do_action('jvbLoginStyles');
- }
- /**
- * Login header - used for both login and registration
- */
- public function loginHeader(): void
- {
- ?>
- <script type="text/javascript">
- document.addEventListener('DOMContentLoaded', function() {
- let loginLabel = document.querySelector('label[for="user_login"');
- loginLabel.innerHTML = '<?= jvbIcon('email', ['size' => 20]); ?> Your Email';
+ // Redirect wp-login.php to custom page
+ add_action('login_init', [$this, 'redirectToCustomLogin']);
+ add_action('template_include', [$this, 'renderLoginPage']);
- let passwordLabel = document.querySelector('label[for="user_pass"');
- passwordLabel.innerHTML = '<?= jvbIcon('password', ['size' => 20]); ?> Your Password';
+ add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15);
- document.querySelector('form').classList.add('loaded');
- });
+ // Login success handling
+ add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
- </script>
- <?php
- }
+ 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 static function getInstance():self
+ {
+ return self::$instance;
+ }
- /**
- * Login footer with donate section
- */
- public function loginFooter(): void
- {
- do_action('jvbLoginFooter');
+ public function excludeLoginSitemap(array $ids): array
+ {
+ $ids[] = $this->getLoginPage();
+ return $ids;
+ }
+ /**************************************************************************
+ * SETUP & CONFIGURATION
+ **************************************************************************/
- }
+ /**
+ * Redirect wp-login.php to custom login page
+ */
+ 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/');
+ $query_args = $_GET;
- /**
- * Logo URL
- */
- public function logoUrl(): string
- {
- return home_url();
- }
+ // Remove WordPress internal args
+ unset($query_args['interim-login'], $query_args['wp-auth-check']);
- /**
- * Logo title
- */
- public function logoTitle(): string
- {
- return get_bloginfo('name');
- }
+ if (!empty($query_args)) {
+ $custom_login_page = add_query_arg($query_args, $custom_login_page);
+ }
- /**
- * Login message - handles both login and registration
- */
- public function loginMessage(string $message): string
- {
- if ($this->isRegistrationPage()) {
- if (jvbSiteHasInvitations() && $this->fromInvite()) {
- $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>';
- }
- if (jvbSiteHasFavourites() && $this->fromFavourites()) {
- return '<h2>'.JVB_LOGIN['login_from_favourite_header']??'Save your Favourites'.'</h2>';
- }
- return '<h2>'.JVB_LOGIN['join_header'].'</h2>';
- } else {
- if (jvbSiteHasFavourites()) {
- $login = (!$this->fromFavourites()) ? '<h2>'.JVB_LOGIN['login_header'].'</h2>' : '<h2>'.JVB_LOGIN['login_from_favourite_header'].'</h2>';
- } else {
- $login = '<h2>'.JVB_LOGIN['login_header'].'</h2>';
+ wp_safe_redirect($custom_login_page);
+ exit;
+ }
+ protected function getRegistrationFormFields():array
+ {
+ $form = get_option(BASE.'registration_form_fields');
+ if (!$form) {
+ $form = [];
+
+ $select = [];
+ //Basic fields, for any
+ $fields = [
+ 'user_name' => [
+ 'type' => 'text',
+ 'required' => true,
+ 'label' => 'Your Name',
+ 'placeholder'=> 'Mister Meseeks'
+ ],
+ 'user_email' => [
+ 'type' => 'email',
+ 'required' => true,
+ 'label' => 'Your Email',
+ 'placeholder'=> 'look@me.com'
+ ]
+ ];
+ 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[$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' => $role,
+ 'operator' => '=='
+ ];
+ $fields[] = $field;
+ }
+ }
+ }
+ if (!empty($select)) {
+ $select = array_merge(
+ [
+ 'subscriber' => 'Subscriber',
+ ],
+ $select
+ );
+ $form = array_merge(
+ [
+ 'user_select' => [
+ 'type' => 'radio',
+ 'label' => 'Register as',
+ 'options' => $select,
+ 'required' => true,
+ 'default' => 'subscriber'
+ ]
+ ],
+ $fields
+ );
+ }
+ }else {
+ $form = $fields;
+ }
+ update_option(BASE.'registration_form_fields', $form);
+ }
+ return $form;
+
+ }
+
+ protected function setupFields():void
+ {
+ $this->fields = $this->getFieldsForAction($this->action);
+ }
+
+ protected function getFieldsForAction(string $action):array
+ {
+ $fields = [];
+ switch($action) {
+ case 'register':
+ $fields = $this->getRegistrationFormFields();
+ break;
+ case 'lostpassword':
+ case 'magic':
+ $fields = [
+ 'user_email' => [
+ 'type' => 'email',
+ 'label' => __('Email Address', 'jvb'),
+ 'required' => true,
+ 'placeholder' => 'look@me.com',
+ ],
+ ];
+ break;
+ case 'rp':
+ case 'resetpass':
+ $fields = [
+ 'pass1' => [
+ 'type' => 'text',
+ 'subtype' => 'password',
+ 'label' => __('New Password', 'jvb'),
+ 'required' => true,
+ ],
+ 'pass2' => [
+ 'type' => 'text',
+ 'subtype' => 'password',
+ 'label' => __('Confirm Password', 'jvb'),
+ 'required' => true,
+ ],
+ ];
+ break;
+ case 'login':
+ $fields = [
+ 'user_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' => [
+ 'type' => 'true_false',
+ 'label' => __('Remember Me', 'jvb'),
+ 'default' => true
+ ]
+ ];
+ break;
+ case 'postpass':
+ $fields = [
+ 'post_password' => [
+ 'type' => 'text',
+ 'subtype' => 'password',
+ 'label' => __('Password', 'jvb'),
+ 'required' => true,
+ 'hint' => 'This post is password protected. Please enter the password to view it.',
+ ],
+ ];
+ break;
+ case 'confirmaction':
+
+ break;
+
+ }
+ return $fields;
+ }
+
+
+ /**
+ * Ensure login page exists
+ */
+ protected function ensureLoginPageExists(): void
+ {
+ $login_page = $this->getLoginPage();
+
+ if (!$login_page || !is_int($login_page)) {
+ $page_id = get_page_by_path('login');
+ if (!$page_id) {
+ $page_id = wp_insert_post([
+ 'post_title' => 'Login',
+ 'post_name' => 'login',
+ 'post_content' => '[jvb_login_form]',
+ 'post_status' => 'publish',
+ 'post_type' => 'page',
+ 'post_author' => 1
+ ]);
}
- return (empty($message)) ? $login : $login.$message;
- }
- }
-
- protected function fromFavourites():bool
+ if ($page_id && !is_wp_error($page_id)) {
+ if (is_object($page_id)) {
+ $page_id = (int)$page_id->ID;
+ }
+ update_option(BASE.'login_page', $page_id);
+ // Hide from menus/search
+ update_post_meta($page_id, '_wp_page_template', 'default');
+ update_post_meta($page_id, BASE . 'exclude_from_search', true);
+ }
+ }
+ }
+ public function loginUrl(string $login_url, ?string $redirect, bool $force_reauth):string
{
- return array_key_exists('type', $_GET) && $_GET['type'] === 'favourites';
+ // 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;
}
- /**
- * Customize login error messages
- */
- public function loginErrors(string $error): string
- {
- return str_replace(
- array(
- 'The password you entered for the username',
- 'Invalid username',
- 'Unknown username',
- 'Unknown email address'
- ),
- array(
- 'Wrong password',
- 'We can\'t find that username',
- 'We can\'t find that username',
- 'We can\'t find that email'
- ),
- $error
- );
- }
+ 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);
- /**
- * Handle successful login
- */
- public function handleSuccessfulLogin(string $username, WP_User $user): void
- {
- if (isOurPeople() && !user_can($user, 'manage_options')) {
- wp_redirect(get_home_url(2, '/dash'));
- exit;
- }
- }
-
- // ===== REGISTRATION-SPECIFIC METHODS =====
-
- /**
- * Set user login for registration
- */
- public function setUserLogin(string $login): string
- {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
- if (!empty($user_type)) {
- $email_field = $user_type . '_email';
- if (isset($_POST[$email_field])) {
- $email = sanitize_email($_POST[$email_field]);
- if (is_email($email)) {
- return $email;
- }
- }
- }
- return $login;
- }
-
- /**
- * Set user email for registration
- */
- public function setUserEmail(string $email): string
- {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
- if (!empty($user_type)) {
- $email_field = $user_type . '_email';
- if (isset($_POST[$email_field])) {
- $email = sanitize_email($_POST[$email_field]);
- if (is_email($email)) {
- return $email;
- }
- }
- }
- return $email;
- }
-
- /**
- * Modify registration form
- */
- public function modifyRegistrationForm(): void
- {
- if (!$this->isRegistrationPage()) {
- return;
- }
-
- ?>
- <script type="text/javascript">
- document.addEventListener('DOMContentLoaded', function() {
- // Hide default fields
- const defaultFields = document.getElementById('registerform').querySelectorAll('p');
- defaultFields.forEach(field => {
- if (field.querySelector('label[for="user_login"]') ||
- field.querySelector('label[for="user_email"]')) {
- field.remove();
- }
- });
-
- // Hide the default registration info text
- const regInfo = document.querySelector('.message.register');
- if (regInfo) {
- regInfo.style.display = 'none';
- }
-
- <?php
- if ($this->fromInvite()) {
- $this->handleArtistInvitation();
- }
- ?>
-
- // Move submit button to the end of the form
- const submitButton = document.getElementById('registerform').querySelector('.submit');
- if (submitButton) {
- document.getElementById('registerform').appendChild(submitButton);
- }
- });
- </script>
- <?php
- }
-
- /**
- * Handle artist invitation pre-fill
- */
- protected function handleArtistInvitation(): void
- {
- $token = sanitize_text_field($_GET['invite']);
- $email = sanitize_email($_GET['email']);
- $data = JVB()->routes('invites')->verifyInvitation($token, $email);
-
- ?>
- document.querySelector('input#artist').checked = true;
- document.querySelector('#artist_first_name').value = '<?=$data->name?>';
- document.querySelector('#artist_email').value = '<?=$email?>';
- <?php
- if ($data->to_shop) {
- ?>
- document.querySelector('#artist_shop').value = '<?=$data->shop?>';
- <?php
- }
- ?>
- let form = document.getElementById('registerform')
- let input = document.createElement('input');
- let email = input.cloneNode(true);
- input.type = 'hidden';
- input.name = 'invite_token';
- input.value = '<?= $token ?>';
- email.type = 'hidden';
- email.name = 'invite_email';
- email.value = '<?= $email?>';
- form.append(input);
- form.append(email);
- <?php
- }
-
- /**
- * Add upload support for registration
- */
- public function addUploadSupport(): void
- {
- ?>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- const form = document.getElementById('registerform');
- if (form) {
- form.enctype = 'multipart/form-data';
- }
- });
- </script>
- <?php
- }
-
- /**
- * Add registration script
- */
- public function addRegistrationScript(): void
- {
- if (!$this->isRegistrationPage()) {
- return;
- }
- ?>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
-
- // Initialize user type selection
- function initUserTypeSelection() {
- const userTypeRadios = document.querySelectorAll('input[name="user_type"]');
- const fieldGroups = document.querySelectorAll('.field-group');
-
- userTypeRadios.forEach(radio => {
- radio.addEventListener('change', function() {
- fieldGroups.forEach(group => group.classList.remove('active'));
- const selectedType = this.value;
- const targetGroup = document.querySelector(`.field-group[data-type="${selectedType}"]`);
- if (targetGroup) {
- targetGroup.classList.add('active');
- }
- });
- });
-
- const checkedRadio = document.querySelector('input[name="user_type"]:checked');
- if (checkedRadio) {
- const targetGroup = document.querySelector(`.field-group[data-type="${checkedRadio.value}"]`);
- if (targetGroup) {
- targetGroup.classList.add('active');
- }
- }
- }
-
- // Initialize shop selection
- function initShopSelection() {
- let form = document.getElementById('registerform');
- form.addEventListener('change', (e) => {
- if(e.target.id === 'artist_shop' || e.target.id === 'artist_city'){
- let next = e.target.parentNode.nextElementSibling;
- let input = next.querySelector('input');
-
- if(e.target.value === 'other'){
- next.style.display = 'block';
- next.style.animation = 'fadeIn 0.3s ease';
- input.required = true;
- input.focus();
- }else{
- input.required = false;
- input.value = '';
- }
- }
- });
- }
-
- // Initialize file upload handling
- function initFileUpload() {
- const fileInput = document.getElementById('certification_file');
- const filePreview = document.querySelector('.file-preview');
- const filePreviewName = document.querySelector('.file-preview-name');
- const fileError = document.querySelector('.file-error');
- const removeButton = document.querySelector('.file-preview-remove');
-
- if (!fileInput || !filePreview || !filePreviewName || !fileError || !removeButton) {
- return;
- }
-
- const maxSize = parseInt(fileInput.dataset.maxSize || 5242880);
-
- fileInput.addEventListener('change', function(e) {
- const file = e.target.files[0];
- fileError.classList.remove('active');
-
- if (file) {
- const validTypes = ['.jpg','.jpeg','.png','.gif','.pdf'];
- const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
-
- if (!validTypes.includes(fileExtension)) {
- showError('Please upload a valid file type (JPG, PNG, GIF, or PDF)');
- fileInput.value = '';
- return;
- }
-
- if (file.size > maxSize) {
- showError('File size must be less than 5MB');
- fileInput.value = '';
- return;
- }
-
- filePreviewName.textContent = file.name;
- filePreview.classList.add('active');
- } else {
- filePreview.classList.remove('active');
- }
- });
-
- removeButton.addEventListener('click', function() {
- fileInput.value = '';
- filePreview.classList.remove('active');
- fileError.classList.remove('active');
- });
-
- function showError(message) {
- fileError.textContent = message;
- fileError.classList.add('active');
- filePreview.classList.remove('active');
- }
- }
-
- // Initialize all components
- initUserTypeSelection();
- initShopSelection();
- initFileUpload();
- });
- </script>
- <?php
- }
-
- /**
- * Add registration fields
- */
- public function addRegistrationFields(): void
- {
- echo '<input type="hidden" name="user_pass" value="' . wp_generate_password() . '">';
- ?>
- <div class="registration-intro">
- <?php
- foreach (JVB_LOGIN['join_intro']??[] as $intro) {
- echo '<p>'.$intro.'</p>';
- }
- ?>
-
- <?php if ($this->fromFavourites()): ?>
- <div class="favourites-login-message">
- <ul class="benefits-list">
- <?php
- foreach (JVB_LOGIN['from_favourites_benefits']??[] as $benefit) {
- echo '<li>'.$benefit.'</li>';
- }
- ?>
- </ul>
- </div>
- <?php endif; ?>
- </div>
-
- <?php
- if (array_key_exists('choose', JVB_LOGIN)) {
- ?>
- <h3><?= JVB_LOGIN['choose']?></h3>
- <?php
+ if (!empty($redirect)) {
+ $logout_url = add_query_arg('redirect_to', urlencode($redirect), $logout_url);
}
- ?>
- <?php
- if (count(JVB_USER) > 1) {
- $this->renderUserTypeSelection();
+ // 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');
+ }
+
+ public function isLoginPage():bool
+ {
+ return is_page($this->getLoginPage());
+ }
+
+ public static function isLogin():bool
+ {
+ $self = new self;
+ return $self->isLoginPage();
+ }
+
+ protected function initMagicLinkSupport(): void
+ {
+ if (!Site::has('magicLink')) {
+ return;
+ }
+ }
+
+ /*********************************************************************
+ RENDERING
+ *********************************************************************/
+ public function renderLoginPage(string $template):string
+ {
+ 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(),
+ function() {
+ return $this->renderPage();
+ },
+ 5
+ );
+
+ echo $page;
+ return '';
+ }
+ protected function renderPage() {
+ ob_start();
+ jvbInlineStyles('nav');
+ jvbInlineStyles('dash');
+ jvbInlineStyles('forms');
+ $this->customStyles();
+
+ $this->renderHeader();
+ $this->renderForms();
+ $this->renderFooter();
+
+ return ob_get_clean();
+ }
+
+ protected function getAction():string
+ {
+ if (array_key_exists('action', $_GET)) {
+ switch ($_GET['action']){
+ case 'lostpassword':
+ case 'retrievepassword': // Alias
+ $action = 'lostpassword';
+ break;
+ case 'rp':
+ case 'resetpass':
+ $action = 'resetpass';
+ break;
+ default:
+ $action = $_GET['action'];
+ }
} else {
- ?>
- <p>
- <label for="first_name" class="required-field">First Name</label>
- <input type="text" id="first_name" name="first_name" class="input">
- </p>
- <p>
- <label for="email" class="required-field">Email</label>
- <input type="email" id="email" name="email" class="input">
- </p>
- <?php
+ $action = 'login';
}
- if ($this->invitation_data) {
- ?>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- const artistRadio = document.getElementById('artist');
- if (artistRadio) {
- artistRadio.checked = true;
- artistRadio.dispatchEvent(new Event('change'));
- }
+ return $action;
+ }
- const emailField = document.getElementById('artist_email');
- if (emailField) {
- emailField.value = '<?= esc_js($this->invitation_data['email']); ?>';
- emailField.readOnly = true;
- }
+ 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();
+ }
- const shopSelect = document.getElementById('artist_shop');
- if (shopSelect) {
- shopSelect.value = '<?= esc_js($this->invitation_data['shop_id']); ?>';
- shopSelect.readOnly = true;
- }
- });
- </script>
- <input type="hidden" name="invitation_token" value="<?= sanitize_text_field($_GET['invite']) ?>">
- <input type="hidden" name="invitation_email" value="<?= sanitize_email($_GET['email']) ?>">
+ protected function setupTitle():void
+ {
+ switch ($this->action) {
+ case 'lostpassword':
+ $title = 'Lost Your Password?';
+ break;
+ case 'resetpass':
+ $title = 'Reset Your Password';
+ break;
+ case 'register':
+ $title = 'Create Your Account';
+ break;
+ default:
+ $title = 'Log In To Your Account';
+ }
+ $this->title = $title;
+ }
+
+ protected function customStyles():void
+ {
+ $logo = get_theme_mod('custom_logo');
+ if ($logo) {
+ $small = wp_get_attachment_image_src($logo, 'medium')[0]??'';
+ $large = wp_get_attachment_image_src($logo, 'large')[0]??'';
+ }
+ echo '<style>
+ .login header,
+ .login footer {
+ display: none;
+ }
+ .login main {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ justify-content: center;
+ position: relative;
+ }
+ .login .fstatus.fstatus {
+ --wrap: nowrap;
+ top:0;
+ bottom:unset;
+ right: 0;
+ }
+ .login main::before {
+ background-size: 20vw;
+ inset: 0;
+ z-index: 0;
+ content: "";
+ background-image: url("'.$small.'");
+ background-repeat: no-repeat;
+ position: absolute;
+ background-position: 40vw 1rem;
+ }
+ .login main .login-box {
+ --gap: .75rem;
+ padding: 1rem;
+ background-color:rgba(var(--base-rgb),var(--op-6));
+ border-radius: var(--outerRadius);
+ box-shadow: var(--shadow-right), var(--shadow-down);
+ margin: 15vh auto 0!important;
+ }
+ .login main .login-box,
+ .login main .navigation {
+ z-index: 5;
+ max-width: 90vw!important;
+ }
+ .login main .navigation {
+ padding: 0 1rem;
+ margin: 0 auto!important;
+ font-size: var(--small);
+ }
+ .login-box .button {
+ --height: 2.5rem;
+ width: 100%;
+ }
+ .login-box .options {
+ padding: 0 .5rem;
+ }
+ label[for="user_select-subscriber"] {
+ position: absolute;
+ left: var(--offScreen);
+ }
+
+ @media (min-width:768px) {
+ .login main .navigation,
+ .login main .login-box {
+ max-width: 60vw!important;
+ padding-right: 4rem!important;
+ margin: 0 0 0 auto!important;
+ }
+ .login main .login-box {
+ padding: 2rem;
+ --gap: 2rem;
+ }
+ .login main .navigation {
+ padding: 0 var(--offHeight);
+ }
+
+ .login-box .options {
+ padding: 0 4rem;
+ }
+ .login main::before {
+ background-size: 80vw;
+ inset: -5vw;
+ background-image: url("'.$large.'");
+ opacity: .25;
+ transform: rotate(-5deg);
+ background-position: -10vw center;
+ }
+ }
+ </style>';
+ }
+
+ protected function renderForms():void
+ {
+ $form = $this->action.'form';
+ ?>
+ <section class="login-box col y-btw">
+ <h1><?=$this->labels['title']?></h1>
+ <?= $this->labels['description'] ?>
+
+ <?= $this->renderLoginForm($this->action); ?>
+
+
+ <?php
+ if (is_array($this->labels['extra'])) {
+ echo '<div class="extra">';
+ foreach($this->labels['extra'] as $extra) {
+ echo '<p>'.$extra.'</p>';
+ }
+ echo '</div>';
+ } else if ($this->labels['extra']!=='') {
+ echo '<div class="extra">'.$this->labels['extra'].'</div>';
+ }
+ ?>
+
+ <div class="options row x-btw">
+ <?php
+ switch ($this->action) {
+ case 'login': ?>
+ <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
+ <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
+ <?php
+ break;
+ case 'register': ?>
+ <a href="<?= get_the_permalink() ?>">Or Login</a>
+ <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
+ <?php
+ break;
+ 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
+ break;
+
+ }
+ ?>
+
+ </div>
+ </section>
+ <div class="navigation row x-btw">
+ <a href="<?= get_home_url() ?>">Home</a>
+ <?php
+ $privacy = get_privacy_policy_url();
+ if ($privacy !== '') { ?>
+ <a href="<?= $privacy ?>">Our Privacy Policy</a>
+ <?php } ?>
+ </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
+ {
+ ?>
+ <!DOCTYPE html>
+ <html <?php language_attributes(); ?>>
+ <head>
+ <title><?= $this->title ?> | <?= get_bloginfo('name') ?></title>
+ <meta charset="<?php bloginfo('charset'); ?>">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="preconnect" href="<?= get_home_url()?>"/>
+ <?php wp_head(); ?>
+ </head>
+ <body class="login">
+ <?php jvbAccessibility();?>
+ <header>
<?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="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']).
+ jvbIcon('moon', ['title'=>'Dark Mode']).
+ '</span></label>';
+ ?>
+ <p class="title">
+ <a href="<?= get_home_url(); ?>" rel="home" title="Back to Site">
+ <?php
+ $icon = (int) get_option( 'site_icon' );
+ $out = '';
+ if ($icon > 0) {
+ $url = wp_get_attachment_image_url( $icon);
+ if ($url) {
+ $out = '<img src="'.$url.'">';
+ }
+ }
+ if ($out == '') {
+ $out =jvbIcon('house');
+ }
+ ?><?= $out ?>
+ </a>
+ </p>
+ </header>
+ <main>
+ <?php
}
- protected function renderUserTypeSelection():void
+ protected function renderFooter():void
+ {
+ ?>
+
+ <footer class="col">
+ <?= $this->labels['footer'] ?>
+ <?= jvbLoadingScreen() ?>
+ <?= TaxonomySelector::outputSelectorModal() ?>
+ <?php
+ do_action('jvbLoginFooter');
+ ?>
+ <p>Made with ♡ by <a href="https://jakevan.ca/">JakeVan</a></p>
+ </footer>
+
+ <?php wp_footer(); ?>
+
+ </body>
+ </html>
+
+ <?php
+ }
+
+ /**********************************************************************
+ TOKEN PROCESSING
+ **********************************************************************/
+ protected function processTokenHandlers(int $user_id, string $email): void
{
+ foreach ($this->tokenHandlers as $priority => $handlers) {
+ foreach ($handlers as $token_key => $handler) {
+ if (isset($_POST[$token_key]) || isset($_GET[$token_key])) {
+ $token_value = $_POST[$token_key] ?? $_GET[$token_key];
+ call_user_func($handler, sanitize_text_field($token_value), $email, $user_id);
+ }
+ }
+ }
+ }
+ /*************************************************************************
+ * SECURITY & VALIDATION
+ *************************************************************************/
- // Get list of tattoo shops and cities
- $shops = get_terms(array(
- 'taxonomy' => 'jvb_shop',
- 'hide_empty' => true
- ));
+ protected function checkRequestId(): bool
+ {
+ $request_id = $_POST['request_id'] ?? '';
+ if (empty($request_id)) {
+ return true; // No request_id provided, allow (for backward compat)
+ }
- $cities = get_terms(array(
- 'taxonomy' => 'jvb_city',
- 'hide_empty' => false,
- ));
+ $cache_key = 'request_' . $request_id;
+ if (get_transient($cache_key)) {
+ return false; // Duplicate request
+ }
+
+ // Store request ID for 1 minute to prevent duplicates
+ set_transient($cache_key, true, 60);
+ return true;
+ }
+
+ protected function maybeTurnstile(): void
+ {
+ if (!Site::hasIntegration('cloudflare')) {
+ return;
+ }
+ JVB()->connect('cloudflare')->renderTurnstile();
+ }
+
+ protected function maybeTurnstileScripts(): void
+ {
+ if (!Site::hasIntegration('cloudflare')) {
+ return;
+ }
+ JVB()->connect('cloudflare')->enqueueTurnstileScripts();
+ }
+
+ protected function verifyTurnstile(): bool
+ {
+ if (!Site::hasIntegration('cloudflare')) {
+ return true; // Not enabled, pass verification
+ }
+
+ $token = $_POST['cf-turnstile-response'] ?? '';
+ if (empty($token)) {
+ return false;
+ }
+
+ return JVB()->connect('cloudflare')->verifyTurnstile($token);
+ }
+
+ /************************************************************************
+ LABELS & UI
+ ************************************************************************/
+ protected function setupLabels(): void
+ {
+ $default = $this->getDefaultLabels();
+ $default = apply_filters('jvbLoginLabels', $default, $_GET);
+
+ 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;
+ }
+ }
+ }
+
+ 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 Site::login()->getLabels('register');
+ case 'lostpassword':
+ return Site::login()->getLabels('lostPassword');
+ case 'resetpass':
+ 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 Site::login()->getLabels('login');
+ }
+ }
+
+ protected function maybeMagicLink(): void
+ {
+ if (!JVB()->magicLink() || !in_array($this->action, ['login', 'lostpassword'])) {
+ return;
+ }
?>
- <div class="user-type-section">
-
- <?php
- $i = 1;
- $radio = '<input type="radio" id="user0" name="user_type" value="subscriber" required checked>
- <label for="user0"></label>';
- $descriptions = '';
- foreach (JVB_USER as $role => $config) {
- if (jvbCheck('can_register', $config)) {
- $radio .= '<input type="radio" id="user'.$i.'" name="user_type" value="'.$role.'" required';
- $radio .= ($role === 'enthusiast' && $this->fromFavourites()) ? 'checked' : '';
- $radio .= '><label for="user'.$i.'">'.jvbIcon($role, ['title' =>$config['label'], 'size'=>40]).'<h4>'.$config['label'].'</h4><p>';
- $radio .= $config['join_text']??'';
- $radio .= '</p></label>';
-
- $descriptions .= '<div class="user'.$i.'">'.is_array($config['join_description']) ? implode('', array_map(function ($item) { return '<p>'.$item.'</p>'; }, $config['join_description'])) : '<p>'.$config['join_description'].'</p>'.'</div>';
-
- $i++;
- }
- }
-
- echo $radio;
- echo $descriptions;
- ?>
- <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>>
- <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label>
- <input type="radio" id="artist" name="user_type" value="artist" required>
- <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
- <input type="radio" id="partner" name="user_type" value="partner" required>
- <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
- <p class="enthusiast">Save your favourites. Get notified.</p>
- <p class="artist">Show off your work.</p>
- <p class="partner">Support the community.</p>
- </div>
-
- <!-- Enthusiast Fields -->
- <div class="field-group" data-type="enthusiast">
- <h4>Welcome to the scene.</h4>
- <p>Sign up with your email to:</p>
- <ul>
- <li>Save your favourites for easy access</li>
- <li>Get notified when your favourite artists add new content</li>
- <li>Stay in the loop with local flash days and events</li>
- <li>Discover styles and artists that match your vision</li>
- </ul>
- <p>
- <label for="enthusiast_first_name" class="required-field">First Name</label>
- <input type="text" id="enthusiast_first_name" name="enthusiast_first_name" class="input">
- </p>
- <p>
- <label for="enthusiast_email" class="required-field">Email</label>
- <input type="email" id="enthusiast_email" name="enthusiast_email" class="input">
- </p>
- <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
- </div>
-
- <!-- Artist Fields -->
- <div class="field-group" data-type="artist">
- <h4>Welcome to the scene!</h4>
- <p>We'll start small, with the basics. Before your profile goes live, we need to verify:</p>
- <ul>
- <li>you are who you say you are</li>
- <li>you work at the shop you listed</li>
- <li>your certification</li>
- </ul>
- <p>
- <label for="artist_first_name" class="required-field">First Name</label>
- <input type="text" id="artist_first_name" name="artist_first_name" class="input">
- </p>
- <p>
- <label for="artist_last_name" class="required-field">Last Name</label>
- <input type="text" id="artist_last_name" name="artist_last_name" class="input">
- </p>
- <p>
- <label for="artist_email" class="required-field">Email</label>
- <input type="email" id="artist_email" name="artist_email" class="input">
- </p>
- <p>
- <label for="artist_shop" class="required-field">Shop</label>
- <select id="artist_shop" name="artist_shop" class="input">
- <option value="">Select a shop</option>
- <option value="other">Add New Shop</option>
- <?php foreach ($shops as $shop) : ?>
- <option value="<?= esc_attr($shop->term_id); ?>"><?= esc_html($shop->name); ?></option>
- <?php endforeach; ?>
- </select>
- </p>
- <p id="other_shop_field" style="display: none;">
- <label for="artist_shop_other" class="required-field">Shop Name</label>
- <input type="text" id="artist_shop_other" name="artist_shop_other" class="input" placeholder="Shop name">
- </p>
-
- <p>
- <label for="artist_type" class="required-field">Type</label>
- <input type="radio" id="type-tattoo-artist" name="artist_type" value="tattoo-artist">
- <label for="type-tattoo-artist">Tattoo Artist</label>
- <input type="radio" id="type-piercer" name="artist_type" value="piercer">
- <label for="type-piercer">Piercer</label>
- <input type="radio" id="type-other" name="artist_type" value="other">
- <label for="type-other">Other</label>
- </p>
- <p>
- <label for="artist_city" class="required-field">City</label>
- <select id="artist_city" name="artist_city" class="input">
- <option value="">Select a city</option>
- <option value="other">Add New City</option>
- <?php foreach ($cities as $city) : ?>
- <option value="<?= esc_attr($city->term_id); ?>"><?= esc_html($city->name); ?></option>
- <?php endforeach; ?>
- </select>
- </p>
- <p id="other_city_field" style="display: none;">
- <label for="artist_city_other" class="required-field">City Name</label>
- <input type="text" id="artist_city_other" name="artist_city_other" class="input" placeholder="City">
- </p>
-
- <div class="file-upload-container">
- <label class="file-upload-label">Certification or Training Documents</label>
- <p><i>Optional</i> — If you've been certified in bloodborne pathogen safety, or any other tattoo safety course, pass along your certificate. This just eases the verification process.</p>
- <div class="file-upload-wrapper">
- <input type="file" name="certification_file" id="certification_file" accept=".jpg,.jpeg,.png,.gif,.pdf" data-max-size="<?= $this->max_file_size; ?>">
- <p class="file-upload-text">
- <strong>Click to upload</strong> or drag and drop<br>
- JPG, PNG, GIF or PDF (max. 5MB)
- </p>
- </div>
- <div class="file-preview">
- <div class="file-preview-content">
- <span class="file-preview-name"></span>
- <button type="button" class="file-preview-remove">Remove</button>
- </div>
- </div>
- <div class="file-error"></div>
- </div>
- <p>Once you click register:</p>
- <ul>
- <li>We'll start looking into your information (usually within 24-48 hours)</li>
- <li>You'll get a password reset email</li>
- <li>Upon setting your password, you can start filling in your profile - but it won't go live until we've verified your information.</li>
- </ul>
- <p>If you have any questions or concerns - or anything you'd like to follow up on - email us at get@edmonton.ink or message us on <a target="_blank" href="https://www.instagram.com/edmonton.ink/" title="@edmonton.ink on Instagram">Instagram</a>.</p>
- <div><p><b>BONUS</b>: Everything's free. And always will be. We work with partners chosen by and for the community to keep the lights on.</p></div>
- </div>
-
- <!-- Partner Fields -->
- <div class="field-group" data-type="partner">
- <h4>Howdy, partner!</h4>
- <p>We appreciate your interest!</p>
- <p>edmonton.ink is a great place to showcase what you do, whether you:</p>
- <ul>
- <li>provide goods or services that tattoo artists could use</li>
- <li>provide goods or services that are tattoo adjacent (such as art, merch, etc)</li>
- <li>provide goods or services that folks who love tattoos could also love</li>
- </ul>
-
- <p>We'll start with some basics, then we'll reach out to follow up (usually within 24-48 hours).</p>
- <p>
- <label for="partner_name" class="required-field">Contact Name</label>
- <input type="text" id="partner_name" name="partner_name" class="input">
- </p>
- <p>
- <label for="partner_email" class="required-field">Email</label>
- <input type="email" id="partner_email" name="partner_email" class="input">
- </p>
- <p>
- <label for="partner_business" class="required-field">Business Name</label>
- <input type="text" id="partner_business" name="partner_business" class="input">
- </p>
- <p>
- <label for="partner_website">Business Website</label>
- <input type="url" id="partner_website" name="partner_website" class="input">
- </p>
- <p>
- <label for="partner_description">Why would you be a good fit?</label>
- <textarea id="partner_description" name="partner_description" rows="8"></textarea>
- </p>
- <p><i>Note:</i> — you must have good standing in the tattoo community to stay a partner of edmonton.ink.</p>
- <p>If we receive multiple requests to terminate a partnership with you from member artists, we reserve the right to cancel your listings.</p>
- </div>
+ <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
}
- /**
- * Registration errors filter
- */
- public function registrationErrorsFilter(WP_Error $errors, string $sanitized_user_login, string $user_email): WP_Error
- {
- error_log('Registration Data: '.print_r($_POST, true));
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : '';
- if (empty($user_type)) {
- $errors->add('user_type_error', 'Please select your user type.');
- return $errors;
- }
-
- // Get email based on user type
- $email_field = $user_type . '_email';
- $email = isset($_POST[$email_field]) ? sanitize_email($_POST[$email_field]) : '';
-
- // Remove WordPress's default username error
- $errors = new WP_Error();
-
- // If this is an invited artist, validate the invitation
- $invite = (array_key_exists('invite_token', $_POST)) ? sanitize_text_field($_POST['invite_token']) : false;
- if ($invite && array_key_exists('role', $_POST)) {
- $handler = JVB()->routes('invites');
- $invitation = $handler->verifyInvitation($invite, sanitize_email($_POST['invite_email']), sanitize_text_field($_POST['role']));
-
- if (!$invitation) {
- $errors->add('invalid_invitation', 'Invalid invitation token.');
- } elseif (strtotime($invitation->expires_at) < current_time('timestamp')) {
- $errors->add('expired_invitation', 'This invitation has expired.');
- }
- }
-
- // Validate email first
- if (empty($email)) {
- $errors->add('email_error', 'Email is required.');
- } elseif (!is_email($email)) {
- $errors->add('email_error', 'Please enter a valid email address.');
- } elseif (email_exists($email)) {
- $errors->add('email_error', 'This email is already registered.');
- }
-
- switch ($user_type) {
- case 'enthusiast':
- if (empty($_POST['enthusiast_first_name'])) {
- $errors->add('first_name_error', 'First name is required.');
- }
- break;
-
- case 'artist':
- $required_fields = array(
- 'artist_first_name' => 'First name',
- 'artist_last_name' => 'Last name',
- 'artist_shop' => 'Shop',
- 'artist_city' => 'City',
- 'artist_type' => 'Type',
- );
- foreach ($required_fields as $field => $label) {
- if (empty($_POST[$field])) {
- $errors->add($field . '_error', $label . ' is required.');
- }
- }
- break;
-
- case 'partner':
- $required_fields = array(
- 'partner_name' => 'Contact name',
- 'partner_business' => 'Business name'
- );
-
- foreach ($required_fields as $field => $label) {
- if (empty($_POST[$field])) {
- $errors->add($field . '_error', $label . ' is required.');
- }
- }
- break;
- }
-
- if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
- $file = $_FILES['certification_file'];
-
- // Validate file type
- if (!in_array($file['type'], $this->allowed_file_types)) {
- $errors->add('file_type_error', 'Please upload a valid file type (JPG, PNG, GIF, or PDF)');
- }
-
- // Validate file size
- if ($file['size'] > $this->max_file_size) {
- $errors->add('file_size_error', 'File size must be less than 5MB');
- }
- }
-
- return $errors;
+ /************************************************************************
+ SCRIPTS
+ ************************************************************************/
+ public function enqueueScripts(): void
+{
+ if (!$this->isLoginPage()) {
+ return;
}
- /**
- * Save registration fields
- */
- public function saveRegistrationFields(int $user_id, array $userdata): void
- {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : false;
- if (!$user_type) {
- return;
- }
+ $this->maybeTurnstileScripts();
+ wp_enqueue_script('jvb-form');
+ $action = $this->getAction();
- // Set user role based on type
- $user = new WP_User($user_id);
- $caps = JVB()->roles();
- $email = false;
- $upload_dir = wp_upload_dir();
- $base_dir = $upload_dir['basedir'];
+ $redirect_to = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : '';
+ $has_turnstile = Site::hasIntegration('cloudflare');
- switch ($user_type) {
- case 'artist':
- $user->set_role('jvb_artist');
- $user->remove_role('subscriber');
+ ob_start();
- $email = sanitize_email($_POST['artist_email']);
- $first = sanitize_text_field($_POST['artist_first_name']);
- $last = sanitize_text_field($_POST['artist_last_name']);
- $display_name = $first . ' ' . $last;
+ ?>
- // Save artist fields
- $temp = wp_update_user([
- 'ID' => $user_id,
- 'first_name' => $first,
- 'last_name' => $last,
- 'display_name' => $display_name
- ]);
- $user = get_userdata($temp);
+ document.addEventListener('DOMContentLoaded', async function () {
+ const hasTurnstile = <?= json_encode($has_turnstile) ?>;
+ const redirectTo = <?= json_encode($redirect_to) ?>;
- $link = $caps->addUserLink($user, 'artist');
- $meta = new MetaManager($link, 'post');
- $meta->setAll([
- 'first_name' => $first,
- 'email' => $email
- ]);
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ const form = document.querySelector('.login form');
+ if (!form || !window.jvbForm) return;
- // If this was an invited artist, handle the invitation
- if (array_key_exists('invite_token', $_POST)) {
- $handler = JVB()->routes('invites');
- $handler->acceptInvitation(sanitize_text_field($_POST['invite_token']), sanitize_email($_POST['invite_email']), $user->ID);
- }
+ window.jvbForm.registerForm(form, {
+ endpoint: '<?= $action ?>',
+ showStatus: false,
+ cache: false,
+ });
- if (absint($_POST['artist_shop']) > 0) {
- JVB()->routes('shop')->requestShopAdmission($user_id, absint($_POST['artist_shop']));
- }
- if (absint($_POST['artist_city']) > 0) {
- wp_set_post_terms($link, (int)absint($_POST['artist_city']), BASE.'city');
- }
+ window.jvbForm.subscribe((event, data) => {
+ if (event === 'form-submit') {
+ const { config } = data;
+ const formElement = config.element;
- //Create approval request and notify verified users
- JVB()->routes('approvals')->createArtistApprovalRequest($user_id);
+ // Collect current form data
+ const formData = new FormData(formElement);
+ const formObject = Object.fromEntries(formData.entries());
- //Make base directories
- $artist_dir = $base_dir . '/artists/' . $user_id;
- wp_mkdir_p($artist_dir);
- wp_mkdir_p($artist_dir . '/artwork');
- wp_mkdir_p($artist_dir . '/events');
- wp_mkdir_p($artist_dir . '/profile');
- wp_mkdir_p($artist_dir . '/temp');
+ 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');
+ }
- switch ($_POST['artist_type']) {
- case 'tattoo-artist':
- $caps->setUserAs($user, 'tattoo-artist');
- $term = get_term_by('name', 'Tattoo Artists', BASE.'type');
- if ($term && !is_wp_error($term)) {
- wp_set_post_terms($link, $term->term_id, BASE.'type');
- }
- wp_mkdir_p($artist_dir . '/tattoos');
- break;
- case 'piercer':
- $caps->setUserAs($user, 'piercer');
- $term = get_term_by('name', 'Piercers', BASE.'type');
- if ($term && !is_wp_error($term)) {
- wp_set_post_terms($link, $term->term_id, BASE.'type');
- }
- wp_mkdir_p($artist_dir . '/piercings');
- break;
- }
- break;
+ // Add redirect_to from URL
+ if (redirectTo) {
+ formObject.redirect_to = redirectTo;
+ }
- case 'partner':
- $user->set_role('jvb_partner');
- $user->remove_role('subscriber');
- $name = sanitize_text_field($_POST['partner_name']);
- $email = sanitize_email($_POST['partner_email']);
+ const submit = formElement.querySelector('[type=submit]');
+ const oldText = submit.textContent;
- $caps->setUserAs($user, 'partner');
- $link = $caps->addUserLink($user, 'partner');
+ window.jvbForm.showFormStatus(config.id, 'uploading');
- // Save partner fields
- update_user_meta($user_id, 'contact_name', sanitize_text_field($_POST['partner_name']));
- update_user_meta($user_id, 'business_name', sanitize_text_field($_POST['partner_business']));
- update_user_meta($user_id, 'business_website', esc_url_raw($_POST['partner_website']));
+ submit.disabled = true;
+ submit.textContent = 'Loading...';
- // Create partner base directory
- $partner_dir = $base_dir . '/partners/' . $user_id;
- wp_mkdir_p($partner_dir);
- wp_mkdir_p($partner_dir . '/offers');
- wp_mkdir_p($partner_dir . '/events');
- wp_mkdir_p($partner_dir . '/profile');
- wp_mkdir_p($partner_dir . '/temp');
- break;
+ 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;
+ }
- case 'enthusiast':
- $user->set_role('jvb_enthusiast');
- $user->remove_role('subscriber');
- $caps->setUserAs($user, 'enthusiast');
- $name = sanitize_text_field($_POST['enthusiast_first_name']);
- $email = sanitize_email($_POST['enthusiast_email']);
+ window.jvbForm.showFormStatus(config.id, 'submitted');
- // Save enthusiast fields
- $temp = wp_update_user([
- 'ID' => $user_id,
- 'first_name' => $name,
- 'user_email' => $email,
- ]);
- break;
- }
+ if (result.message) {
+ window.jvbForm.handleFormSuccess(formElement, result);
+ }
- // Handle file upload for artists
- if (isset($_POST['user_type']) && $_POST['user_type'] === 'artist' && !empty($_FILES['certification_file']['name'])) {
- $file = $_FILES['certification_file'];
+ if (window.auth?.handleLogin && result.auth) {
+ return window.auth.handleLogin(result.auth).then(() => {
+ if (result.redirect) {
+ setTimeout(() => {
+ window.location.href = result.redirect;
+ }, 20);
+ }
+ });
+ } else if (result.redirect) {
+ setTimeout(() => {
+ window.location.href = result.redirect;
+ }, 20);
+ }
+ })
+ .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;
+ });
+ }
+ });
+ }
+ });
+ });
- // Setup upload directory
- $upload_dir = wp_upload_dir();
- $user_directory = 'artist-certifications/' . $user_id;
- $target_dir = $upload_dir['basedir'] . '/' . $user_directory;
+ <?php
+ $script = ob_get_clean();
+ wp_add_inline_script('jvb-form', $script);
+ }
- // Create directory if it doesn't exist
- wp_mkdir_p($target_dir);
+ /*************************************************************************
+ SUCCESS HANDLING
+ *************************************************************************/
+ public function handleSuccessfulLogin(string $username, WP_User $user): void
+ {
+ if (isOurPeople() && !user_can($user, 'manage_options')) {
+ wp_redirect(get_home_url(null, '/dash'));
+ exit;
+ }
+ }
- // Generate unique filename
- $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
- $filename = 'certification-' . time() . '.' . $file_extension;
- $target_file = $target_dir . '/' . $filename;
- // Move uploaded file
- if (move_uploaded_file($file['tmp_name'], $target_file)) {
- // Save file information in user meta
- update_user_meta($user_id, 'certification_file', array(
- 'url' => $upload_dir['baseurl'] . '/' . $user_directory . '/' . $filename,
- 'file' => $target_file,
- 'type' => $file['type'],
- 'original_name' => $file['name']
- ));
- }
- }
+ /**
+ * Handle login errors
+ */
+ protected function handleLoginError(WP_Error $error): void
+ {
+ $login_url = wp_login_url();
+ $login_url = add_query_arg('login_error', urlencode($error->get_error_code()), $login_url);
- // Handle list invitation acceptance
- if (isset($_GET['list_token']) && !empty($_GET['list_token']) && isset($_GET['email'])) {
- $token = sanitize_text_field($_GET['list_token']);
- $email = sanitize_email($_GET['email']);
+ if (isset($_REQUEST['redirect_to'])) {
+ $login_url = add_query_arg('redirect_to', urlencode($_REQUEST['redirect_to']), $login_url);
+ }
- if ($email) {
- JVB()->routes('favourites')->acceptListInvitation($token, $email, $user_id);
- }
- }
- }
+ wp_safe_redirect($login_url);
+ exit;
+ }
- /**
- * Registration success message
- */
- public function registrationSuccessMessage(WP_Error $errors, string $redirect_to): WP_Error
- {
- if (isset($errors->errors['registered']) && isset($_POST['invitation_token'])) {
- // Custom message for invited artists
- $message = "WELCOME ABOARD!<br><br>" .
- "Password setup is in your inbox. <br>" .
- "Since you were invited by a shop, you can skip the verification wait and start building your profile right away! ♡";
+ public function saveRegistrationFields(int $user_id, array $userdata):void
+ {
- unset($errors->errors['registered']);
- $errors->add('registered', $message, 'message');
- }
-
- if (isset($errors->errors['registered'])) {
- $user_type = isset($_POST['user_type']) ? $_POST['user_type'] : 'user';
-
- switch ($user_type) {
- case 'enthusiast':
- $message = "YOU'RE IN!<br><br>Check your inbox - we've sent password setup details.<br>Get ready to build your dream artist collection! ♡";
- break;
-
- case 'artist':
- $message = "HELL YEAH!<br><br>Password setup is in your inbox. <br>While we verify your info (24-48hrs), you can start building your profile. <br>Just remember - it stays underground until you're cleared. ♡";
- break;
-
- case 'partner':
- $message = "ROCK ON!<br><br>Check your inbox - we've sent password setup details.<br>We'll check out your pitch in the next 24-48hrs. <br><br>Meanwhile, you can start prepping your presence - but you won't hit the streets until we give the nod. ♡";
- break;
-
- default:
- $message = "YOU'RE ON THE LIST!<br><br>Check your inbox for the next steps. ♡";
- }
-
- // Replace the default message
- unset($errors->errors['registered']);
- $errors->add('registered', $message, 'message');
- }
-
- return $errors;
- }
-
- /**
- * Check if registration is from invite
- */
- protected function fromInvite(): bool
- {
- return isset($_GET['invite']) && isset($_GET['email']);
- }
-
- /**
- * Custom register message
- */
- public function customRegisterMessage(string $message): string
- {
- return "Join Edmonton's tattoo community";
- }
+ }
+ public function setAction(string $action = 'login'):void
+ {
+ $this->action = $action;
+ $this->setup();
+ }
}
-// Initialize the consolidated auth manager
+// Initialize the login manager
new LoginManager();
--
Gitblit v1.10.0