| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\meta\MetaManager; |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\meta\Form; |
| | | |
| | | use JVBase\utility\Features; |
| | | 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 Features $siteFeatures; |
| | | 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 = ''; |
| | | |
| | | // 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'; |
| | | } |
| | | public function __construct() |
| | | { |
| | | $this->siteFeatures = Features::forSite(); |
| | | |
| | | /** |
| | | * 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')); |
| | | |
| | | // Remove default username requirement for registration |
| | | remove_filter('registration_errors', 'registration_auth_pass_filter', 10); |
| | | } |
| | | $this->cache = Cache::for('login'); |
| | | |
| | | /** |
| | | * Combined login styles for both login and registration |
| | | */ |
| | | public function loginStyles(): void |
| | | { |
| | | do_action('jvbLoginStyles'); |
| | | } |
| | | // Initialize magic link support if enabled |
| | | if ($this->siteFeatures->has('magicLink')) { |
| | | $this->initMagicLinkSupport(); |
| | | } |
| | | |
| | | /** |
| | | * 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'; |
| | | // Create login page if it doesn't exist |
| | | $this->ensureLoginPageExists(); |
| | | |
| | | let passwordLabel = document.querySelector('label[for="user_pass"'); |
| | | passwordLabel.innerHTML = '<?= jvbIcon('password', ['size' => 20]); ?> Your Password'; |
| | | |
| | | document.querySelector('form').classList.add('loaded'); |
| | | }); |
| | | // Redirect wp-login.php to custom page |
| | | add_action('login_init', [$this, 'redirectToCustomLogin']); |
| | | add_action('template_include', [$this, 'renderLoginPage']); |
| | | |
| | | </script> |
| | | <?php |
| | | } |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15); |
| | | |
| | | /** |
| | | * Login footer with donate section |
| | | */ |
| | | public function loginFooter(): void |
| | | { |
| | | do_action('jvbLoginFooter'); |
| | | // 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); |
| | | } |
| | | |
| | | /** |
| | | * Logo URL |
| | | */ |
| | | public function logoUrl(): string |
| | | { |
| | | return home_url(); |
| | | } |
| | | public function excludeLoginSitemap(array $ids): array |
| | | { |
| | | $ids[] = $this->getLoginPage(); |
| | | return $ids; |
| | | } |
| | | /************************************************************************** |
| | | * SETUP & CONFIGURATION |
| | | **************************************************************************/ |
| | | |
| | | /** |
| | | * Logo title |
| | | */ |
| | | public function logoTitle(): string |
| | | { |
| | | return get_bloginfo('name'); |
| | | } |
| | | /** |
| | | * 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; |
| | | |
| | | /** |
| | | * 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>'; |
| | | // Remove WordPress internal args |
| | | unset($query_args['interim-login'], $query_args['wp-auth-check']); |
| | | |
| | | if (!empty($query_args)) { |
| | | $custom_login_page = add_query_arg($query_args, $custom_login_page); |
| | | } |
| | | |
| | | 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 (Features::forSite()->has('referrals')) { |
| | | $fields['referral_code'] = [ |
| | | 'type' => 'text', |
| | | 'required'=> false, |
| | | 'label' => 'Referral Code', |
| | | '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'] ?? ''; |
| | | $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) { |
| | | $field['condition'] = [ |
| | | 'field' => 'user_select', |
| | | 'value' => $slug, |
| | | '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 |
| | | { |
| | | $fields = []; |
| | | switch($this->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; |
| | | |
| | | } |
| | | $this->fields = $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(null, '/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 (!Features::forSite()->has('magicLink')) { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | /********************************************************************* |
| | | RENDERING |
| | | *********************************************************************/ |
| | | public function renderLoginPage(string $template):string |
| | | { |
| | | if (!$this->isLoginPage()) { |
| | | 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'); |
| | | 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; |
| | | border-radius: var(--outerRadius); |
| | | background-color: var(--overlay-heavy); |
| | | 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; |
| | | margin: 0 2rem 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 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 |
| | | 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'])) { |
| | | 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 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 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 |
| | | } |
| | | 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="toggle-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 (!Features::hasIntegration('cloudflare')) { |
| | | return; |
| | | } |
| | | JVB()->connect('cloudflare')->renderTurnstile(); |
| | | } |
| | | |
| | | protected function maybeTurnstileScripts(): void |
| | | { |
| | | if (!Features::hasIntegration('cloudflare')) { |
| | | return; |
| | | } |
| | | JVB()->connect('cloudflare')->enqueueTurnstileScripts(); |
| | | } |
| | | |
| | | protected function verifyTurnstile(): bool |
| | | { |
| | | if (!Features::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 [ |
| | | '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.)'], |
| | | ]; |
| | | 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'], |
| | | ]; |
| | | 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', |
| | | ]; |
| | | 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'] ?? '', |
| | | ]; |
| | | 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', |
| | | |
| | | ]; |
| | | 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', |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | 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 = Features::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, { |
| | | autosave: false, |
| | | endpoint: '<?= $action ?>', |
| | | formStatus: 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; |
| | | }, 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; |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // 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"; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Initialize the consolidated auth manager |
| | | // Initialize the login manager |
| | | new LoginManager(); |