prefix-derived table names or keys from hardcoded internal arrays (never request input). Direct queries hit plugin-owned tables that have no WP REST/cache layer. Variables flagged as non-prefixed are local helpers / loop indices, not globals. CSV import/export streams use fopen()/fread()/fwrite()/fclose() because WP_Filesystem_API has no equivalent streaming interface and the operations are admin-only. set_time_limit()/ini_set() are needed inside long-running CSV exports + activation cleanup; they're capability-gated and short-circuit when running under PHP-FPM. /* Plugin Name: AI eShop Optimizer Plugin URI: https://rooster.systems/get-aieo/ Description: Adaptive productivity, conversion & retention for eshops and content sites. Anthropic/Voyage AI: messaging, recommendations, attribution, loyalty, merchandising. Version: 5.3 Requires at least: 5.7 Tested up to: 6.9 Requires PHP: 7.4 Author: Oxford Metadata Ltd Author URI: https://oxfordmetadata.co.uk License: GPL3 Tags: ai-chat, content-recommendations, semantic-search, conversational-ai, claude */ // Ensure the plugin is only loaded within WordPress if (!defined('ABSPATH')) { exit; } /** * Centralised debug logger. Calls error_log() only when WP_DEBUG and * WP_DEBUG_LOG are both enabled, so production sites stay quiet by default. * Used in place of bare error_log() throughout the plugin. * * @param string|mixed $message Message or value to log. */ if (!function_exists('aieo_debug_log')) { function aieo_debug_log($message) { if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Single gated entry point for plugin diagnostics. error_log(is_scalar($message) ? (string) $message : wp_json_encode($message)); } } } // Load text domain IMMEDIATELY for AJAX requests (before any classes load) // This ensures translations are available during whitelisted AJAX requests if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Fallback for non-WordPress.org hosted translations load_plugin_textdomain( 'ai-eshop-optimizer', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); } define('AIEO_VERSION', '5.3'); define('AIEO_FILE', __FILE__); define('AIEO_BASE', trailingslashit(plugin_basename(AIEO_FILE))); define('AIEO_DIR', plugin_dir_path(__FILE__)); define('AIEO_URI', plugin_dir_url(__FILE__)); define('AIEO_orders_upload_endpoint', 'https://uploads.eshop-optimizer.com/wp-json/aieo/v1/order_uploads'); // Capability required to access the plugin admin pages // Using 'edit_pages' allows editor, shop_manager, and admin roles to access // (edit_pages is shared by Editor, Shop Manager, and Administrator) define('AIEO_CAPABILITY', 'edit_pages'); // Declare WooCommerce HPOS compatibility if WooCommerce is active add_action('before_woocommerce_init', function () { if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true); } }); // Load the plugin class require_once plugin_dir_path(__FILE__) . '/includes/class-eshop-optimizer.php'; require_once plugin_dir_path(__FILE__) . '/includes/sql/sprocs.php'; // Load Session Tracker (Core analytics functionality). // AIEO Session Tracker has a canonical copy at // ai-eshop-optimizer-runtime/includes/class-aieo-session-tracker.php // (moved 2026-05-16 — see runtime plugin's STATUS.md). When the // autoloader mu-plugin AND the runtime plugin are present, this // require_once is a no-op (the autoloader resolved the class first). // When the admin plugin is installed STANDALONE — typical for a fresh // wp.org download where neither the autoloader nor the runtime ship — // the require_once loads our own fallback copy. This guard is the // stability boundary that lets the admin plugin work on any host // without external prerequisites. if ( ! class_exists( 'AIEO_Session_Tracker' ) ) { require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-session-tracker.php'; } // Load Session Schema Migrator (Database migration tools) require_once plugin_dir_path(__FILE__) . '/includes/class-session-schema-migrator.php'; // Load Recommendations Cache (Multi-tier cache for product recommendations) require_once plugin_dir_path(__FILE__) . '/includes/class-recommendations-cache.php'; // Central wp_set_script_translations() wiring for all JS handles that use wp.i18n. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-script-translations.php'; // Load Wishlist (favorites storage + AJAX endpoints; phase-1 of the // migration from om-product-page-essentials). // AIEO_Wishlist has a canonical copy in the runtime plugin // (ai-eshop-optimizer-runtime/includes/class-aieo-wishlist.php — moved // 2026-05-06). With autoloader+runtime present, the require_once below // is a no-op. Without them, the admin plugin loads its own fallback // copy so wishlist still works on a standalone install. if ( ! class_exists( 'AIEO_Wishlist' ) ) { require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-wishlist.php'; } // Phase-2 PPE compatibility shim — proxies om_ppe_wishlist_toggle → // aieo_wishlist_toggle so themes / widgets / external callers that // reference the legacy endpoint keep working after PPE's wishlist // module is disabled. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-wishlist-compat.php'; // Star Ratings (AIEO_Ratings) — canonical copy lives in the runtime // plugin (ai-eshop-optimizer-runtime/includes/class-aieo-ratings.php — // moved 2026-05-06). With autoloader+runtime present, the require_once // below is a no-op. Without them, the admin plugin loads its own // fallback copy so ratings still work on a standalone install. The // REST sibling class is admin-only and always loads from here. if ( ! class_exists( 'AIEO_Ratings' ) ) { require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-ratings.php'; } require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-ratings-rest.php'; // Per-brand flat projection (sibling of core_product_vitals). Read+refresh // model for wp_aieo_core_brand_vitals — sole front-end source for every // brand-shopping surface (A-Z catalogue, sliders, archive, navigator). require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-core-brand-vitals.php'; // Classic-PHP shortcode wrappers for the three "post-related-*" blocks // (brands / categories / needs). Re-registers the legacy Pods-era // `[related_brand_shortcode]` and `[secondary_brand_shortcode]` aliases // so themes that hard-code them in single.php keep working. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-post-relations-shortcodes.php'; // Per-customer engagement-signal query helpers (favourite brands, // wishlisted brands, rated brands) — bridges brand_affinity_json + // wp_aieo_wishlist + wp_aieo_ratings to blocks/navigator/push triggers. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-customer-intelligence-query.php'; // Attribution — source tracking for every add-to-cart event. // See ATTRIBUTION-SCHEMA-BRIEF.md for the schema + GA4 contract. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution.php'; // Tier-2 session × attribution bridge — query helpers JOINing the // Session Tracker tables with the Attribution Engine table for joint // engagement-attribution analytics. Backs 11 Claude abilities. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-session-attribution-bridge.php'; // Heuristic conversion-probability model — buckets historical sessions // on engagement (scroll-depth × time-on-page) and reports empirical // P(cart-add) per bucket. Backs the buy-probability-* abilities. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-conversion-probability-model.php'; // Perf — source-level PageSpeed mitigations: non-critical CSS deferral // + LCP image preload on PDP. Hookless until front-end render. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-perf.php'; // AIEO Star Ratings → SEO-plugin schema bridge. Feeds the materialized // `wp_aieo_rating_stats` aggregate into Product JSON-LD emitted by // AIOSEO / Yoast / Rank Math so Google's rich snippet uses the AIEO // star data (WC reviews are off on this site). Falls back to its own // JSON-LD block if no SEO plugin is detected. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-ratings-schema.php'; // Product Swatches runtime + AJAX — canonical copies live in the // runtime plugin (ai-eshop-optimizer-runtime/includes/dmm/modules/). // With the runtime present, these require_once calls are no-ops // (class_exists short-circuits). Without it — i.e. on sites that // run the main plugin standalone — the admin plugin loads its own // bundled copy so the frontend swatch render still works. if ( ! class_exists( 'AIEO_DMM_Swatches_Runtime' ) ) { require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-swatches-runtime.php'; } if ( ! class_exists( 'AIEO_DMM_Swatches_Ajax' ) ) { require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-swatches-ajax.php'; } // Load AI Chat addon (Claude-powered conversational search) require_once plugin_dir_path(__FILE__) . '/ai-eshop-chat-addon.php'; // Load Discount & Marketplace Management (DMM) bootstrap. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-dmm.php'; // Load Claude Abilities API bootstrap — registers 21 read-only abilities // (audience priorities, product needs, recos, attribution, ratings, // brand vitals, customer lookalikes) under the `aieo` category for the // woocommerce-claude MCP server / Claude Code skill harness to call. // Exposed at /wp-json/wp-abilities/v1/abilities/ when WP 6.9+. require_once plugin_dir_path(__FILE__) . '/includes/claude/class-aieo-claude-bootstrap.php'; add_action('init', array('AIEO\\Claude\\Bootstrap', 'init'), 5); // Merchant Insights Chat - wp-admin chat page. if ( is_admin() || ( defined('DOING_AJAX') && DOING_AJAX ) ) { require_once plugin_dir_path(__FILE__) . '/includes/chat/class-aieo-merchant-chat.php'; add_action('init', array('AIEO_Merchant_Chat', 'init'), 20); } // External-database connection registry (ERP / sister-store DBs). // Always loaded — its hooks are admin-conditional internally, but the // connection helper (AIEO_ERP::for_connection_id) needs to be reachable // from cron and wp-cli too. require_once plugin_dir_path(__FILE__) . '/includes/erp/class-aieo-erp.php'; // Retention + Remote-Export bootstrap (2026-05). // Schedules the nightly aggregate-rollup cron, the configurable remote // export cron, and binds AE/Reco archive handlers to the shared // `aieo_archive_run` hook fired by AIEO_Session_Tracker::archive_sessions(). // Installs the 5 rollup tables + remote-export state bookkeeping table. // MUST load AFTER class-aieo-erp.php — the remote-export manager calls // AIEO_ERP::for_connection_id() to talk to the destination DB. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-retention-bootstrap.php'; AIEO_Retention_Bootstrap::init(); // IAPI control panel (Phase C-α+ admin UI). if ( is_admin() ) { require_once plugin_dir_path(__FILE__) . '/includes/admin/class-aieo-iapi-admin.php'; } // Ctrl/Cmd+K command palette — overlay restricted to AIEO admin pages only. if ( is_admin() ) { require_once plugin_dir_path(__FILE__) . '/includes/admin/class-aieo-command-palette.php'; } // Initialize the plugin function AIEO_init() { new AIEO(); // ALWAYS initialize Session Tracker to register REST endpoints // The tracker will handle whether to enqueue scripts based on settings AIEO_Session_Tracker::get_instance(); // Initialize Recommendations Cache hooks (invalidation on product save/delete) // Only when WooCommerce is active (recommendations are WooCommerce-specific) if ( class_exists( 'WooCommerce' ) && class_exists( 'AIEO_Recommendations_Cache' ) ) { AIEO_Recommendations_Cache::init_hooks(); } // Discount & Marketplace Management bootstrap (menu, modules, hooks). if ( class_exists( 'AIEO_DMM' ) ) { AIEO_DMM::instance(); } // IAPI control panel — admin-only. if ( is_admin() && class_exists( 'AIEO_IAPI_Admin' ) ) { AIEO_IAPI_Admin::instance(); } // Wishlist — table install runs lazily on plugins_loaded via the // class's own maybe_install(); this just kicks the singleton so // the AJAX hooks + asset localisation register on every load. if ( class_exists( 'AIEO_Wishlist' ) ) { AIEO_Wishlist::instance(); } // PPE compatibility shim — routes legacy `om_ppe_wishlist_toggle` // AJAX calls to the AIEO endpoint + emits a shim `omPPEWishlist` // global. Safe to run alongside PPE (our handler wins on priority). if ( class_exists( 'AIEO_Wishlist_PPE_Compat' ) ) { AIEO_Wishlist_PPE_Compat::instance(); } // Star Ratings — table install runs lazily; this kicks the singleton // so the AJAX hooks + asset enqueue + admin menu register on every load. if ( class_exists( 'AIEO_Ratings' ) ) { AIEO_Ratings::instance(); } if ( class_exists( 'AIEO_Ratings_REST' ) ) { AIEO_Ratings_REST::instance(); } // Product Swatches runtime — frontend renderer that emits per-card // swatch markup from `wp_aieo_dmm_swatches_cache` without // instantiating WC_Product_Variation objects. Gated internally by // `aieo_dmm_swatches_runtime_enabled` so the kill-switch flip // happens via the Swatches admin tab. AJAX class always registers // its endpoints so the cart/quick-view round-trips work regardless // of the render flag. if ( class_exists( 'AIEO_DMM_Swatches_Runtime' ) ) { AIEO_DMM_Swatches_Runtime::instance(); } if ( class_exists( 'AIEO_DMM_Swatches_Ajax' ) ) { AIEO_DMM_Swatches_Ajax::instance(); } // Core Brand Vitals — registers auto-mark-dirty hooks on product/term // saves and the AJAX bundle endpoint for the brand-slider block. if ( class_exists( 'AIEO_Core_Brand_Vitals' ) ) { AIEO_Core_Brand_Vitals::instance(); } // Attribution — boots the singleton so AJAX endpoints + schema // install + order backfill hook all register. Cheap no-op when // the table is already in place. if ( class_exists( 'AIEO_Attribution' ) ) { AIEO_Attribution::instance(); } // Let the pricing engine preload per-request on archive/single views. if ( class_exists( 'AIEO_DMM_Pricing_Engine' ) ) { AIEO_DMM_Pricing_Engine::instance()->register_preload_hooks(); } } add_action('plugins_loaded', 'AIEO_init'); function aieo_is_index_wp_mysql_for_speed_active() { if (!function_exists('is_plugin_active')) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; } return is_plugin_active('index-wp-mysql-for-speed/index-wp-mysql-for-speed.php'); } function aieo_set_default_options() { global $wpdb; if (get_option('aieo_version') === false) { add_option('aieo_version', AIEO_VERSION, '', 'no'); } // Check if the option already exists. If not, add it. if (get_option('aieo_site_prefix') === false) { // Set the value for 'aieo_site_prefix' option $prefix_name = $wpdb->prefix; add_option('aieo_site_prefix', $prefix_name, '', 'no'); } // Always ensure correct table names (fix old installations with wrong suffix) $correct_orders_table = $wpdb->prefix . 'aieo_temp_orders'; // Use update_option instead of add_option to ensure immediate availability update_option('aieo_site_orders_table', $correct_orders_table, 'no'); $correct_store_orders_table = $wpdb->prefix . 'aieo_temp_store_orders'; update_option('aieo_store_orders_table', $correct_store_orders_table, 'no'); update_option('aieo_offline_orders', $correct_store_orders_table, 'no'); $correct_products_table = $wpdb->prefix . 'aieo_temp_product_catalogue'; update_option('aieo_site_products_table', $correct_products_table, 'no'); if (get_option('aieo_core_attribute_brands') === false) { // Set the value for 'aieo_core_attribute_brands' option $default_attribute_for_brand = 'product_brand'; add_option('aieo_core_attribute_brands', $default_attribute_for_brand, '', 'no'); } if (get_option('aieo_core_attribute_size') === false) { // Set the value for 'aieo_core_attribute_size' option $default_attribute_for_size = 'pa_size,pa_noumero,pa_megethos'; add_option('aieo_core_attribute_size', $default_attribute_for_size, '', 'no'); } if (get_option('aieo_core_attribute_color') === false) { // Set the value for 'aieo_core_attribute_color' option $default_attribute_for_color = 'pa_color,pa_colour'; add_option('aieo_core_attribute_color', $default_attribute_for_color, '', 'no'); } if (get_option('aieo_core_attribute_need') === false) { // Set the value for 'aieo_core_attribute_need' option $default_attribute_for_need = 'pa_need'; add_option('aieo_core_attribute_need', $default_attribute_for_need, '', 'no'); } if (get_option('aieo_season_days_past') === false) { // Set the value for 'aieo_season_days_past' option $default_season_days_past = '15'; add_option('aieo_season_days_past', $default_season_days_past, '', 'no'); } if (get_option('aieo_analytics_eponymous_id_field') === false) { // Set the value for 'aieo_analytics_eponymous_id_field' option $default_analytics_eponymous_id_field = 'EponymousId'; add_option('aieo_analytics_eponymous_id_field', $default_analytics_eponymous_id_field, '', 'no'); } if (get_option('aieo_season_days_after') === false) { // Set the value for 'aieo_season_days_after' option $default_season_days_after = '60'; add_option('aieo_season_days_after', $default_season_days_after, '', 'no'); } // Use update_option instead of add_option to ensure immediate availability $table_name = $wpdb->prefix . 'aieo_core_product_attributes'; update_option('aieo_core_product_attributes_table', $table_name, 'no'); $table_name = $wpdb->prefix . 'aieo_core_product_category_hierarchy'; update_option('aieo_core_product_category_hierarchy_table', $table_name, 'no'); $table_name = $wpdb->prefix . 'aieo_core_category_brand_popularity'; update_option('aieo_core_category_brand_popularity_table', $table_name, 'no'); $table_name = $wpdb->prefix . 'aieo_core_product_vitals'; update_option('aieo_core_product_vitals_table', $table_name, 'no'); $table_name = $wpdb->prefix . 'aieo_core_variation_vitals'; update_option('aieo_core_variation_vitals_table', $table_name, 'no'); $table_name = $wpdb->prefix . 'aieo_core_reciprocal_products'; update_option('aieo_core_reciprocal_products_table', $table_name, 'no'); $table_name = $wpdb->prefix . 'aieo_core_brand_vitals'; update_option('aieo_core_brand_vitals_table', $table_name, 'no'); if (get_option('aieo_erp_store_products_table') === false) { // Set the value for 'aieo_erp_store_products_table' option $table_name = $wpdb->prefix . 'aieo_erp_store_products'; add_option('aieo_erp_store_products_table', $table_name, '', 'no'); } if (get_option('aieo_erp_store_orders_table') === false) { // Set the value for 'aieo_erp_store_orders_table' option $table_name = $wpdb->prefix . 'aieo_erp_store_orders'; add_option('aieo_erp_store_orders_table', $table_name, '', 'no'); } if (get_option('aieo_erp_store_customers_table') === false) { // Set the value for 'aieo_erp_store_customers_table' option $table_name = $wpdb->prefix . 'aieo_erp_store_customers'; add_option('aieo_erp_store_customers_table', $table_name, '', 'no'); } if (get_option('aieo_google_user_journey_table') === false) { // Set the value for 'aieo_google_user_journey_table' option $table_name = $wpdb->prefix . 'aieo_google_user_journey'; add_option('aieo_google_user_journey_table', $table_name, '', 'no'); } if (get_option('aieo_order_id') === false) { add_option('aieo_order_id', '1', '', 'no'); } if (get_option('aieo_max_records') === false) { add_option('aieo_max_records', '1000', '', 'no'); } if (get_option('aieo_batch_size') === false) { add_option('aieo_batch_size', '100', '', 'no'); } if (get_option('aieo_batch_sleep') === false) { add_option('aieo_batch_sleep', '0', '', 'no'); } if (get_option('aieo_include_text') === false) { add_option('aieo_include_text', '1', '', 'no'); } if (get_option('aieo_include_attributes') === false) { add_option('aieo_include_attributes', '1', '', 'no'); } // Export checkbox options - default to '0' (unchecked) so user chooses what to run if (get_option('aieo_create_attributes') === false) { add_option('aieo_create_attributes', '0', '', 'no'); } if (get_option('aieo_create_needs') === false) { add_option('aieo_create_needs', '0', '', 'no'); } if (get_option('aieo_create_need_suggestions') === false) { add_option('aieo_create_need_suggestions', '0', '', 'no'); } if (get_option('aieo_create_catalogue') === false) { add_option('aieo_create_catalogue', '0', '', 'no'); } if (get_option('aieo_create_orders') === false) { add_option('aieo_create_orders', '0', '', 'no'); } if (get_option('aieo_erp_consolidation') === false) { add_option('aieo_erp_consolidation', '0', '', 'no'); } if (get_option('aieo_erp_merge_orders') === false) { add_option('aieo_erp_merge_orders', '0', '', 'no'); } if (get_option('aieo_include_unsold') === false) { add_option('aieo_include_unsold', '0', '', 'no'); } if (get_option('aieo_include_aggregate_stats') === false) { add_option('aieo_include_aggregate_stats', '0', '', 'no'); } if (get_option('aieo_include_aggregate_category_brand_stats') === false) { add_option('aieo_include_aggregate_category_brand_stats', '0', '', 'no'); } if (get_option('aieo_include_aggregate_cust_std_stats') === false) { add_option('aieo_include_aggregate_cust_std_stats', '0', '', 'no'); } if (get_option('aieo_include_aggregate_cust_adv_stats') === false) { add_option('aieo_include_aggregate_cust_adv_stats', '0', '', 'no'); } if (get_option('aieo_create_uuids') === false) { add_option('aieo_create_uuids', '0', '', 'no'); } if (get_option('aieo_create_export') === false) { add_option('aieo_create_export', '0', '', 'no'); } if (get_option('aieo_include_full_text') === false) { add_option('aieo_include_full_text', '0', '', 'no'); } if (get_option('aieo_include_price') === false) { add_option('aieo_include_price', '1', '', 'no'); } if (get_option('aieo_variations_choice') === false) { add_option('aieo_variations_choice', '1', '', 'no'); } if (get_option('aieo_include_eponymous') === false) { add_option('aieo_include_eponymous', '1', '', 'no'); } if (get_option('aieo_include_reciprocity') === false) { add_option('aieo_include_reciprocity', '0', '', 'no'); } if (get_option('aieo_include_seasonality') === false) { add_option('aieo_include_seasonality', '0', '', 'no'); } if (get_option('aieo_include_restricted_seasonality') === false) { add_option('aieo_include_restricted_seasonality', '0', '', 'no'); } if (get_option('aieo_instore_anonymous') === false) { add_option('aieo_instore_anonymous', '0', '', 'no'); } if (get_option('aieo_activate_utm_stats') === false) { add_option('aieo_activate_utm_stats', '0', '', 'no'); } if (get_option('aieo_google_ga4') === false) { add_option('aieo_google_ga4', '0', '', 'no'); } if (get_option('aieo_display_upsells') === false) { add_option('aieo_display_upsells', '0', '', 'no'); } if (get_option('aieo_display_cross_sells') === false) { add_option('aieo_display_cross_sells', '0', '', 'no'); } if (get_option('aieo_display_related') === false) { add_option('aieo_display_related', '0', '', 'no'); } if (get_option('aieo_display_recently_viewed_products') === false) { add_option('aieo_display_recently_viewed_products', '0', '', 'no'); } if (get_option('aieo_display_same_needs') === false) { add_option('aieo_display_same_needs', '0', '', 'no'); } if (get_option('aieo_display_categories_by_sales') === false) { add_option('aieo_display_categories_by_sales', '0', '', 'no'); } if (get_option('aieo_category_sales_period') === false) { add_option('aieo_category_sales_period', 'all_time', '', 'no'); } if (get_option('aieo_season_days_past') === false) { add_option('aieo_season_days_past', '15', '', 'no'); } if (get_option('aieo_season_days_after') === false) { add_option('aieo_season_days_after', '60', '', 'no'); } // Record initial activation date (only set once, never updated) if (get_option('aieo_initial_activation_date') === false) { add_option('aieo_initial_activation_date', current_time('mysql', true), '', 'no'); } } register_activation_hook(__FILE__, 'aieo_set_default_options'); // ----------------------------------------------------------------------------- // MU-plugin installer // ----------------------------------------------------------------------------- // AIEO ships two companion mu-plugins: // - 0-aieo-action-scheduler-core.php — AS query-limiting + retention + bypass // switch. The `0-` prefix forces it to load first (alphabetical order in // mu-plugins). Required by the new AIEO_DMM_As_Cleanup module. // - aieo-autoload.php — class autoloader for ai-eshop-optimizer + // ai-eshop-optimizer-runtime so neither plugin needs `require_once` chains // for AIEO_* classes. // Both ship inside this plugin at install/mu-plugins/. On activation we copy // them into wp-content/mu-plugins/ if either is missing OR if the bundled // version's mtime is newer than what's already there (auto-refresh on update). // Filesystem errors are reported via a transient admin notice so the operator // knows to copy them by hand on hosts where mu-plugins/ isn't writable. register_activation_hook(__FILE__, 'aieo_install_bundled_mu_plugins'); function aieo_install_bundled_mu_plugins() { $src_dir = plugin_dir_path(__FILE__) . 'install/mu-plugins/'; if (!is_dir($src_dir)) { return; } $dst_dir = defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : WP_CONTENT_DIR . '/mu-plugins'; if (!is_dir($dst_dir)) { // wp_mkdir_p() honours fs perms set by the host's WP config. if (!wp_mkdir_p($dst_dir)) { set_transient( 'aieo_mu_install_error', sprintf('Could not create %s. Copy install/mu-plugins/* there manually.', $dst_dir), DAY_IN_SECONDS ); return; } } // Files we ship, with optional `supersedes` clauses. Listed peer files // are legacy forks owned by the OM Optimizer Hub family. AIEO is the // canonical owner of these mu-plugins going forward — we install the // AIEO version and then DELETE the legacy peer so only one provider // of each function lives in mu-plugins/ at a time. AIEO's files // include om_* function aliases (wrapped in `function_exists()` guards // — see install/mu-plugins/0-aieo-action-scheduler-core.php tail) so // any caller of the old om_as_* names keeps working after the legacy // file is removed. $files = array( '0-aieo-action-scheduler-core.php' => array( 'supersedes' => array('0-om-action-scheduler-core.php'), ), 'aieo-autoload.php' => array(), 'aieo-as-ajax-query-protection.php' => array( 'supersedes' => array('om-as-ajax-query-protection.php'), ), ); $failed = array(); foreach ($files as $name => $opts) { $src = $src_dir . $name; $dst = $dst_dir . '/' . $name; if (!file_exists($src)) { continue; } // 1. Copy the bundled AIEO version into mu-plugins/ (idempotent — // skip if the target is missing OR bundled is newer). $needs_copy = !file_exists($dst) || @filemtime($dst) < @filemtime($src); if ($needs_copy) { if (!@copy($src, $dst)) { $failed[] = $name; continue; } } // 2. Retire legacy forks AFTER the replacement is in place. // Two-step (copy then unlink) — never leaves the site without // the function definitions, even for a single request. Bundled // AIEO file exposes om_as_* aliases so legacy callers keep // working transparently. if (!empty($opts['supersedes']) && is_array($opts['supersedes'])) { foreach ($opts['supersedes'] as $peer) { if ($peer === $name) { continue; } $peer_path = $dst_dir . '/' . $peer; if (file_exists($peer_path)) { @unlink($peer_path); } } } } if (!empty($failed)) { set_transient( 'aieo_mu_install_error', 'Could not write to ' . $dst_dir . '. Copy these by hand from install/mu-plugins/: ' . implode(', ', $failed), DAY_IN_SECONDS ); } } // Surface the install error (if any) as an admin notice on the next page load. add_action('admin_notices', function () { $msg = get_transient('aieo_mu_install_error'); if (!$msg || !current_user_can('manage_options')) { return; } printf( '

