}> */ public static function hooks(): array { static $cache = null; if ( null !== $cache ) { return $cache; } $file = __DIR__ . '/hooks.json'; $data = is_readable( $file ) ? json_decode( (string) file_get_contents( $file ), true ) : null; $cache = is_array( $data['hooks'] ?? null ) ? $data['hooks'] : array(); return $cache; } public function routes(): void { register_rest_route( self::NAMESPACE, '/dev/status', array( 'methods' => 'GET', 'callback' => array( $this, 'status' ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), ) ); register_rest_route( self::NAMESPACE, '/dev/maintenance', array( 'methods' => 'POST', 'callback' => array( $this, 'maintenance' ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), 'args' => array( 'task' => array( 'type' => 'string', 'required' => true, 'enum' => array( 'flush_transients', 'flush_rewrites', 'rerun_migration', 'clear_quarantine', 'reanalyze_all' ), ), ), ) ); } public function status(): WP_REST_Response { global $wpdb; $tables = array(); foreach ( array( 'redirects' => Database::redirects(), '404s' => Database::not_found(), 'links' => Database::links(), 'keywords' => Database::keywords(), 'ranks' => Database::ranks(), 'mentions' => Database::mentions(), 'found_links' => Database::found_links(), ) as $label => $table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $tables[] = array( 'name' => $table, 'rows' => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i", $table ) ), ); } $crons = array(); foreach ( array( 'airanksuite_reports_send', 'airanksuite_ai_visibility_check' ) as $hook ) { $next = wp_next_scheduled( $hook ); $crons[] = array( 'hook' => $hook, 'next' => $next ? gmdate( 'Y-m-d H:i', $next ) . ' UTC' : null, ); } $routes = array(); $server = rest_get_server(); foreach ( $server->get_routes( self::NAMESPACE ) as $route => $handlers ) { $methods = array(); foreach ( $handlers as $handler ) { $methods = array_merge( $methods, array_keys( (array) ( $handler['methods'] ?? array() ) ) ); } $routes[] = array( 'route' => $route, 'methods' => array_values( array_unique( $methods ) ), ); } $kernel = \AiRankSuite\Kernel::instance(); return new WP_REST_Response( array( 'environment' => array( 'plugin' => AIRANKSUITE_VERSION, 'db_schema' => (string) get_option( 'airanksuite_db_version', '' ), 'wordpress' => get_bloginfo( 'version' ), 'php' => PHP_VERSION, 'mysql' => $wpdb->db_version(), 'memory' => WP_MEMORY_LIMIT, 'permalinks' => (bool) get_option( 'permalink_structure' ), 'ssl' => is_ssl() || str_starts_with( home_url(), 'https://' ), 'locale' => get_locale(), 'multisite' => is_multisite(), 'theme' => wp_get_theme()->get( 'Name' ) . ' ' . wp_get_theme()->get( 'Version' ), ), 'modules' => array( 'active' => $kernel->moduleIds(), 'quarantine' => (array) get_option( 'airanksuite_quarantine', array() ), 'disabled' => (array) get_option( 'airanksuite_disabled_modules', array() ), ), // Callbacks on wp_head, template_redirect and friends that // threw and were caught so the page could finish. The visitor // saw a working site; somebody still has to see this. 'hook_faults' => \AiRankSuite\Support\Guard::failures(), 'tables' => $tables, 'crons' => $crons, 'routes' => $routes, 'hooks' => self::hooks(), 'constants' => array( 'WP_DEBUG' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'WP_CRON' => ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ), 'OBJECT_CACHE' => wp_using_ext_object_cache(), ), ) ); } public function maintenance( WP_REST_Request $request ): WP_REST_Response { global $wpdb; $task = (string) $request->get_param( 'task' ); switch ( $task ) { case 'flush_transients': // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s OR option_name LIKE %s', $wpdb->options, $wpdb->esc_like( '_transient_airanksuite_' ) . '%', $wpdb->esc_like( '_transient_timeout_airanksuite_' ) . '%' ) ); $message = __( 'All plugin caches cleared.', 'ai-rank-suite' ); break; case 'flush_rewrites': \AiRankSuite\Modules\LlmsTxt\LlmsTxtModule::add_rewrite(); \AiRankSuite\Modules\Sitemap\SitemapModule::add_rewrite(); \AiRankSuite\Modules\IndexNow\IndexNowModule::add_rewrite(); flush_rewrite_rules(); $message = __( 'Rewrite rules rebuilt — llms.txt, sitemaps and the IndexNow key file re-registered.', 'ai-rank-suite' ); break; case 'rerun_migration': delete_option( 'airanksuite_db_version' ); \AiRankSuite\Support\Migrator::maybe_upgrade(); $message = __( 'Database migration re-executed.', 'ai-rank-suite' ); break; case 'clear_quarantine': delete_option( 'airanksuite_quarantine' ); \AiRankSuite\Support\Guard::clear(); $message = __( 'Quarantine cleared — failed modules will retry on the next load, and the front-end fault log is empty again.', 'ai-rank-suite' ); break; case 'reanalyze_all': // Drop cached analyses; pages re-analyze lazily on next view. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s", '_airanksuite_analysis' ) ); $message = __( 'Analysis cache dropped — every page will be re-scored on next access.', 'ai-rank-suite' ); break; default: $message = ''; } return new WP_REST_Response( array( 'message' => $message ) ); } }