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://eshop-optimizer.com/plugin Description: Free, customizable AI chat & recommendations with Claude/GPT/Voyage for WooCommerce & content sites. Embeddings & analytics. Version: 5.0 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.0'); 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