container = $container; } /** * Register all hooks. * * @return void */ public function register(): void { $has_woocommerce = function_exists( 'aicommerce_has_woocommerce' ) && aicommerce_has_woocommerce(); // Website-centric features — run on any site, with or without WooCommerce. $this->register_core_hooks(); $this->register_schema_hooks(); $this->register_manifest_hooks(); $this->register_crawler_hooks(); $this->register_llms_txt_hooks(); $this->register_rest_hooks(); $this->register_admin_hooks(); $this->register_multilingual_hooks(); $this->register_whitelabel_hooks(); $this->register_addon_hooks(); $this->register_metrics_hooks(); // Translations are auto-loaded by WordPress.org since WP 4.6; no // load_plugin_textdomain() call is needed. // Ecommerce features — only when WooCommerce is active. if ( $has_woocommerce ) { $this->register_feed_hooks(); $this->register_gmc_hooks(); $this->register_bing_api_hooks(); $this->register_builder_hooks(); $this->register_content_hooks(); $this->register_bulk_hooks(); $this->register_image_alt_hooks(); $this->register_attribution_hooks(); $this->register_digest_hooks(); $this->register_mcp_hooks(); $this->register_soa_hooks(); } } /** * Register core (front-end + back-end) hooks. * * @return void */ private function register_core_hooks(): void { /** @var Database $database */ $database = $this->container->get( Database::class ); // Run migrations on admin_init so dbDelta has access to upgrade.php. add_action( 'admin_init', array( $database, 'migrate' ) ); // Plugin action links on Plugins page. add_filter( 'plugin_action_links_' . AICOMMERCE_PLUGIN_BASENAME, array( $this, 'add_action_links' ) ); } /** * Register Schema Engine hooks. * * Injects JSON-LD at priority 5 so it's among the first things in
. * Also registers conflict detection and suppression, and cache invalidation. * * @return void */ private function register_schema_hooks(): void { if ( ! $this->container->has( SchemaEngine::class ) ) { return; } /** @var SchemaEngine $schema_engine */ $schema_engine = $this->container->get( SchemaEngine::class ); $has_woocommerce = function_exists( 'aicommerce_has_woocommerce' ) && aicommerce_has_woocommerce(); // Inject site-level schema (WebSite + Organization/OnlineStore/LocalBusiness) // on the home/shop page for every website type. if ( $this->container->has( \AICommerce\Schema\SiteSchema::class ) ) { /** @var \AICommerce\Schema\SiteSchema $site_schema */ $site_schema = $this->container->get( \AICommerce\Schema\SiteSchema::class ); add_action( 'wp_head', array( $site_schema, 'inject' ), 5 ); } // Inject content schema (Article for posts, WebPage for pages, breadcrumbs) // on singular non-product content — this works on any site. if ( $this->container->has( \AICommerce\Schema\ContentSchemaEngine::class ) ) { /** @var \AICommerce\Schema\ContentSchemaEngine $content_schema */ $content_schema = $this->container->get( \AICommerce\Schema\ContentSchemaEngine::class ); add_action( 'wp_head', array( $content_schema, 'inject' ), 5 ); } // The remaining product-specific wiring requires WooCommerce. if ( ! $has_woocommerce ) { return; } // Inject product JSON-LD on product pages — priority 5, before most themes. add_action( 'wp_head', array( $schema_engine, 'inject' ), 5 ); /** @var ConflictDetector $conflict_detector */ $conflict_detector = $schema_engine->get_conflict_detector(); // Detect and suppress conflicting SEO plugin schema on init. add_action( 'wp', static function () use ( $conflict_detector ): void { if ( function_exists( 'is_product' ) && is_product() ) { $conflict_detector->detect(); $conflict_detector->suppress(); } } ); // Clear validation + schema cache when a product is saved. if ( $this->container->has( Validator::class ) ) { /** @var Validator $validator */ $validator = $this->container->get( Validator::class ); add_action( 'woocommerce_update_product', static function ( int $product_id ) use ( $validator, $schema_engine ): void { $validator->clear_cache( $product_id ); $schema_engine->clear_schema_cache( $product_id ); } ); } } /** * Register feed generation hooks. * * Sets up Action Scheduler chunk processing, product update triggers * for dirty marking, and recurring generation schedules. * * @return void */ private function register_feed_hooks(): void { if ( ! $this->container->has( FeedGenerator::class ) ) { return; } /** @var FeedGenerator $generator */ $generator = $this->container->get( FeedGenerator::class ); /** @var ChangeDetector $change_detector */ $change_detector = $this->container->get( ChangeDetector::class ); // Register Action Scheduler chunk handler. add_action( FeedGenerator::CHUNK_ACTION, array( $generator, 'process_chunk' ), 10, 1 ); // Handle the recurring generation trigger. add_action( FeedGenerator::GENERATE_ACTION, static function () use ( $generator ): void { $generator->schedule_full_generation(); } ); // Schedule recurring feed generation on admin_init. add_action( 'admin_init', static function () use ( $generator ): void { $generator->schedule_recurring(); } ); // Mark products dirty when updated via WooCommerce. $mark_dirty = static function ( int $product_id ) use ( $change_detector ): void { $change_detector->mark_dirty( $product_id ); }; add_action( 'woocommerce_update_product', $mark_dirty ); add_action( 'woocommerce_new_product', $mark_dirty ); add_action( 'woocommerce_variation_saved', $mark_dirty ); // This hook passes a WC_Product object, not an int. add_action( 'woocommerce_product_object_updated_props', static function ( $product ) use ( $change_detector ): void { if ( $product instanceof \WC_Product ) { $change_detector->mark_dirty( $product->get_id() ); } } ); // Stock status and quantity changes. add_action( 'woocommerce_product_set_stock_status', static function ( int $product_id ) use ( $change_detector ): void { $change_detector->mark_dirty( $product_id ); } ); add_action( 'woocommerce_product_set_stock', static function ( $product ) use ( $change_detector ): void { if ( $product instanceof \WC_Product ) { $change_detector->mark_dirty( $product->get_id() ); } } ); // Register custom cron interval as fallback. add_filter( 'cron_schedules', array( FeedGenerator::class, 'add_cron_interval' ) ); } /** * Register /.well-known/ai-commerce.json manifest hooks. * * @return void */ private function register_manifest_hooks(): void { if ( ! $this->container->has( ManifestEndpoint::class ) ) { return; } /** @var ManifestEndpoint $manifest */ $manifest = $this->container->get( ManifestEndpoint::class ); add_action( 'init', array( $manifest, 'register_rewrite_rule' ) ); add_action( 'template_redirect', array( $manifest, 'maybe_serve' ) ); add_filter( 'query_vars', array( $manifest, 'add_query_var' ) ); } /** * Register Google Merchant Center hooks. * * @return void */ private function register_gmc_hooks(): void { if ( ! $this->container->has( GMCApiClient::class ) ) { return; } /** @var GMCApiClient $gmc_client */ $gmc_client = $this->container->get( GMCApiClient::class ); // Daily feed submission to GMC. add_action( 'aicommerce_gmc_submit', static function () use ( $gmc_client ): void { $file_manager = new \AICommerce\Feed\FeedFileManager(); $feed_url = $file_manager->get_feed_url( 'google-feed.xml' ); $gmc_client->submit_feed( $feed_url ); } ); // Schedule daily submission on admin_init. add_action( 'admin_init', static function () use ( $gmc_client ): void { $gmc_client->schedule_daily_submission(); } ); } /** * Register crawler monitoring hooks. * * @return void */ private function register_crawler_hooks(): void { if ( ! $this->container->has( CrawlerMonitor::class ) ) { return; } /** @var CrawlerMonitor $crawler */ $crawler = $this->container->get( CrawlerMonitor::class ); // Detect and log AI bot visits on front-end requests. add_action( 'template_redirect', array( $crawler, 'handle_request' ), 1 ); // Async log writer. add_action( 'aicommerce_log_crawler_visit', array( $crawler, 'log_visit' ), 10, 1 ); // Robots.txt filter. if ( $this->container->has( RobotsManager::class ) ) { /** @var RobotsManager $robots */ $robots = $this->container->get( RobotsManager::class ); add_filter( 'robots_txt', array( $robots, 'filter_robots_txt' ), 100, 2 ); } } /** * Register /llms.txt endpoint hooks. * * @return void */ private function register_llms_txt_hooks(): void { if ( ! $this->container->has( LlmsTxtGenerator::class ) ) { return; } /** @var LlmsTxtGenerator $llms */ $llms = $this->container->get( LlmsTxtGenerator::class ); add_action( 'init', array( $llms, 'register_rewrite_rule' ) ); add_action( 'template_redirect', array( $llms, 'maybe_serve' ) ); add_filter( 'query_vars', array( $llms, 'add_query_var' ) ); } /** * Register Bing IndexNow API hooks. * * @return void */ private function register_bing_api_hooks(): void { if ( ! $this->container->has( BingApiClient::class ) ) { return; } /** @var BingApiClient $bing_client */ $bing_client = $this->container->get( BingApiClient::class ); // Serve IndexNow key verification file. add_action( 'template_redirect', array( $bing_client, 'maybe_serve_key_file' ) ); // Schedule daily IndexNow ping. add_action( 'admin_init', static function () use ( $bing_client ): void { $bing_client->schedule_daily_ping(); } ); // IndexNow ping action. add_action( 'aicommerce_indexnow_ping', static function () use ( $bing_client ): void { $file_manager = new \AICommerce\Feed\FeedFileManager(); $feed_url = $file_manager->get_feed_url( 'bing-feed.xml' ); $bing_client->notify_feed_updated( $feed_url ); } ); } /** * Register REST API hooks. * * @return void */ private function register_rest_hooks(): void { if ( ! $this->container->has( RestController::class ) ) { return; } /** @var RestController $rest */ $rest = $this->container->get( RestController::class ); add_action( 'rest_api_init', array( $rest, 'register_routes' ) ); // Clean any stray output (e.g. PHP notices from other plugins) before // the REST API sends its JSON response. Without this, WP_DEBUG_DISPLAY // can prepend HTML that corrupts the JSON body. add_filter( 'rest_pre_serve_request', static function ( bool $served ): bool { $level = ob_get_level(); while ( $level > 0 ) { ob_end_clean(); --$level; } return $served; }, 1 ); // Rate limiting. if ( $this->container->has( RateLimiter::class ) ) { /** @var RateLimiter $rate_limiter */ $rate_limiter = $this->container->get( RateLimiter::class ); add_filter( 'rest_pre_dispatch', array( $rate_limiter, 'maybe_limit' ), 10, 3 ); } } /** * Register admin-only hooks. * * @return void */ private function register_admin_hooks(): void { if ( ! is_admin() ) { return; } add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); // Admin menu. if ( $this->container->has( AdminMenu::class ) ) { /** @var AdminMenu $admin_menu */ $admin_menu = $this->container->get( AdminMenu::class ); $admin_menu->register(); } // Classic Editor meta box. if ( $this->container->has( MetaBox::class ) ) { /** @var MetaBox $meta_box */ $meta_box = $this->container->get( MetaBox::class ); $meta_box->register(); } // Gutenberg sidebar panel. if ( $this->container->has( GutenbergPanel::class ) ) { /** @var GutenbergPanel $gutenberg_panel */ $gutenberg_panel = $this->container->get( GutenbergPanel::class ); $gutenberg_panel->register(); } } /** * Register page-builder and shortcode integrations. * * Wires Elementor widget + dynamic tag, Divi module, Bricks element, * the [aicommerce_score] shortcode, and front-end badge CSS. * * @return void */ private function register_builder_hooks(): void { // Front-end badge CSS. add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_badge_styles' ) ); // [aicommerce_score] shortcode. add_shortcode( 'aicommerce_score', array( $this, 'render_score_shortcode' ) ); // Elementor integration. add_action( 'elementor/widgets/register', static function ( $widgets_manager ): void { $widgets_manager->register( new \AICommerce\Integrations\ElementorWidget() ); } ); add_action( 'elementor/elements/categories_registered', static function ( $elements_manager ): void { $elements_manager->add_category( 'aicommerce', array( 'title' => __( 'Tyralix AI Discoverability', 'ai-discoverability-kit' ), 'icon' => 'eicon-star-o', ) ); } ); add_action( 'elementor/dynamic_tags/register', static function ( $dynamic_tags_manager ): void { $dynamic_tags_manager->register( new \AICommerce\Integrations\ElementorDynamicTag() ); } ); // Divi integration. add_action( 'et_builder_ready', static function (): void { if ( class_exists( '\ET_Builder_Module' ) ) { new \AICommerce\Integrations\DiviModule(); } } ); // Bricks integration. add_action( 'init', static function (): void { if ( defined( 'BRICKS_VERSION' ) && class_exists( '\Bricks\Elements' ) ) { \Bricks\Elements::register_element( AICOMMERCE_PLUGIN_DIR . 'src/Integrations/BricksElement.php' ); } }, 11 ); } /** * Render the [aicommerce_score] shortcode. * * @param array|string $atts Shortcode attributes. * * @return string HTML output. */ public function render_score_shortcode( $atts ): string { $atts = shortcode_atts( array( 'style' => 'badge', 'show_label' => 'yes', 'color' => '#00e58a', 'font_size' => '24px', ), $atts, 'aicommerce_score' ); $product_id = get_the_ID(); if ( ! function_exists( 'wc_get_product' ) ) { return ''; } $product = wc_get_product( $product_id ); if ( ! $product ) { return ''; } if ( ! $this->container->has( \AICommerce\Analytics\ReadinessScore::class ) ) { return ''; } $readiness = $this->container->get( \AICommerce\Analytics\ReadinessScore::class ); $result = $readiness->calculate_and_cache( $product ); $score = (int) ( $result['overall'] ?? 0 ); $show_label = 'yes' === $atts['show_label']; if ( ! function_exists( 'aicommerce_render_score_badge' ) ) { return ''; } return aicommerce_render_score_badge( $score, $atts['style'], $show_label, $atts['color'], $atts['font_size'] ); } /** * Enqueue front-end score badge CSS. * * @return void */ public function enqueue_badge_styles(): void { if ( ! function_exists( 'is_product' ) || ! is_product() ) { return; } wp_register_style( 'aicommerce-badge', false, array(), AICOMMERCE_VERSION ); wp_enqueue_style( 'aicommerce-badge' ); $css = ' .aicommerce-score-badge { display:inline-block; text-align:center; } .aicommerce-badge-circle { display:inline-flex; align-items:center; justify-content:center; width:80px; height:80px; border-radius:50%; border:4px solid currentColor; font-weight:700; } .aicommerce-score-bar-wrap { width:100%; height:12px; background:#e0e0e0; border-radius:6px; overflow:hidden; margin-bottom:4px; } .aicommerce-score-bar { height:100%; border-radius:6px; transition:width .4s ease; } '; wp_add_inline_style( 'aicommerce-badge', $css ); } /** * Register content optimizer hooks (front-end toggle system). * * Injects AI-optimized descriptions and short descriptions when * the merchant has toggled them on for a specific product. * * @return void */ private function register_content_hooks(): void { if ( ! $this->container->has( ContentOptimizer::class ) ) { return; } /** @var ContentOptimizer $optimizer */ $optimizer = $this->container->get( ContentOptimizer::class ); // Inject optimized short description on product pages. add_filter( 'woocommerce_short_description', array( $optimizer, 'maybe_inject_short_description' ) ); // Inject optimized description on product pages. add_filter( 'the_content', array( $optimizer, 'maybe_inject_description' ) ); } /** * Register bulk optimization hooks. * * Wires Action Scheduler chunk processing for bulk content optimization. * * @return void */ private function register_bulk_hooks(): void { if ( ! $this->container->has( BulkQueue::class ) ) { return; } /** @var BulkQueue $bulk_queue */ $bulk_queue = $this->container->get( BulkQueue::class ); // Action Scheduler chunk handler. add_action( BulkQueue::CHUNK_ACTION, array( $bulk_queue, 'process_chunk' ), 10, 2 ); } /** * Register image alt text injection hooks. * * Injects AI-generated alt text on product images via the * wp_get_attachment_image_attributes filter. * * @return void */ private function register_image_alt_hooks(): void { if ( ! $this->container->has( ImageAltTextOptimizer::class ) ) { return; } /** @var ImageAltTextOptimizer $alt_optimizer */ $alt_optimizer = $this->container->get( ImageAltTextOptimizer::class ); add_filter( 'wp_get_attachment_image_attributes', array( $alt_optimizer, 'inject_alt_text' ), 10, 3 ); } /** * Register AI attribution tracking hooks. * * Detects AI referral sessions and records AI-attributed orders. * * @return void */ private function register_attribution_hooks(): void { if ( ! $this->container->has( AttributionTracker::class ) ) { return; } /** @var AttributionTracker $tracker */ $tracker = $this->container->get( AttributionTracker::class ); // Detect AI referral sessions on front-end page loads. add_action( 'wp', array( $tracker, 'detect_ai_session' ) ); // Record AI-attributed orders on checkout completion. add_action( 'woocommerce_thankyou', array( $tracker, 'record_ai_order' ), 10, 1 ); } /** * Register multilingual integration hooks (WPML + Polylang). * * @return void */ private function register_multilingual_hooks(): void { // WPML integration. if ( $this->container->has( WPMLIntegration::class ) && WPMLIntegration::is_available() ) { /** @var WPMLIntegration $wpml */ $wpml = $this->container->get( WPMLIntegration::class ); $wpml->register(); } // Polylang integration. if ( $this->container->has( PolylangIntegration::class ) && PolylangIntegration::is_available() ) { /** @var PolylangIntegration $polylang */ $polylang = $this->container->get( PolylangIntegration::class ); $polylang->register(); } } /** * Register white-label hooks (Agency tier only). * * @return void */ private function register_whitelabel_hooks(): void { if ( ! $this->container->has( WhiteLabel::class ) ) { return; } /** @var WhiteLabel $whitelabel */ $whitelabel = $this->container->get( WhiteLabel::class ); $whitelabel->apply(); } /** * Register weekly digest cron hooks. * * @return void */ private function register_digest_hooks(): void { if ( ! $this->container->has( WeeklyDigest::class ) ) { return; } /** @var WeeklyDigest $digest */ $digest = $this->container->get( WeeklyDigest::class ); // Schedule weekly cron. add_action( 'admin_init', array( $digest, 'schedule' ) ); // Digest send action. add_action( WeeklyDigest::CRON_HOOK, array( $digest, 'send' ) ); } /** * Register MCP (Model Context Protocol) hooks. * * Registers the MCP REST routes, discovery endpoint, and log writer. * * @return void */ private function register_mcp_hooks(): void { if ( ! $this->container->has( MCPServer::class ) ) { return; } /** @var MCPServer $mcp_server */ $mcp_server = $this->container->get( MCPServer::class ); /** @var AgentAuthenticator $agent_auth */ $agent_auth = $this->container->get( AgentAuthenticator::class ); $container = $this->container; // Register MCP REST routes. add_action( 'rest_api_init', static function () use ( $mcp_server, $agent_auth, $container ): void { // Main MCP endpoint. register_rest_route( 'aicommerce/v1', '/mcp', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $mcp_server, 'handle_request' ), 'permission_callback' => array( $agent_auth, 'verify_request' ), ) ); // MCP schema/discovery endpoint (public). register_rest_route( 'aicommerce/v1', '/mcp/schema', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $mcp_server, 'get_schema' ), 'permission_callback' => '__return_true', ) ); // MCP API key management. register_rest_route( 'aicommerce/v1', '/mcp/keys', array( array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $agent_auth, 'handle_generate_key' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'MCP API keys' ); }, ), array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $agent_auth, 'handle_list_keys' ), 'permission_callback' => static function () { return \AICommerce\API\TierGate::require_business( 'MCP API keys' ); }, ), ) ); register_rest_route( 'aicommerce/v1', '/mcp/keys/(?P