load_dependencies(); $this->load_settings(); // Load settings first $this->init_hooks(); $this->init_components(); } /** * Load plugin dependencies */ private function load_dependencies() { // Load API client. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/api/class-activetrail-api-client.php'; // Load helpers (before services that use them). require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/helpers/class-activetrail-consent-helper.php'; // Load service handlers. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-order-handler.php'; require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-cart-handler.php'; require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-coupon-handler.php'; require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-event-handler.php'; require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-user-registration-handler.php'; require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-catalog-handler.php'; require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/services/class-historical-sync-handler.php'; // Load settings. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/settings/class-settings.php'; // Always load admin components (needed for WooCommerce settings hooks). require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'admin/class-admin-page.php'; } /** * Initialize WordPress hooks */ private function init_hooks() { // Text domain loading is handled in the main plugin file on 'init' hook. // to avoid triggering WooCommerce translations too early. // Admin hooks. if ( is_admin() ) { add_action( 'admin_init', array( $this, 'init_admin' ) ); } // Frontend hooks. add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); // AJAX hooks. add_action( 'wp_ajax_activetrail_test_api', array( $this, 'ajax_test_api' ) ); add_action( 'wp_ajax_activetrail_get_groups', array( $this, 'ajax_get_groups' ) ); add_action( 'wp_ajax_activetrail_save_group', array( $this, 'ajax_save_group' ) ); add_action( 'wp_ajax_activetrail_get_debug_info', array( $this, 'ajax_get_debug_info' ) ); // Product feed endpoint. // 1) מוסיפים rewrite rules ל-/activetrail-feed.xml ו-/activetrail-feed.json // 2) רושמים query var בשם activetrail_feed כדי ש-WordPress לא יזרוק אותו // 3) ב-template_redirect בודקים את ה-query var ומחזירים XML/JSON בפועל. add_action( 'init', array( $this, 'add_feed_endpoint' ) ); add_filter( 'query_vars', array( $this, 'add_feed_query_var' ) ); add_action( 'template_redirect', array( $this, 'handle_feed_request' ) ); // Product sync hooks. add_action( 'activetrail_sync_products', array( $this, 'handle_scheduled_product_sync' ) ); // Checkout hooks. add_action( 'woocommerce_checkout_fields', array( $this, 'add_checkout_consent_field' ) ); add_action( 'woocommerce_checkout_process', array( $this, 'validate_checkout_consent' ) ); add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'save_checkout_consent' ) ); add_filter( 'woocommerce_form_field', array( $this, 'remove_optional_text_from_consent_field' ), 9999, 4 ); } /** * Initialize plugin components */ private function init_components() { // Initialize API client. $this->api_client = new ActiveTrail_API_Client( $this->get_setting( 'api_key' ) ); // Initialize admin page (needed for WooCommerce settings hooks). $admin_page = new ActiveTrail_Admin_Page( $this->api_client ); // Initialize handlers. $this->order_handler = new ActiveTrail_Order_Handler( $this->api_client, $this->settings ); $this->cart_handler = new ActiveTrail_Cart_Handler( $this->api_client, $this->settings ); $this->coupon_handler = new ActiveTrail_Coupon_Handler( $this->api_client, $this->settings ); $this->event_handler = new ActiveTrail_Event_Handler( $this->api_client, $this->settings ); // Initialize user registration handler. $this->user_registration_handler = new ActiveTrail_User_Registration_Handler( $this->api_client, $this->settings ); // Initialize historical sync handler. $this->historical_sync_handler = new ActiveTrail_Historical_Sync_Handler( $this->api_client, $this->settings ); // Initialize catalog handler. new ActiveTrail_Catalog_Handler( $this->api_client, $this->settings ); } /** * Load plugin text domain * * Note: Text domain loading is now handled in the main plugin file (activetrail-for-woocommerce.php) * on the 'init' hook to avoid triggering WooCommerce translations too early. * This method is kept for backward compatibility but does nothing. */ public function load_textdomain() { // Text domain is loaded in activetrail_wc_load_textdomain() on 'init' hook. // No action needed here to prevent duplicate loading. } /** * Load plugin settings */ public function load_settings() { $this->settings = get_option( 'activetrail_wc_settings', array() ); // Merge with defaults (without translations for now). $defaults = array( 'api_key' => '', 'group_id' => '', 'order_statuses' => array( 'completed' ), 'enable_checkout_consent' => true, 'consent_text' => 'I agree to receive marketing emails', // Will be translated later 'enable_registration_consent' => false, 'sync_without_consent' => false, ); $this->settings = wp_parse_args( $this->settings, $defaults ); } /** * Initialize admin components */ public function init_admin() { // Admin page is already initialized in init_components(). // This method is kept for compatibility but does nothing. } /** * Enqueue frontend scripts */ public function enqueue_frontend_scripts() { // Load on checkout and registration pages. if ( is_checkout() || is_account_page() || is_page( 'my-account' ) ) { // Define ajaxurl for frontend. wp_enqueue_script( 'activetrail-ajax', ACTIVETRAIL_WC_PLUGIN_URL . 'assets/js/ajax-handler.js', array( 'jquery' ), ACTIVETRAIL_WC_VERSION, true ); // Localize script to provide ajaxurl and consent validation params. wp_localize_script( 'activetrail-ajax', 'activetrail_params', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'activetrail_nonce' ), 'consent_required' => false, 'consent_required_message' => __( 'You must approve marketing emails in order to continue.', 'activetrail-for-woocommerce' ), ) ); wp_enqueue_script( 'activetrail-checkout', ACTIVETRAIL_WC_PLUGIN_URL . 'assets/js/checkout.js', array( 'jquery', 'activetrail-ajax' ), ACTIVETRAIL_WC_VERSION, true ); wp_enqueue_style( 'activetrail-checkout', ACTIVETRAIL_WC_PLUGIN_URL . 'assets/css/checkout.css', array(), ACTIVETRAIL_WC_VERSION ); } } /** * AJAX handler for testing API connection */ public function ajax_test_api() { check_ajax_referer( 'activetrail_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions', 'activetrail-for-woocommerce' ) ); } $api_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : ''; $test_client = new ActiveTrail_API_Client( $api_key ); $result = $test_client->test_connection(); // Update connection status in settings. ActiveTrail_Settings::update_connection_status( $result['success'], $result['message'] ); wp_send_json( $result ); } /** * AJAX handler for getting groups */ public function ajax_get_groups() { check_ajax_referer( 'activetrail_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions', 'activetrail-for-woocommerce' ) ); } $api_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : ''; // Use enhanced settings method with caching. $force_refresh = isset( $_POST['force_refresh'] ) && sanitize_text_field( wp_unslash( $_POST['force_refresh'] ) ); // If force refresh is requested, clear cache first. if ( $force_refresh ) { ActiveTrail_Settings::clear_groups_cache(); if ( function_exists( 'activetrail_logger' ) ) { activetrail_logger()->info( 'Force refresh requested - cache cleared', 'GENERAL' ); } } // Temporarily set API key for this request. ActiveTrail_Settings::update( 'api_key', $api_key ); $groups = ActiveTrail_Settings::get_activetrail_groups( $force_refresh ); // Log for debugging. if ( $force_refresh && function_exists( 'activetrail_logger' ) ) { activetrail_logger()->info( 'Groups refreshed from API - Count: ' . ( isset( $groups['data'] ) ? count( $groups['data'] ) : 0 ), 'GENERAL' ); } wp_send_json( $groups ); } /** * AJAX handler for saving group */ public function ajax_save_group() { check_ajax_referer( 'activetrail_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions', 'activetrail-for-woocommerce' ) ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on next line $group_id_raw = isset( $_POST['group_id'] ) ? wp_unslash( $_POST['group_id'] ) : ''; $group_id = sanitize_text_field( $group_id_raw ); if ( empty( $group_id ) ) { wp_send_json_error( esc_html__( 'Group ID is required', 'activetrail-for-woocommerce' ) ); } // Try to save to both systems with error trapping. $wc_result = false; $unified_result = false; try { $wc_result = update_option( 'activetrail_group_id', $group_id ); // Note: update_option returns false if value hasn't changed, so verify by checking saved value. if ( $wc_result === false ) { $saved_wc_value = get_option( 'activetrail_group_id', '' ); // If the saved value matches what we tried to save, it's actually a success. if ( $saved_wc_value === $group_id ) { $wc_result = true; } elseif ( function_exists( 'activetrail_logger' ) ) { $error = error_get_last(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Debug only, error data needed $error_string = is_array( $error ) ? wp_json_encode( $error ) : (string) $error; activetrail_logger()->warning( 'update_option returned false. Last PHP error: ' . $error_string, 'GENERAL' ); } } } catch ( Exception $e ) { if ( function_exists( 'activetrail_logger' ) ) { activetrail_logger()->error( 'Exception saving group_id to option: ' . $e->getMessage(), 'GENERAL' ); } } try { $unified_result = ActiveTrail_Settings::update( 'group_id', $group_id ); // Note: update_option returns false if value hasn't changed, so verify by checking saved value. if ( $unified_result === false ) { $saved_unified_value = ActiveTrail_Settings::get( 'group_id', '' ); // If the saved value matches what we tried to save, it's actually a success. if ( $saved_unified_value === $group_id ) { $unified_result = true; } elseif ( function_exists( 'activetrail_logger' ) ) { activetrail_logger()->warning( 'ActiveTrail_Settings::update returned false for group_id.', 'GENERAL' ); } } } catch ( Exception $e ) { if ( function_exists( 'activetrail_logger' ) ) { activetrail_logger()->error( 'Exception updating group_id in settings: ' . $e->getMessage(), 'GENERAL' ); } } // Debug logging always enabled here for better tracing. if ( function_exists( 'activetrail_logger' ) ) { $logger = activetrail_logger(); $logger->debug( 'AJAX Save Group - Received Group ID: ' . $group_id_raw, 'GENERAL' ); $logger->debug( 'AJAX Save Group - Sanitized Group ID: ' . $group_id, 'GENERAL' ); $logger->debug( 'AJAX Save Group - WC Result: ' . ( $wc_result ? 'success' : 'failed' ), 'GENERAL' ); $logger->debug( 'AJAX Save Group - Unified Result: ' . ( $unified_result ? 'success' : 'failed' ), 'GENERAL' ); } if ( $wc_result && $unified_result ) { wp_send_json_success( array( 'message' => esc_html__( 'Group saved successfully', 'activetrail-for-woocommerce' ), 'group_id' => $group_id, 'timestamp' => current_time( 'mysql' ), ) ); } else { wp_send_json_error( esc_html__( 'Failed to save group', 'activetrail-for-woocommerce' ) ); } } /** * AJAX handler for getting debug info */ public function ajax_get_debug_info() { check_ajax_referer( 'activetrail_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions', 'activetrail-for-woocommerce' ) ); } $wc_option = get_option( 'activetrail_group_id', '' ); $unified_setting = ActiveTrail_Settings::get( 'group_id', '' ); $sync_status = 'synchronized'; if ( $wc_option !== $unified_setting ) { $sync_status = 'not_synchronized'; } elseif ( empty( $wc_option ) && empty( $unified_setting ) ) { $sync_status = 'both_empty'; } wp_send_json_success( array( 'wc_option' => $wc_option ?: 'empty', 'unified_setting' => $unified_setting ?: 'empty', 'sync_status' => $sync_status, 'timestamp' => current_time( 'mysql' ), ) ); } /** * Register custom query var for feed endpoint * * Without this, WordPress מתעלם מ-activetrail_feed וה-URL /activetrail-feed.xml * פשוט יטען את עמוד הבית במקום את הקטלוג. * * @param array $vars Existing public query vars. * @return array */ public function add_feed_query_var( $vars ) { $vars[] = 'activetrail_feed'; return $vars; } /** * Add product feed endpoint */ public function add_feed_endpoint() { add_rewrite_rule( '^activetrail-feed\.xml$', 'index.php?activetrail_feed=xml', 'top' ); add_rewrite_rule( '^activetrail-feed\.json$', 'index.php?activetrail_feed=json', 'top' ); } /** * Handle feed request */ public function handle_feed_request() { $feed_type = get_query_var( 'activetrail_feed' ); if ( ! $feed_type ) { return; } // Use the unified Product Feed Generator directly for this pretty URL endpoint. // כאן אנחנו לא משתמשים במפתח גישה (key), זה feed "גלוי" שמיועד בעיקר לחיבור מול ActiveTrail. require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'includes/class-product-feed-generator.php'; $generator = new ActiveTrail_Product_Feed_Generator(); if ( $feed_type === 'xml' ) { $xml = $generator->generate_google_xml(); // כותרות מתאימות לקטלוג XML. header( 'Content-Type: application/xml; charset=utf-8' ); header( 'Cache-Control: no-cache, must-revalidate' ); header( 'Expires: Sat, 26 Jul 1997 05:00:00 GMT' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML feed content must be output as-is. echo $xml; exit; } if ( $feed_type === 'json' ) { $json = $generator->generate_facebook_json(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-cache, must-revalidate' ); header( 'Expires: Sat, 26 Jul 1997 05:00:00 GMT' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON feed content must be output as-is. echo $json; exit; } } /** * Add consent field to checkout */ public function add_checkout_consent_field( $fields ) { // Get settings from ActiveTrail_Settings class. $settings = ActiveTrail_Settings::get_all(); if ( ! isset( $settings['enable_checkout_consent'] ) || ! $settings['enable_checkout_consent'] ) { return $fields; } // Use custom consent text from settings, or fall back to translated default. $consent_text = ! empty( $settings['consent_text'] ) ? $settings['consent_text'] : esc_html__( 'I agree to receive marketing emails', 'activetrail-for-woocommerce' ); // Default checked when "Marketing consent checked by default" is enabled. $default_checked = ! empty( $settings['consent_checked_by_default'] ) ? 1 : 0; $fields['billing']['activetrail_consent'] = array( 'type' => 'checkbox', 'label' => $consent_text, 'required' => false, 'class' => array( 'form-row-wide' ), 'priority' => 120, 'default' => $default_checked, ); return $fields; } /** * Validate checkout consent (consent is optional – no blocking). */ public function validate_checkout_consent() { // Consent is not required for checkout; no validation. return; } /** * Remove optional text from consent field */ public function remove_optional_text_from_consent_field( $field, $key, $args, $value ) { // Only modify the consent field (key may be activetrail_consent or billing_activetrail_consent). if ( $key === 'activetrail_consent' || $key === 'billing_activetrail_consent' || strpos( $key, 'activetrail_consent' ) !== false ) { // Remove the optional text span and any surrounding spaces/nbsp (works for any language). $field = preg_replace( '/ ?\s*.*?<\/span>/', '', $field ); // Clean leftover multiple spaces. $field = preg_replace( '/\s+/', ' ', $field ); } return $field; } /** * Save checkout consent */ public function save_checkout_consent( $order_id ) { // Get the order object. $order = wc_get_order( $order_id ); if ( ! $order ) { return; } // Check if consent checkbox was checked (supports activetrail_consent and billing_activetrail_consent). // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WooCommerce checkout form handles nonce verification if ( ActiveTrail_Consent_Helper::has_consent_from_data( $_POST ) ) { // Store consent in order meta. update_post_meta( $order_id, '_activetrail_consent', 'yes' ); // If user is logged in, also store in user meta. $customer_id = $order->get_customer_id(); if ( $customer_id ) { update_user_meta( $customer_id, '_activetrail_consent', 'yes' ); update_user_meta( $customer_id, '_activetrail_consent_date', current_time( 'mysql' ) ); } } else { // Always store a value for _activetrail_consent, even when not checked. // This ensures we have an explicit value for all orders. update_post_meta( $order_id, '_activetrail_consent', 'no' ); // We don't update user meta when consent is not given. // This preserves any previous consent the user might have given. } } /** * Handle scheduled product sync */ public function handle_scheduled_product_sync() { require_once ACTIVETRAIL_WC_PLUGIN_DIR . 'templates/product-feed.php'; $feed = new ActiveTrail_Product_Feed(); $feed->handle_scheduled_sync(); } /** * Get plugin setting * * @param string $key Setting key * @param mixed $default Default value * @return mixed */ public function get_setting( $key, $default = null ) { // Use ActiveTrail_Settings class for consistency. return ActiveTrail_Settings::get( $key, $default ); } /** * Update plugin setting * * @param string $key Setting key * @param mixed $value Setting value */ public function update_setting( $key, $value ) { // Use ActiveTrail_Settings class for consistency. ActiveTrail_Settings::update( $key, $value ); } /** * Get API client instance * * @return ActiveTrail_API_Client */ public function get_api_client() { return $this->api_client; } /** * Get order handler instance * * @return ActiveTrail_Order_Handler */ public function get_order_handler() { return $this->order_handler; } /** * Get cart handler instance * * @return ActiveTrail_Cart_Handler */ public function get_cart_handler() { return $this->cart_handler; } /** * Get coupon handler instance * * @return ActiveTrail_Coupon_Handler */ public function get_coupon_handler() { return $this->coupon_handler; } /** * Get event handler instance * * @return ActiveTrail_Event_Handler */ public function get_event_handler() { return $this->event_handler; } /** * Get user registration handler instance * * @return ActiveTrail_User_Registration_Handler */ public function get_user_registration_handler() { return $this->user_registration_handler; } /** * Get historical sync handler instance * * @return ActiveTrail_Historical_Sync_Handler */ public function get_historical_sync_handler() { return $this->historical_sync_handler; } /** * Log message * * @param string $message Log message * @param string $level Log level (info, warning, error) */ public function log( $message, $level = 'info' ) { if ( function_exists( 'activetrail_logger' ) ) { $level_map = array( 'info' => 'INFO', 'debug' => 'DEBUG', 'warning' => 'WARNING', 'error' => 'ERROR', ); $mapped_level = isset( $level_map[ $level ] ) ? $level_map[ $level ] : 'INFO'; activetrail_logger()->log( $message, $mapped_level, 'GENERAL' ); } } }