version = '34.1.1'; $this->plugin_name = 'ailinkmaster-internal-auto-manager'; } public function run() { $this->load_dependencies(); // Auto-migrate data from v1 architecture on first load after update. AILM_Migrator::maybe_migrate(); if (is_admin()) { $this->define_admin_hooks(); } $this->define_public_hooks(); } private function load_dependencies() { // Unified link management system (v2 architecture) require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-link-db.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-link-repository.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-migrator.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-elementor-handler.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-dom-helper.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-link-builder.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-link-validator.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-reconciler.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-sync-queue.php'; require_once plugin_dir_path(__DIR__) . 'includes/class-ailm-import-existing.php'; // Include 404 Scout module core (DB, Scanner, and Checker) require_once plugin_dir_path(__DIR__) . 'modules/404-scout/class-ailm-scout-db.php'; require_once plugin_dir_path(__DIR__) . 'modules/404-scout/class-ailm-scout-scanner.php'; require_once plugin_dir_path(__DIR__) . 'modules/404-scout/class-ailm-scout-checker.php'; // Clicks Stats Module require_once plugin_dir_path(__DIR__) . 'modules/clicks-stats/class-ailm-clicks-db.php'; require_once plugin_dir_path(__DIR__) . 'modules/clicks-stats/class-ailm-clicks-tracker.php'; // Include AI Core module require_once plugin_dir_path(__DIR__) . 'modules/ai-core/interfaces/interface-ailm-ai-provider.php'; require_once plugin_dir_path(__DIR__) . 'modules/ai-core/providers/class-ailm-openai-provider.php'; require_once plugin_dir_path(__DIR__) . 'modules/ai-core/class-ailm-ai-bridge.php'; // Include Gutenberg Sidebar module (block editor integration) require_once plugin_dir_path(__DIR__) . 'modules/gutenberg-sidebar/class-ailm-gutenberg-sidebar.php'; if (is_admin()) { require_once plugin_dir_path(__DIR__) . 'admin/class-ailm-admin.php'; require_once plugin_dir_path(__DIR__) . 'modules/orphan-rescue/class-ailm-orphan-admin.php'; // Include UI and AJAX handlers for 404 Scout (admin only) require_once plugin_dir_path(__DIR__) . 'modules/404-scout/class-ailm-scout-admin.php'; require_once plugin_dir_path(__DIR__) . 'modules/404-scout/class-ailm-scout-background-processor.php'; // Load Keyword Intelligence module files require_once plugin_dir_path(__DIR__) . 'modules/keyword-intelligence/class-ailm-ki-admin.php'; require_once plugin_dir_path(__DIR__) . 'modules/keyword-intelligence/class-ailm-ki-analyzer.php'; require_once plugin_dir_path(__DIR__) . 'modules/keyword-intelligence/class-ailm-ki-data-provider.php'; } // URL Migrator module (redirects + content rewriting on permalink change). require_once plugin_dir_path(__DIR__) . 'modules/url-migrator/class-ailm-url-redirect-db.php'; require_once plugin_dir_path(__DIR__) . 'modules/url-migrator/class-ailm-url-snapshot.php'; require_once plugin_dir_path(__DIR__) . 'modules/url-migrator/class-ailm-url-redirect-handler.php'; require_once plugin_dir_path(__DIR__) . 'modules/url-migrator/class-ailm-url-content-rewriter.php'; require_once plugin_dir_path(__DIR__) . 'modules/url-migrator/class-ailm-url-migrator-engine.php'; require_once plugin_dir_path(__DIR__) . 'modules/url-migrator/class-ailm-url-rollback.php'; } private function define_admin_hooks() { $plugin_admin = new AILM_Admin($this->get_plugin_name(), $this->get_version(), $this); add_action('admin_menu', array($plugin_admin, 'add_plugin_admin_menu')); new AILM_Scout_Admin(); $keyword_intelligence_admin = new AILM_KI_Admin(); add_action('admin_enqueue_scripts', array($keyword_intelligence_admin, 'enqueue_scripts')); AILM_Scout_Background_Processor::init(); new AILM_Orphan_Admin($this->get_plugin_name(), $this->get_version(), $this); // Phase 6: Bulk import tool for existing sites. AILM_Import_Existing::init(); // URL Migrator module: admin AJAX endpoints + asset enqueue. require_once plugin_dir_path(__DIR__) . 'modules/url-migrator/class-ailm-url-migrator-admin.php'; AILM_URL_Migrator_Admin::init(); add_action('admin_enqueue_scripts', array('AILM_URL_Migrator_Admin', 'enqueue_assets')); // URL Migrator: content rewriter AJAX endpoints + cron fallback. // Registers ailm_url_rewrite_start/status/cancel and the cron interval. // Without this, the optional "Rewrite content URLs" feature cannot // advance (progress stays at 0%). AILM_URL_Content_Rewriter::init(); // 2. Auto-update scanner when links are "Frozen" by the main plugin // DEPRECATED v2.0: _ailm_frozen_map meta is replaced by wp_ailm_link_rules. // The scanner is now triggered explicitly after rule changes via AILM_Link_Repository::sync_donor(). /* add_action( 'updated_post_meta', array( $this, 'scout_sync_on_freeze' ), 10, 4 ); add_action( 'added_post_meta', array( $this, 'scout_sync_on_freeze' ), 10, 4 ); */ } private function define_public_hooks() { $clicks_tracker = new AILM_Clicks_Tracker(); $clicks_tracker->init(); // Gutenberg Sidebar module must be initialised outside the is_admin() // guard because REST API requests (wp-json) are NOT admin requests. // The enqueue_block_editor_assets hook self-gates to the block editor. new AILM_Gutenberg_Sidebar(); add_action('save_post', array('AILM_Scout_Scanner', 'process_post'), 10, 1); add_action('save_post', array($this, 'handle_auto_index_on_save'), 10, 3); // Reconciliation engine: runs AFTER the scanner (priority 10) so that // wp_ailm_links is already updated. Brings wp_ailm_link_rules in sync // with what the scanner found in the HTML. add_action('save_post', array('AILM_Reconciler', 'handle_save_post'), 20, 3); // Auto-reindex cron: processes posts flagged by handle_auto_index_on_save // every 10 minutes (instead of instantly on every save_post/autosave). // This prevents token drain when editors write articles with WordPress // autosaving every 60 seconds. add_filter('cron_schedules', array($this, 'add_cron_schedule_10_minutes')); add_action('ailm_auto_reindex_cron', array($this, 'handle_auto_reindex_cron')); if (!wp_next_scheduled('ailm_auto_reindex_cron')) { wp_schedule_event(time(), 'ailm_every_10_minutes', 'ailm_auto_reindex_cron'); } // Phase 5: SaaS async replication. Schedules the cron worker that // pushes queued rule changes to the SaaS server every 5 minutes. AILM_Sync_Queue::init(); // URL Migrator: front-end 301 redirect handler on parse_request // (priority 1, before redirect_canonical). NOT inside is_admin(). if ( class_exists( 'AILM_URL_Redirect_Handler' ) ) { AILM_URL_Redirect_Handler::init(); } // URL Migrator: capture the permalink structure as a baseline on first // load so future changes are detectable. if ( class_exists( 'AILM_URL_Snapshot' ) && ! AILM_URL_Snapshot::get() ) { AILM_URL_Snapshot::capture_current(); } } public function get_plugin_name() { return $this->plugin_name; } public function get_version() { return $this->version; } public function scout_sync_on_freeze($meta_id, $post_id, $meta_key, $meta_value) { // DEPRECATED v2.0: _ailm_frozen_map meta is replaced by wp_ailm_link_rules. /* if ( '_ailm_frozen_map' === $meta_key ) { if ( class_exists( 'AILM_Scout_Scanner' ) ) { AILM_Scout_Scanner::process_post( $post_id ); } } */ } public function handle_auto_index_on_save($post_id, $post, $update) { // Prevent autosaves if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) { return; } $allowed_types = get_option('ailm_allowed_post_types', ['post', 'page']); if (!in_array($post->post_type, $allowed_types)) { return; } delete_post_meta($post_id, '_ailm_is_indexed'); // Auto-index: flag the post for reindexing instead of sending immediately. // The actual SaaS request is handled by the ailm_auto_reindex_cron event // (every 5 minutes) to avoid burning tokens on every autosave (WordPress // autosaves published posts every 60 seconds, which previously caused // tight re-index loops). Manual reindexing (admin button, REST API, // smart-link apply, batch operations) remains instant. if (get_option('ailm_auto_index', 0)) { // Do not flag if the hard limit has been reached if (get_option('linkmesh_index_limit_reached', false)) { return; } update_post_meta($post_id, '_ailm_needs_reindex', 1); } } /** * Cron handler: process posts flagged for auto-reindexing. * * Runs every 5 minutes via the ailm_auto_reindex_cron event. Finds all * posts with _ailm_needs_reindex = 1, sends them to the SaaS for indexing * and clears the flag. This batches multiple autosaves into a single * index operation per 5-minute window. * * Note: only the index request is sent here. The SaaS server performs * content-hash deduplication: if the content has not changed since the * last successful indexing, the server returns "already_indexed" without * spending tokens. When the content has changed, the server-side task * replaces the old chunks (upsert). Sending an explicit DELETE before * indexing would wipe the chunks and defeat the deduplication check, so * it is intentionally omitted here. */ public function handle_auto_reindex_cron() { if (!get_option('ailm_auto_index', 0)) { return; } // Do not send index requests if the hard limit has been reached if (get_option('linkmesh_index_limit_reached', false)) { return; } if (!class_exists('AILM_OpenAI_Provider')) { require_once plugin_dir_path(__DIR__) . 'modules/ai-core/providers/class-ailm-openai-provider.php'; } $allowed_types = get_option('ailm_allowed_post_types', ['post', 'page']); // Find posts flagged for reindexing (limit to 50 per run to avoid timeouts) $flagged_posts = get_posts([ 'post_type' => $allowed_types, 'post_status' => 'publish', 'posts_per_page' => 50, 'meta_key' => '_ailm_needs_reindex', 'meta_value' => 1, 'fields' => 'ids', ]); if (empty($flagged_posts)) { return; } $provider = new AILM_OpenAI_Provider(); foreach ($flagged_posts as $post_id) { $post = get_post($post_id); if (!$post || empty($post->post_content)) { delete_post_meta($post_id, '_ailm_needs_reindex'); continue; } // Index the latest content. The SaaS server deduplicates by // content hash: unchanged content returns "already_indexed" (no // tokens spent); changed content triggers a server-side upsert // that replaces the old chunks. No explicit DELETE is sent, // because wiping chunks would bypass the deduplication check. $provider->saas_index_document($post_id, $post->post_content); // Clear the flag regardless of success/failure (avoids infinite retry) delete_post_meta($post_id, '_ailm_needs_reindex'); } } /** * Register custom cron schedule: every 5 minutes. * * The internal key 'ailm_every_10_minutes' is kept for backward * compatibility so that already-scheduled events on existing installs * keep matching a registered schedule after the plugin update. Only the * interval value changes (600s -> 300s); WordPress picks up the new * interval on the next scheduled run. * * @param array $schedules Existing cron schedules. * @return array Modified schedules. */ public function add_cron_schedule_10_minutes($schedules) { $schedules['ailm_every_10_minutes'] = [ 'interval' => 300, 'display' => __('Every 5 Minutes', 'ailinkmaster-internal-auto-manager'), ]; return $schedules; } }