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: Rooster One Plugin URI: https://rooster.systems/one/ Description: The unified AI commerce & content suite โ one plugin does the work of a whole stack. Anthropic/Voyage AI: messaging, recommendations, attribution, loyalty, merchandising. Version: 11.1.13 Requires at least: 6.7 Tested up to: 7.1 Requires PHP: 7.4 WC tested up to: 11.1 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)); } } } /** * Prefer the plugin's BUNDLED translations over an incomplete wp.org language * pack. WordPress loads .mo/.l10n.php from wp-content/languages/plugins/ (the * translate.wordpress.org GlotPress pack) BEFORE the plugin's own languages/ * dir and returns early โ so a stub pack (e.g. the 10-string Greek pack) would * shadow our full bundled file and leave most of the admin UI in English. * Redirect any global-dir load for our domain to the bundled file whenever we * ship one for that locale; other locales keep using their wp.org packs. */ if (!function_exists('aieo_prefer_bundled_textdomain')) { function aieo_prefer_bundled_textdomain($mofile, $domain) { if ('ai-eshop-optimizer' !== $domain) { return $mofile; } // Only override when WP is about to load from the global languages dir. if (0 === strpos($mofile, WP_LANG_DIR)) { $bundled = plugin_dir_path(__FILE__) . 'languages/' . basename($mofile); if (is_readable($bundled)) { return $bundled; } } return $mofile; } } add_filter('load_textdomain_mofile', 'aieo_prefer_bundled_textdomain', 10, 2); /** * ๐จ AND THE SAME REDIRECT FOR THE PHP-FORMAT CATALOGUE, which is the one * WordPress actually prefers now. * * `load_textdomain_mofile` above only ever sees the .mo path. Since 6.5 core * resolves translations through load_textdomain() โ an ordered list of * candidate files with the .l10n.php one FIRST, each passed through a * different filter, `load_translation_file` (wp-includes/l10n.php). Filtering * only the .mo hook therefore leaves the modern path unguarded: an incomplete * wp.org pack at wp-content/languages/plugins/ai-eshop-optimizer-el.l10n.php * loads first, succeeds, and core returns before the .mo is ever considered. * The bundled full catalogue is never read and the whole plugin falls back to * English on a fully translated locale. * * Symptom that led here (sophiasamara.com, WordPress 7.1, Greek): the magic-link * modal rendered "Sign in with email" while the button beside it read * ฮฃฮฅฮฮฮฮฃฮ ฮฮ EMAIL. Both are ours and both are on the same page, but the button * was a HARDCODED Greek literal at the time and the modal was a __() call. One * page proving that PHP translation was not resolving at all. * * โ ๏ธ That also makes this a prerequisite: the hardcoded Greek has since been * replaced with a proper __(), so on a site where the catalogue does not load, * that button would REGRESS to English without this filter. * * Same rule as above: only redirect a load from the global languages dir, and * only when we actually ship a file of that name, so other locales keep their * wp.org packs. */ add_filter('load_translation_file', 'aieo_prefer_bundled_textdomain', 10, 2); /** * Stop WordPress from (re)installing the incomplete translate.wordpress.org * language pack for this plugin. We are listed on wp.org, so WP's translations * updater keeps fetching the community GlotPress pack โ which currently has * only ~6 Greek strings โ into wp-content/languages/plugins/ on every * translations cron and plugin update. That stub then shows up as a second, * "stale" Greek file in Loco; although aieo_prefer_bundled_textdomain() above * already redirects loading back to our COMPLETE bundled catalogue, the stub * keeps reappearing, confuses translators and clutters the languages dir. * Dropping our slug from the update transient means the stub is never fetched. */ if (!function_exists('aieo_block_wporg_translation_pack')) { function aieo_block_wporg_translation_pack($transient) { if (isset($transient->translations) && is_array($transient->translations)) { foreach ($transient->translations as $i => $t) { if (isset($t['slug']) && 'ai-eshop-optimizer' === $t['slug']) { unset($transient->translations[$i]); } } $transient->translations = array_values($transient->translations); } return $transient; } } add_filter('pre_set_site_transient_update_plugins', 'aieo_block_wporg_translation_pack'); add_filter('site_transient_update_plugins', 'aieo_block_wporg_translation_pack'); // 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' ); } /** * RE-LOAD THE TEXT DOMAIN ONCE WORDPRESS KNOWS WHO IS READING. * * THE BUG THIS FIXES. Several classes call __() from a settings-defaults array * built in their constructor, and those constructors run on `plugins_loaded`, * before WordPress has resolved the logged-in user. The first such call makes * WordPress load this plugin's translations just-in-time using * `determine_locale()` as it stands at that moment, which is the SITE's * language, and cache it for the rest of the request. Everything afterwards โ * the admin menu, the tab strips, the action labels โ then comes back in the * site's language whatever the reader's own account is set to. On a Greek shop * an English-locale developer got a fully Greek admin and it looked like a * translation problem rather than a load-order one. * * WHY HERE AND NOT AT EVERY CALL SITE. There are eight such constructor paths, * four of them in runtime checkout code, and a ninth can be added tomorrow by * someone who has never heard of this. Fixing them one at a time treats the * symptom and leaves the trap armed. This corrects the outcome for all of them * and for anything added later: by `init` the current user IS resolved, so if * the language WordPress would choose now differs from the one already loaded, * the loaded one was chosen too early and is simply wrong. Drop it and load the * right one. * * โ ๏ธ FIRST THING ON `init`, and that is not a detail. By `init` the user IS * resolved, so `determine_locale()` is finally right; the only wrong language * is the one loaded before it. Anything that reads a translation earlier in * `init` than this gets the stale one and can latch it โ which is exactly what * happened at priority 1: a settings memo was populated during an `init:0` * callback and held Greek for the rest of the request even after the domain was * corrected a moment later. Run before every other `init` callback so there is * no window. * * Costs nothing in the normal case, which is a single string comparison and no * reload, because on a single-language site the two agree. */ // Remember which language was actually loaded. `get_locale()` cannot answer // this: it reports the SITE's language, which is precisely the wrong one in the // case being corrected. The .mo filename carries the truth. $GLOBALS['aieo_loaded_locale'] = ''; add_filter( 'load_textdomain_mofile', function ( $mofile, $domain ) { if ( 'ai-eshop-optimizer' !== $domain ) { return $mofile; } // Strip the DOMAIN PREFIX, do not pattern-match the locale. A locale can // itself contain a hyphen (`ca-valencia`) and the domain contains two, so // "everything after a hyphen" captured `eshop-optimizer-en_GB` and never // equalled `en_GB`. The comparison then always failed, which meant a // reload of the .mo on every single request including single-language // sites, replacing a correctness bug with a slow one. $base = basename( (string) $mofile ); $base = preg_replace( '/\.(mo|l10n\.php)$/', '', $base ); if ( 0 === strpos( $base, 'ai-eshop-optimizer-' ) ) { $GLOBALS['aieo_loaded_locale'] = substr( $base, strlen( 'ai-eshop-optimizer-' ) ); } return $mofile; }, 99, 2 ); /** * Load the BUNDLED catalogue for $locale by explicit path. * * ๐จ NEVER load_plugin_textdomain() FOR THIS. Measured on roosterp 2026-08-01: * with Loco Translate active it is filtered to Loco's own directory, which does * not exist for this plugin. The call still returns true while * is_textdomain_loaded() comes back false, and every __() falls through to the * English msgid. An explicit path cannot be redirected. WordPress still prefers * the sibling .l10n.php automatically, so this keeps the fast loader. * * @param string $locale * @return bool True when a catalogue was loaded. */ function aieo_load_textdomain_by_path( $locale ) { $locale = (string) $locale; if ( '' === $locale || ! function_exists( 'load_textdomain' ) ) { return false; } $dir = plugin_dir_path( __FILE__ ) . 'languages/'; $tries = array( $locale ); if ( false !== strpos( $locale, '_' ) ) { // el_GR โ el. The bundled catalogues are language-only. $tries[] = substr( $locale, 0, strpos( $locale, '_' ) ); } foreach ( $tries as $try ) { $mofile = $dir . 'ai-eshop-optimizer-' . $try . '.mo'; if ( is_readable( $mofile ) && load_textdomain( 'ai-eshop-optimizer', $mofile, $try ) ) { return true; } } return false; } /** * ๐จ A LOCALE SWITCH AFTER `init` LEFT THE CATALOGUE BEHIND โ THE EMAIL BUG. * * The `init` hook below re-syncs the catalogue ONCE, when WordPress finally * knows who is reading. That is right for a page render and useless for an * email, because WooCommerce sends customer mail inside * wc_switch_to_site_locale() โ switch_to_locale(), which happens LONG after * `init`. Nothing here listened for that, so the catalogue stayed on whatever * language `init` settled on. * * REPORTED SYMPTOM: a Greek order email whose withdrawal BUTTON read ยซPress * here to withdrawยป in English, on a shop whose admin had already been switched * to Greek. The page was Greek because `init` had corrected it; the email was * English because the switch that produced the email came later and no one was * listening. The string was translated the whole time. * * WordPress does re-load registered textdomains on a switch, but it resolves * the path through the textdomain registry โ the same redirection documented in * aieo_load_textdomain_by_path(). So core's reload is exactly the one that * cannot be relied on here, and we load by path ourselves. * * This matters most for the ONE string the statute is specific about. Law * 5317/2026 asks for ยซฮ ฮฑฯฮฎฯฯฮต ฮตฮดฯ ฮณฮนฮฑ ฯ ฯฮฑฮฝฮฑฯฯฯฮทฯฮทยป; an English button on a * Greek email is not "equally clear wording", it is a compliance defect. */ add_action( 'change_locale', function ( $locale ) { if ( ! function_exists( 'unload_textdomain' ) ) { return; } $locale = (string) $locale; if ( '' === $locale || $locale === (string) $GLOBALS['aieo_loaded_locale'] ) { return; } if ( is_textdomain_loaded( 'ai-eshop-optimizer' ) ) { unload_textdomain( 'ai-eshop-optimizer' ); } // Unlike the `init` pass we do NOT bail when nothing is loaded: a switch is // an explicit request for a specific language, and just-in-time loading // would resolve the path through the registry we cannot trust. if ( ! aieo_load_textdomain_by_path( $locale ) ) { // Nothing bundled for this locale (en_US, or a language we do not // ship). Unloaded is CORRECT โ __() returns the English msgid, which is // the source text. Clear the record so the next comparison is honest. $GLOBALS['aieo_loaded_locale'] = ''; } }, -PHP_INT_MAX ); add_action( 'init', function () { if ( ! function_exists( 'determine_locale' ) || ! function_exists( 'unload_textdomain' ) ) { return; } // Nothing loaded yet is the healthy path: the first __() after this point // loads it, and by now the locale is settled. if ( ! is_textdomain_loaded( 'ai-eshop-optimizer' ) ) { return; } $wanted = determine_locale(); $loaded = (string) $GLOBALS['aieo_loaded_locale']; // An unrecorded load, or one that already matches, needs nothing. On a // single-language site these always agree and this costs one comparison. if ( '' === $loaded || $wanted === $loaded ) { return; } unload_textdomain( 'ai-eshop-optimizer' ); // ๐จ RELOAD BY PATH. Do NOT use load_plugin_textdomain() here โ see // aieo_load_textdomain_by_path(), which holds the measurement and the // reason. Loading by path is why this hook stopped "fixing" a wrong-language // admin by emptying the catalogue: it looked correct from the direction it // was written for (an English reader stopped seeing Greek) and was silently // wrong in the mirror image, a GREEK reader also getting English on a plugin // whose Greek catalogue is 12,800 strings deep. if ( aieo_load_textdomain_by_path( $wanted ) ) { return; } // Nothing bundled for this locale โ en_US, or a language we do not ship. // Fall back to the normal loader, which is correct for a wp.org-installed // pack living in WP_LANG_DIR. // 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' ); }, -PHP_INT_MAX ); /** * VERSIONING FOLLOWS WOOCOMMERCE, from 11.0 onward (operator ruling, 2026-07-31). * * While WooCommerce is on 11.0 we ship 11.0, then 11.0.1, 11.0.2 โฆ; when they * move to 11.1 we go to 11.1.0 and start again. The point is that an operator * can read our version and know which WooCommerce it was built against, which * matters for a plugin that lives this close to Woo's order, product and * checkout internals. * * Do NOT bump this to chase a Woo release we have not actually tested against. * The number is a claim about compatibility, not a marketing counter. * * Two things key on it, so a bump is never cosmetic: aieo_check_for_plugin_update() * refreshes the stored procedures and prunes removed files, and the capability * registry + welcome search index transients are fingerprinted on it (so a bump * invalidates them for free, and a mid-version edit does not). */ if (!defined('AIEO_VERSION')) { define('AIEO_VERSION', '11.1.13'); } // Schema/data-migration counter, independent of the plugin version. Bumped when // aieo_run_db_migrations() gains a new step. v1=#33 sessions index trim, // v2=#34 reco reverse-ref table + backfill, v3=#35 analytics atomic-swap procs. if (!defined('AIEO_DB_VERSION')) { define('AIEO_DB_VERSION', 3); } // Guarded: the runtime plugin loads FIRST (WP sorts active_plugins and // "-runtime" sorts before "/"), and it pre-seeds AIEO_FILE/AIEO_DIR/AIEO_URI // (pointing at THIS plugin's path, same values) so its bundled classes work // even when the core fatals. Unguarded re-defines here warned on every // request under PHP 8.5 and become fatal on PHP 9. if (!defined('AIEO_FILE')) { define('AIEO_FILE', __FILE__); } if (!defined('AIEO_BASE')) { define('AIEO_BASE', trailingslashit(plugin_basename(AIEO_FILE))); } if (!defined('AIEO_DIR')) { define('AIEO_DIR', plugin_dir_path(__FILE__)); } if (!defined('AIEO_URI')) { 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 reach the plugin's admin surfaces. * * OUR OWN CAPABILITY, NOT A BORROWED ONE (2026-07-31). This was 'edit_pages', * chosen because WooCommerce's shop_manager role happens to include it โ which * is true of a stock install, and is why shop managers have always had access. * But it is a coincidence we were relying on, not a rule: * * - Sites customise roles. femme-fatale.gr and beautynet.gr both run a role * manager, and a shop_manager stripped of `edit_pages` would silently lose * the whole plugin โ every menu, every tab, every AJAX endpoint โ with no * error to explain it. * - "Can edit a page" is not the question we mean to ask. The question is * "does this person run the shop", and the answer for a WooCommerce install * is `manage_woocommerce`. * * So the constant now names a capability we own, and the filter below grants it * to whoever holds EITHER of the two real capabilities. Every one of the ~84 * `current_user_can(AIEO_CAPABILITY)` checks and every `add_menu_page()` gate * across the three plugins picks this up with no call-site change, because they * all read the constant. * * WIDENING, NEVER NARROWING: nobody who could reach the plugin yesterday loses * access, and shop managers are guaranteed it rather than getting it by accident. * Granting a capability of our own also means we hand out nothing else โ adding * `edit_pages` to shop_manager to solve this would have let them edit the site's * Pages, which is not ours to give. * * The one deliberate exception the operator named stays where it is: plugin-usage * / activation control is gated separately and is NOT widened here. */ define('AIEO_CAPABILITY', 'aieo_access'); /** * Grant AIEO_CAPABILITY to shop managers and editors alike. * * Reads the already-resolved $allcaps map rather than calling current_user_can(), * which would recurse through this same filter on every capability test in the * request. Two array lookups, no queries. */ add_filter('user_has_cap', function ($allcaps) { if (empty($allcaps[AIEO_CAPABILITY]) && (!empty($allcaps['manage_woocommerce']) || !empty($allcaps['edit_pages'])) ) { $allcaps[AIEO_CAPABILITY] = true; } return $allcaps; }, 10, 1); // Declare WooCommerce feature compatibility if WooCommerce is active. // `variation_gallery` + `wc-visual-attribute` are WC 10.9+ experimental features; // AIEO writes its data into WC's canonical keys so they Just Work when toggled on. 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); \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('variation_gallery', __FILE__, true); \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('wc-visual-attribute', __FILE__, true); } }); // Load the plugin class require_once plugin_dir_path(__FILE__) . '/includes/class-eshop-optimizer.php'; // Is the front-end runtime installed, and does it match core? ONE detector, // three consumers (the Install status line, the attribution funnel's // runtime_active, and the version-mismatch warning). Loaded here, before the // signup and notifier classes that report it, because all three of those put // the same answer on a payload or a screen. Static methods only, no hooks of // its own beyond a cache flush โ costs nothing on a request that never asks. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-runtime-presence.php'; // Which SITE the cached entitlement was issued for, so a copied database stops // opening every Pro gate on a second domain. Dual copy with the Pro plugin. // Required explicitly rather than left to the autoloader: a host with an // unwritable mu-plugins/ has no autoloader, and a licence check that disappears // on exactly those hosts is not a licence check. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-entitlement-signature.php'; require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-entitlement-binding.php'; // Runtime-before-core update ordering: one line on core's update row when the // runtime is behind, and a nudge that puts the runtime's own update on the // screen first. WARNS, never blocks โ the class docblock says why. Admin-only // hooks, so a storefront request pays one is_admin() check. if ( is_admin() ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-update-order.php'; AIEO_Update_Order::register(); } // Home โ Getting set up (plan ยง5): what kind of shop this is, as a stated // prior, and the staged self-detecting checklist. Static, hook-free classes; // the DMM module that renders them registers the admin-post handlers. Loaded // here rather than inside the module because the pin strip and the welcome // handlers read the seeded-pin state on pages that do not boot the module. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-shop-shape.php'; // Who is looking at the screen: the SECOND input to relevance(), read by the // pill bar on every admin page, so it loads beside the shape and not inside a // module (docs/infographics/PLAN-2026-09-03-PERSONAS-DETAILED.md). require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-personas.php'; require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-setup-checklist.php'; // The set-up wizard as one component, rendered on Getting set up and, through // two AJAX endpoints registered at load, inside the welcome modal. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-wizard.php'; // Home โ Overlaps: the replaced-plugins journey and the one `kept` gate that // SEO โ General and the checklist consult before nudging (plan ยง5.4). require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-overlap.php'; // The Install status line (plan ยง5.7): Pro, the Pro plugin and the front-end // runtime as three rows, rendered in exactly two places. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-install-status.php'; // The question a locked screen answers, with this shop's own numbers in it // (plan ยง5.8, Tier 2): the floor for every Pro paywall, local and free. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-pro-question.php'; // Which reports rendered zero rows when opened (plan ยง5.12): the instrument // behind "the report is empty", kept locally, read by Getting set up. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-dark-screens.php'; // The last real figure each report showed while entitled, dated: the lapsed // state of a locked screen (plan ยง5.8), written by report pages, read by the // paywall. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-remembered-figures.php'; // Shape + module switches + overlap decisions as one block of JSON (plan // ยง5.11.4): the three re-detectable things an agency sets the same way twice. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-shop-preset.php'; if (is_admin()) { // The one bounded notice the checklist may raise: the EU withdrawal // button, on a trading EU shop where it is off, dismissable forever. AIEO_Setup_Checklist::init(); } // Low-balance AI-credit email alerts (daily cron poll of the Rooster Portal + // operator-chosen billing email/threshold). Self-contained. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-credit-notifier.php'; AIEO_Credit_Notifier::init(); // Self-serve Rooster account signup (email โ double opt-in โ auto-connect โ // 14-day Pro trial). Optional by construction: registers admin handlers and a // dismissible notice only; no request leaves the site until the operator // explicitly submits the form. Core plugin only. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-rooster-signup.php'; // Shared transport to rooster.systems for operator-initiated submissions // (feedback today, forms next). Owns enrolment, request signing and the retry // queue so the callers own only their payload shape. Registers hooks and one // REST route; nothing reaches the network until an operator presses Send. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-portal-client.php'; AIEO_Portal_Client::init(); // Send-feedback modal behind the top-bar icon. Admin-only, opt-in by // construction: opening it costs no request, submitting IS the consent event. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-feedback.php'; AIEO_Feedback::init(); // The wordpress.org review ask. Earned (7 days AND real use), dismissible per // user, Dashboard and Plugins only, and never shown beside the signup notice. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-review-prompt.php'; AIEO_Review_Prompt::init(); // Bundled surveys. Definitions ship in the release (includes/dmm/data/forms.php) // so their strings are translatable and a question can never reach an install // that lacks the feature it asks about. Audience rules are evaluated locally. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-forms.php'; AIEO_Forms::init(); // Operator-authored forms: the `aieo_form` CPT holds field definitions, two // owned tables hold entries, and AIEO_Form_Entries::record() is the only way in. // NO shortcode and NO public endpoint โ caller code renders and records, so // nothing here is reachable by an anonymous visitor. The substrate for a proper // form builder later; the storage shape is what has to be right now, because // entries are the expensive thing to migrate. require_once plugin_dir_path(__FILE__) . '/includes/forms/class-aieo-form-cpt.php'; require_once plugin_dir_path(__FILE__) . '/includes/forms/class-aieo-form-entries.php'; require_once plugin_dir_path(__FILE__) . '/includes/forms/class-aieo-form-entries-ui.php'; AIEO_Form_CPT::register(); AIEO_Form_Entries_UI::register(); // Canonical AI-credit attribution source slugs (X-OM-Source). Loaded early so // every AI caller can tag itself. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-ai-sources.php'; // SQL layer split (Phase-1 perf, 2026-06-13). sprocs.php (~6.3k lines of // CREATE PROCEDURE installers, which itself pulls in dmm-schema.php ~3.5k // lines + brand-vitals-schema.php) used to load HERE on every request, // including anonymous storefront page views that never touch it. Now: // โข sql-runtime-helpers.php โ the handful of helpers runtime code calls // (charset/collate, aieo_dbdelta, table probes, role columns) โ always; // โข the full DDL layer โ only where installers/exports actually run: // admin, AJAX, cron, CLI, plus activation/deactivation/upgrade via // aieo_load_sql_layer() (called inside those entry points). require_once plugin_dir_path(__FILE__) . '/includes/sql/sql-runtime-helpers.php'; // Armed at load, not on an init hook: a stored-procedure CALL can be issued from // anything (cron, CLI, an mu-plugin, another plugin), so the guard has to be in // place before the first query we could possibly be blamed for. Idempotent, and // one client-side flag check per query. if (function_exists('aieo_install_sproc_drain_guard')) { aieo_install_sproc_drain_guard(); } /** * Load the full SQL/DDL layer (sprocs + dmm-schema + brand-vitals schema) * on demand. Idempotent. MUST run before any aieo_create_* / aieo_drop_sp_* * call โ the activation / deactivation / upgrade entry points all invoke it * as their first statement, so every call path is covered regardless of * request context. */ function aieo_load_sql_layer() { require_once plugin_dir_path(__FILE__) . '/includes/sql/sprocs.php'; } if (is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) { aieo_load_sql_layer(); } // 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'; // What this install runs on: WooCommerce present, multisite, a host asset // optimiser like Breeze. Loaded BEFORE AIEO_Asset_Min because that class asks // it whether to stand its URL rewrite down. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-host-environment.php'; if (is_admin()) { // Notices only. Nothing on the storefront path. AIEO_Host_Environment::init(); } // Stored-procedure install state: verification, the resume cron, and the // notice that says a routine could not be created. Loaded unconditionally // because the resume runs on cron, where is_admin() is false. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-procedure-installer.php'; AIEO_Procedure_Installer::init(); // First install of the two PAID downloads, driven by WordPress's own // Plugin_Upgrader against a Portal-issued package URL. Admin only: there is // deliberately no cron and no admin_init path into it, nothing auto-installs, // and it renders nothing at all until a licence key exists. if (is_admin()) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-one-installer.php'; AIEO_One_Installer::init(); add_action('admin_notices', array('AIEO_One_Installer', 'render_result')); } // Serve built *.min.css / *.min.js via a global URL-rewrite filter (no // per-enqueue edits). No-ops when SCRIPT_DEBUG is on or a .min file is absent. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-asset-min.php'; AIEO_Asset_Min::register(); // 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'; require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-admin-color.php'; // Per-device (desktop/mobile) value controls for EVERY AIEO block โ adds the // editor "Responsive (per device)" panel + render-time scoped CSS via global // filters keyed on aieo/* (covers all 19 blocks + any future one automatically). require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-responsive-controls.php'; AIEO_Responsive_Controls::register(); // "Duplicate" for the plugin's own post types. Registered centrally rather than // from one module so every list screen can draw the link, and so the per-type // exclusion map (what must NOT be copied) has a single home. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-post-duplicator.php'; AIEO_Post_Duplicator::register(); // wpDataTables report-page enhancements (glass tooltips + origin tagging), // ported from the standalone om-wpdt-enhancements plugin. register() only // adds hooks โ the front-end enqueue is page-scoped (aieo_wpdt_enhance_pages // option, managed on Tools โ Product/Orders analytics) and the origin-tag // fires solely on the wdt_save_table_frontend AJAX action. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-wpdt-enhancements.php'; AIEO_WPDT_Enhancements::register(); // 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'; // WooCommerce native reviews as a first-class rating source. Most shops run // WC reviews as their default (often only) rating signal, so this mirrors // them into wp_aieo_ratings permanently โ backfill + full comment lifecycle. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-ratings-wc-bridge.php'; // A variation with no gallery of its own shows the product's, rather than // emptying the gallery on screen. Dual copy with the runtime; guarded so // whichever plugin boots first registers it once. if ( ! class_exists( 'AIEO_Variation_Gallery_Fallback', false ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-variation-gallery-fallback.php'; AIEO_Variation_Gallery_Fallback::register(); } // Storefront half of the product gallery layout, the Rooster One lightbox // and auto-open-on-arrival. Dual copy with the runtime; guarded so whichever // plugin boots first registers it once. The DMM modules only hold its admin. if ( ! class_exists( 'AIEO_Gallery_Lightbox_Frontend', false ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-gallery-lightbox-frontend.php'; } AIEO_Gallery_Lightbox_Frontend::register(); // One build of get_available_variations() per product per request. WooCommerce // calls it once per page; a theme that resolves per-term swatch data by // scanning the variation list calls it once per SWATCH, which is O(N^2): // measured at 42.4s of a 46.9s Rey page render. Nothing between two calls in // one request can change the answer, so the second one is free. Guarded rather // than dual-copied: the memo has no renderer and belongs wherever core is. if ( ! class_exists( 'AIEO_Variation_Payload', false ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-variation-payload.php'; AIEO_Variation_Payload::register(); } // Themes that ship their own variation swatches hook the same WooCommerce seam // we do, at a higher priority, so ours is built and then discarded. The adapter // registry knows where each host sits and how to stand it down. Inert until the // operator picks a design source, so an unconfigured site renders as it does // today. Core only; the dual-copied renderer reaches it via class_exists. if ( ! class_exists( 'AIEO_Theme_Adapters', false ) ) { require_once plugin_dir_path(__FILE__) . '/includes/dmm/swatches/class-aieo-swatch-type-detector.php'; require_once plugin_dir_path(__FILE__) . '/includes/theme-adapters/class-aieo-theme-adapters.php'; AIEO_Theme_Adapters::register(); } // `[aieo_product_swatches]`, so the picker can be placed in Elementor, Bricks or // any builder that has no block loop to read a product from. Same renderer and // same cache as the block; only the product resolution differs. if ( ! class_exists( 'AIEO_Swatch_Shortcode', false ) ) { require_once plugin_dir_path(__FILE__) . '/includes/dmm/swatches/class-aieo-swatch-shortcode.php'; AIEO_Swatch_Shortcode::register(); } // 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'; // FAQ content type (`faq`) + grouping taxonomy (`faq_group`) + the [faq] / // [aieo_faq] accordion shortcode (self-contained, own scoped CSS). The entry // list is surfaced from the DMM "Custom Post Types" nav group, not a stray // top-level menu. Self-instantiates at file end. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-faq.php'; // The store CPTs โ theme-owned `store_location` + plugin-owned `aieo_store` โ // are surfaced from the DMM "Custom Post Types" nav group, so drop their stray // top-level sidebar menus. (The `faq` CPT already registers show_in_menu=false.) add_filter('register_post_type_args', function ($args, $post_type) { if (in_array($post_type, array('store_location', 'aieo_store'), true)) { $args['show_in_menu'] = false; } return $args; }, 20, 2); // `aieo/menu` block โ renders a WP menu as an inline strip (header) or // accordion (footer); replaces the roosterx-ff [ff_simple_menu] shortcode + // the hand-coded footer markup with a proper menu-picker block. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-menu-block.php'; // `aieo/header-actions` block โ extensible header icon row (menu/navigator burger // now; account/stores later). Sits inside .ff-header__actions; the menu icon opens // the Navigator via the theme's data-aieo-navigator-trigger handler. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-header-actions-block.php'; // `aieo/trust-badges` block โ configurable trust strip (icon + title + // subtitle columns); replaces the hand-coded ff-trust-strip / roosterx trust // tiles. Bundled in the runtime too (class_exists-guarded). require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-trust-badges-block.php'; // `aieo/social-icons` block โ social links (FB/IG/Pinterest/YouTube/TikTok) with // colour, shape (round/square) + responsive gap. Bundled in the runtime too. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-social-icons-block.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'; // Attribution CLIENT โ the enqueue+localize half, dual-shipped so the runtime // can serve it on a runtime-only install (where this file never loads and cart // adds were previously recorded nowhere at all). // // ๐จ THE class_exists GUARD IS NOT OPTIONAL. This is a shared, byte-identical // file: an unguarded require_once would load CORE's copy on every paired // install, because require_once bypasses the autoloader entirely. __FILE__ // inside the class would then resolve to core's folder and core's asset would // be served even when the runtime is present โ the exact inverse of "when the // runtime is present, the runtime rules". Guarded, the runtime's copy wins // whenever the autoloader has already provided it. if (!class_exists('AIEO_Attribution_Client') && file_exists(plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution-client.php')) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution-client.php'; } if (class_exists('AIEO_Attribution_Client')) { AIEO_Attribution_Client::register(); } // A/B arm assignment for recommendation experiments. Loaded next to // attribution because it is read on the same paths: the arm is decided while a // page renders and stamped onto the reco rows that page produces. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-reco-experiment.php'; require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-reco-experiment-report.php'; // One display vocabulary for every promotional surface โ popups, custom slots, // recommendation rails and promo regions โ plus the A/B seam that lets an arm // override any of its attributes. Both are DUAL-BUNDLED (class_exists-guarded, // byte-identical in the runtime plugin), because the OM whitelist can drop this // plugin from a front-end request and a display gate is nothing but hooks. // // Loaded here rather than lazily so admin screens can reach the vocabulary; the // storefront path self-heals through AIEO_Popup_Display::ensure_conditions(). if ( ! class_exists( 'AIEO_Display_Conditions' ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-display-conditions.php'; } if ( ! class_exists( 'AIEO_Display_Experiment' ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-display-experiment.php'; } // THE EXTERNAL INSTIGATOR: what sent this visitor โ a web push, a newsletter, a // collaborator's link. Also dual-bundled, because the event logger that stamps // it onto a row runs on the runtime-only whitelist. It stores the raw tuple and // NEVER a resolved identity, so the contributor rules that name a person stay // retroactive exactly as they are on the order plane. if ( ! class_exists( 'AIEO_Display_Origin' ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-display-origin.php'; } // The shared CONTROL for those conditions. Core-only and NOT dual-bundled: it is // all admin markup, and the runtime plugin exists to serve a visitor's page // without carrying the admin half. Same split as AIEO_Popup_CPT beside // AIEO_Popup_Display. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-display-conditions-ui.php'; // The WRITE side and the INSPECTION side of the display plane: one create / // update entry point that dispatches to whichever of the four stores owns a // surface, and an enumeration of what a site actually has. Core-only for the // same reason โ nothing here runs on a storefront request. // // It exists for automation. The conditions are one vocabulary now, but the // CONTAINERS are still a CPT, an option row, an ordering entry and two DB // tables, so a campaign generator would otherwise have to know all four. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-display-registry.php'; // What a split test actually did. Core-only for the same reason, and separate // from AIEO_Reco_Experiment_Report because every assumption that one makes โ // one experiment, two fixed arm names, product-card outcomes โ is wrong for a // display surface. It SHARES that class's statistics rather than copying them. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-display-experiment-report.php'; // Canonical read-through for per-order facts (revenue + channel). D1 single- // source-of-truth accessor โ see documentation/ORDER_DATA_MAP.md. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-order-facts.php'; // ROI Wizard cost ledger (operator/connector ad-spend per dimension per period). // See documentation/ROI_WIZARD_PLAN.md. require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution-cost.php'; if ( class_exists( 'AIEO_Attribution_Cost' ) ) { AIEO_Attribution_Cost::init(); } // Shared attribution-block renderer (styled table + channel vocabulary) used // by BOTH the order-email origin block and the Gravity notification block. // Bundled identically in the runtime plugin (checkout-whitelist delegate) โ // class_exists-guarded so whichever copy loads first wins. if ( ! class_exists( 'AIEO_Attribution_Block' ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution-block.php'; } // Order origin + attribution block in the staff (admin) order emails โ replaces // the unreliable PixelYourSite origin line (marketplace vs Site + channel/campaign). // Also bundled in the runtime plugin: optimised sites trim THIS plugin off // checkout requests, where synchronous gateways (cheque/COD/bacs) send the // staff new-order email โ the runtime delegate renders the block there. if ( ! class_exists( 'AIEO_Order_Origin_Email' ) ) { require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-order-origin-email.php'; } if ( class_exists( 'AIEO_Order_Origin_Email' ) ) { AIEO_Order_Origin_Email::register(); } // Google Ads generation (ยง20) โ compose a campaign from what the shop already // knows, create it PAUSED, and let a human decide whether it ever spends. The // classes load unconditionally because the composer is also reachable from the // Claude abilities, not only from the admin tab. require_once plugin_dir_path( __FILE__ ) . '/includes/ads/class-aieo-ads-plan.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/ads/class-aieo-ads-composer.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/ads/class-aieo-ads-google-driver.php'; // The shared presentation vocabulary and the control layer. // // `AIEO_Variants` is the one variant dictionary โ the names a badge, a button // or a row action may carry, and what the retired ones normalise to. It loads // unconditionally and early because the report definitions validate against it // at LOAD time, and a definition can be read by a CLI script that never boots // the admin bundle. // // `AIEO_Controls` is the markup contract those variants paint: an admin panel // is a huge form, a survey is a form, and they are the same controls. It is // admin-only in use today; the storefront questionnaire renderer (Wave 4 of // docs/PLAN-2026-08-30-CUSTOMER-SURVEYS.md) is what makes it a runtime-owned // surface, and at that point it needs its copy in the runtime plugin. require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-variants.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-controls.php'; // Gravity Forms choice-field distributions โ turns every checkbox/select answer // into an audience breakdown that ad copy and targeting can be grounded in. require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-form-insights.php'; // Questionnaires as a capability rather than a hand-wired form. // // The blueprint declares a question's SEMANTIC on four axes and never its // widget; the two adapters normalise Gravity and native answers into the SAME // answer store keyed by question code; the register remembers what an answer // means across every form the shop will ever have. Loaded unconditionally // because the event engine, the audience resolver and the reports all ask about // questionnaires, not only the admin screens. require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-blueprint.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-overlay.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-choice-register.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-value-stage.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-mapping.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-source.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-source-gravity.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-source-native.php'; // The storefront half: where a questionnaire appears, who may answer it, and // what asking one costs a workflow. // // ๐จ THIS IS WHAT RETIRES THE THEME FILE. `roosterx-ff/inc/post-purchase-survey.php` // renders the reference shop's survey from the THEME, hard-wired to one hook at // one priority. A survey is a capability of the shop, not of its theme, and a // shop that changes theme should not lose three years of feedback collection. // Both render on `woocommerce_before_thankyou`, so the theme file MUST be // deleted in the same deploy as this or every customer gets two surveys on one // page โ see ยงC of docs/DEPLOY-CUSTOMER-SURVEYS-TO-APOLLO.md. require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-placement.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-renderer.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-ask.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/surveys/class-aieo-survey-health.php'; AIEO_Survey_Overlay::register(); AIEO_Survey_Placement::register(); AIEO_Survey_Renderer::register(); // A new submission changes the numbers, so drop the cached report rather than // letting the merchant read a distribution that is up to 15 minutes stale. add_action( 'gform_after_submission', function ( $entry, $form ) { if ( class_exists( 'AIEO_Form_Insights' ) && is_array( $form ) && ! empty( $form['id'] ) ) { AIEO_Form_Insights::flush( (int) $form['id'] ); } unset( $entry ); }, 20, 2 ); // ROI Wizard subtab โ self-registers onto the DMM module bus when it loads // (same pattern as the marketplace-order-importer). See documentation/ROI_WIZARD_PLAN.md. add_action( 'aieo_dmm_register_dynamic_modules', function ( $dmm ) { if ( ! class_exists( 'AIEO_DMM_Module_Base' ) ) { return; } require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-roi-wizard.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-ad-connectors.php'; require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-utm-explorer.php'; // Guarded: the Ads tab is optional, and a partial deploy that is missing it // must degrade to "no Ads tab" rather than fataling the whole admin. $aieo_ads_module = plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-ads.php'; if ( file_exists( $aieo_ads_module ) ) { require_once $aieo_ads_module; } if ( is_object( $dmm ) && method_exists( $dmm, 'register_module' ) ) { if ( class_exists( 'AIEO_DMM_ROI_Wizard' ) ) { $dmm->register_module( new AIEO_DMM_ROI_Wizard() ); } if ( class_exists( 'AIEO_DMM_Ad_Connectors' ) ) { $dmm->register_module( new AIEO_DMM_Ad_Connectors() ); } if ( class_exists( 'AIEO_DMM_Ads' ) ) { $dmm->register_module( new AIEO_DMM_Ads() ); } // UTM Explorer is consolidated onto the ROI Wizard page (the ROI Wizard // composes + renders it), so it is required above but NOT a separate tab. } }, 10, 1 ); // 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'; // SEO engine (Stage A). The offline head BUILDER (AIEO_SEO_Head_Builder) // materialises wp_aieo_seo_head_cache on save/cron; the thin, bundled // AIEO_SEO_Head emitter serves that cache with one read. All AIEO_SEO_* // classes autoload from includes/seo/ via the mu-plugin autoloader. The // builder + store (AIEO_SEO_Store) are core-only; the emitter is bundled in // both plugins. The builder self-gates on AIEO_SEO_Store::is_active_provider() // so registering its hooks is harmless while AIOSEO is still active. if ( class_exists( 'AIEO_SEO_Head_Builder' ) ) { AIEO_SEO_Head_Builder::register_build_hooks(); } // SEO editor metabox (admin only) โ the canonical-category picker + SEO title / // meta / canonical-override / noindex, all written to wp_aieo_seo_posts // (nothing in postmeta). Allowed regardless of provider state so operators can // pin canonical categories before cut-over. if ( is_admin() && class_exists( 'AIEO_SEO_Post_Metabox' ) ) { AIEO_SEO_Post_Metabox::register(); } // Slug monitor (admin only, A2) โ auto-create a 301 when a published object's // slug/URL changes so existing links don't 404. Writes via AIEO_SEO_Redirects. if ( is_admin() && class_exists( 'AIEO_SEO_Slug_Monitor' ) ) { AIEO_SEO_Slug_Monitor::register(); } // SEO schema self-heal โ the same maybe_install() delta pattern every other // feature uses (Carts/Ratings/Attribution/Consent). Creates/upgrades the SEO // tables when the bundled schema version is newer than what's installed, so an // existing site picks up new tables (e.g. the A2 redirects + 404 log) on the // next admin load after a file upload โ no reactivation / version bump needed. if ( is_admin() ) { add_action( 'admin_init', static function () { $seo_schema = AIEO_DIR . 'includes/dmm/sql/seo-schema.php'; if ( file_exists( $seo_schema ) ) { require_once $seo_schema; if ( function_exists( 'aieo_seo_maybe_install' ) ) { aieo_seo_maybe_install(); } } }, 5 ); // Identity substrate (Customer Intelligence Core, Phase A) โ same self-heal // net: wp_aieo_subjects + wp_aieo_session_identity install on the next admin // load after a file upload, no reactivation / version bump needed. add_action( 'admin_init', static function () { $subjects_schema = AIEO_DIR . 'includes/dmm/sql/subjects-schema.php'; if ( file_exists( $subjects_schema ) ) { require_once $subjects_schema; if ( function_exists( 'aieo_subjects_maybe_install' ) ) { aieo_subjects_maybe_install(); } } }, 5 ); // Social (site-owned apps, master plan R3โฒ) local schema self-heal. add_action( 'admin_init', static function () { $social_schema = AIEO_DIR . 'includes/dmm/sql/social-schema.php'; if ( file_exists( $social_schema ) ) { require_once $social_schema; if ( function_exists( 'aieo_social_install_schema' ) && get_option( 'aieo_social_schema_version' ) !== ( defined( 'AIEO_SOCIAL_SCHEMA_VERSION' ) ? AIEO_SOCIAL_SCHEMA_VERSION : '1' ) ) { aieo_social_install_schema(); update_option( 'aieo_social_schema_version', defined( 'AIEO_SOCIAL_SCHEMA_VERSION' ) ? AIEO_SOCIAL_SCHEMA_VERSION : '1', false ); } } }, 5 ); // Stripped-text read model (wp_aieo_content_text) self-heal โ without // this net the table materializes only when the data-prep cron stage // first runs. class_exists loads the class where the mu autoloader is // installed; elsewhere the load-on-touch net inside the scheduler stage // still covers it. add_action( 'admin_init', static function () { if ( class_exists( 'AIEO_Content_Text' ) ) { AIEO_Content_Text::maybe_install(); } }, 5 ); } // โโ Social (site-owned) runtime bootstrap โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ // Loaded on EVERY request (not just admin): the OAuth callback + publish paths // are REST/cron, not admin. Each require is file_exists-guarded so a partial // deploy can never fatal. The shop owns its platform apps + tokens locally // (R3โฒ); nothing here is portal-brokered. add_action( 'plugins_loaded', static function () { foreach ( array( 'includes/social/class-aieo-social-store.php', 'includes/social/connectors/class-aieo-social-connector-x.php', 'includes/social/connectors/class-aieo-social-connector-linkedin.php', 'includes/social/connectors/class-aieo-social-connector-tiktok.php', 'includes/social/class-aieo-social-oauth.php', 'includes/social/drivers/class-aieo-social-driver-base.php', 'includes/social/drivers/class-aieo-social-driver-facebook.php', 'includes/social/drivers/class-aieo-social-driver-instagram.php', 'includes/social/drivers/class-aieo-social-driver-x.php', 'includes/social/drivers/class-aieo-social-driver-linkedin.php', 'includes/social/drivers/class-aieo-social-driver-tiktok.php', 'includes/social/class-aieo-social-ai-writer.php', 'includes/social/class-aieo-social-video.php', 'includes/social/class-aieo-social-video-metabox.php', 'includes/social/class-aieo-social-metabox.php', 'includes/social/class-aieo-social-jobs.php', 'includes/social/class-aieo-social-broadcast.php', 'includes/social/class-aieo-social-outcome-reporter.php', 'includes/social/class-aieo-social-boost.php', 'includes/class-aieo-product-affinity.php', // Before the boost slate and the SEO pillars, both of which read it. 'includes/class-aieo-basket-role.php', 'includes/class-aieo-boost-candidates.php', 'includes/class-aieo-campaign-forecasts.php', ) as $rel ) { $abs = AIEO_DIR . $rel; if ( file_exists( $abs ) ) { require_once $abs; } } if ( class_exists( 'AIEO_Social_Jobs' ) ) { AIEO_Social_Jobs::init(); } if ( class_exists( 'AIEO_Social_Outcome_Reporter' ) ) { AIEO_Social_Outcome_Reporter::init(); } if ( class_exists( 'AIEO_Boost_Candidates' ) ) { AIEO_Boost_Candidates::init(); } // Behaviour-informed SEO opportunity grid. Sits next to the boost scorer // because the boost scorer READS its table (the seo_opportunity component). // It shipped for a long time with a rebuild() nobody scheduled โ see // AIEO_SEO_Opportunity_Scorer::init() for why that mattered. Self-gated on // seo.enabled; scheduling happens on `init`. // Explicit require rather than trusting the mu-plugin autoloader: this is // the one class whose absence was invisible for months, so its wiring does // not get to depend on a loader that may not be installed everywhere. $aieo_opp_file = AIEO_DIR . 'includes/seo/class-aieo-seo-opportunity-scorer.php'; if ( file_exists( $aieo_opp_file ) ) { require_once $aieo_opp_file; } if ( class_exists( 'AIEO_SEO_Opportunity_Scorer' ) ) { AIEO_SEO_Opportunity_Scorer::init(); } if ( class_exists( 'AIEO_Campaign_Forecasts' ) ) { AIEO_Campaign_Forecasts::init(); } if ( class_exists( 'AIEO_Social_OAuth' ) ) { add_action( 'rest_api_init', array( 'AIEO_Social_OAuth', 'register_routes' ) ); } if ( class_exists( 'AIEO_Social_Metabox' ) ) { AIEO_Social_Metabox::register(); } if ( class_exists( 'AIEO_Social_Video_Metabox' ) ) { AIEO_Social_Video_Metabox::register(); } // Register the site-owned platform drivers built on the Facebook pattern. add_filter( 'aieo_social_platforms', static function ( $drivers ) { foreach ( array( 'instagram' => 'AIEO_Social_Driver_Instagram', 'x' => 'AIEO_Social_Driver_X', 'linkedin' => 'AIEO_Social_Driver_Linkedin', 'tiktok' => 'AIEO_Social_Driver_Tiktok', ) as $code => $class ) { if ( class_exists( $class ) ) { $drivers[ $code ] = $class; } } return $drivers; } ); }, 9 ); // Signal registry โ the declarative catalogue of EVERY metric this plugin // computes (table, producer, cron, consumers) plus cheap live health. Read it // before building any new metric: this exists because a scorer that already // shipped got rebuilt from scratch by someone who had no way to discover it. // Admin/CLI only: it is a diagnostic catalogue, not a runtime dependency, and // the front end must not pay to load it. Everything that needs it outside those // contexts (the Claude ability, the opportunity-scorer cron) lazy-requires it. // file_exists-guarded like every other require in this file: an UNGUARDED // require_once here white-screens all of wp-admin AND every WP-CLI command on // any build where the file is missing (a .distignore slip, a half-finished // rsync, the runtime/core plugin split where only one copy carries it). A // diagnostic catalogue must never be able to take the site down; the three // callers that need it outside this path already lazy-require it defensively. if ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { $aieo_registry_file = plugin_dir_path( __FILE__ ) . 'includes/class-aieo-signal-registry.php'; if ( file_exists( $aieo_registry_file ) ) { require_once $aieo_registry_file; } } // SEO score column on the product/post/page list tables (toggle via Screen // Options). Reads stored scores in one query per page; gated on seo.enabled. if ( is_admin() && class_exists( 'AIEO_SEO_Admin_Columns' ) ) { AIEO_SEO_Admin_Columns::register(); } // Goal tracker (site-profiles Phase 3): server-side conversion events (form // submits, newsletter signups, LearnDash enrol/complete, registrations) into // the session event stream โ the content analog of add-to-cart. Consent-safe // by construction (records only alongside the consented session cookie). // Capability shim (products are content, content is products): 60+ AIEO // handlers historically gate on manage_woocommerce, which NO user holds on // sites without WooCommerce (quhoma, alfavitari, portals) - every rebuild/ // export button dies with Permission denied there. Grant it to // administrators when WooCommerce is absent; zero change on shops. add_filter('user_has_cap', function ($allcaps) { if (!empty($allcaps['manage_options']) && empty($allcaps['manage_woocommerce']) && !class_exists('WooCommerce')) { $allcaps['manage_woocommerce'] = true; } return $allcaps; }); // Canonical landing/campaign stats โ parses landing_path + utm_* out of the // session statics and keeps the durable daily rollup. Registers only the // aieo_archive_run hook here; the heavy lifting runs inside the crons. if ( file_exists( AIEO_DIR . 'includes/class-aieo-landing-stats.php' ) ) { require_once AIEO_DIR . 'includes/class-aieo-landing-stats.php'; if ( class_exists( 'AIEO_Landing_Stats' ) ) { AIEO_Landing_Stats::register(); } } // Goal collector โ byte-identical dual-copy (ยง21 Gap 1). The require is // class_exists-guarded (runtime's copy may already be loaded via the autoloader) // and the file itself carries a co-load return guard. // // ๐จ REGISTER LATE, AND DO NOT MOVE THIS BACK TO TOP LEVEL. // // This used to be a top-level `! defined( 'AIEO_RUNTIME_OWNS_GOALS' )`, which is // a guard that CANNOT FAIL. `active_plugins` is stored [ai-eshop-optimizer, // ai-eshop-optimizer-pro, โฆ-runtime], so core is included first and no // AIEO_RUNTIME_OWNS_* constant exists yet. Identical finding, and identical // remedy, to the consent gate at :1299 (measured 2026-08-08), the AJAX shipping // methods at :3870 and the SSO display at :3905. // // It looked correct because two accidents covered for it: the autoloader hands // core the RUNTIME's copy of the class, and both plugins add the identical // static callable at the identical priority, which WordPress dedupes by callback // id. Change either side to a closure, an instance method or another priority // and the goal tracker silently registers TWICE. On a conversion signal, double // counting is the kind of bug that is believed for months. // // โ ๏ธ AND THE CONSTANT IS THE RIGHT SOURCE, not a presence test. An earlier fix // here asked AIEO_Runtime_Presence::activated() instead, which is subtly worse: // the runtime can be active and still DECLINE to own the front end. It already // does exactly that at its own file scope when the version floor is unmet // (ai-eshop-optimizer-runtime.php:147 returns after defining // AIEO_RUNTIME_VERSION but before the OWNS constants), and it now does it again // when unlicensed. A presence test reads true in both cases, core stands back, // the stood-down runtime never registers, and the signal lands NOWHERE. Reading // the constant on plugins_loaded asks the only question that matters โ "did the // runtime actually claim this?" โ and is correct for every present and future // stand-down reason without core reproducing any of them. // // init@20 still happens, because init fires after plugins_loaded. register() // keeps its static-flag guard and the callable stays a plain static pair, so the // dedupe remains a second net rather than the mechanism. if ( ! class_exists( 'AIEO_Goal_Tracker' ) && file_exists( AIEO_DIR . 'includes/class-aieo-goal-tracker.php' ) ) { require_once AIEO_DIR . 'includes/class-aieo-goal-tracker.php'; } add_action( 'plugins_loaded', static function () { if ( defined( 'AIEO_RUNTIME_OWNS_GOALS' ) ) { return; // the runtime claimed it and registered its own copy } if ( class_exists( 'AIEO_Goal_Tracker' ) ) { add_action( 'init', array( 'AIEO_Goal_Tracker', 'register' ), 20 ); } }, 11 ); // On-site search demand collector (semantic engine Sub-plan 9) โ core ?s= // provider + FiboSearch provider (live hook, standalone-endpoint shim, // one-time 30-day stats backfill) into wp_aieo_search_demand_daily. The // mu-plugin autoloader resolves includes/-root classes; the guarded require // mirrors the Goal Tracker block above for installs where it is stale. if ( ! class_exists( 'AIEO_Search_Signal' ) && file_exists( AIEO_DIR . 'includes/class-aieo-search-signal.php' ) ) { require_once AIEO_DIR . 'includes/class-aieo-search-signal.php'; } if ( class_exists( 'AIEO_Search_Signal' ) ) { add_action( 'init', array( 'AIEO_Search_Signal', 'bootstrap' ), 20 ); } // Front-end emit: when the runtime plugin is present it OWNS the SEO head // (defines AIEO_RUNTIME_OWNS_SEO_HEAD + registers AIEO_SEO_Head itself); // otherwise the core registers the emitter standalone. DEFERRED to // plugins_loaded@11: the runtime defines its OWNS constant at its own file // load, which runs AFTER this core file is included (plugins load // alphabetically: ai-eshop-optimizer before ai-eshop-optimizer-runtime), so an // include-time !defined() check would always be true and the core would // register too. Checking on plugins_loaded โ after every plugin's main file // has run โ makes the skip reliable. Mirrors the AIEO_RUNTIME_OWNS_SSO_DISPLAY // gate precedent; register() is idempotent as a second safety net. add_action( 'plugins_loaded', static function () { // Canonical-category permalink filters (A1.3) โ registered UNCONDITIONALLY // (NOT under the OWNS_SEO_HEAD gate): register() is idempotent (static // $done), so whichever plugin calls first wins and the other no-ops. This // decouples the permalink filter from the head-emitter OWNS constant, so it // can never end up registered in NEITHER plugin. Self-gated on // active-provider; harmless no-op without %product_cat%. NOT admin-gated: // must be active during the offline build so get_permalink() materialises // the canonical-category URL. if ( class_exists( 'AIEO_SEO_Primary_Term' ) ) { AIEO_SEO_Primary_Term::register(); } // Visible breadcrumb shortcode [aieo_breadcrumb] (the aieo/breadcrumb block // auto-registers via block.json). Idempotent; shares AIEO_SEO_Breadcrumb's // resolver with the JSON-LD BreadcrumbList so the trail uses the pinned // canonical category. if ( class_exists( 'AIEO_SEO_Breadcrumb' ) ) { AIEO_SEO_Breadcrumb::register(); } // Static sitemap (A1.4): the /sitemap.xml PHP fallback + the robots.txt // Sitemap line โ self-gated on active-provider (core-free). In production // nginx serves the static file at the edge; the build runs offline via the // seo_sitemap data-prep stage. if ( class_exists( 'AIEO_SEO_Sitemap' ) ) { AIEO_SEO_Sitemap::register(); } // Redirect engine (A2): serves 301/302/307/410 + logs 404s. The runtime OWNS // it when present โ its bundled copy + the aieo-seo-redirects mu-plugin serve // front-end redirects, so the core must NOT touch the class here. Referencing // it while the runtime copy is already declared triggered a "Cannot redeclare // class AIEO_SEO_Redirects" fatal on admin requests (e.g. product save). The // core registers it ONLY standalone (runtime absent). if ( ! defined( 'AIEO_RUNTIME_OWNS_SEO_HEAD' ) && class_exists( 'AIEO_SEO_Redirects' ) ) { AIEO_SEO_Redirects::register(); } // Head emitter: the runtime OWNS it when present (defines // AIEO_RUNTIME_OWNS_SEO_HEAD + registers its bundled copy); the core // registers it only standalone. if ( defined( 'AIEO_RUNTIME_OWNS_SEO_HEAD' ) ) { return; } if ( class_exists( 'AIEO_SEO_Head' ) ) { AIEO_SEO_Head::register(); } }, 11 ); // 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'; // The matching substrate (AIEO_Match_Text), the attribute-axis resolver and // the duplicate-products arc (groups / merge / WC-feature check) used to be // required here as "core rather than Pro" utilities. The 2026-08-20 operator // ruling moved them, with the catalogue importer and the exchanger, to the // Pro plugin's engines classmap (ai-eshop-optimizer-pro/includes/engines/): // they concentrate the Greek commercial-text matching IP and are consumed // only by Pro features. Free code reaches them through class_exists() โ the // Pro loader registers at its main-file scope, so the classes resolve at // every hook priority whenever Pro is installed, and the module shells show // an install pointer when it is not. // The capability registry โ what this install exposes and what each part // costs. Loaded unconditionally because WP-CLI, the assistant and the Home // counter all read it, and a lazily-required registry is one that is missing // exactly when support needs it. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-capability-registry.php'; // Which of those capabilities are documented on rooster.systems, and as of which // plugin version. Sits beside the registry deliberately: the pair is what turns // "we shipped a change" into "โฆand these manual pages now describe the old // behaviour". Reads a committed JSON index, so it costs nothing until asked. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-docs-registry.php'; // Which of those capabilities a shop actually USES. Gated: the free build must // not phone home (wp.org guidelines), so collection needs Pro plus a recorded // consent โ one check, in AIEO_Action_Telemetry::is_collecting(). The class // loads unconditionally because the SCRIPT it configures ships in both builds // and the gate has to be answerable either way. require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-action-telemetry.php'; AIEO_Action_Telemetry::init(); // Saved & Abandoned Carts subsystem โ storefront save/share/resume on a // dedicated table pair (wp_aieo_carts + wp_aieo_cart_items) PLUS a permanent // abandoned-cart log captured from wc-checkout-draft orders (with outcome // tracking) before WooCommerce purges them. Bootstrap loads the data layer, // the storefront feature, and the hourly capture job. require_once plugin_dir_path(__FILE__) . '/includes/carts/class-aieo-carts.php'; // Server Events (Track B) โ server-side conversion forwarding (Facebook CAPI // today; GA4 Measurement Protocol later). Loaded + booted UNCONDITIONALLY: the // server side is pure backend with no front-end surface and must run with or // without the runtime. Hook callbacks lazy-reference their deps, so load order // is irrelevant. The browser pixel surface (B2b) is wired separately behind a // dedicated AIEO_RUNTIME_OWNS_PIXEL constant. require_once plugin_dir_path(__FILE__) . '/includes/server-events/class-aieo-server-events.php'; AIEO_Server_Events::instance(); // First-party GDPR consent (Track C) โ one source of truth (cookie + allows() // API), Google Consent Mode v2 signals, and a thin first-party banner. The // session-tracker consent gates consult this first; external CMPs remain the // fallback when the banner is off. // // DUAL-BUNDLED since 2026-08-08. The class is byte-identical in the runtime // plugin, which serves the banner when both are installed so a storefront // request can be answered by the lighter plugin alone, and so the banner // survives this plugin being trimmed off the front end entirely. // // class_exists guards the require because require_once keys on the PATH, not // the class: the runtime's copy lives at a different path, so without this a // second declaration of the same class would load. if ( ! class_exists( 'AIEO_Consent', false ) ) { require_once plugin_dir_path(__FILE__) . '/includes/consent/class-aieo-consent.php'; } // ๐จ REGISTER LATE, AND DO NOT MOVE THIS BACK TO TOP LEVEL. // // Not `if ( ! defined( 'AIEO_RUNTIME_OWNS_CONSENT' ) )` โ that guard reads as // obvious and is always false here. Measured 2026-08-08: active_plugins is // stored [ai-eshop-optimizer, ai-eshop-optimizer-pro, โฆ-runtime], so THIS // plugin loads first and the runtime has not defined its constant yet. Both // copies would then register: two banners, two Consent Mode blocks, a doubled // audit row. // // plugins_loaded@99 runs after the runtime's own registration at @10, and // register() is idempotent, so the runtime wins when it is present and this // call covers a standalone install. Order is settled at run time, which is the // only place it is actually known. add_action( 'plugins_loaded', static function () { if ( class_exists( 'AIEO_Consent' ) ) { AIEO_Consent::instance()->register(); } }, 99 ); // Checkout-integrity guards + abandoned-cart live capture. All three are DUAL // files โ byte-identical in both plugins at the same path, same class name, // declaration self-guarded with class_exists(..., false) โ so whichever plugin // loads first wins and ::instance() being a singleton means the hooks register // exactly once even with both plugins active. // // Deliberately NOT gated on AIEO_RUNTIME_OWNS_AJAX. An earlier version was, and // that made construction ORDER-DEPENDENT: whichever bootstrap ran first built // the object, and if that was this one while the constant was already defined, // the constructor returned early and the hooks were never registered by either // plugin. The singleton plus the in-file guard is the correct mechanism, and it // is the same one class-aieo-runtime-birthday.php has always used. add_action('plugins_loaded', static function () { $aieo_dual = array( 'AIEO_Runtime_Payment_Fee_Guard' => 'includes/class-aieo-runtime-payment-fee-guard.php', 'AIEO_Runtime_Stock_Hold_Guard' => 'includes/class-aieo-runtime-stock-hold-guard.php', 'AIEO_Runtime_Carts_Live_Capture' => 'includes/class-aieo-runtime-carts-live-capture.php', ); foreach ($aieo_dual as $aieo_cls => $aieo_rel) { if (!class_exists($aieo_cls)) { $aieo_file = plugin_dir_path(__FILE__) . $aieo_rel; if (file_exists($aieo_file)) { require_once $aieo_file; } } if (class_exists($aieo_cls)) { $aieo_cls::instance(); } } }, 20); // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ // SIGN-IN SECURITY โ two-factor authentication, the private sign-in address and // the failed-sign-in lockout. Settings live on Set Up โ Security; ENFORCEMENT // lives here, because all three have to run on wp-login.php, and wp-login.php // loads plugins but never loads a DMM module. // // โ ๏ธ REGISTERED AT plugins_loaded@1, EARLIER THAN ANYTHING ELSE IN THIS FILE. // AIEO_Login_URL::route() hooks `wp_loaded` and must have its filters in place // before WordPress generates a single login URL; the lockout gate must be on // `authenticate` before any other plugin's. Moving this later does not fail // loudly โ it fails as a password-reset email pointing at a door that now // refuses it. // // Each class self-gates on AIEO_Login_Security::feature_active(), so registering // the hooks costs a settings read and nothing else on a shop that uses none of // this. Nothing here runs when AIEO_LOGIN_SECURITY_BYPASS is defined in // wp-config.php, which is the documented way back in. // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ add_action( 'plugins_loaded', static function () { $aieo_ls_dir = plugin_dir_path( __FILE__ ) . 'includes/login-security/'; foreach ( array( 'class-aieo-login-security.php', // settings + the shared role list 'class-aieo-qr.php', // enrolment QR, drawn locally 'class-aieo-totp.php', // RFC 6238 'class-aieo-two-factor.php', // the challenge at sign-in 'class-aieo-login-url.php', // the private address 'class-aieo-login-lockout.php', // repeated-failure throttle ) as $aieo_ls_file ) { $aieo_ls_path = $aieo_ls_dir . $aieo_ls_file; if ( file_exists( $aieo_ls_path ) ) { require_once $aieo_ls_path; } } if ( ! class_exists( 'AIEO_Login_Security' ) ) { return; // Partial install; leave sign-in exactly as WordPress ships it. } if ( class_exists( 'AIEO_Two_Factor' ) ) { AIEO_Two_Factor::register(); } if ( class_exists( 'AIEO_Login_URL' ) ) { AIEO_Login_URL::register(); } // The lockout needs its log table; the schema file is required here rather // than relying on the DMM bootstrap, which does not run on wp-login.php. if ( class_exists( 'AIEO_Login_Lockout' ) ) { $aieo_ls_schema = plugin_dir_path( __FILE__ ) . 'includes/dmm/sql/login-security-schema.php'; if ( file_exists( $aieo_ls_schema ) ) { require_once $aieo_ls_schema; } if ( function_exists( 'aieo_login_attempts_table' ) ) { AIEO_Login_Lockout::register(); } } }, 1 ); // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ // SITE HARDENING (Tools & Settings -> Security -> "Security Tightening"). // // Same shape as the block above and for the same reason: hiding the version, // refusing feeds, restraining XML-RPC and sending response headers all happen // on requests that never load a DMM module. The screen only reads and writes // the settings row. // // โ PRIORITY 1 IS NOT DECORATION. The file-editor defence filters // map_meta_cap, and it has to be in place before anything asks whether the // current user can edit_plugins -- the admin menu is built out of those // answers. Registering later leaves the editors in the menu for that request. // // Every setting is off until an operator switches it on, so on a shop that // uses none of this the cost is one autoloaded option read. // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ add_action( 'plugins_loaded', static function () { $aieo_sh_path = plugin_dir_path( __FILE__ ) . 'includes/security-hardening/class-aieo-security-hardening.php'; if ( ! file_exists( $aieo_sh_path ) ) { return; // Partial install; leave the site exactly as WordPress ships it. } require_once $aieo_sh_path; if ( class_exists( 'AIEO_Security_Hardening' ) ) { AIEO_Security_Hardening::register(); } }, 1 ); // Google One Tap (recognise-only) โ thin front-end loader. Renders the GSI // loader for logged-out visitors when the merchant enables One Tap (Consent/GDPR // page) + a Google client_id is set (Social Login). The JS gates on the // `personalize` consent flag + skips logged-in browsers. Default-OFF. require_once plugin_dir_path(__FILE__) . '/includes/social-login/class-aieo-one-tap.php'; AIEO_One_Tap::register(); // Panel manifest โ the single descriptor the Rooster panel polls // (uploads/aieo-panel/manifest.json). Self-contained; rewrites on push/SEO engine // + VAPID changes (debounced). Registers the `wp aieo push prepare` / // `verify-readiness` CLI commands. Activation also defaults a FRESH site's push // engine to paused (add_option no-op โ existing live sites untouched). require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-panel-manifest.php'; AIEO_Panel_Manifest::register(); register_activation_hook(__FILE__, 'aieo_panel_manifest_activate'); // WC 10.9 forward-compat migration โ relocates botiga_variation_gallery // + product_attribute_* term metas into WooCommerce's canonical keys // (`_product_image_gallery`, `color`) so that when a merchant enables // the `variation_gallery` / `wc-visual-attribute` feature toggles the // data is already where WC expects it. Idempotent, batched via Action // Scheduler; also exposed as `wp aieo migrate wc-compat`. require_once plugin_dir_path(__FILE__) . '/includes/migrations/class-aieo-wc-compat-migration.php'; AIEO_WC_Compat_Migration::bootstrap(); // Audience-vocabulary migration โ rewrites SAVED audience rule trees after the // segment rename ('lapsed' โ 'overdue') and the deletion of the 'dormant' // segment. This MUST be wired: nothing validates a segment slug at compile time // (AIEO_Audience_Rules::compile() turns any slug into a perfectly valid // `segments_json LIKE '%"x"%'`), so a saved rule naming a dead segment stays // syntactically fine and silently starts matching ZERO customers the moment the // assignment sproc stops writing that slug โ 13,883 โ 0 for 'dormant', 1,872 โ 0 // for 'lapsed' on the reference bed. The workflow gated on such an audience does // not error; it just stops sending. Same shape as the wc-compat migration above: // version-gated self-heal plus `wp aieo migrate audience-vocabulary [--dry-run]`. require_once plugin_dir_path(__FILE__) . '/includes/migrations/class-aieo-audience-vocabulary-migration.php'; AIEO_Audience_Vocabulary_Migration::bootstrap(); // Un-bake English wording frozen into settings before the defaults were // deferred to render time. A code fix alone cannot repair this: the render-time // fill only fills BLANKS, on purpose, so a site that had already saved its // settings keeps rendering English and looks exactly as unfixed as before. That // is why this defect has been closed more than once and kept returning. Resets // a stored value only when it is byte-identical to a default this plugin // shipped, so an operator's own wording is never touched. See the class header // for why the allow-list is curated and must never be generated. require_once plugin_dir_path(__FILE__) . '/includes/migrations/class-aieo-baked-i18n-repair.php'; add_action('admin_init', array('AIEO_Baked_I18n_Repair', 'maybe_run'), 40); // 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'; } // The Ctrl/Cmd+K palette lives in assets/js/aieo-admin-navigator.js, enqueued by // AIEO_DMM::enqueue_admin_assets(). AIEO_Command_Palette was its predecessor and // had already disabled itself โ init() executed a bare `return;` before // registering a single hook, so that it could not render a second competing // overlay. Class and both its JS files removed in 11.0. // Initialize the plugin function AIEO_init() { // Phase-1 perf gate: class AIEO is the admin monolith (menus, CSV-export // pipeline, Rooster API client, DB optimizer โ ~6.4k lines). Its hooks // are admin_menu / admin_bar_menu / admin_post_* / wp_ajax_* plus ONE // frontend-relevant hook: the wp_head admin-bar CSS fix. So: // โข admin + AJAX requests construct it eagerly (status quo); // โข all other requests defer to init and construct it only for // logged-in users โ the only visitors who can see the admin bar. // Deciding at init (not here at plugins_loaded) avoids forcing // early user determination, which breaks late-registered auth // filters (session tokens, 2FA). // Anonymous storefront / REST / cron traffic skips the class entirely: // none of its hooks can fire there. if (is_admin() || wp_doing_ajax()) { new AIEO(); } else { add_action('init', function () { if (is_user_logged_in()) { new AIEO(); } }, 0); } // 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(); } // Saved & Abandoned Carts โ boots the storefront save/share/resume // hooks + the hourly abandoned-cart capture job. Tables install lazily. // Skipped when the runtime plugin is present: it owns the frontend // surface (AIEO_RUNTIME_OWNS_AJAX) and instantiates AIEO_Carts itself, // so the feature keeps working on trimmed-whitelist requests where this // admin plugin isn't loaded. (AIEO_Carts::instance() is a singleton, so // even un-gated double calls are harmless โ this just honours the contract.) if ( class_exists( 'AIEO_Carts' ) && ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) { AIEO_Carts::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(); } // Identity substrate resolver (Customer Intelligence Core, Phase A) โ // registers the hourly Action-Scheduler tick that drains the sessionโsubject // queue (wp_aieo_session_identity โ wp_aieo_subjects) and the wp_login // identification rung. No-op when the tables/queue are empty. if ( class_exists( 'AIEO_Subject_Resolver' ) ) { AIEO_Subject_Resolver::bootstrap(); } // Behavioural embedding (Customer Intelligence Core, ยง15) โ the interaction // aggregation job (raw firehose โ wp_aieo_interaction_summary), the per-subject // vector builder (interaction-weighted aggregation of item Voyage vectors โ // wp_aieo_chat_embeddings post_type='user'), and the serving layer // (recommendations / lookalikes / instigators + digest personalisation). Each // AS job is a cheap no-op until the substrate has data. // Explicit requires so the behavioural layer loads even on installs without // the mu-plugin autoloader (#27) โ otherwise class_exists() below is false and // the AS jobs silently never schedule. foreach ( array( 'class-aieo-interaction-summary.php', 'class-aieo-behavioural-vector.php', 'class-aieo-behavioural-reco.php' ) as $aieo_bh_file ) { $aieo_bh_path = AIEO_DIR . 'includes/' . $aieo_bh_file; if ( file_exists( $aieo_bh_path ) ) { require_once $aieo_bh_path; } } if ( class_exists( 'AIEO_Interaction_Summary' ) ) { AIEO_Interaction_Summary::bootstrap(); } if ( class_exists( 'AIEO_Behavioural_Vector' ) ) { AIEO_Behavioural_Vector::bootstrap(); } if ( class_exists( 'AIEO_Behavioural_Reco' ) ) { AIEO_Behavioural_Reco::bootstrap(); } // Operator intelligence surfaces (ยง17 profile, ยง19 audiences) + the manual // campaign/workflow engine. Explicit requires so they load without the // mu-autoloader; the runtime tick fires triggers + walks steps (INERT until // the channel engines are flipped live). foreach ( array( 'class-aieo-i18n.php', 'class-aieo-campaign-i18n.php', 'class-aieo-coupon-minter.php', 'class-aieo-merge-tags.php', 'class-aieo-customer-profile.php', 'class-aieo-customer-purchases.php', 'class-aieo-audience-rules.php', 'class-aieo-contact-discovery.php', 'class-aieo-order-rules.php', 'class-aieo-workflows.php', 'class-aieo-workflow-runtime.php', 'class-aieo-workflow-stats.php', 'class-aieo-workflow-incrementality.php', 'class-aieo-send-time.php', 'class-aieo-workflow-ab.php', 'class-aieo-order-touchpoints.php', 'class-aieo-marketing-stats.php' ) as $aieo_wf_file ) { $aieo_wf_path = AIEO_DIR . 'includes/' . $aieo_wf_file; if ( file_exists( $aieo_wf_path ) ) { require_once $aieo_wf_path; } } // Registered here rather than from the DMM module: the batch handler has to // exist in the Action Scheduler / cron request too, where admin modules are // not loaded, or a queued conversion run would never advance. if ( class_exists( 'AIEO_Contact_Discovery' ) ) { AIEO_Contact_Discovery::register_hooks(); } // Native-first coupon minter (ยง12) โ registers order-redemption tracking + // the daily cleanup of expired-unredeemed coupons. Mints nothing on its own. // (Also registers the shared AIEO_Offer_Link apply-link consumer.) if ( class_exists( 'AIEO_Coupon_Minter' ) ) { AIEO_Coupon_Minter::bootstrap(); } // Birthday capture (ยง21 Gap 2) โ shared byte-identical class; boot the core // copy so a core-only site (no runtime) also registers the checkout field + // syncs the value. When runtime is active it boots the same singleton first; // instance() is idempotent so the field registers once. Reads aieo_dmm_birthday. // Explicit require (not just file_exists) so it loads even without the mu // autoloader (#27), same idiom as the behavioural layer above. if ( ! class_exists( 'AIEO_Runtime_Birthday' ) && file_exists( AIEO_DIR . 'includes/class-aieo-runtime-birthday.php' ) ) { require_once AIEO_DIR . 'includes/class-aieo-runtime-birthday.php'; } if ( class_exists( 'AIEO_Runtime_Birthday' ) ) { AIEO_Runtime_Birthday::instance(); } // Unified contact-consent service (ยงVII.18) โ single source of truth for marketing // consent (email suppression + Newsletter Plugin), shared by the checkout opt-in and // the My-Account "Email & marketing preferences" toggle. Static utility; just load it. if ( ! class_exists( 'AIEO_Contact_Consent' ) && file_exists( AIEO_DIR . 'includes/class-aieo-contact-consent.php' ) ) { require_once AIEO_DIR . 'includes/class-aieo-contact-consent.php'; } // Checkout marketing opt-in checkbox (ยงVII.18) โ shared byte-identical class; boot the // core copy so a core-only site also registers the Store-API field. Idempotent singleton // (reads aieo_dmm_marketing_optin; zero overhead when disabled). if ( ! class_exists( 'AIEO_Runtime_Marketing_Optin' ) && file_exists( AIEO_DIR . 'includes/class-aieo-runtime-marketing-optin.php' ) ) { require_once AIEO_DIR . 'includes/class-aieo-runtime-marketing-optin.php'; } if ( class_exists( 'AIEO_Runtime_Marketing_Optin' ) ) { AIEO_Runtime_Marketing_Optin::instance(); } // AutomateWoo decommission/migration jobs (ยง12b) โ birthday migration, AW // coupon ledger import, opt-out import. Registers AS/WP-CLI/AJAX entry points // only; NOTHING runs until an operator triggers it. $aieo_awdec_path = AIEO_DIR . 'includes/class-aieo-aw-decommission.php'; if ( ! class_exists( 'AIEO_AW_Decommission' ) && file_exists( $aieo_awdec_path ) ) { require_once $aieo_awdec_path; } if ( class_exists( 'AIEO_AW_Decommission' ) ) { AIEO_AW_Decommission::bootstrap(); } if ( class_exists( 'AIEO_Workflow_Runtime' ) ) { AIEO_Workflow_Runtime::bootstrap(); } // ยงIV.3 lifetime "ever bought X" rollup โ nightly TRUNCATE+INSERT of // wp_aieo_customer_purchases from the order snapshots, feeding the Audience // Builder's ever_purchased_* fields. Registers only its nightly AS job; sends // nothing and rebuilds nothing until the schedule fires (or an operator runs it). if ( class_exists( 'AIEO_Customer_Purchases' ) ) { AIEO_Customer_Purchases::bootstrap(); } // Per-workflow OUTCOME rollup (campaign funnel: sentโclickedโredeemedโrevenue) โ // reconciles the dispatch + coupon-grant ledgers (+ optional last-click order_stats) // into wp_aieo_workflow_stats. Registers a 6-hourly AS rebuild; feeds the Campaigns // builder funnel + the AI workflow-performance ability. Reads ledgers only; sends nothing. if ( class_exists( 'AIEO_Workflow_Stats' ) ) { AIEO_Workflow_Stats::bootstrap(); } // Unified "All Marketing Performance" rollup โ reconciles workflows + push triggers + // broadcasts + paid ad campaigns into wp_aieo_marketing_stats (owned + paid, one surface) // for the campaigns card + the aieo/marketing-performance chat ability. Reads only. if ( class_exists( 'AIEO_Marketing_Stats' ) ) { AIEO_Marketing_Stats::bootstrap(); } // ยง13 efficiency bandit โ nightly Thompson-sampling variant reweight of // wp_aieo_push_templates.weight + the conversion-join reward signal + the // msg_stats rollup. Registers only its nightly AS job + schema self-heal; // reweights nothing until a pool has โฅ2 active variants with data. $aieo_bandit_path = AIEO_DIR . 'includes/class-aieo-bandit.php'; if ( file_exists( $aieo_bandit_path ) ) { require_once $aieo_bandit_path; if ( class_exists( 'AIEO_Bandit' ) ) { AIEO_Bandit::bootstrap(); } } // ยง19.8 AI auto-suggestion โ nightly opportunity scanner โ the proposals inbox // (T0 suggest-only). Registers the nightly AS scan; proposes nothing until it // runs, and accepting a proposal only creates a DRAFT workflow. $aieo_proposal_path = AIEO_DIR . 'includes/class-aieo-proposal-engine.php'; if ( file_exists( $aieo_proposal_path ) ) { require_once $aieo_proposal_path; if ( class_exists( 'AIEO_Proposal_Engine' ) ) { AIEO_Proposal_Engine::bootstrap(); } } // Email channel (Customer Intelligence Core, Phase B) โ INERT by default: // aieo_email_engine_status defaults to 'paused' and the sender re-checks it, // so NOTHING sends until an operator explicitly flips it live. Load the // classes, ensure the paused default exists, and register the dormant digest // (its recurring tick is only scheduled while the engine is live). $aieo_email_dir = AIEO_DIR . 'includes/dmm/email/'; foreach ( array( 'class-aieo-email-engine.php', 'class-aieo-email-sender.php', 'class-aieo-email-channel.php', 'class-aieo-email-tracking.php', 'class-aieo-email-unsubscribe.php', 'class-aieo-newsletter-digest.php', 'class-aieo-channel-cascade.php' ) as $aieo_email_file ) { if ( file_exists( $aieo_email_dir . $aieo_email_file ) ) { require_once $aieo_email_dir . $aieo_email_file; } } if ( class_exists( 'AIEO_Email_Engine' ) ) { AIEO_Email_Engine::ensure_default(); } // ยงVII.5 โ email open/click measurement (adds opened_at; serves the pixel + click redirect). if ( class_exists( 'AIEO_Email_Tracking' ) ) { AIEO_Email_Tracking::bootstrap(); } // ยงVII.14 โ one-click unsubscribe endpoint (footer link + List-Unsubscribe headers on every send). if ( class_exists( 'AIEO_Email_Unsubscribe' ) ) { AIEO_Email_Unsubscribe::bootstrap(); } if ( class_exists( 'AIEO_Newsletter_Digest' ) ) { AIEO_Newsletter_Digest::bootstrap(); } // Channel cascade (ยง11) โ push-first, email-fallback primitive. Registers only // its fallback AS handler; runs nothing until a trigger calls dispatch(). if ( class_exists( 'AIEO_Channel_Cascade' ) ) { AIEO_Channel_Cascade::bootstrap(); } // Embedding backfill (Customer Intelligence Core, Phase C) โ operator- // initiated, metered posts/pages embedding via Action Scheduler + a cost // preview. Registers the AS tick + admin AJAX; idle until an operator runs it. $aieo_embed_backfill = AIEO_DIR . 'includes/chat/class-aieo-embedding-backfill.php'; if ( file_exists( $aieo_embed_backfill ) ) { require_once $aieo_embed_backfill; if ( class_exists( 'AIEO_Embedding_Backfill' ) ) { AIEO_Embedding_Backfill::bootstrap(); } } // Semantic onboarding backfill โ same AIEO_Batch_Runner engine driving // index-drain โ bounded data-plane push. Idle until an operator starts // it from SEO โ Internal Linking โ Settings; no-ops without the pro engine. $aieo_sem_backfill = AIEO_DIR . 'includes/class-aieo-semantic-backfill.php'; if ( file_exists( $aieo_sem_backfill ) ) { require_once $aieo_sem_backfill; if ( class_exists( 'AIEO_Semantic_Backfill' ) ) { AIEO_Semantic_Backfill::bootstrap(); } } // CLI driver for the same runner โ `wp aieo semantic onboard` spawns one // fresh PHP process per chunk (the 5k-catalogue OOM fix). The file guards // on WP_CLI itself and registers its own command. if ( defined( 'WP_CLI' ) && WP_CLI ) { $aieo_sem_backfill_cli = AIEO_DIR . 'includes/class-aieo-semantic-backfill-cli.php'; if ( file_exists( $aieo_sem_backfill_cli ) ) { require_once $aieo_sem_backfill_cli; } } // Sub-plan 9 fusion: behavioral percentiles for the semantic consumers // (related ordering, graph node sizing). Pure read API โ no hooks; the // co-occurrence rollup rides the aggregate-cron chain, not init. $aieo_sem_fusion = AIEO_DIR . 'includes/class-aieo-semantic-fusion.php'; if ( file_exists( $aieo_sem_fusion ) ) { require_once $aieo_sem_fusion; } // The storefront's reader for semantic related content. Required here, not // left to an autoloader, because MOST INSTALLS ARE CORE-ONLY: there is no // Pro, and the shared mu-plugin autoloader is a fleet convenience that a // wp.org install does not have. It is a pure reader โ one bounded SELECT // against a table it checks for first โ so on a site that has never had the // engine it answers "no data", the rail renders nothing, and nothing warns // or fatals. Loading it costs one require of a small file. // The two relation policies the reader consults. Loaded BEFORE it, and // unconditionally for the same reason: they are pure readers over settings // and terms, both ship OFF, and on a site that has never enabled either one // the reader's first gate returns before it touches them. foreach ( array( 'class-aieo-semantic-audience.php', 'class-aieo-semantic-temporality.php' ) as $aieo_sem_policy ) { $aieo_sem_policy_path = AIEO_DIR . 'includes/' . $aieo_sem_policy; if ( file_exists( $aieo_sem_policy_path ) ) { require_once $aieo_sem_policy_path; } } $aieo_sem_source = AIEO_DIR . 'includes/class-aieo-semantic-reco-source.php'; if ( file_exists( $aieo_sem_source ) ) { require_once $aieo_sem_source; } // ...and the placement that gives its cards somewhere to land when the // theme's template has not placed the block itself. Front-end only: on a // core-only install its first gate finds no data and it does nothing. $aieo_sem_place = AIEO_DIR . 'includes/class-aieo-semantic-related-placement.php'; if ( file_exists( $aieo_sem_place ) && ! is_admin() ) { require_once $aieo_sem_place; AIEO_Semantic_Related_Placement::bootstrap(); } // design_source = 'theme': hand the precomputed ids to the THEME's own // related block instead of rendering a rail of ours. Loaded unconditionally // (the admin needs supported() for the settings notice); engage() is // front-end only and returns immediately unless the mode is selected. // class_exists guard, not just require_once: require_once dedupes by PATH, // so if a runtime copy of this file ever appeared the runtime-first // autoloader would define the class from there and this require would // fatal with "Cannot declare class". Same pattern as AIEO_Session_Tracker // above. (There is deliberately NO runtime copy โ the runtime registers // nothing on the related-products surface and does not know design_source.) $aieo_theme_native = AIEO_DIR . 'includes/class-aieo-theme-native-recos.php'; if ( file_exists( $aieo_theme_native ) && ! class_exists( 'AIEO_Theme_Native_Recos' ) ) { require_once $aieo_theme_native; } if ( class_exists( 'AIEO_Theme_Native_Recos' ) ) { AIEO_Theme_Native_Recos::bootstrap(); } // The rail everywhere the product bundle does not reach: articles, the // shortcode, and a hand-placed block. Moved here from Pro โ Pro is a // back-end plugin, and this was the last front-end thing it did. register() // must run in the admin too (the block editor needs the block registered). $aieo_sem_display = AIEO_DIR . 'includes/class-aieo-semantic-related-display.php'; if ( file_exists( $aieo_sem_display ) ) { require_once $aieo_sem_display; AIEO_Semantic_Related_Display::register(); if ( ! is_admin() ) { AIEO_Semantic_Related_Display::bootstrap(); } } // Sub-plan 8 graph cache + block ship in Rooster One Pro (tiered // split); the free side carries only the recompute-queue dispatch seam. // 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(), // Lean front-end redirect server โ must-use so it survives the // active_plugins load-optimisation that trims the AIEO runtime/core on // storefront + 404 paths. Loads ONLY the bundled AIEO_SEO_Redirects // class (via the autoloader above), never the full plugin. 'aieo-seo-redirects.php' => array(), 'aieo-as-ajax-query-protection.php' => array( 'supersedes' => array('om-as-ajax-query-protection.php'), ), // Collapses runaway recurring AS jobs (AutomateWoo's // `delete_old_logs/start`, which the AS-ajax read protection above // would otherwise let duplicate on every admin_init/heartbeat) back to // a single pending action. See the file header for the full mechanism. 'aieo-as-dedup-recurring-jobs.php' => array(), ); $failed = array(); $installed = array(); // Files newly placed into mu-plugins/ this run. $refused = array(); // name => installed version we declined to downgrade. foreach ($files as $name => $opts) { $src = $src_dir . $name; $dst = $dst_dir . '/' . $name; if (!file_exists($src)) { continue; } $is_new = !file_exists($dst); // 1. Copy the bundled AIEO version into mu-plugins/ (idempotent โ // skip when the installed file is already byte-identical). // // Content comparison, not mtime. mtime is unreliable across // unzip/rsync/scp deploys, and trusting it is how the fleet drifted // into two live versions of the AS core file โ 15 of 36 sites spent // months running one with a known duplicate-minting bug. The bundled // copy is the source of truth; a hand-edit on a site is drift, not // something to preserve. $needs_copy = $is_new || md5_file($src) !== md5_file($dst); // 1a. DIRECTION GUARD โ md5 difference does not say WHICH WAY. // // "Different, so overwrite" is right for drift and wrong for a // downgrade, and it cannot tell them apart. Any route that puts an // older plugin on a site whose mu-plugins are newer โ a core // rollback, a stale package, a restored backup, an operator // hotfixing the AS core ahead of the release that carries it โ // silently reverts a NEWER Action Scheduler core to an older one, // reintroducing whatever it fixed. The fleet has already lost // months to two live versions of this exact file. // // This matters far more on co-installed hosting. These files change // Action Scheduler behaviour (query limits, retention, recurring-job // dedup) for EVERY consumer on the site, not just us: on a // Cloudways app that means WooCommerce, Omnisend and anything else // scheduling work. Silently downgrading the component all of them // run is not ours to do. // // Version headers, not mtime, and only when BOTH sides declare one. // An undeclared version is treated as "older", preserving today's // behaviour for the two bundled files that carry no Version header. if ($needs_copy && !$is_new && function_exists('get_file_data')) { $src_v = get_file_data($src, array('Version' => 'Version')); $dst_v = get_file_data($dst, array('Version' => 'Version')); $src_v = isset($src_v['Version']) ? trim((string) $src_v['Version']) : ''; $dst_v = isset($dst_v['Version']) ? trim((string) $dst_v['Version']) : ''; if ('' !== $src_v && '' !== $dst_v && version_compare($dst_v, $src_v, '>')) { $refused[$name] = $dst_v; $needs_copy = false; } } if ($needs_copy) { if (!@copy($src, $dst)) { $failed[] = $name; continue; } if ($is_new) { $installed[] = $name; } } // 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 ); } // TELL THE OPERATOR WE PUT FILES IN mu-plugins/. // // A must-use plugin cannot be seen on the Plugins screen and cannot be // deactivated from wp-admin, so installing one silently means the site now // runs code its administrator has no way to find. That is defensible for // our own fleet and not defensible on somebody else's hosting, where these // files change Action Scheduler's behaviour for every plugin on the site. // // First install only โ recorded in an option, not re-announced on every // upgrade re-sync. A notice that reappears forever is one that gets // dismissed without reading. if (!empty($installed) && !get_option('aieo_mu_install_announced')) { update_option('aieo_mu_install_announced', 1, false); set_transient('aieo_mu_install_notice', array_values($installed), WEEK_IN_SECONDS); } // A refused downgrade is worth saying out loud: it means the site is // running something newer than this release ships, which is either a // deliberate hotfix (fine, and we just protected it) or a rollback nobody // meant to do (needs looking at). Either way, silence would be wrong. if (!empty($refused)) { set_transient('aieo_mu_downgrade_refused', $refused, WEEK_IN_SECONDS); } } // ----------------------------------------------------------------------------- // MU-plugin disclosure notices // ----------------------------------------------------------------------------- // Two bounded, dismissible notices, both about files the operator cannot see on // the Plugins screen. Deliberately NOT part of the general notice surface: they // are shown once, they name the exact paths, and they state the removal path, // because "how do I get rid of this" must be answerable without a support // ticket. See docs/PARTNER-CONDUCT.md โ we install nothing we will not name. add_action('admin_notices', 'aieo_render_mu_plugin_notices'); function aieo_render_mu_plugin_notices() { if (!current_user_can('activate_plugins')) { return; } $dir = defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : WP_CONTENT_DIR . '/mu-plugins'; $installed = get_transient('aieo_mu_install_notice'); if (is_array($installed) && !empty($installed)) { echo '
' . esc_html__('Rooster One installed some must-use plugins', 'ai-eshop-optimizer') . '
' . esc_html__('These files keep Action Scheduler (the background-job queue WooCommerce and many other plugins share) from running away on small servers, and they load before everything else. Must-use plugins do not appear on your Plugins screen, so they are listed here:', 'ai-eshop-optimizer') . '
' . esc_html($dir) . '
' . esc_html((string) $name) . '' . esc_html__('To remove them, delete those files. Nothing in Rooster One stops working without them; Action Scheduler simply returns to its own defaults. They are put back if you deactivate and reactivate Rooster One.', 'ai-eshop-optimizer') . '
' . esc_html__('Rooster One left a newer must-use plugin alone', 'ai-eshop-optimizer') . '
' . esc_html__('This site has newer versions of these files than this release of Rooster One carries, so they were not overwritten. If somebody applied a fix here on purpose, that fix is safe. If you did not expect this, the plugin may have been rolled back to an older version:', 'ai-eshop-optimizer') . '
' . esc_html((string) $name) . ' โ ' . esc_html(sprintf(
/* translators: %s: version number found installed on this site */
__('installed version %s', 'ai-eshop-optimizer'),
(string) $ver
)) . 'AIEO: %s