trpos($hook, 'aict-') === false) { return; } } /** * Register REST API routes */ public function register_rest_routes() { $namespace = 'aict/v1'; // Health / version endpoint (public) register_rest_route($namespace, '/health', [ 'methods' => 'GET', 'callback' => [$this, 'rest_health'], 'permission_callback' => '__return_true', ]); // Blog parity snapshot (admin only — fast SQL count) register_rest_route($namespace, '/blog/parity', [ 'methods' => 'GET', 'callback' => [$this, 'rest_blog_parity'], 'permission_callback' => function() { return current_user_can('manage_options'); }, ]); // Cache purge endpoint (admin only, for post-deploy cache clearing) register_rest_route($namespace, '/cache/purge', [ 'methods' => 'POST', 'callback' => [$this, 'rest_cache_purge'], 'permission_callback' => function() { return current_user_can('manage_options'); }, ]); // Tool execute endpoint (public, rate-limited by usage tracker) register_rest_route($namespace, '/tool/execute', [ 'methods' => 'POST', 'callback' => [$this, 'rest_tool_execute'], 'permission_callback' => '__return_true', ]); } /** * Global rate limit for all AICT REST POST endpoints. * Allows 30 POST requests per IP per minute across all tool endpoints. * Exempts webhooks and GET requests. */ public function global_rate_limit($result, $server, $request) { // Only rate-limit POST requests to our namespace $route = $request->get_route(); if ($request->get_method() !== 'POST' || strpos($route, '/aict/v1/') === false) { return $result; } // Exempt webhook (Stripe verifies its own signature) if (strpos($route, '/checkout/webhook') !== false) { return $result; } $ip = class_exists('AICT_Usage_Tracker') ? AICT_Usage_Tracker::get_client_ip() : ($_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); $rate_key = 'aict_global_rate_' . md5($ip); $attempts = (int) get_transient($rate_key); if ($attempts >= 30) { return new WP_Error( 'rate_limit_exceeded', 'Too many requests. Please slow down.', ['status' => 429] ); } set_transient($rate_key, $attempts + 1, 60); return $result; } /** * GET /wp-json/aict/v1/health */ public function rest_health() { $tool_count = class_exists('AICT_Dynamic_Routes') ? count(AICT_Dynamic_Routes::get_tools()) : 0; $lang_count = class_exists('AICT_Multilingual') ? count(AICT_Multilingual::SUPPORTED_LANGUAGES) : 18; // Public health check returns minimal info; admin gets full details if (current_user_can('manage_options')) { global $wpdb; $paid = (int) $wpdb->get_var("SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE meta_key='aict_pro_status' AND meta_value='active'"); $agency = (int) $wpdb->get_var("SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE meta_key='aict_agency_status' AND meta_value='active'"); $biz_all = (int) $wpdb->get_var("SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE meta_key='aict_business_status' AND meta_value='active'"); $creator = (int) $wpdb->get_var("SELECT COUNT(DISTINCT user_id) FROM {$wpdb->usermeta} WHERE meta_key='aict_tier' AND meta_value='creator'"); $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->users}"); return rest_ensure_response([ 'status' => 'ok', 'version' => AICT_VERSION, 'build' => AICT_BUILD_VERSION, 'php' => PHP_VERSION, 'wp' => get_bloginfo('version'), 'environment' => defined('WP_ENVIRONMENT_TYPE') ? wp_get_environment_type() : 'production', 'site_url' => home_url(), 'tool_count' => $tool_count, 'language_count' => $lang_count, 'brave_search' => defined('BRAVE_SEARCH_API_KEY') && BRAVE_SEARCH_API_KEY ? 'configured' : 'not_configured', 'smtp' => defined('BREVO_API_KEY') && BREVO_API_KEY ? 'configured' : 'not_configured', 'tier_distribution' => [ 'free' => $total - $paid, 'pro' => max(0, $paid - $biz_all - $creator), 'creator' => $creator, 'business' => max(0, $biz_all - $agency), 'agency' => $agency, ], 'timestamp' => gmdate('c'), 'uptime_check' => true, ]); } return rest_ensure_response([ 'status' => 'ok', 'tool_count' => $tool_count, 'language_count' => $lang_count, 'timestamp' => gmdate('c'), ]); } /** * GET /wp-json/aict/v1/blog/parity (admin only) * Fast SQL-based blog parity snapshot — counts published posts per _aict_language. * Returns in < 100ms vs the WP REST meta_query approach which times out. */ public function rest_blog_parity() { @set_time_limit(300); global $wpdb; $results = $wpdb->get_results( "SELECT pm.meta_value AS lang, COUNT(*) AS cnt FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_aict_language' WHERE p.post_type = 'post' AND p.post_status = 'publish' GROUP BY pm.meta_value ORDER BY cnt DESC", ARRAY_A ); $languages = []; $total = 0; foreach ($results as $row) { $languages[$row['lang']] = (int) $row['cnt']; $total += (int) $row['cnt']; } // Count posts without language meta $no_lang = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} p WHERE p.post_type = 'post' AND p.post_status = 'publish' AND NOT EXISTS ( SELECT 1 FROM {$wpdb->postmeta} pm WHERE pm.post_id = p.ID AND pm.meta_key = '_aict_language' )" ); $en = $languages['en'] ?? 0; $target_langs = ['de','cs','es','fr','it','pt','pt_eu','nl','pl','sv','ja','ko','zh','ar','tr','hi','ru']; $backlog = 0; $gaps = []; foreach ($target_langs as $l) { $c = $languages[$l] ?? 0; $gap = max(0, $en - $c); $backlog += $gap; $gaps[$l] = ['count' => $c, 'gap' => $gap, 'parity' => $en > 0 ? round($c / $en * 100) : 0]; } return rest_ensure_response([ 'timestamp' => gmdate('c'), 'en_baseline' => $en, 'total_posts' => $total, 'no_lang_meta' => $no_lang, 'languages' => $languages, 'gaps' => $gaps, 'backlog' => $backlog, 'eta_40h' => round($backlog / 40, 1), 'eta_100h' => round($backlog / 100, 1), ]); } /** * POST /wp-json/aict/v1/cache/purge * Admin-only endpoint to clear all caches after deployment. * Tries: WP object cache, 10Web Booster, LiteSpeed, WP Super Cache. */ public function rest_cache_purge() { $cleared = []; // WordPress object cache wp_cache_flush(); $cleared[] = 'wp_object_cache'; // 10Web Booster / TenWeb Optimizer if (class_exists('TenWebOptimizer\\OptimizerCache')) { try { \TenWebOptimizer\OptimizerCache::clear_all_cache(); $cleared[] = 'tenweb_optimizer'; } catch (\Throwable $e) { $cleared[] = 'tenweb_optimizer_error: ' . $e->getMessage(); } } // 10Web Booster alternative class if (function_exists('two_clear_all_cache')) { two_clear_all_cache(); $cleared[] = 'two_clear_all_cache'; } // LiteSpeed Cache (common on 10Web) if (class_exists('LiteSpeed\\Purge')) { \LiteSpeed\Purge::purge_all(); $cleared[] = 'litespeed'; } // WP Super Cache if (function_exists('wp_cache_clear_cache')) { wp_cache_clear_cache(); $cleared[] = 'wp_super_cache'; } // Try generic 10Web purge via their REST-like internal hooks do_action('tenweb_purge_all_caches'); $cleared[] = 'tenweb_action_fired'; error_log('AICT: Cache purge triggered by admin. Cleared: ' . implode(', ', $cleared)); return rest_ensure_response([ 'status' => 'ok', 'cleared' => $cleared, 'time' => gmdate('c'), ]); } /** * POST /wp-json/aict/v1/tool/execute * Standardized tool execution gateway */ public function rest_tool_execute($request) { $slug = sanitize_text_field($request->get_param('slug')); $input = $request->get_param('input'); if (empty($slug)) { return new WP_Error('missing_slug', 'Tool slug is required.', ['status' => 400]); } // Look up tool in registry if (!class_exists('AICT_Dynamic_Routes')) { return new WP_Error('no_routes', 'Dynamic routes not loaded.', ['status' => 500]); } $tool = AICT_Dynamic_Routes::get_tool($slug); if (!$tool) { return new WP_Error('tool_not_found', "Tool '$slug' not found.", ['status' => 404]); } // Determine execution backend from tool metadata $metadata_path = AICT_PLUGIN_DIR . 'data/tool_metadata_registry.json'; $endpoint = null; if (file_exists($metadata_path)) { $registry = json_decode(file_get_contents($metadata_path), true); if (isset($registry[$slug]['api_endpoint'])) { $endpoint = $registry[$slug]['api_endpoint']; } } if (!$endpoint) { return rest_ensure_response([ 'success' => false, 'slug' => $slug, 'error' => 'No API endpoint configured for this tool. Use the tool page directly.', ]); } // Forward to configured endpoint $response = wp_remote_post($endpoint, [ 'timeout' => 30, 'headers' => ['Content-Type' => 'application/json'], 'body' => wp_json_encode(['slug' => $slug, 'input' => $input]), ]); if (is_wp_error($response)) { return rest_ensure_response([ 'success' => false, 'slug' => $slug, 'error' => $response->get_error_message(), ]); } $body = json_decode(wp_remote_retrieve_body($response), true); return rest_ensure_response([ 'success' => true, 'slug' => $slug, 'result' => $body, ]); } /** * Initialize all plugin modules */ public function init_modules() { // Initialize core AICT_Core::init(); // Initialize Dynamic Routes (tool pages, category pages) if (class_exists('AICT_Dynamic_Routes')) { AICT_Dynamic_Routes::init(); } // Initialize Tool Template system (JSON-driven tool pages) if (class_exists('AICT_Tool_Template')) { AICT_Tool_Template::init(); } // Initialize Diagnostics & Logging if (class_exists('AICT_Diagnostics')) { AICT_Diagnostics::init(); } } } /** * Initialize the plugin */ function aict_core_init() { return AICT_Core_Plugin::get_instance(); } // Start the plugin aict_core_init();