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(
'
',
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;
});