=' )
&& 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;
}
}
);