=' ) && version_compare( $wp_version, AI_O_Optimiser_MIN_WP_VERSION, '>=' ); } if ( ! ai_o_optimiser_meets_minimum_requirements() ) { add_action( 'admin_notices', function() { if ( ! current_user_can( 'activate_plugins' ) ) { return; } global $wp_version; echo '

' . sprintf( /* translators: Placeholder values are replaced with contextual values shown in this message. */ esc_html__( 'AI-O Optimiser requires WordPress %1$s or later and PHP %2$s or later (this site is running WordPress %3$s and PHP %4$s). The plugin has been deactivated until the minimum platform requirements are met.', 'ai-o-optimiser' ), esc_html( AI_O_Optimiser_MIN_WP_VERSION ), esc_html( AI_O_Optimiser_MIN_PHP_VERSION ), esc_html( $wp_version ), esc_html( PHP_VERSION ) ) . '

'; } ); add_action( 'admin_init', function() { if ( current_user_can( 'activate_plugins' ) ) { deactivate_plugins( plugin_basename( __FILE__ ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The standard WordPress idiom for suppressing the activation notice when a plugin stands itself down; the value is discarded, not read. if ( isset( $_GET['activate'] ) ) { unset( $_GET['activate'] ); } } } ); return; } function ai_o_optimiser_ensure_sensitive_capability() { $administrator = get_role( 'administrator' ); if ( $administrator && ! $administrator->has_cap( AI_O_Optimiser_SENSITIVE_CAPABILITY ) ) { $administrator->add_cap( AI_O_Optimiser_SENSITIVE_CAPABILITY ); } } function ai_o_optimiser_is_schema_admin_request() { if ( ! is_admin() ) { return false; } $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing only. $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing only. return 'ai-o-optimiser-schema-review' === $page || 0 === strpos( $action, 'ai_o_optimiser_schema_' ) || 0 === strpos( $action, 'ai_o_optimiser_run_schema_' ); } require_once AI_O_Optimiser_PLUGIN_DIR . 'modules/core/bootstrap.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'modules/visibility/bootstrap.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'modules/guidance/bootstrap.php'; // Core-owned: the registration boundary a Pro add-on uses to tell core it is // present, before core's admin_menu callbacks run. Required unconditionally // and early, same as the modules above, so it exists before anything -- core // or Pro -- reaches for it. See the class docblock for the full contract. require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-extensions.php'; function ai_o_optimiser_load_admin_classes() { ai_o_optimiser_core_load_db(); ai_o_optimiser_visibility_load_ga4(); ai_o_optimiser_guidance_load_connector_client(); ai_o_optimiser_guidance_load_system_classes(); require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-notice-sections.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-admin-notice-boundary.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-menu-state.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-system-impact.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-admin.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-primary-navigation.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-feature-cards.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-workspace-landing.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-admin-rail.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-admin-live-feedback.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-admin-components.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-monthly-report.php'; } /** Install the canonical AI-O Optimiser resources for the current site. */ function ai_o_optimiser_install_current_site() { ai_o_optimiser_ensure_sensitive_capability(); ai_o_optimiser_core_install(); ai_o_optimiser_guidance_activate(); if ( ! wp_next_scheduled( 'ai_o_optimiser_daily_cleanup' ) ) { wp_schedule_event( time(), 'daily', 'ai_o_optimiser_daily_cleanup' ); } AI_O_Optimiser_DB::ensure_aggregate_backfill_scheduled(); // Autoloaded for the same reason the upgrade path writes it autoloaded: the // gate in ai_o_optimiser_maybe_upgrade() reads it on every request. Written // non-autoloaded here, a fresh install paid a query per page view until the // next version bump, which is the one path that rewrites it. update_option( 'ai_o_optimiser_version', AI_O_Optimiser_VERSION, true ); } function ai_o_optimiser_activate_plugin( $network_wide = false ) { require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-migration.php'; $site_ids = AI_O_Optimiser_Migration::prepare_activation( (bool) $network_wide ); if ( is_multisite() && $network_wide ) { foreach ( $site_ids as $site_id ) { switch_to_blog( (int) $site_id ); ai_o_optimiser_install_current_site(); AI_O_Optimiser_Migration::finalize_current_site(); restore_current_blog(); } return; } ai_o_optimiser_install_current_site(); AI_O_Optimiser_Migration::finalize_current_site(); } register_activation_hook( AI_O_Optimiser_PLUGIN_FILE, 'ai_o_optimiser_activate_plugin' ); function ai_o_optimiser_deactivate_plugin() { wp_clear_scheduled_hook( 'ai_o_optimiser_daily_cleanup' ); wp_clear_scheduled_hook( 'ai_o_optimiser_email_report_send' ); wp_clear_scheduled_hook( 'ai_o_optimiser_visibility_aggregate_backfill' ); // Not wp_clear_scheduled_hook(): both carry args its implicit array() misses. wp_unschedule_hook( 'ai_o_optimiser_local_citation_nap_batch_event' ); wp_unschedule_hook( 'ai_o_optimiser_local_profile_discovery_event' ); ai_o_optimiser_guidance_deactivate(); } register_deactivation_hook( AI_O_Optimiser_PLUGIN_FILE, 'ai_o_optimiser_deactivate_plugin' ); function ai_o_optimiser_maybe_upgrade() { $installed_version = get_option( 'ai_o_optimiser_version' ); /* * Decide whether there is anything to do BEFORE loading the migration class * and letting it look for legacy state. * * AI_O_Optimiser_Migration::needed() probes every legacy option name and * runs SHOW TABLES for every legacy table. On a site that never held legacy * data it finds nothing, persists nothing, and returns false -- so it did * all of that again on the next request, including ordinary public page * views. Roughly 39 option reads and 16 queries per request, forever. * * An install whose recorded version is current, and which has no migration * state at all (never had legacy data) or has one that already reported * itself complete, has nothing to migrate. Two small option reads settle it. * * A future migration generation raises AI_O_Optimiser_Migration::TARGET_VERSION * alongside the plugin version, so the version comparison fails first and * this gate opens on its own. */ $migration_state = get_option( AI_O_Optimiser_MIGRATION_STATE_OPTION, false ); $migration_open = is_array( $migration_state ) && 'complete' !== ( $migration_state['status'] ?? '' ); if ( AI_O_Optimiser_VERSION === $installed_version && ! $migration_open ) { return; } require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-migration.php'; $migration_pending = AI_O_Optimiser_Migration::prepare_current_site(); ai_o_optimiser_ensure_sensitive_capability(); ai_o_optimiser_core_install(); ai_o_optimiser_visibility_upgrade(); ai_o_optimiser_guidance_upgrade(); ai_o_optimiser_autoload_per_request_options(); AI_O_Optimiser_DB::ensure_aggregate_backfill_scheduled(); AI_O_Optimiser_Migration::finalize_current_site(); $ai_o_optimiser_domain_settings = get_option( 'ai_o_optimiser_settings', array() ); $ai_o_optimiser_legacy_hub = 'https://ai-o.' . 'co'; if ( is_array( $ai_o_optimiser_domain_settings ) && isset( $ai_o_optimiser_domain_settings['connector_hub_url'] ) && $ai_o_optimiser_legacy_hub === untrailingslashit( (string) $ai_o_optimiser_domain_settings['connector_hub_url'] ) ) { $ai_o_optimiser_domain_settings['connector_hub_url'] = 'https://hub.ai-o.com.au'; update_option( 'ai_o_optimiser_settings', $ai_o_optimiser_domain_settings, false ); } // Autoloaded: the upgrade gate above reads this on every request, and it is // a version string. Keeping it out of alloptions bought nothing and cost a // query per request. update_option( 'ai_o_optimiser_version', AI_O_Optimiser_VERSION, true ); } /** * Autoload the two options every public request reads. * * The upgrade gate reads the migration state and the frontend boot reads the * managed-fallback flag, on every request including ordinary page views. Held * out of alloptions they cost a query each per uncached page on a site with no * persistent object cache. * * Writing them with autoload enabled only fixes installs that write them again, * and a completed migration never does. Existing installs are moved here, once * per upgrade. * * @return void */ function ai_o_optimiser_autoload_per_request_options() { if ( ! function_exists( 'wp_set_option_autoload' ) ) { return; } foreach ( array( AI_O_Optimiser_MIGRATION_STATE_OPTION, 'ai_o_optimiser_schema_has_fallbacks' ) as $option ) { if ( false !== get_option( $option, false ) ) { wp_set_option_autoload( $option, true ); } } } /** * Refresh the published crawler address lists used to verify bot identity. * * Only reaches the network when the site owner has turned verification on: it * fetches from vendor domains, which is not something to do on a site that has * not asked for it. * * @return void */ function ai_o_optimiser_refresh_bot_ranges() { require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-client-ip.php'; require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-bot-verification.php'; AI_O_Optimiser_Bot_Verification::refresh_ranges(); } /** * Re-check that the Connector Hub still accepts this plugin's OAuth returns. * * Daily and in cron on purpose: a mismatch appears when the Hub is redeployed, * not when someone opens an admin page, and this must never make a customer * wait on a network call. No-ops on sites with no Hub connection. * * @return void */ function ai_o_optimiser_refresh_hub_compatibility() { if ( ! function_exists( 'ai_o_optimiser_guidance_load_connector_client' ) ) { return; } ai_o_optimiser_guidance_load_connector_client(); require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-hub-compatibility.php'; AI_O_Optimiser_Hub_Compatibility::refresh( true ); } add_action( 'ai_o_optimiser_daily_cleanup', 'ai_o_optimiser_core_daily_cleanup' ); add_action( 'ai_o_optimiser_daily_cleanup', 'ai_o_optimiser_refresh_bot_ranges', 40 ); add_action( 'ai_o_optimiser_daily_cleanup', 'ai_o_optimiser_refresh_hub_compatibility', 50 ); add_action( 'ai_o_optimiser_visibility_aggregate_backfill', 'ai_o_optimiser_core_process_aggregate_backfill' ); add_action( AI_O_Optimiser_NIGHTLY_HEALTH_HOOK, 'ai_o_optimiser_guidance_run_scheduled_health_check' ); add_action( 'plugins_loaded', function() { ai_o_optimiser_maybe_upgrade(); ai_o_optimiser_visibility_boot(); ai_o_optimiser_guidance_boot(); // Admin/cron-only Content Pal evidence, Hub bridge and client email reports. if ( is_admin() || ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { require_once __DIR__ . '/class-ai-o-optimiser-email-report.php'; AI_O_Optimiser_Email_Report::boot(); require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-content-pal-performance-signals.php'; AI_O_Optimiser_Content_Pal_Performance_Signals::boot(); require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-hub-bridge.php'; AI_O_Optimiser_Hub_Bridge::boot(); } if ( is_admin() ) { ai_o_optimiser_ensure_sensitive_capability(); ai_o_optimiser_load_admin_classes(); require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-category-health-scope-guard.php'; AI_O_Optimiser_Category_Health_Scope_Guard::boot(); new AI_O_Optimiser_Admin_Notice_Boundary(); new AI_O_Optimiser_System_Check(); new AI_O_Optimiser_Environment_Check(); new AI_O_Optimiser_LLMS_Admin(); AI_O_Optimiser_Feature_Cards::boot(); require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-site-profile.php'; AI_O_Optimiser_Site_Profile::boot(); $ai_o_optimiser_admin_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing only. // Shared with the Free edition -- not part of the Local/NAP // Pro-exclusive workspace, so never gated by entitlement. $admin_boot = array( 'class-ai-o-optimiser-advisory-disclaimers.php' => array( 'AI_O_Optimiser_Advisory_Disclaimers', 'boot' ), 'class-ai-o-optimiser-site-profile-ui.php' => array( 'AI_O_Optimiser_Site_Profile_UI', 'boot' ), 'class-ai-o-optimiser-site-profile-guidance.php' => array( 'AI_O_Optimiser_Site_Profile_Guidance', 'boot' ), 'class-ai-o-optimiser-site-profile-ai-guidance.php' => array( 'AI_O_Optimiser_Site_Profile_AI_Guidance', 'boot' ), ); foreach ( $admin_boot as $file => $callback ) { require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/' . $file; call_user_func( $callback ); } require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-migration.php'; AI_O_Optimiser_Migration::boot_admin_notice(); new AI_O_Optimiser_Menu_State(); new AI_O_Optimiser_System_Impact(); require_once AI_O_Optimiser_PLUGIN_DIR . 'includes/class-ai-o-optimiser-pro-features.php'; add_action( 'admin_enqueue_scripts', array( 'AI_O_Optimiser_Pro_Features', 'enqueue_styles' ), 12 ); $ai_o_optimiser_admin = new AI_O_Optimiser_Admin(); new AI_O_Optimiser_Primary_Navigation( $ai_o_optimiser_admin ); AI_O_Optimiser_Admin_Live_Feedback::boot(); new AI_O_Optimiser_Admin_Components(); new AI_O_Optimiser_Monthly_Report(); return; } } );