validate_registration($submitted_data);
$errors = array_merge($errors, $validation_errors); // Combine file errors and validation errors
// --- Process if No Errors ---
if (empty($errors)) {
error_log('[HVAC REG DEBUG] Validation passed. Attempting account creation...');
$user_id = $this->create_trainer_account($submitted_data, $profile_image_data);
if (is_wp_error($user_id)) {
$errors['account'] = $user_id->get_error_message();
error_log('[HVAC REG DEBUG] Account creation WP_Error: ' . $user_id->get_error_message());
} elseif ($user_id) {
error_log('[HVAC REG DEBUG] Account creation SUCCESS. User ID: ' . $user_id);
error_log('[HVAC REG DEBUG] Sending admin notification...');
$this->send_admin_notification($user_id, $submitted_data);
// $this->send_user_pending_notification($user_id); // TODO
// Set flag for success message display instead of redirecting immediately
$registration_success = true;
error_log('[HVAC REG DEBUG] Registration success flag set.');
// Clear submitted data on success so form doesn't repopulate
$submitted_data = [];
// Optionally redirect here if preferred, but displaying message avoids issues
// wp_safe_redirect(home_url('/registration-pending/'));
// exit;
} else {
$errors['account'] = 'An unknown error occurred during registration. Please contact support.';
error_log('[HVAC REG DEBUG] Account creation failed silently (returned false/0).');
}
} else {
error_log('[HVAC REG DEBUG] Validation errors found: ' . print_r($errors, true));
}
}
}
// --- End Handle POST Submission ---
// --- Render Form ---
ob_start();
// Display general/account errors above the form
if (isset($errors['nonce']) || isset($errors['account'])) {
echo '
';
if (isset($errors['nonce'])) echo '
' . esc_html($errors['nonce']) . '
';
if (isset($errors['account'])) echo '
' . esc_html($errors['account']) . '
';
echo '
';
}
// Display success message OR the form
if ($registration_success) {
echo ''; // Add a class for styling
echo '
Thank you for registering!
';
echo '
Your account is currently pending approval. You will receive an email once your account is approved.
';
echo '
';
} else {
// Display the form HTML, passing errors and submitted data
$this->display_form_html($submitted_data, $errors);
}
return ob_get_clean();
// --- End Render Form ---
}
/**
* Displays the actual form HTML.
* Receives submitted data and errors as arguments.
*/
private function display_form_html($data = [], $errors = []) {
// This method remains largely the same as before,
// ensuring it uses the passed $data and $errors arrays.
?>
post_content, 'hvac_trainer_registration')) {
wp_enqueue_style(
'hvac-registration-style',
HVAC_CE_PLUGIN_URL . 'assets/css/hvac-registration.css', // Ensure this CSS file exists and is styled
array(),
HVAC_CE_VERSION
);
wp_enqueue_script(
'hvac-registration-js',
HVAC_CE_PLUGIN_URL . 'assets/js/hvac-registration.js', // Ensure this JS file exists
array('jquery'),
HVAC_CE_VERSION,
true
);
// Localize script to pass states/provinces data and AJAX URL
wp_localize_script('hvac-registration-js', 'hvacRegistrationData', array(
'ajax_url' => admin_url('admin-ajax.php'), // Needed if JS fetches states/provinces via AJAX
'states' => $this->get_us_states(), // Pass US states
'provinces' => $this->get_canadian_provinces(), // Pass CA provinces
// Pass other country data if needed, or handle via AJAX
));
}
}
// Removed the separate process_registration method
/**
* Handle profile image upload after user is created.
*/
private function handle_profile_image_upload($user_id, $file_data) {
if (!$user_id || empty($file_data) || !isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) {
return false; // No valid user or file
}
// These files need to be included as dependencies when on the front-end.
require_once(ABSPATH . 'wp-admin/includes/image.php');
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
// Pass the file array directly to media_handle_sideload
$attachment_id = media_handle_sideload($file_data, 0); // 0 means don't attach to a post
if (is_wp_error($attachment_id)) {
// Handle upload error
error_log('Profile image upload error for user ' . $user_id . ': ' . $attachment_id->get_error_message());
// Optionally add this error to be displayed to the user via transient?
return false;
} else {
// Store the attachment ID as user meta
update_user_meta($user_id, 'profile_image_id', $attachment_id);
return $attachment_id;
}
}
/**
* Validate registration form data
*/
public function validate_registration($data) {
$errors = array();
// Required field validation
$required_fields = [
'user_email', 'user_pass', 'confirm_password', 'first_name', 'last_name',
'display_name', 'description', 'business_name', 'business_phone',
'business_email', 'business_description', 'user_country', 'user_state',
'user_city', 'user_zip', 'create_venue', 'business_type',
'application_details'
];
// Required checkbox groups
$required_checkboxes = [
'training_audience', 'training_formats', 'training_locations', 'training_resources'
];
foreach ($required_fields as $field) {
// Special handling for state dropdown if 'Other' is selected
if ($field === 'user_state' && isset($data[$field]) && $data[$field] === 'Other') {
if (empty(trim($data['user_state_other']))) {
$errors['user_state_other'] = 'Please enter your state/province.';
}
continue; // Skip the main 'user_state' empty check if 'Other' is selected
}
if (empty(trim($data[$field]))) {
$errors[$field] = 'This field is required.';
}
}
foreach ($required_checkboxes as $field) {
// Checkboxes send value only if checked, check if the key exists and is an array with items
if (empty($data[$field]) || !is_array($data[$field]) || count($data[$field]) === 0) {
$errors[$field] = 'Please select at least one option.';
}
}
// Email validation
if (!empty($data['user_email']) && !is_email($data['user_email'])) {
$errors['user_email'] = 'Please enter a valid email address.';
} elseif (email_exists($data['user_email'])) {
$errors['user_email'] = 'This email address is already registered.';
}
// Check business email format, but allow it to be the same as user email
if (!empty($data['business_email']) && !is_email($data['business_email'])) {
$errors['business_email'] = 'Please enter a valid business email address.';
}
// Password validation
if (!empty($data['user_pass'])) {
if (strlen($data['user_pass']) < 8 ||
!preg_match('/[A-Z]/', $data['user_pass']) ||
!preg_match('/[a-z]/', $data['user_pass']) ||
!preg_match('/[0-9]/', $data['user_pass'])) {
$errors['user_pass'] = 'Password must be at least 8 characters with uppercase, lowercase and numbers.';
} elseif (empty($data['confirm_password'])) {
$errors['confirm_password'] = 'Please confirm your password.';
} elseif ($data['user_pass'] !== $data['confirm_password']) {
$errors['confirm_password'] = 'Passwords do not match.';
}
} else {
// This case should be caught by the required check, but added for robustness
if (!isset($errors['user_pass'])) $errors['user_pass'] = 'Password is required.';
}
// URL validation (optional fields)
if (!empty($data['user_url']) && !filter_var($data['user_url'], FILTER_VALIDATE_URL)) {
$errors['user_url'] = 'Please enter a valid website URL (e.g., https://example.com).';
}
if (!empty($data['user_linkedin']) && !filter_var($data['user_linkedin'], FILTER_VALIDATE_URL)) {
$errors['user_linkedin'] = 'Please enter a valid LinkedIn Profile URL.';
}
if (!empty($data['business_website']) && !filter_var($data['business_website'], FILTER_VALIDATE_URL)) {
$errors['business_website'] = 'Please enter a valid business website URL.';
}
// Conditional State/Province validation
if (isset($data['user_country']) && $data['user_country'] !== 'United States' && $data['user_country'] !== 'Canada') {
// If country is not US/CA
if (isset($data['user_state']) && $data['user_state'] !== 'Other') {
$errors['user_state'] = 'Please select "Other" for state/province if outside US/Canada.';
} elseif (empty(trim($data['user_state_other']))) {
// This case is handled by the required check above if user_state is 'Other'
// $errors['user_state_other'] = 'Please enter your state/province.';
}
} elseif (isset($data['user_country']) && ($data['user_country'] === 'United States' || $data['user_country'] === 'Canada')) {
// If country IS US/CA
if (empty($data['user_state'])) {
$errors['user_state'] = 'Please select your state/province.';
} elseif ($data['user_state'] === 'Other') {
$errors['user_state'] = 'Please select your state/province from the list, not "Other".';
}
}
// File upload validation is handled before this function is called
return $errors;
}
/**
* Create HVAC trainer account
*/
private function create_trainer_account($data, $profile_image_data = null) {
// Generate username from email - use user_email now
$email_parts = explode('@', $data['user_email']);
$username_base = sanitize_user($email_parts[0], true);
// Add random suffix to further ensure uniqueness if base is common
$username_base = preg_replace('/[^a-zA-Z0-9]/', '', $username_base); // Clean base username
if (strlen($username_base) > 50) { // Limit base length
$username_base = substr($username_base, 0, 50);
}
$username = $username_base;
$counter = 1;
// Ensure username is unique
while (username_exists($username)) {
$username = $username_base . $counter++;
if ($counter > 100) { // Safety break
return new WP_Error('registration_failed_username', 'Could not generate a unique username. Please try a different email address.');
}
}
// Create user with pending status initially
$user_data = array(
'user_login' => $username,
'user_pass' => $data['user_pass'],
'user_email' => $data['user_email'],
'first_name' => sanitize_text_field($data['first_name']),
'last_name' => sanitize_text_field($data['last_name']),
'display_name' => sanitize_text_field($data['display_name']),
'description' => wp_kses_post($data['description']), // Allow some HTML in bio
'user_url' => esc_url_raw($data['user_url'] ?? ''),
'role' => 'pending_hvac_trainer' // Assign custom pending role defined in class-hvac-roles.php
);
$user_id = wp_insert_user($user_data);
if (is_wp_error($user_id)) {
error_log('HVAC Reg Error wp_insert_user: ' . $user_id->get_error_message());
return $user_id; // Return WP_Error object
}
// Save custom user meta fields
update_user_meta($user_id, 'user_linkedin', esc_url_raw($data['user_linkedin'] ?? ''));
update_user_meta($user_id, 'personal_accreditation', sanitize_text_field($data['personal_accreditation'] ?? ''));
update_user_meta($user_id, 'business_name', sanitize_text_field($data['business_name']));
update_user_meta($user_id, 'business_phone', sanitize_text_field($data['business_phone']));
update_user_meta($user_id, 'business_email', sanitize_email($data['business_email'])); // Use business email meta
update_user_meta($user_id, 'business_website', esc_url_raw($data['business_website'] ?? ''));
update_user_meta($user_id, 'business_description', wp_kses_post($data['business_description'])); // Allow some HTML
update_user_meta($user_id, 'user_country', sanitize_text_field($data['user_country']));
// Save state/province correctly based on 'Other'
if ($data['user_state'] === 'Other') {
update_user_meta($user_id, 'user_state', sanitize_text_field($data['user_state_other']));
} else {
update_user_meta($user_id, 'user_state', sanitize_text_field($data['user_state']));
}
update_user_meta($user_id, 'user_city', sanitize_text_field($data['user_city']));
update_user_meta($user_id, 'user_zip', sanitize_text_field($data['user_zip']));
update_user_meta($user_id, 'create_venue', sanitize_text_field($data['create_venue']));
update_user_meta($user_id, 'business_type', sanitize_text_field($data['business_type']));
update_user_meta($user_id, 'training_audience', array_map('sanitize_text_field', $data['training_audience'] ?? []));
update_user_meta($user_id, 'training_formats', array_map('sanitize_text_field', $data['training_formats'] ?? []));
update_user_meta($user_id, 'training_locations', array_map('sanitize_text_field', $data['training_locations'] ?? []));
update_user_meta($user_id, 'training_resources', array_map('sanitize_text_field', $data['training_resources'] ?? []));
update_user_meta($user_id, 'application_details', wp_kses_post($data['application_details'])); // Allow some HTML
if (isset($data['annual_revenue_target']) && is_numeric($data['annual_revenue_target'])) {
update_user_meta($user_id, 'annual_revenue_target', intval($data['annual_revenue_target']));
} else {
delete_user_meta($user_id, 'annual_revenue_target'); // Remove if empty or non-numeric
}
// Handle profile image upload
if ($profile_image_data) {
$this->handle_profile_image_upload($user_id, $profile_image_data);
}
// Create Events Calendar organizer profile (using business details)
$this->create_organizer_profile($user_id, $data);
// Create venue if requested
if (isset($data['create_venue']) && $data['create_venue'] === 'Yes') {
$this->create_training_venue($user_id, $data);
}
return $user_id; // Return the user ID on success
}
/**
* Create organizer profile in The Events Calendar
*/
private function create_organizer_profile($user_id, $data) {
if (!function_exists('tribe_create_organizer')) {
error_log('HVAC Reg Error: tribe_create_organizer function does not exist.');
return false;
}
$organizer_data = array(
'Organizer' => sanitize_text_field($data['business_name']),
'Phone' => sanitize_text_field($data['business_phone']),
'Email' => sanitize_email($data['business_email']),
'Website' => esc_url_raw($data['business_website'] ?? ''),
'Description' => wp_kses_post($data['business_description']), // Add description
'post_status' => 'publish', // Ensure organizer is published
'post_author' => $user_id // Assign user as author
);
$organizer_id = tribe_create_organizer($organizer_data);
if (is_wp_error($organizer_id)) {
error_log('HVAC Reg Error creating organizer: ' . $organizer_id->get_error_message());
return false;
} elseif ($organizer_id) {
// Associate organizer with user
update_user_meta($user_id, '_hvac_organizer_id', $organizer_id);
// TEC Community Events might use this meta key to link user and organizer CPT
// update_post_meta($organizer_id, '_tribe_organizer_user_id', $user_id); // Check if TEC CE uses this
return true;
}
error_log('HVAC Reg Error: tribe_create_organizer returned non-error false/0.');
return false;
}
/**
* Create a training venue in The Events Calendar
*/
private function create_training_venue($user_id, $data) {
if (!function_exists('tribe_create_venue')) {
error_log('HVAC Reg Error: tribe_create_venue function does not exist.');
return false;
}
$state = ($data['user_state'] === 'Other') ? sanitize_text_field($data['user_state_other']) : sanitize_text_field($data['user_state']);
$venue_data = array(
'Venue' => sanitize_text_field($data['business_name']), // Use business name for venue name
// 'Address' => '', // TEC often uses individual fields below
'City' => sanitize_text_field($data['user_city']),
'Province' => $state, // Use Province field for TEC
'State' => $state, // Also set State for compatibility
'Zip' => sanitize_text_field($data['user_zip']),
'Country' => sanitize_text_field($data['user_country']),
'Phone' => sanitize_text_field($data['business_phone']), // Add phone to venue
'ShowMap' => true,
'ShowMapLink' => true,
'post_status' => 'publish', // Ensure venue is published
'post_author' => $user_id // Assign user as author
);
$venue_id = tribe_create_venue($venue_data);
if (is_wp_error($venue_id)) {
error_log('HVAC Reg Error creating venue: ' . $venue_id->get_error_message());
return false;
} elseif ($venue_id) {
// Associate venue with user
update_user_meta($user_id, '_hvac_training_venue_id', $venue_id);
// TEC Community Events might use this meta key
// update_post_meta($venue_id, '_tribe_venue_user_id', $user_id); // Check if TEC CE uses this
return true;
}
error_log('HVAC Reg Error: tribe_create_venue returned non-error false/0.');
return false;
}
/**
* Send admin notification about new trainer registration (pending approval)
*/
private function send_admin_notification($user_id, $data) {
// Use settings or default admin email
$options = get_option('hvac_ce_options'); // Assuming settings are stored here
$emails_str = $options['notification_emails'] ?? get_option('admin_email');
$emails = array_filter(array_map('trim', explode(',', $emails_str)), 'is_email');
if (empty($emails)) {
error_log('HVAC Reg Error: No valid admin notification emails configured.');
return false;
}
$user = get_userdata($user_id);
if (!$user) return false;
$subject = sprintf(__('New HVAC Trainer Registration Pending Approval: %s', 'hvac-community-events'), $user->display_name);
$message = __('A new HVAC trainer has registered and requires approval:', 'hvac-community-events') . "\n\n";
$message .= __('Name:', 'hvac-community-events') . ' ' . $user->first_name . ' ' . $user->last_name . "\n";
$message .= __('Email:', 'hvac-community-events') . ' ' . $user->user_email . "\n";
$message .= __('Business:', 'hvac-community-events') . ' ' . ($data['business_name'] ?? 'N/A') . "\n";
$message .= __('Phone:', 'hvac-community-events') . ' ' . ($data['business_phone'] ?? 'N/A') . "\n";
$state_display = ($data['user_state'] === 'Other') ? ($data['user_state_other'] ?? 'N/A') : ($data['user_state'] ?? 'N/A');
$message .= __('Location:', 'hvac-community-events') . ' ' . ($data['user_city'] ?? 'N/A') . ', ' . $state_display . "\n\n";
$message .= __('Business Type:', 'hvac-community-events') . ' ' . ($data['business_type'] ?? 'N/A') . "\n";
$message .= __('Training Audience:', 'hvac-community-events') . ' ' . implode(', ', $data['training_audience'] ?? ['N/A']) . "\n";
$message .= __('Application Details:', 'hvac-community-events') . "\n" . ($data['application_details'] ?? 'N/A') . "\n\n";
// Add link to approve/deny user (might need a custom admin page later)
$message .= __('View/Approve User Profile:', 'hvac-community-events') . ' ' . admin_url('user-edit.php?user_id=' . $user_id) . "\n";
// Or link to Users list filtered by pending role
$message .= __('Approve/Deny Users:', 'hvac-community-events') . ' ' . admin_url('users.php?role=pending_hvac_trainer');
$headers = array('Content-Type: text/plain; charset=UTF-8');
foreach ($emails as $email) {
wp_mail($email, $subject, $message, $headers);
}
return true;
}
// --- Helper methods for dropdowns ---
private function get_country_list() {
// Basic list, consider a library for a full list or WP options
// Using Name as value to match requirements doc example, but using Code (US, CA) might be better for JS
return [
'United States' => 'United States',
'Canada' => 'Canada',
'Afghanistan' => 'Afghanistan', 'Albania' => 'Albania', 'Algeria' => 'Algeria',
'Mexico' => 'Mexico', 'United Kingdom' => 'United Kingdom', 'Germany' => 'Germany', 'Australia' => 'Australia',
// Add many more countries as needed
];
}
private function get_us_states() {
// Key/Value should match what JS expects/populates
// Using full name as value based on E2E test attempt `selectOption({ label: 'California' })`
return [
'Alabama' => 'Alabama', 'Alaska' => 'Alaska', 'Arizona' => 'Arizona', 'Arkansas' => 'Arkansas', 'California' => 'California',
'Colorado' => 'Colorado', 'Connecticut' => 'Connecticut', 'Delaware' => 'Delaware', 'District Of Columbia' => 'District Of Columbia',
'Florida' => 'Florida', 'Georgia' => 'Georgia', 'Hawaii' => 'Hawaii', 'Idaho' => 'Idaho', 'Illinois' => 'Illinois',
'Indiana' => 'Indiana', 'Iowa' => 'Iowa', 'Kansas' => 'Kansas', 'Kentucky' => 'Kentucky', 'Louisiana' => 'Louisiana',
'Maine' => 'Maine', 'Maryland' => 'Maryland', 'Massachusetts' => 'Massachusetts', 'Michigan' => 'Michigan', 'Minnesota' => 'Minnesota',
'Mississippi' => 'Mississippi', 'Missouri' => 'Missouri', 'Montana' => 'Montana', 'Nebraska' => 'Nebraska', 'Nevada' => 'Nevada',
'New Hampshire' => 'New Hampshire', 'New Jersey' => 'New Jersey', 'New Mexico' => 'New Mexico', 'New York' => 'New York',
'North Carolina' => 'North Carolina', 'North Dakota' => 'North Dakota', 'Ohio' => 'Ohio', 'Oklahoma' => 'Oklahoma', 'Oregon' => 'Oregon',
'Pennsylvania' => 'Pennsylvania', 'Rhode Island' => 'Rhode Island', 'South Carolina' => 'South Carolina', 'South Dakota' => 'South Dakota',
'Tennessee' => 'Tennessee', 'Texas' => 'Texas', 'Utah' => 'Utah', 'Vermont' => 'Vermont', 'Virginia' => 'Virginia',
'Washington' => 'Washington', 'West Virginia' => 'West Virginia', 'Wisconsin' => 'Wisconsin', 'Wyoming' => 'Wyoming'
];
}
private function get_canadian_provinces() {
// Key/Value should match what JS expects/populates
// Using full name as value based on E2E test attempt
return [
'Alberta' => 'Alberta', 'British Columbia' => 'British Columbia', 'Manitoba' => 'Manitoba', 'New Brunswick' => 'New Brunswick',
'Newfoundland and Labrador' => 'Newfoundland and Labrador', 'Nova Scotia' => 'Nova Scotia', 'Ontario' => 'Ontario',
'Prince Edward Island' => 'Prince Edward Island', 'Quebec' => 'Quebec', 'Saskatchewan' => 'Saskatchewan',
'Northwest Territories' => 'Northwest Territories', 'Nunavut' => 'Nunavut', 'Yukon' => 'Yukon'
];
}
}