=' ) && version_compare( get_bloginfo( 'version' ), ATTENDANT_REQUIRED_WP, '>=' ); } if ( ! attendant_requirements_met() ) { add_action( 'admin_notices', function (): void { printf( '

%s

', wp_kses_post( sprintf( /* translators: 1: Required PHP version, 2: Required WP version */ __( 'Attendant requires PHP %1$s and WordPress %2$s or higher. Please upgrade your environment.', 'attendant' ), esc_html( ATTENDANT_REQUIRED_PHP ), esc_html( ATTENDANT_REQUIRED_WP ) ) ) ); } ); return; // Do not load any further plugin code. } // ------------------------------------------------------------------------- // Main plugin class — singleton. // Loaded only after requirements are confirmed. // ------------------------------------------------------------------------- /** * Main plugin class. * * Responsible only for bootstrapping: loading dependencies and registering * top-level WordPress hooks. Business logic lives in dedicated classes. */ final class Attendant_Plugin { /** * Single instance of this class. * * @var Attendant_Plugin|null */ private static ?Attendant_Plugin $instance = null; /** * Get or create the singleton instance. * * @return Attendant_Plugin */ public static function instance(): Attendant_Plugin { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Constructor — private to enforce singleton. */ private function __construct() { $this->load_dependencies(); $this->register_hooks(); } /** * Prevent cloning of the singleton instance. */ private function __clone() {} /** * Load all required class files. * * Admin-only and CLI-only files are loaded conditionally to avoid * loading dead code on the frontend or in non-CLI contexts. */ private function load_dependencies(): void { // Core utilities (always needed). require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-encryption.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-field-config.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-onboarding.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-billing.php'; // Schema classes — needed by REST API and cron handlers. require_once ATTENDANT_PLUGIN_DIR . 'includes/schema/class-attendant-schema-cache.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/schema/class-attendant-field-detector.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/schema/class-attendant-schema-discovery.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/schema/class-attendant-schema-catalog.php'; // Indexing pipeline — loaded on every request because: // a) WP-Cron fires via HTTP on any page load. // b) Auto-sync hooks (save_post, before_delete_post) fire everywhere. // c) REST /index/start calls ATTENDANT_Index_Manager from any origin. require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-content-fetcher.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-text-extractor.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-chunker.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-embedder.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-index-manager.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-auto-sync.php'; // Chat engine — loaded on every request because REST is always active. require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-rag-retriever.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-query-builder.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-qa-manager.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-conversation-handler.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-chat-log.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-leads.php'; require_once ATTENDANT_PLUGIN_DIR . 'includes/integrations/class-attendant-slack.php'; // REST API (always needed — REST is active on all requests). require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-rest-api.php'; // Admin-only classes. if ( is_admin() ) { require_once ATTENDANT_PLUGIN_DIR . 'admin/class-attendant-admin.php'; } // Frontend-only class (not needed in admin or WP-Cron context). if ( ! is_admin() ) { require_once ATTENDANT_PLUGIN_DIR . 'public/class-attendant-frontend.php'; } } /** * Register all WordPress action and filter hooks. * * Hooks that drive admin UI, REST registration, and background tasks. * No business logic here — just hookup. */ private function register_hooks(): void { // Register our custom 5-minute cron interval for the indexing queue. add_filter( 'cron_schedules', array( $this, 'add_cron_schedules' ) ); // REST API registration runs on every request (REST is stateless). add_action( 'rest_api_init', array( $this, 'init_rest_api' ) ); // Admin initialisation only in the dashboard. if ( is_admin() ) { add_action( 'init', array( $this, 'init_admin' ) ); } // Frontend widget — only on frontend requests (not admin, not WP-Cron). if ( ! is_admin() ) { add_action( 'init', array( $this, 'init_frontend' ) ); } // Cron action handlers — must be registered on all requests so WP-Cron // can call them via HTTP when they are scheduled to fire. add_action( 'attendant_weekly_schema_scan', array( $this, 'run_weekly_schema_scan' ) ); add_action( 'attendant_process_index_queue', array( $this, 'process_index_queue' ) ); // Background-mode indexing loopback endpoint. Both hooks are required: // loopback requests carry no cookies, so they always arrive // unauthenticated (nopriv); the secret-key check happens inside. add_action( 'wp_ajax_attendant_async_index', array( 'ATTENDANT_Index_Manager', 'handle_async_request' ) ); add_action( 'wp_ajax_nopriv_attendant_async_index', array( 'ATTENDANT_Index_Manager', 'handle_async_request' ) ); // Post lifecycle hooks — auto-sync must fire on every request type // (admin, frontend, REST, WP-Cron) since post saves can happen anywhere. ATTENDANT_Auto_Sync::init(); } /** * Register custom WP-Cron intervals used by this plugin. * * @param array $schedules Existing schedules. * @return array Modified schedules. */ public function add_cron_schedules( array $schedules ): array { if ( ! isset( $schedules['attendant_five_minutes'] ) ) { $schedules['attendant_five_minutes'] = array( 'interval' => 5 * MINUTE_IN_SECONDS, 'display' => __( 'Every 5 minutes (Attendant)', 'attendant' ), ); } return $schedules; } /** * Initialise the REST API controller. * * Callback for the `rest_api_init` action. */ public function init_rest_api(): void { $controller = new ATTENDANT_REST_API(); $controller->register_routes(); } /** * Initialise the admin area. * * Callback for the `init` action (admin-only). */ public function init_admin(): void { ATTENDANT_Admin::instance(); } /** * Initialise the frontend widget. * * Callback for the `init` action (frontend-only). * Bootstraps ATTENDANT_Frontend which registers enqueue and footer hooks. */ public function init_frontend(): void { ATTENDANT_Frontend::instance(); } /** * Run the weekly schema re-scan. * * Callback for the `attendant_weekly_schema_scan` WP-Cron action. * Discovers all post types, taxonomies, and custom fields; caches * the result in wp_options so the AI always has an up-to-date picture * of the site's content structure. */ public function run_weekly_schema_scan(): void { ATTENDANT_Schema_Discovery::run(); } /** * Process the next batch of the content indexing queue. * * Callback for the `attendant_process_index_queue` WP-Cron action. * Fires every 5 minutes. If the queue is empty or a concurrency lock * is held, the method exits immediately without doing any work. */ public function process_index_queue(): void { ATTENDANT_Index_Manager::process_queue_batch(); } /** * Retrieve a plugin setting. * * @param string|null $key Setting key, or null to return all settings. * @param mixed $default Default value when the key is absent. * @return mixed */ public static function get_setting( ?string $key = null, mixed $default = null ): mixed { $settings = get_option( 'attendant_settings', array() ); if ( null === $key ) { return $settings; } return $settings[ $key ] ?? $default; } /** * Persist a single plugin setting. * * @param string $key Setting key. * @param mixed $value New value. */ public static function update_setting( string $key, mixed $value ): void { $settings = self::get_setting(); $settings[ $key ] = $value; update_option( 'attendant_settings', $settings ); } } /** * Global helper — returns the single plugin instance. * * Allows other code to call attendant() without using statics directly. * * @return Attendant_Plugin */ function attendant(): Attendant_Plugin { return Attendant_Plugin::instance(); } // Boot the plugin on `plugins_loaded` so that all plugins are available and // WordPress is in a consistent state before we do anything. add_action( 'plugins_loaded', 'attendant' ); /** * Run schema/option migrations when plugin files were updated without the * activation hook firing (normal WP update flow). Cheap check per request; * real work happens once, until attendant_db_version catches up. * * Admin/cron requests only — migrations include an ALTER TABLE that must not * land on a random visitor pageview, and every update flow involves wp-admin. * Priority 20: the bootstrap (priority 10) registers the custom cron * schedule that schedule_cron_events() relies on. */ function attendant_maybe_run_upgrade_migrations(): void { if ( ! is_admin() && ! wp_doing_cron() ) { return; } $stored = (string) get_option( 'attendant_db_version', '0.0.0' ); if ( version_compare( $stored, ATTENDANT_VERSION, '>=' ) ) { return; } if ( ! class_exists( 'ATTENDANT_Activator' ) ) { require_once ATTENDANT_PLUGIN_DIR . 'includes/class-attendant-activator.php'; } ATTENDANT_Activator::run_migrations(); } add_action( 'plugins_loaded', 'attendant_maybe_run_upgrade_migrations', 20 );