'', 'group_id' => '', 'order_statuses' => array( 'completed', 'processing' ), // Added processing by default 'status_mapping' => array(), 'enable_checkout_consent' => true, 'consent_text' => '', 'consent_checked_by_default' => false, // If true, marketing consent checkbox is checked by default on frontend. 'consent_required' => false, // If true, user must check marketing consent to continue checkout/registration. 'enable_registration_consent' => true, // Enable registration consent by default 'sync_without_consent' => false, 'enable_tracking_consent' => false, // Separate CNIL tracking-consent checkbox (checkout + registration). 'tracking_consent_text' => '', // Editable label for the tracking-consent checkbox. 'enable_cart_tracking' => true, 'cart_update_frequency' => 900, // Default to 15 minutes (900 seconds) 'cart_event_interval_minutes' => 15, // Default to 15 minutes 'enable_product_feed' => true, 'feed_title' => '', 'feed_description' => '', 'debug_mode' => false, 'last_connection_test' => '', 'connection_status' => '', 'groups_cache' => array(), 'groups_cache_time' => 0, 'groups_cache_api_key_hash' => '', // Store hash of API key used for cache 'language' => 'en_US', // Default language // New ActiveTrail integration settings. 'enable_coupon_sync' => true, 'coupon_sync_logs' => array(), ); /** * Get all settings * * @return array */ public static function get_all() { $settings = get_option( self::OPTION_NAME, array() ); // Set default consent text if empty (always store in English for translation). if ( empty( $settings['consent_text'] ) ) { $settings['consent_text'] = 'I agree to receive marketing emails'; } // Set default tracking-consent text if empty (always store in English for translation). if ( empty( $settings['tracking_consent_text'] ) ) { $settings['tracking_consent_text'] = 'I agree to the use of tracking for analytics and marketing purposes'; } // Set default feed title if empty. if ( empty( $settings['feed_title'] ) ) { $settings['feed_title'] = get_bloginfo( 'name' ) . ' - ' . esc_html__( 'Product Catalog', 'activetrail-for-woocommerce' ); } // Set default feed description if empty. if ( empty( $settings['feed_description'] ) ) { $settings['feed_description'] = get_bloginfo( 'description' ); } return wp_parse_args( $settings, self::$defaults ); } /** * Get specific setting * * @param string $key Setting key. * @param mixed $default Default value. * @return mixed */ public static function get( $key, $default = null ) { $settings = self::get_all(); if ( isset( $settings[ $key ] ) ) { return $settings[ $key ]; } if ( $default !== null ) { return $default; } return isset( self::$defaults[ $key ] ) ? self::$defaults[ $key ] : null; } /** * Update setting * * @param string $key Setting key. * @param mixed $value Setting value. * @return bool */ public static function update( $key, $value ) { // Check permissions for sensitive settings. if ( $key === 'api_key' && ! current_user_can( 'manage_options' ) ) { return false; } $settings = self::get_all(); // Check if this is a new API key or an update. $is_new_api_key = false; $is_api_key_changed = false; if ( 'api_key' === $key ) { $value = sanitize_text_field( $value ); $old_value = isset( $settings[ $key ] ) ? $settings[ $key ] : ''; // Check if this is a new API key (empty before, not empty now). $is_new_api_key = ( empty( $old_value ) && ! empty( $value ) ); // Check if API key was changed (different value). $is_api_key_changed = ( ! empty( $old_value ) && ! empty( $value ) && $old_value !== $value ); } $settings[ $key ] = $value; // Ensure enable_product_feed is always true (always enabled). if ( $key === 'enable_product_feed' || isset( $settings['enable_product_feed'] ) ) { $settings['enable_product_feed'] = true; } $update_result = update_option( self::OPTION_NAME, $settings ); // Fire action hook for API key change. if ( $update_result && $is_new_api_key ) { do_action( 'activetrail_api_key_changed', $value ); } // If API key was changed (even if not "new"), clear groups cache. // This ensures groups are reloaded when switching between API keys. if ( $update_result && $is_api_key_changed ) { self::clear_groups_cache(); // Fire action hook for API key change (even if not "new"). do_action( 'activetrail_api_key_changed', $value ); } return $update_result; } /** * Trigger store connection to ActiveTrail * * @param string $api_key API key. * @return array|null Connection result or null if not triggered */ private static function trigger_store_connection( $api_key ) { // Get existing status for logging purposes. $existing_status = get_option( 'activetrail_store_status', '' ); // Always clear the existing status to force a new connection attempt. delete_option( 'activetrail_store_status' ); // Log the connection attempt start. if ( function_exists( 'activetrail_logger' ) ) { activetrail_logger()->info( 'Triggering store connection. Previous status: ' . $existing_status, 'SETTINGS' ); } // Initialize API client with the new API key. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; $api_client = new ActiveTrail_API_Client( $api_key ); // Connect the store. $result = $api_client->connect_store(); // Log the connection attempt. if ( function_exists( 'activetrail_logger' ) ) { $logger = activetrail_logger(); if ( $result['success'] ) { $logger->info( 'ActiveTrail Store Connected', $result ); } else { $logger->error( 'ActiveTrail Store Connection Failed', $result ); } } // Always log to logger for debugging. if ( function_exists( 'activetrail_logger' ) ) { $logger = activetrail_logger(); if ( $result['success'] ) { $logger->info( 'Store Connected', 'SETTINGS', $result ); } else { $logger->error( 'Store Connection Failed', 'SETTINGS', $result ); } } return $result; } /** * Update multiple settings * * @param array $new_settings New settings. * @return bool */ public static function update_all( $new_settings ) { $settings = self::get_all(); $settings = array_merge( $settings, $new_settings ); // Ensure enable_product_feed is always true (always enabled). $settings['enable_product_feed'] = true; return update_option( self::OPTION_NAME, $settings ); } /** * Delete all settings * * @return bool */ public static function delete_all() { return delete_option( self::OPTION_NAME ); } /** * Reset to defaults * * @return bool */ public static function reset() { return update_option( self::OPTION_NAME, self::$defaults ); } /** * Validate settings * * @param array $settings Settings to validate. * @return array Validation results */ public static function validate( $settings ) { $errors = array(); $warnings = array(); // Validate API key. if ( empty( $settings['api_key'] ) ) { $errors[] = esc_html__( 'API key is required', 'activetrail-for-woocommerce' ); } elseif ( strlen( $settings['api_key'] ) < 10 ) { $warnings[] = esc_html__( 'API key seems too short', 'activetrail-for-woocommerce' ); } // Validate group ID. if ( empty( $settings['group_id'] ) ) { $warnings[] = esc_html__( 'No group selected - customers will be added to default group', 'activetrail-for-woocommerce' ); } // Validate order statuses. if ( empty( $settings['order_statuses'] ) || ! is_array( $settings['order_statuses'] ) ) { $errors[] = esc_html__( 'At least one order status must be selected', 'activetrail-for-woocommerce' ); } // Validate consent text. if ( $settings['enable_checkout_consent'] && empty( $settings['consent_text'] ) ) { $warnings[] = esc_html__( 'Consent text is empty - using default text', 'activetrail-for-woocommerce' ); } return array( 'errors' => $errors, 'warnings' => $warnings, 'valid' => empty( $errors ), ); } /** * Sanitize settings * * @param array $settings Settings to sanitize. * @return array Sanitized settings */ public static function sanitize( $settings ) { $sanitized = array(); // Sanitize API key. $sanitized['api_key'] = sanitize_text_field( $settings['api_key'] ?? '' ); // Sanitize group ID. $sanitized['group_id'] = sanitize_text_field( $settings['group_id'] ?? '' ); // Sanitize order statuses. if ( isset( $settings['order_statuses'] ) && is_array( $settings['order_statuses'] ) ) { $sanitized['order_statuses'] = array_map( 'sanitize_text_field', $settings['order_statuses'] ); } else { $sanitized['order_statuses'] = array( 'completed' ); } // Sanitize boolean settings. $boolean_settings = array( 'enable_checkout_consent', 'consent_checked_by_default', 'consent_required', 'enable_registration_consent', 'sync_without_consent', 'enable_tracking_consent', 'enable_cart_tracking', 'enable_product_feed', 'debug_mode', ); foreach ( $boolean_settings as $key ) { $sanitized[ $key ] = isset( $settings[ $key ] ) && $settings[ $key ] ? true : false; } // Sanitize text settings. $text_settings = array( 'consent_text', 'tracking_consent_text', 'feed_title', 'feed_description', ); foreach ( $text_settings as $key ) { $sanitized[ $key ] = sanitize_textarea_field( $settings[ $key ] ?? '' ); } return $sanitized; } /** * Get available order statuses * * @return array */ public static function get_order_statuses() { $statuses = wc_get_order_statuses(); // Remove 'wc-' prefix for cleaner display. $clean_statuses = array(); foreach ( $statuses as $key => $label ) { $clean_key = str_replace( 'wc-', '', $key ); $clean_statuses[ $clean_key ] = $label; } return $clean_statuses; } /** * Get order statuses with detailed information * * @return array */ public static function get_order_statuses_detailed() { $statuses = wc_get_order_statuses(); $detailed_statuses = array(); // Default status descriptions. $default_descriptions = array( 'pending' => esc_html__( 'Order received, awaiting payment', 'activetrail-for-woocommerce' ), 'processing' => esc_html__( 'Payment received, order is being processed', 'activetrail-for-woocommerce' ), 'on-hold' => esc_html__( 'Order on hold, awaiting action', 'activetrail-for-woocommerce' ), 'completed' => esc_html__( 'Order completed successfully', 'activetrail-for-woocommerce' ), 'cancelled' => esc_html__( 'Order cancelled by customer or admin', 'activetrail-for-woocommerce' ), 'refunded' => esc_html__( 'Order refunded', 'activetrail-for-woocommerce' ), 'failed' => esc_html__( 'Payment failed or order failed', 'activetrail-for-woocommerce' ), ); // Default sync recommendations. $sync_recommendations = array( 'pending' => false, 'processing' => true, 'on-hold' => false, 'completed' => true, 'cancelled' => false, 'refunded' => false, 'failed' => false, ); foreach ( $statuses as $key => $label ) { $clean_key = str_replace( 'wc-', '', $key ); // Get order count for this status. $order_count = self::get_status_order_count( $clean_key ); $detailed_statuses[ $clean_key ] = array( 'label' => $label, 'description' => isset( $default_descriptions[ $clean_key ] ) ? $default_descriptions[ $clean_key ] : esc_html__( 'Custom order status', 'activetrail-for-woocommerce' ), 'is_custom' => ! isset( $default_descriptions[ $clean_key ] ), 'order_count' => $order_count, 'recommended_sync' => isset( $sync_recommendations[ $clean_key ] ) ? $sync_recommendations[ $clean_key ] : true, // Default to true for custom statuses 'category' => self::get_status_category( $clean_key ), ); } return $detailed_statuses; } /** * Get order count for specific status * * @param string $status Status key. * @return int */ private static function get_status_order_count( $status ) { $orders = wc_get_orders( array( 'status' => $status, 'limit' => -1, 'return' => 'ids', ) ); return count( $orders ); } /** * Get status category for grouping * Each status gets its own category for clearer display * * @param string $status Status key. * @return string */ private static function get_status_category( $status ) { // Give each status its own category for clearer display in the table. $categories = array( 'pending' => 'pending', 'processing' => 'processing', 'on-hold' => 'on-hold', 'completed' => 'completed', 'cancelled' => 'cancelled', 'refunded' => 'refunded', 'failed' => 'failed', ); return isset( $categories[ $status ] ) ? $categories[ $status ] : 'custom'; } /** * Get status mapping with sync settings * * @return array */ public static function get_status_mapping() { $mapping = self::get( 'status_mapping', array() ); $detailed_statuses = self::get_order_statuses_detailed(); // Ensure all statuses have mapping. foreach ( $detailed_statuses as $status_key => $status_info ) { if ( ! isset( $mapping[ $status_key ] ) ) { $mapping[ $status_key ] = array( 'sync' => $status_info['recommended_sync'], 'description' => $status_info['description'], ); } } return $mapping; } /** * Update status mapping * * @param array $mapping Status mapping. * @return bool */ public static function update_status_mapping( $mapping ) { return self::update( 'status_mapping', $mapping ); } /** * Get available WooCommerce currencies * * @return array */ public static function get_currencies() { return get_woocommerce_currencies(); } /** * Export settings * * @return array */ public static function export() { $settings = self::get_all(); // Remove sensitive data. unset( $settings['api_key'] ); return array( 'version' => ACTIVETRAIL_WC_VERSION, 'exported_at' => current_time( 'mysql' ), 'site_url' => get_site_url(), 'settings' => $settings, ); } /** * Import settings * * @param array $import_data Import data. * @return array Result */ public static function import( $import_data ) { if ( ! isset( $import_data['settings'] ) || ! is_array( $import_data['settings'] ) ) { return array( 'success' => false, 'message' => esc_html__( 'Invalid import data', 'activetrail-for-woocommerce' ), ); } $settings = $import_data['settings']; // Don't import API key for security. unset( $settings['api_key'] ); // Sanitize imported settings. $settings = self::sanitize( $settings ); // Validate settings. $validation = self::validate( $settings ); if ( ! $validation['valid'] ) { return array( 'success' => false, 'message' => esc_html__( 'Import validation failed', 'activetrail-for-woocommerce' ), 'errors' => $validation['errors'], ); } // Merge with current settings (preserve API key). $current_settings = self::get_all(); $settings['api_key'] = $current_settings['api_key']; // Update settings. $updated = self::update_all( $settings ); if ( $updated ) { return array( 'success' => true, 'message' => esc_html__( 'Settings imported successfully', 'activetrail-for-woocommerce' ), 'warnings' => $validation['warnings'], ); } else { return array( 'success' => false, 'message' => esc_html__( 'Failed to save imported settings', 'activetrail-for-woocommerce' ), ); } } /** * Get debug information * * @return array */ public static function get_debug_info() { $settings = self::get_all(); return array( 'plugin_version' => ACTIVETRAIL_WC_VERSION, 'wordpress_version' => get_bloginfo( 'version' ), 'woocommerce_version' => defined( 'WC_VERSION' ) ? WC_VERSION : 'Not installed', 'php_version' => PHP_VERSION, 'api_key_configured' => ! empty( $settings['api_key'] ), 'group_id_configured' => ! empty( $settings['group_id'] ), 'order_statuses_count' => count( $settings['order_statuses'] ), 'checkout_consent_enabled' => $settings['enable_checkout_consent'], 'cart_tracking_enabled' => $settings['enable_cart_tracking'], 'product_feed_enabled' => $settings['enable_product_feed'], 'debug_mode' => $settings['debug_mode'], 'site_url' => get_site_url(), 'admin_email' => get_option( 'admin_email' ), 'timezone' => get_option( 'timezone_string' ), 'currency' => get_woocommerce_currency(), 'active_plugins' => get_option( 'active_plugins' ), 'active_theme' => get_option( 'stylesheet' ), ); } /** * Check if API is configured * * @return bool */ public static function is_api_configured() { $api_key = self::get( 'api_key' ); return ! empty( $api_key ); } /** * Check if group is configured * * @return bool */ public static function is_group_configured() { $group_id = self::get( 'group_id' ); return ! empty( $group_id ); } /** * Check if plugin is fully configured * * @return bool */ public static function is_configured() { return self::is_api_configured() && self::is_group_configured(); } /** * Get configuration status * * @return array */ public static function get_configuration_status() { return array( 'api_configured' => self::is_api_configured(), 'group_configured' => self::is_group_configured(), 'fully_configured' => self::is_configured(), 'missing_requirements' => self::get_missing_requirements(), ); } /** * Get missing requirements * * @return array */ public static function get_missing_requirements() { $missing = array(); if ( ! self::is_api_configured() ) { $missing[] = esc_html__( 'API key not configured', 'activetrail-for-woocommerce' ); } if ( ! self::is_group_configured() ) { $missing[] = esc_html__( 'Group not selected', 'activetrail-for-woocommerce' ); } return $missing; } /** * Get ActiveTrail groups with caching * * @param bool $force_refresh Force refresh from API. * @return array */ public static function get_activetrail_groups( $force_refresh = false ) { $settings = self::get_all(); $api_key = $settings['api_key']; if ( empty( $api_key ) ) { return array( 'success' => false, 'message' => esc_html__( 'API key not configured', 'activetrail-for-woocommerce' ), 'data' => array(), ); } // Check cache first (cache for 1 hour). $cache_time = $settings['groups_cache_time']; $cached_groups = $settings['groups_cache']; $cached_api_key_hash = isset( $settings['groups_cache_api_key_hash'] ) ? $settings['groups_cache_api_key_hash'] : ''; $current_api_key_hash = md5( $api_key ); // Hash current API key for comparison // Verify cache is valid: exists, not expired, and matches current API key. if ( ! $force_refresh && ! empty( $cached_groups ) && ( time() - $cache_time ) < 3600 && $cached_api_key_hash === $current_api_key_hash ) { return array( 'success' => true, 'message' => esc_html__( 'Groups retrieved from cache', 'activetrail-for-woocommerce' ), 'data' => $cached_groups, ); } // If API key changed, clear old cache. if ( $cached_api_key_hash !== '' && $cached_api_key_hash !== $current_api_key_hash ) { self::clear_groups_cache(); } // Get fresh data from API. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; $api_client = new ActiveTrail_API_Client( $api_key ); $result = $api_client->get_groups(); if ( $result['success'] && ! empty( $result['data'] ) ) { // Cache the results along with API key hash. self::update( 'groups_cache', $result['data'] ); self::update( 'groups_cache_time', time() ); self::update( 'groups_cache_api_key_hash', md5( $api_key ) ); } return $result; } /** * Update connection status * * @param bool $success Connection success status. * @param string $message Status message. * @return bool */ public static function update_connection_status( $success, $message = '' ) { $status = $success ? 'connected' : 'failed'; $timestamp = current_time( 'mysql' ); self::update( 'connection_status', $status ); self::update( 'last_connection_test', $timestamp ); return true; } /** * Get connection status with formatted display * * @return array */ public static function get_connection_status() { $settings = self::get_all(); $status = $settings['connection_status']; $last_test = $settings['last_connection_test']; $display_status = ''; $css_class = ''; switch ( $status ) { case 'connected': $display_status = '✅ ' . esc_html__( 'Connection successful', 'activetrail-for-woocommerce' ); $css_class = 'success'; break; case 'failed': $display_status = '❌ ' . esc_html__( 'Connection failed', 'activetrail-for-woocommerce' ); $css_class = 'error'; break; default: $display_status = '⚪ ' . esc_html__( 'Not tested', 'activetrail-for-woocommerce' ); $css_class = 'neutral'; break; } if ( ! empty( $last_test ) ) { $display_status .= ' (' . sprintf( /* translators: %s: Last test date and time */ esc_html__( 'Last tested: %s', 'activetrail-for-woocommerce' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $last_test ) ) ) . ')'; } return array( 'status' => $status, 'display' => $display_status, 'css_class' => $css_class, 'last_test' => $last_test, ); } /** * Sanitize settings with enhanced security * * @param array $settings Settings to sanitize. * @return array Sanitized settings */ public static function sanitize_enhanced( $settings ) { $sanitized = self::sanitize( $settings ); // Additional security for API key. if ( ! empty( $sanitized['api_key'] ) ) { // Remove any potential harmful characters. $sanitized['api_key'] = preg_replace( '/[^a-zA-Z0-9_\-]/', '', $sanitized['api_key'] ); } return $sanitized; } /** * Get groups for select dropdown with proper formatting * * @return array */ public static function get_groups_for_select() { $groups_result = self::get_activetrail_groups(); $options = array( '' => esc_html__( 'Select a group...', 'activetrail-for-woocommerce' ) ); if ( ! $groups_result['success'] ) { $options[''] = $groups_result['message']; return $options; } if ( empty( $groups_result['data'] ) ) { $options[''] = esc_html__( 'No groups found', 'activetrail-for-woocommerce' ); return $options; } foreach ( $groups_result['data'] as $group ) { // Handle both possible API response formats. $id = isset( $group['Id'] ) ? $group['Id'] : ( isset( $group['id'] ) ? $group['id'] : '' ); $name = isset( $group['Name'] ) ? $group['Name'] : ( isset( $group['name'] ) ? $group['name'] : '' ); if ( ! empty( $id ) && ! empty( $name ) ) { $options[ $id ] = $name; } } return $options; } /** * Log API errors for debugging * * @param string $context Error context. * @param string $error_message Error message. * @param array $additional_data Additional debug data. */ public static function log_api_error( $context, $error_message, $additional_data = array() ) { if ( ! self::get( 'debug_mode' ) || ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return; } $log_entry = array( 'timestamp' => current_time( 'mysql' ), 'context' => $context, 'error' => $error_message, 'data' => $additional_data, ); if ( function_exists( 'activetrail_logger' ) ) { activetrail_logger()->log( $error_message, 'ERROR', 'SETTINGS', $additional_data ); } } /** * Clear groups cache * * @return bool */ public static function clear_groups_cache() { self::update( 'groups_cache', array() ); self::update( 'groups_cache_time', 0 ); self::update( 'groups_cache_api_key_hash', '' ); return true; } /** * Get formatted settings for display * * @return array */ public static function get_formatted_settings() { $settings = self::get_all(); $formatted = array(); // Format API key (show only first/last chars for security). if ( ! empty( $settings['api_key'] ) ) { $api_key = $settings['api_key']; if ( strlen( $api_key ) > 8 ) { $formatted['api_key'] = substr( $api_key, 0, 4 ) . '...' . substr( $api_key, -4 ); } else { $formatted['api_key'] = '***'; } } else { $formatted['api_key'] = esc_html__( 'Not configured', 'activetrail-for-woocommerce' ); } // Format group ID with name if available. $formatted['group_id'] = $settings['group_id']; if ( ! empty( $settings['group_id'] ) ) { $groups = self::get_groups_for_select(); if ( isset( $groups[ $settings['group_id'] ] ) ) { $formatted['group_name'] = $groups[ $settings['group_id'] ]; } } // Format order statuses. $all_statuses = self::get_order_statuses(); $formatted['order_statuses_names'] = array(); foreach ( $settings['order_statuses'] as $status ) { if ( isset( $all_statuses[ $status ] ) ) { $formatted['order_statuses_names'][] = $all_statuses[ $status ]; } } return array_merge( $settings, $formatted ); } /** * Get available languages * Returns language names in their native language (not translated) * * @return array */ public static function get_available_languages() { return array( 'en_US' => 'English', 'he_IL' => 'עברית', 'es_ES' => 'Español', 'fr_FR' => 'Français', ); } /** * Get current language * * @return string */ public static function get_current_language() { // Prefer explicitly selected plugin language if available (legacy option), // to avoid situations where the dropdown shows one language but the // plugin still loads translations for another. $legacy_language = get_option( 'activetrail_language', '' ); if ( ! empty( $legacy_language ) ) { return $legacy_language; } // Fallback to unified settings storage. return self::get( 'language', 'en_US' ); } /** * Update language setting * * @param string $language Language code. * @return bool */ public static function update_language( $language ) { $available_languages = array_keys( self::get_available_languages() ); if ( ! in_array( $language, $available_languages, true ) ) { return false; } return self::update( 'language', $language ); } /** * Check if current language is RTL * * @return bool */ public static function is_rtl_language() { $rtl_languages = array( 'he_IL', 'ar', 'fa_IR', 'ur_PK' ); $current_language = self::get_current_language(); return in_array( $current_language, $rtl_languages, true ); } /** * Get language direction * * @return string 'rtl' or 'ltr' */ public static function get_language_direction() { return self::is_rtl_language() ? 'rtl' : 'ltr'; } }