AIEO: %s

', esc_html($msg) ); }); // Inside your plugin's main file or in a file that gets loaded during plugin activation // Register a hook to execute your table creation function during plugin activation register_activation_hook(__FILE__, 'aieo_create_temp_tables_activation_hook'); // Define the activation hook function function aieo_create_temp_tables_activation_hook() { global $wpdb; // Enable error reporting for debugging $wpdb->show_errors(); aieo_debug_log('AIEO: Starting table creation activation hook'); // Load WordPress database manipulation functions require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); // CRITICAL FIX: Set options for backward compatibility with stored procedures // that still read these options aieo_set_default_options(); aieo_debug_log('AIEO: Options set'); // CRITICAL FIX: Call table creation functions WITHOUT parameters // Table names are now constructed internally like chat tables (using $wpdb->prefix directly) // This eliminates dependency on options and prevents race conditions aieo_debug_log('AIEO: Creating temp_orders table'); aieo_create_temp_orders_table(); aieo_debug_log('AIEO: Creating temp_store_orders table'); aieo_create_temp_store_orders_table(); aieo_debug_log('AIEO: Creating temp_products table'); aieo_create_temp_products_table(); aieo_debug_log('AIEO: Creating core_product_attributes table'); aieo_create_core_product_attributes_table(); aieo_debug_log('AIEO: Creating core_product_category_hierarchy table'); aieo_create_core_product_category_hierarchy_table(); aieo_debug_log('AIEO: Creating core_category_brand_popularity table'); aieo_create_core_category_brand_popularity_table(); aieo_debug_log('AIEO: Creating core_product_vitals table'); aieo_create_core_product_vitals_table(); aieo_debug_log('AIEO: Creating core_variation_vitals table'); aieo_create_core_variation_vitals_table(); aieo_debug_log('AIEO: Creating core_reciprocal_products table'); aieo_create_core_reciprocal_products_table(); aieo_debug_log('AIEO: Creating core_brand_vitals table'); aieo_create_core_brand_vitals_table(); // Create AI Chat tables aieo_debug_log('AIEO: Creating chat_embeddings table'); aieo_create_chat_embeddings_table(); aieo_debug_log('AIEO: Creating chat_history table'); aieo_create_chat_history_table(); // Create Session Tracker tables aieo_debug_log('AIEO: Creating sessions table'); aieo_create_sessions_table(); aieo_debug_log('AIEO: Creating session archive tables'); aieo_create_session_archive_tables(); // Create AI Models Registry table aieo_debug_log('AIEO: Creating ai_models table'); aieo_create_ai_models_table(); // Create Product Recommendations Cache table aieo_debug_log('AIEO: Creating product_recommendations table'); aieo_create_product_recommendations_table(); // Create Product Need Suggestions table aieo_debug_log('AIEO: Creating product_need_suggestions table'); aieo_create_product_need_suggestions_table(); // Create Discount & Marketplace Management (DMM) tables and migrate legacy data. if (function_exists('aieo_dmm_create_tables')) { aieo_debug_log('AIEO: Creating DMM tables'); aieo_dmm_create_tables(); } // Provision the /brand-shopping page (idempotent — only inserts if // missing). Loads the installer file directly so activation works // even before plugins_loaded fires. $brand_page_installer = plugin_dir_path(__FILE__) . 'includes/dmm/class-aieo-brand-page-installer.php'; if (file_exists($brand_page_installer)) { require_once $brand_page_installer; if (class_exists('AIEO_Brand_Page_Installer')) { $page_id = AIEO_Brand_Page_Installer::install(); aieo_debug_log('AIEO: Brand-shopping page install -> ' . (is_wp_error($page_id) ? $page_id->get_error_message() : (string) $page_id)); } } aieo_debug_log('AIEO: Table creation activation hook completed'); } function aieo_create_stored_procedures() { global $wpdb; // CRITICAL FIX: Ensure options are set BEFORE trying to read them // This fixes the race condition where stored procedures weren't created on first activation aieo_set_default_options(); $sp_prefix = get_option('aieo_site_prefix'); $sp_orders_table = get_option('aieo_site_orders_table'); $sp_store_orders_table = get_option('aieo_store_orders_table'); $sp_products_table = get_option('aieo_site_products_table'); $sp_core_product_attributes_table_name = get_option('aieo_core_product_attributes_table'); $sp_core_product_category_hierarchy_table_name = get_option('aieo_core_product_category_hierarchy_table'); $sp_core_product_vitals_table_name = get_option('aieo_core_product_vitals_table'); $sp_core_variation_vitals_table_name = get_option('aieo_core_variation_vitals_table'); $sp_erp_store_products_table_name = get_option('aieo_erp_store_products_table'); $sp_erp_store_orders_table_name = get_option('aieo_erp_store_orders_table'); $sp_erp_store_customers_table_name = get_option('aieo_erp_store_customers_table'); $sp_google_user_journey_table_name = get_option('aieo_google_user_journey_table'); $domain_name = get_site_url(); // CRITICAL FIX: Drop all procedures BEFORE creating them // This prevents "PROCEDURE already exists" errors during activation // Suppress errors during drop (procedures might not exist on first activation) $wpdb->hide_errors(); $wpdb->suppress_errors(true); @aieo_drop_sp_AIEO_InsertCoreProductAttributes($sp_prefix); @aieo_drop_sp_AIEO_InsertProductCategoryHierarchy($sp_prefix); @aieo_drop_sp_AIEO_InsertProductVitals($sp_prefix); @aieo_drop_sp_AIEO_InsertVariationVitals($sp_prefix); @aieo_drop_sp_AIEO_CombineVariationVitalsInProductVitals($sp_prefix); @aieo_drop_sp_AIEO_UpdateOrdersWithUnsold($sp_prefix); @aieo_drop_sp_AIEO_FindDifferences($sp_prefix); @aieo_drop_sp_AIEO_Create_Product_Catalogue_HPOS($sp_prefix); @aieo_drop_sp_AIEO_Export_Orders_HPOS($sp_prefix); @aieo_drop_sp_AIEO_Export_Store_Orders($sp_prefix); @aieo_drop_sp_AIEO_Merge_Store_Orders($sp_prefix); @aieo_drop_sp_AIEO_Export_Orders_non_HPOS($sp_prefix); @aieo_drop_sp_AIEO_RenameTable($sp_prefix); @aieo_drop_sp_AIEO_UpdateContentWordCount($sp_prefix); @aieo_drop_sp_AIEO_UpdateContentOutgoingLinks($sp_prefix); @aieo_drop_sp_AIEO_UpdateContentIntroPlain($sp_prefix); @aieo_drop_sp_AIEO_UpdateProductCentricStats($sp_prefix); @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats($sp_prefix); @aieo_drop_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix); @aieo_drop_sp_AIEO_GenerateGraphDBUUIDs($sp_prefix); @aieo_drop_sp_AIEO_GenerateGraphDBUUIDFreqs($sp_prefix); @aieo_drop_sp_AIEO_PopulateProductNeeds($sp_prefix); // Drop MySQL functions @aieo_drop_function_AIEO_StripHTML(); @aieo_drop_function_AIEO_CountWords(); @aieo_drop_function_AIEO_CountLinks(); // Drop legacy procedures that are no longer created but may exist from older versions @aieo_drop_sp_AIEO_Include_Products_Without_Sales(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL statement @$wpdb->query("DROP PROCEDURE IF EXISTS UpdateShortDescriptionForProduct"); // Re-enable error reporting for CREATE statements $wpdb->show_errors(); $wpdb->suppress_errors(false); // Create optimized v2 procedures aieo_create_sp_AIEO_InsertCoreProductAttributes($sp_prefix, $sp_core_product_attributes_table_name); aieo_create_sp_AIEO_InsertProductCategoryHierarchy($sp_prefix, $sp_core_product_category_hierarchy_table_name); aieo_create_sp_AIEO_InsertProductVitals($sp_prefix, $sp_core_product_vitals_table_name, $domain_name); aieo_create_sp_AIEO_InsertVariationVitals($sp_prefix, $sp_core_variation_vitals_table_name); aieo_create_sp_AIEO_CombineVariationVitalsInProductVitals($sp_prefix, $sp_core_product_vitals_table_name, $domain_name); aieo_create_sp_AIEO_UpdateOrdersWithUnsold($sp_prefix, $sp_orders_table, $sp_products_table); aieo_create_sp_AIEO_FindDifferences($sp_prefix); aieo_create_sp_AIEO_Create_Product_Catalogue_HPOS($sp_prefix, $sp_products_table); aieo_create_sp_AIEO_Export_Orders_HPOS($sp_prefix, $sp_orders_table); aieo_create_sp_AIEO_Export_Store_Orders($sp_prefix, $sp_store_orders_table, $sp_erp_store_products_table_name, $sp_erp_store_orders_table_name, $sp_erp_store_customers_table_name); aieo_create_sp_AIEO_Merge_Store_Orders($sp_prefix); aieo_create_sp_AIEO_Export_Orders_non_HPOS($sp_prefix, $sp_orders_table); // UNUSED - Never called in current codebase aieo_create_sp_AIEO_RenameTable($sp_prefix); aieo_create_sp_AIEO_UpdateContentWordCount($sp_prefix); aieo_create_sp_AIEO_UpdateContentOutgoingLinks($sp_prefix); aieo_create_sp_AIEO_UpdateContentIntroPlain($sp_prefix); aieo_create_sp_UpdateProductCentricStats($sp_prefix); aieo_create_sp_AIEO_UpdateCustomerCentricStdStats_Smart($sp_prefix, $sp_orders_table); aieo_create_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix, $sp_orders_table); aieo_create_sp_AIEO_GenerateGraphDBUUIDs($sp_prefix); aieo_create_sp_AIEO_GenerateGraphDBUUIDFreqs($sp_prefix); aieo_create_sp_AIEO_PopulateProductNeeds($sp_prefix); // UNUSED - Never called in current codebase // aieo_create_sp_AIEO_Orchestrate_all_non_HPOS_SPs($sp_prefix); aieo_create_function_AIEO_StripHTML(); aieo_create_function_AIEO_CountWords(); aieo_create_function_AIEO_CountLinks(); } function aieo_drop_stored_procedures() { global $wpdb; aieo_debug_log('AIEO: Starting deactivation - aieo_drop_stored_procedures()'); // Set aggressive timeouts set_time_limit(120); @ini_set('max_execution_time', 120); @ini_set('mysql.connect_timeout', 10); // Suppress errors during cleanup $wpdb->hide_errors(); $wpdb->suppress_errors(true); $sp_prefix = get_option('aieo_site_prefix'); // CRITICAL FIX: Construct table names directly using $wpdb->prefix (same as creation functions) // This ensures tables are properly dropped even if options are empty/outdated $sp_temp_orders_table_name = $wpdb->prefix . 'aieo_temp_orders'; $sp_temp_store_orders_table_name = $wpdb->prefix . 'aieo_temp_store_orders'; $sp_temp_products_table_name = $wpdb->prefix . 'aieo_temp_product_catalogue'; $sp_core_product_attributes_table_name = $wpdb->prefix . 'aieo_core_product_attributes'; $sp_core_product_category_hierarchy_table_name = $wpdb->prefix . 'aieo_core_product_category_hierarchy'; $sp_core_category_brand_popularity_table_name = $wpdb->prefix . 'aieo_core_category_brand_popularity'; $sp_core_product_vitals_table_name = $wpdb->prefix . 'aieo_core_product_vitals'; $sp_core_variation_vitals_table_name = $wpdb->prefix . 'aieo_core_variation_vitals'; $sp_core_reciprocal_products_table_name = $wpdb->prefix . 'aieo_core_reciprocal_products'; $sp_core_brand_vitals_table_name = $wpdb->prefix . 'aieo_core_brand_vitals'; $sp_ai_models_table_name = $wpdb->prefix . 'aieo_ai_models'; $sp_performance_tuning_table_name = $wpdb->prefix . 'aieo_performance_tuning'; aieo_debug_log('AIEO: Table names constructed. Prefix: ' . $wpdb->prefix); aieo_debug_log('AIEO: Will drop: ' . $sp_temp_orders_table_name); @aieo_drop_sp_AIEO_InsertCoreProductAttributes($sp_prefix); @aieo_drop_sp_AIEO_InsertProductCategoryHierarchy($sp_prefix); @aieo_drop_sp_AIEO_InsertProductVitals($sp_prefix); @aieo_drop_sp_AIEO_InsertVariationVitals($sp_prefix); @aieo_drop_sp_AIEO_CombineVariationVitalsInProductVitals($sp_prefix); @aieo_drop_sp_AIEO_UpdateOrdersWithUnsold($sp_prefix); @aieo_drop_sp_AIEO_FindDifferences($sp_prefix); @aieo_drop_sp_AIEO_Create_Product_Catalogue_HPOS($sp_prefix); @aieo_drop_sp_AIEO_Export_Orders_HPOS($sp_prefix); @aieo_drop_sp_AIEO_Export_Store_Orders($sp_prefix); @aieo_drop_sp_AIEO_Export_Orders_non_HPOS($sp_prefix); @aieo_drop_sp_AIEO_Merge_Store_Orders($sp_prefix); // UNUSED - Never called in current codebase // @aieo_drop_sp_AIEO_StripHTMLAndReplicateContentIntro($sp_prefix); @aieo_drop_sp_AIEO_RenameTable($sp_prefix); @aieo_drop_sp_AIEO_UpdateContentWordCount($sp_prefix); @aieo_drop_sp_AIEO_UpdateContentOutgoingLinks($sp_prefix); @aieo_drop_sp_AIEO_UpdateContentIntroPlain($sp_prefix); @aieo_drop_sp_AIEO_UpdateProductCentricStats($sp_prefix); // V1 statistics procedures - DEPRECATED - No longer created // @aieo_drop_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix); @aieo_drop_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix); @aieo_drop_sp_AIEO_GenerateGraphDBUUIDs($sp_prefix); @aieo_drop_sp_AIEO_GenerateGraphDBUUIDFreqs($sp_prefix); @aieo_drop_sp_AIEO_PopulateProductNeeds($sp_prefix); // @aieo_drop_sp_AIEO_Orchestrate_all_non_HPOS_SPs($sp_prefix); // @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats($sp_prefix); @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats_v2($sp_prefix); @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats($sp_prefix); // Drop legacy procedures that are no longer created but may exist from older versions @aieo_drop_sp_AIEO_Include_Products_Without_Sales(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL statement @$wpdb->query("DROP PROCEDURE IF EXISTS UpdateShortDescriptionForProduct"); // Drop functions SECOND (they don't depend on tables) @aieo_drop_function_AIEO_StripHTML(); @aieo_drop_function_AIEO_CountWords(); @aieo_drop_function_AIEO_CountLinks(); // Drop tables LAST (after nothing references them) // These are "working" tables that are rebuilt on activation - safe to drop // SIMPLIFIED: Just drop tables directly - DROP TABLE automatically removes indexes // This avoids the slow index-by-index dropping that was causing hangs aieo_debug_log('AIEO: Dropping working tables (direct DROP TABLE)...'); $tables_to_drop = array( $sp_core_product_attributes_table_name, $sp_core_product_category_hierarchy_table_name, $sp_core_category_brand_popularity_table_name, $sp_core_product_vitals_table_name, $sp_core_variation_vitals_table_name, $sp_core_reciprocal_products_table_name, $sp_core_brand_vitals_table_name, $sp_temp_orders_table_name, $sp_temp_store_orders_table_name, $sp_temp_products_table_name, $sp_ai_models_table_name, $sp_performance_tuning_table_name, ); foreach ($tables_to_drop as $table) { aieo_debug_log('AIEO: Dropping table ' . $table); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name $wpdb->query("DROP TABLE IF EXISTS `{$table}`"); aieo_debug_log('AIEO: Dropped ' . $table); } // Data retention based on user settings (configured in Chat Settings > Process Automation) // Default behavior: keep embeddings and chat history, delete session archives // Chat Embeddings table if (get_option('aieo_keep_embeddings_on_deactivation', '1') !== '1') { $embeddings_table = $wpdb->prefix . 'aieo_chat_embeddings'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name $wpdb->query("DROP TABLE IF EXISTS {$embeddings_table}"); aieo_debug_log('AIEO: Dropped chat embeddings table (user preference)'); } // Chat History table if (get_option('aieo_keep_chat_history_on_deactivation', '1') !== '1') { $chat_history_table = $wpdb->prefix . 'aieo_chat_history'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name $wpdb->query("DROP TABLE IF EXISTS {$chat_history_table}"); aieo_debug_log('AIEO: Dropped chat history table (user preference)'); } // Session Archive tables (default: DROP - not kept unless explicitly requested) if (get_option('aieo_keep_session_archive_on_deactivation', '0') !== '1') { $archive_table = $wpdb->prefix . 'aieo_sessions_archive'; $archive_statics_table = $wpdb->prefix . 'aieo_sessions_statics_archive'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name $wpdb->query("DROP TABLE IF EXISTS {$archive_table}"); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name $wpdb->query("DROP TABLE IF EXISTS {$archive_statics_table}"); aieo_debug_log('AIEO: Dropped session archive tables (user preference)'); } // Session Tracker tables (transient/live data) // Default: DROP - these are working tables that get rebuilt on activation // User can choose to keep them if they want to preserve session data across deactivations if (get_option('aieo_keep_sessions_on_deactivation', '0') !== '1') { $sessions_table = $wpdb->prefix . 'aieo_sessions'; $sessions_statics_table = $wpdb->prefix . 'aieo_sessions_statics'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name $wpdb->query("DROP TABLE IF EXISTS {$sessions_table}"); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name $wpdb->query("DROP TABLE IF EXISTS {$sessions_statics_table}"); aieo_debug_log('AIEO: Dropped session tracker tables (user preference)'); } aieo_debug_log('AIEO: Deactivation complete - tables handled per user data retention settings'); // Re-enable errors $wpdb->show_errors(); $wpdb->suppress_errors(false); } register_deactivation_hook(__FILE__, 'aieo_drop_stored_procedures'); register_activation_hook(__FILE__, 'aieo_create_stored_procedures'); function aieo_check_for_plugin_update() { $stored_version = get_option('aieo_version', false); if ($stored_version === false) { update_option('aieo_version', AIEO_VERSION); return; } if (version_compare(AIEO_VERSION, $stored_version, '>')) { // Refresh the bundled companion mu-plugins (autoloader, AS core, // AJAX query protection). On upgrade-in-place via the WP admin // uploader, register_activation_hook does NOT re-fire, so the // copy step we register on activation is skipped. Re-running it // here on version-bump covers that path. Function is idempotent // — it mtime-compares before copying and skips conflicting peers. if (function_exists('aieo_install_bundled_mu_plugins')) { aieo_install_bundled_mu_plugins(); } // Drop and Create Stored Procedures aieo_drop_stored_procedures(); // Ensure tables are recreated during upgrades (drop routine removes them) aieo_create_temp_tables_activation_hook(); aieo_create_stored_procedures(); // Wishlist table — ensure it exists on every version bump // (covers 4.x → 5.0 upgrades-in-place where the activation // hook never fires). if (class_exists('AIEO_Wishlist')) { AIEO_Wishlist::install(); update_option(AIEO_Wishlist::SCHEMA_OPTION, AIEO_Wishlist::SCHEMA_VERSION, false); } // Ensure initial activation date is set for existing installations upgrading to 3.3.0+ if (get_option('aieo_initial_activation_date') === false) { add_option('aieo_initial_activation_date', current_time('mysql', true), '', 'no'); } // Then update the version value in the database update_option('aieo_version', AIEO_VERSION); } } add_action('plugins_loaded', 'aieo_check_for_plugin_update'); /** * Purge every AIEO-owned cache when WordPress core is upgraded. * * Bug context (NEXT_RELEASE_PRIORITY_BUGS.md issue #3, 2026-05-21): * after WP 7.0 auto-upgrade on femme-fatale.gr (prod1), every product * URL that carried a `?attribute_pa_=` query string * fatalled at `wp-includes/theme.php:325` (the * `apply_filters('template', get_option('template'))` line inside * `get_template()`) with the integer-underflow signature `tried to * allocate 4295229440 bytes (≈ 0xFFFFFFFE)`. That number is the * classic unsigned-underflow value PHP `unserialize()` returns when * it reads a corrupted length field — and the canonical way to * corrupt a length field is to unserialize an object whose class * has CHANGED SHAPE (different property set) or been REMOVED * between the time the blob was stored and the time it's read. * * The WP 7.0 upgrade replaces enough of `wp-includes/` that any * cached blob containing serialised WP/WC core class instances * (WP_Term, WP_Post, WC_Product, etc.) becomes a landmine. WC has * its own variation-prices cache (`wc_var_prices_*` transients); * AIEO has 81+ get_transient/wp_cache_get call sites across the * plugin, several of which page-cache results that may contain * core-class instances directly or transitively. * * Strategy: on WP-core upgrade fire-and-forget delete every AIEO * transient + every WC variation-prices transient + clear the * `aieo_dmm` wp_cache group + truncate AIEO_Recommendations_Cache. * Costs one DELETE on `wp_options` (with an indexed LIKE on * option_name) per upgrade — negligible compared to the OOM risk. * * Hooked on `upgrader_process_complete` with `$hook_extra['type']` * filter so the purge only fires for core upgrades; plugin and * theme upgrades use AIEO's own version-bump path (the * `aieo_check_for_plugin_update` above) instead. * * @param WP_Upgrader $upgrader_object * @param array $hook_extra */ function aieo_purge_caches_on_core_upgrade($upgrader_object, $hook_extra) { if (!is_array($hook_extra)) { return; } $type = isset($hook_extra['type']) ? (string) $hook_extra['type'] : ''; if ($type !== 'core') { return; } aieo_purge_all_persistent_caches('upgrader_process_complete:core'); } add_action('upgrader_process_complete', 'aieo_purge_caches_on_core_upgrade', 10, 2); /** * One-shot retroactive purge. Catches sites where WP core has ALREADY * been auto-upgraded BEFORE this fix landed in the plugin — i.e. the * upgrader_process_complete hook above can't help because it fires at * upgrade time, not at plugin-update time. * * Idempotent — guarded by `aieo_post_core_upgrade_purge_v1`. The * plugin's own version-bump path triggers this on the first * page load after 5.1 → next version lands (and on any subsequent * version bump, since the option is per-version). */ function aieo_retroactive_post_core_upgrade_purge() { $flag_key = 'aieo_post_core_upgrade_purge_v1'; if (get_option($flag_key) === 'done') { return; } aieo_purge_all_persistent_caches('retroactive_one_shot'); update_option($flag_key, 'done', false); } add_action('plugins_loaded', 'aieo_retroactive_post_core_upgrade_purge', 11); /** * Implementation shared by the upgrader hook + the retroactive * one-shot. Deletes by option_name prefix on `wp_options` (no * intermediate get_option calls — direct DB query keeps the purge * O(rows) rather than O(rows × deserialise-cost)). * * Scope: * - `_transient_aieo_*` + `_transient_timeout_aieo_*` * - `_transient_wc_var_prices_*` + matching timeouts (WC's own * variation-prices cache; touched by AIEO_DMM_Main_Price_Filter + * AIEO_DMM_Role_Pricing via their `woocommerce_get_variation_prices_hash` * filters, so corrupted blobs there transitively land in AIEO's * code path) * - object cache group `aieo_dmm` (if `wp_cache_flush_group` is available) * - `wp_aieo_recommendations_cache` table TRUNCATE (if present) * * @param string $reason Free-text breadcrumb for the error log. */ function aieo_purge_all_persistent_caches($reason = '') { global $wpdb; // 1. Transients — direct DELETE keyed on option_name. Both the // blob row and its matching `_timeout_` row. if (isset($wpdb) && is_object($wpdb)) { $wpdb->query( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- literal prefix; no user input; bypasses cache by design. "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_aieo\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\_timeout\\_aieo\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\__aieo\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\_timeout\\__aieo\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\_wc\\_var\\_prices\\_%' ESCAPE '\\\\' OR option_name LIKE '\\_transient\\_timeout\\_wc\\_var\\_prices\\_%' ESCAPE '\\\\'" ); } // 2. Object cache group used by AIEO_DMM cached lookups // (product-needs / ratings / brand-vitals etc.). if (function_exists('wp_cache_flush_group')) { wp_cache_flush_group('aieo_dmm'); wp_cache_flush_group('aieo_core_brand_vitals'); wp_cache_flush_group('aieo_ratings'); } elseif (function_exists('wp_cache_flush')) { // Older WP without group-scoped flush. Defensive full flush; // happens at most once per WP core upgrade, so acceptable // tradeoff against a hard OOM on the next request. wp_cache_flush(); } // 3. Recommendations cache table — full truncate when present. // The table stores serialised reco DTOs that could include // WC_Product references; same unserialise-landmine risk. if (isset($wpdb) && is_object($wpdb)) { $table = $wpdb->prefix . 'aieo_recommendations_cache'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- literal table name; SHOW TABLES guard above prevents fatal on installs that don't ship the table. if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- literal table name verified by SHOW TABLES. $wpdb->query("TRUNCATE TABLE {$table}"); } } // 4. Bump any internal version counters the chat / brand-vitals // layers use to invalidate their in-process memoisation on // the NEXT request. if (class_exists('AIEO_Core_Brand_Vitals') && method_exists('AIEO_Core_Brand_Vitals', 'bump_version')) { try { AIEO_Core_Brand_Vitals::instance()->bump_version(); } catch (Throwable $e) {} } if (defined('WP_DEBUG') && WP_DEBUG && function_exists('aieo_debug_log')) { aieo_debug_log('AIEO cache purge fired: ' . $reason); } } /** * One-shot migration: copy legacy `om_ppe_*` options into their AIEO * counterparts so the runtime stops reading both. Without this, a * legacy record with `show_sku:false` / `show_barcode:false` shadows * the AIEO defaults forever, and the module never renders even after * the user toggles it on from the Product Display dashboard (which * writes only `{enabled:true}`). * * Merge rule: AIEO wins where it has a value, legacy fills gaps. The * `enabled` key is explicitly carried over from AIEO so the toggle * the user just flipped isn't reverted to the legacy disabled state. * After migration, the legacy option is deleted and a done-flag is * set so this never runs again. * * Idempotent — guarded by `aieo_om_ppe_legacy_migration_v1`. */ function aieo_migrate_om_ppe_legacy_options() { if (get_option('aieo_om_ppe_legacy_migration_v1') === 'done') { return; } $pairs = array( 'aieo_dmm_product_meta_settings' => 'om_ppe_product-meta', 'aieo_dmm_payment_logos_settings' => 'om_ppe_payment-logos', 'aieo_dmm_trust_badges_settings' => 'om_ppe_trust-badges', 'aieo_dmm_product_labels_settings'=> 'om_ppe_product-labels', 'aieo_dmm_product_video_settings' => 'om_ppe_product-video', 'aieo_dmm_badges_settings' => 'om_ppe_badges', 'aieo_dmm_add_to_cart_text_settings' => 'om_ppe_add-to-cart-text', ); foreach ($pairs as $new_key => $legacy_key) { $legacy = get_option($legacy_key, false); if (!is_array($legacy)) { continue; } $new = get_option($new_key, array()); if (!is_array($new)) { $new = array(); } // Two cases: // (a) AIEO key has never been written → seed it from legacy // so users who had OM PPE configured don't lose their // labels/colours/custom_class. // (b) AIEO key has been written (even just `{enabled:true}` // from the dashboard toggle) → AIEO is now the source // of truth; legacy is stale. Carrying legacy's boolean // OPT-IN flags (show_sku/show_barcode) forward would // silently shadow the AIEO defaults and the module // would never render even though the user just enabled // it. So in case (b) we simply delete the legacy and // leave AIEO alone — defaults from get_settings() take // over for missing keys. if (empty($new)) { update_option($new_key, $legacy, false); } delete_option($legacy_key); } update_option('aieo_om_ppe_legacy_migration_v1', 'done', false); } add_action('plugins_loaded', 'aieo_migrate_om_ppe_legacy_options', 12); // Load text domain on init for normal page loads (WordPress 4.6+ auto-loads, but this is a fallback) // Note: For AJAX requests, text domain is loaded at the top of this file before any classes load add_action( 'init', function() { // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Fallback for non-WordPress.org hosted translations load_plugin_textdomain( 'ai-eshop-optimizer', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); }, 0 ); function aieo_initialize_aieo_recommendations() { new aieo_display_recommendations(); } add_action('plugins_loaded', 'aieo_initialize_aieo_recommendations'); /** * AJAX handler to dismiss rotating banner */ function aieo_dismiss_banner_ajax_handler() { // Verify nonce check_ajax_referer('aieo_dismiss_banner', 'nonce'); // Get current user ID $user_id = get_current_user_id(); if (!$user_id) { wp_send_json_error('User not logged in'); return; } // Save dismissal preference update_user_meta($user_id, 'aieo_banner_dismissed', '1'); wp_send_json_success('Banner dismissed'); } add_action('wp_ajax_aieo_dismiss_banner', 'aieo_dismiss_banner_ajax_handler'); // Plugins-page row: direct "Settings" link to the AIEO admin page. add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { $url = admin_url('admin.php?page=ai-eshop-optimizer'); $settings_link = '' . esc_html__('Settings', 'ai-eshop-optimizer') . ''; array_unshift($links, $settings_link); return $links; });