/
home
/
altere25
/
.trash
/
wp-to-sanity-migration.11
/
/home/altere25/.trash/wp-to-sanity-migration.11
mkdir
upload
Name
Size
Mode
Actions
acf-json\.gitkeep
0
0644
edit
dl
rm
assets\css\admin.css
12113
0644
edit
dl
rm
assets\js\admin.js
52894
0644
edit
dl
rm
includes\class-ad-admin.php
10358
0644
edit
dl
rm
includes\class-ad-ai-client.php
10724
0644
edit
dl
rm
includes\class-ad-auth.php
9189
0644
edit
dl
rm
includes\class-ad-cli.php
8671
0644
edit
dl
rm
includes\class-ad-extractor.php
17237
0644
edit
dl
rm
includes\class-ad-mcp.php
6429
0644
edit
dl
rm
includes\class-ad-plugin-core.php
10252
0644
edit
dl
rm
includes\class-ad-progress.php
8022
0644
edit
dl
rm
includes\class-ad-rest.php
43443
0644
edit
dl
rm
includes\class-ad-uninstall.php
1966
0644
edit
dl
rm
includes\class-ad-webhook.php
24172
0644
edit
dl
rm
languages\.gitkeep
0
0644
edit
dl
rm
templates\admin\wizard.php
29414
0644
edit
dl
rm
wp-to-sanity-migration\acf-json\.gitkeep
0
0644
edit
dl
rm
wp-to-sanity-migration\assets\
0
0644
edit
dl
rm
wp-to-sanity-migration\assets\css\admin.css
12113
0644
edit
dl
rm
wp-to-sanity-migration\assets\js\admin.js
52894
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-admin.php
10358
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-ai-client.php
10724
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-auth.php
9189
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-cli.php
8671
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-extractor.php
17237
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-mcp.php
6429
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-plugin-core.php
10252
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-progress.php
8022
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-rest.php
43443
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-uninstall.php
1966
0644
edit
dl
rm
wp-to-sanity-migration\includes\class-ad-webhook.php
24172
0644
edit
dl
rm
wp-to-sanity-migration\languages\.gitkeep
0
0644
edit
dl
rm
wp-to-sanity-migration\package.json
220
0644
edit
dl
rm
wp-to-sanity-migration\readme.txt
8174
0644
edit
dl
rm
wp-to-sanity-migration\templates\
0
0644
edit
dl
rm
wp-to-sanity-migration\templates\admin\wizard.php
29414
0644
edit
dl
rm
wp-to-sanity-migration\uninstall.php
365
0644
edit
dl
rm
wp-to-sanity-migration\wp-to-sanity-migration.php
4418
0644
edit
dl
rm
Edit:
/home/altere25/.trash/wp-to-sanity-migration.11/includes\class-ad-rest.php
(43443B)
<?php /** * Altered Digital Migration — REST API * * Registers two parallel namespaces: * - `wp-to-sanity/v1` (back-compat — preserved from v1) * - `altered-digital/v1` (v2 — adds analyze-data, quote, approve) * * Routes: * POST /export — single-post sync (fires webhook) * POST /export/bulk — bulk sync (batched WP_Query for "all") * POST /export/full — full-site queue * GET /status — aggregate stats (admin-locked in v2) * GET /status/post/{id} — per-post last sync (reconciled with D1) * GET /acf-fields — ACF field inventory * POST /connect — Worker /connect handshake (single-tenant) * POST /connect/v2 — Worker /v2/connect handshake (multi-tenant) * GET /connect/test — Worker /health probe * GET /analyze-data — aggregated full-site analysis payload * POST /auth/login — Supabase login (via AD_Auth) * POST /auth/signup — Supabase signup (via AD_Auth) * POST /auth/logout — clear local session * POST /analyze — trigger AI service /analyze * GET /analyze/{id}/status — poll AI service * POST /quote — request AI service quote * POST /approve — AI service approve (capture final 50%) * GET /progress — Worker /v2/migrations (via AD_Progress) * GET /progress/{post_id} — Worker /v2/migrations/{id} * POST /mcp/token — store MCP-scoped Sanity token (via AD_MCP) * GET /mcp/config — fetch existing MCP config (via AD_MCP) * POST /mcp/revoke — revoke the Claude connection (via AD_MCP) * * @package AlteredDigitalMigration */ if (!defined('ABSPATH')) { exit; } class AD_REST { /** @var AD_Plugin_Core */ private $core; /** @var AD_Webhook */ private $webhook; /** @var AD_Extractor */ private $extractor; /** @var AD_Progress */ private $progress; /** @var AD_AI_Client */ private $ai; /** @var AD_Auth */ private $auth; /** @var AD_MCP */ private $mcp; public function __construct( AD_Plugin_Core $core, AD_Webhook $webhook, AD_Extractor $extractor, AD_Progress $progress, AD_AI_Client $ai, AD_Auth $auth, AD_MCP $mcp ) { $this->core = $core; $this->webhook = $webhook; $this->extractor = $extractor; $this->progress = $progress; $this->ai = $ai; $this->auth = $auth; $this->mcp = $mcp; } public function register_hooks(): void { add_action('rest_api_init', [$this, 'register_routes']); } public function register_routes(): void { $this->register_v1_routes(); $this->register_v2_routes(); } // ----------------------------------------------------------------- // v1 back-compat namespace (wp-to-sanity/v1) // ----------------------------------------------------------------- private function register_v1_routes(): void { $ns = 'wp-to-sanity/v1'; register_rest_route($ns, '/export', [ 'methods' => 'POST', 'callback' => [$this, 'handle_export'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/export/bulk', [ 'methods' => 'POST', 'callback' => [$this, 'handle_bulk_export'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/export/full', [ 'methods' => 'POST', 'callback' => [$this, 'handle_full_export'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/status', [ 'methods' => 'GET', 'callback' => [$this, 'handle_status'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/status/post/(?P<id>\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'handle_post_status'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/acf-fields', [ 'methods' => 'GET', 'callback' => [$this, 'handle_acf_fields'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/connect', [ 'methods' => 'POST', 'callback' => [$this, 'handle_connect'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/connect/test', [ 'methods' => 'GET', 'callback' => [$this, 'handle_connect_test'], 'permission_callback' => [$this, 'verify_admin_access'], ]); } // ----------------------------------------------------------------- // v2 namespace (altered-digital/v1) // ----------------------------------------------------------------- private function register_v2_routes(): void { $ns = 'altered-digital/v1'; // Mirror of v1 (same handlers, just re-exposed on the new ns). register_rest_route($ns, '/export', [ 'methods' => 'POST', 'callback' => [$this, 'handle_export'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/export/bulk', [ 'methods' => 'POST', 'callback' => [$this, 'handle_bulk_export'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/status', [ 'methods' => 'GET', 'callback' => [$this, 'handle_status'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/status/post/(?P<id>\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'handle_post_status'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/connect', [ 'methods' => 'POST', 'callback' => [$this, 'handle_connect'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/connect/v2', [ 'methods' => 'POST', 'callback' => [$this, 'handle_connect_v2'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/connect/test', [ 'methods' => 'GET', 'callback' => [$this, 'handle_connect_test'], 'permission_callback' => [$this, 'verify_admin_access'], ]); // v2-only routes. register_rest_route($ns, '/analyze-data', [ 'methods' => 'GET', 'callback' => [$this, 'handle_analyze_data'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/progress', [ 'methods' => 'GET', 'callback' => [$this, 'handle_progress'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/progress/(?P<post_id>\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'handle_progress_post'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/auth/login', [ 'methods' => 'POST', 'callback' => [$this, 'handle_auth_login'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/auth/signup', [ 'methods' => 'POST', 'callback' => [$this, 'handle_auth_signup'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/auth/logout', [ 'methods' => 'POST', 'callback' => [$this, 'handle_auth_logout'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/analyze', [ 'methods' => 'POST', 'callback' => [$this, 'handle_analyze'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/analyze/(?P<id>[a-zA-Z0-9_-]+)/status', [ 'methods' => 'GET', 'callback' => [$this, 'handle_analyze_status'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/quote', [ 'methods' => 'POST', 'callback' => [$this, 'handle_quote'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/checkout', [ 'methods' => 'POST', 'callback' => [$this, 'handle_checkout'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/approve', [ 'methods' => 'POST', 'callback' => [$this, 'handle_approve'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/payment-status', [ 'methods' => 'GET', 'callback' => [$this, 'handle_payment_status'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/schema-generate', [ 'methods' => 'POST', 'callback' => [$this, 'handle_schema_generate'], 'permission_callback' => [$this, 'verify_admin_access'], ]); // MCP hand-off (Phase D5). register_rest_route($ns, '/mcp/token', [ 'methods' => 'POST', 'callback' => [$this, 'handle_mcp_token'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/mcp/config', [ 'methods' => 'GET', 'callback' => [$this, 'handle_mcp_config'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/mcp/revoke', [ 'methods' => 'POST', 'callback' => [$this, 'handle_mcp_revoke'], 'permission_callback' => [$this, 'verify_admin_access'], ]); // Wizard state + AJAX settings + paginated logs (unified wizard). register_rest_route($ns, '/wizard-state', [ 'methods' => 'GET', 'callback' => [$this, 'handle_wizard_state_get'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/wizard-state', [ 'methods' => 'POST', 'callback' => [$this, 'handle_wizard_state_update'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/settings', [ 'methods' => 'POST', 'callback' => [$this, 'handle_settings_save'], 'permission_callback' => [$this, 'verify_admin_access'], ]); register_rest_route($ns, '/logs', [ 'methods' => 'GET', 'callback' => [$this, 'handle_logs'], 'permission_callback' => [$this, 'verify_admin_access'], ]); } // ----------------------------------------------------------------- // Permission gate // ----------------------------------------------------------------- public function verify_admin_access(WP_REST_Request $request) { if (current_user_can('manage_options')) { return true; } return new WP_Error( 'ad_rest_forbidden', __('You do not have permission to perform this action.', 'wp-to-sanity-migration'), ['status' => rest_authorization_required_code()] ); } // ----------------------------------------------------------------- // Export handlers // ----------------------------------------------------------------- public function handle_export(WP_REST_Request $request): WP_REST_Response { $post_id = (int) $request->get_param('post_id'); if (empty($post_id)) { return new WP_REST_Response(['success' => false, 'error' => 'post_id is required'], 400); } $post = get_post($post_id); if (!$post) { return new WP_REST_Response(['success' => false, 'error' => 'Post not found'], 404); } $this->progress->log_pending($post_id, 'export'); $this->webhook->trigger_export($post); return new WP_REST_Response(['success' => true, 'data' => ['post_id' => $post_id]], 200); } public function handle_bulk_export(WP_REST_Request $request): WP_REST_Response { $post_ids = $request->get_param('post_ids'); $post_type = $request->get_param('post_type') ?? 'post'; $export_all = $request->get_param('export_all') === true || $request->get_param('export_all') === 'true' || $request->get_param('export_all') === 1; if ($export_all && empty($post_ids)) { $batch_size = (int) ($this->core->setting('batch_size', 100)); $query = new WP_Query([ 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => $batch_size, 'fields' => 'ids', 'no_found_rows' => true, ]); $post_ids = $query->posts; } if (empty($post_ids) || !is_array($post_ids)) { return new WP_REST_Response(['success' => false, 'error' => 'post_ids array is required'], 400); } $queued = 0; foreach ($post_ids as $id) { $post = get_post((int) $id); if ($post && $post->post_type === $post_type) { $this->progress->log_pending((int) $id, 'export'); $this->webhook->trigger_export($post); $queued++; } } return new WP_REST_Response([ 'success' => true, 'data' => ['queued' => $queued, 'count' => $queued], ], 200); } public function handle_full_export(WP_REST_Request $request): WP_REST_Response { $post_types = $this->core->settings()['post_types'] ?? ['post', 'page']; $batch_size = (int) ($this->core->setting('batch_size', 100)); $total = 0; foreach ($post_types as $pt) { $offset = 0; while (true) { $ids = get_posts([ 'post_type' => $pt, 'post_status' => 'publish', 'posts_per_page' => $batch_size, 'offset' => $offset, 'fields' => 'ids', 'no_found_rows' => true, 'orderby' => 'ID', 'order' => 'ASC', ]); if (empty($ids)) { break; } foreach ($ids as $pid) { $post = get_post((int) $pid); if ($post) { $this->progress->log_pending((int) $pid, 'publish'); $this->webhook->trigger_export($post); $total++; } } $offset += $batch_size; } } // Send redirect rules (Redirection plugin) as a separate batch to the // Worker /v2/redirects endpoint. Best-effort — failures are logged // but don't block the migration result. $redirects_sent = 0; $redirects = $this->extractor->extract_redirects(); if (!empty($redirects)) { $redirects_sent = $this->send_redirects_to_worker($redirects); } return new WP_REST_Response([ 'success' => true, 'data' => [ 'job_id' => 'full-' . wp_generate_uuid4(), 'queued' => $total, 'redirects_sent' => $redirects_sent, ], ], 200); } /** * Send redirect rules to the Worker /v2/redirects endpoint. * * The Worker URL is derived from the webhook_url (same host). Uses the * admin token for Bearer auth. Best-effort: logs errors but returns 0 * on failure so the migration result is unaffected. * * @param array<int,array<string,mixed>> $redirects * @return int Number of redirect rules accepted by the Worker */ private function send_redirects_to_worker(array $redirects): int { $settings = $this->core->settings(); $webhook_url = $settings['webhook_url'] ?? ''; $admin_token = $settings['admin_token'] ?? ''; if (empty($webhook_url) || empty($admin_token) || empty($redirects)) { return 0; } // Derive /v2/redirects URL from the webhook URL (same origin). $parsed = wp_parse_url($webhook_url); if (empty($parsed['host'])) { return 0; } $scheme = isset($parsed['scheme']) ? $parsed['scheme'] : 'https'; $worker_base = $scheme . '://' . $parsed['host']; $v2_url = rtrim($worker_base, '/') . '/v2/redirects'; $resp = wp_remote_post($v2_url, [ 'timeout' => 30, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $admin_token, 'X-AD-Tenant' => $settings['account_id'] ?? '', ], 'body' => wp_json_encode(['redirects' => $redirects]), ]); if (is_wp_error($resp)) { error_log('[AD] send_redirects_to_worker: ' . $resp->get_error_message()); return 0; } $code = wp_remote_retrieve_response_code($resp); if ($code < 200 || $code >= 300) { error_log('[AD] send_redirects_to_worker: Worker returned ' . $code); return 0; } $body = json_decode(wp_remote_retrieve_body($resp), true); return is_array($body) && isset($body['created']) ? (int) $body['created'] : 0; } // ----------------------------------------------------------------- // Status / progress // ----------------------------------------------------------------- public function handle_status(WP_REST_Request $request): WP_REST_Response { // Local log stats (offline-safe). The dashboard polls /progress // for the D1 view; this endpoint covers the meta-box + fallback. $stats = $this->progress->local_stats(); return new WP_REST_Response([ 'success' => true, 'data' => [ 'migration_stats' => $stats, 'plugin_version' => AD_VERSION, ], ], 200); } public function handle_post_status(WP_REST_Request $request): WP_REST_Response { $post_id = (int) $request->get_param('id'); if (empty($post_id)) { return new WP_REST_Response(['success' => false, 'error' => 'Invalid post ID'], 400); } // Reconcile with D1 first, then read local. $this->progress->fetch_post_history($post_id); $last = $this->progress->get_local_last_sync($post_id); return new WP_REST_Response([ 'success' => true, 'data' => ['last_sync' => $last], ], 200); } public function handle_progress(WP_REST_Request $request): WP_REST_Response { $result = $this->progress->fetch_progress(); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_progress_post(WP_REST_Request $request): WP_REST_Response { $post_id = (int) $request->get_param('post_id'); $result = $this->progress->fetch_post_history($post_id); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_acf_fields(WP_REST_Request $request): WP_REST_Response { if (!function_exists('acf')) { return new WP_REST_Response(['success' => false, 'error' => 'ACF plugin is not active'], 400); } $out = []; foreach (acf_get_field_groups() as $group) { $out[] = [ 'group' => $group, 'fields' => function_exists('acf_get_fields') ? acf_get_fields($group) : [], ]; } return new WP_REST_Response(['success' => true, 'data' => $out], 200); } // ----------------------------------------------------------------- // Analysis (AI service) + builder inventory // ----------------------------------------------------------------- public function handle_analyze_data(WP_REST_Request $request): WP_REST_Response { $payload = $this->extractor->build_analysis_payload(); return new WP_REST_Response(['success' => true, 'data' => $payload], 200); } public function handle_analyze(WP_REST_Request $request): WP_REST_Response { $jwt = $this->auth->access_token(); if (empty($jwt)) { return new WP_REST_Response(['success' => false, 'error' => 'not_authenticated'], 401); } if ($this->auth->is_expired()) { $refresh = $this->auth->refresh(); if (empty($refresh['success'])) { return new WP_REST_Response(['success' => false, 'error' => 'session_expired'], 401); } $jwt = $this->auth->access_token(); } $site_url = home_url(); $payload = $this->extractor->build_analysis_payload(); $result = $this->ai->analyze($site_url, $jwt, $payload); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_analyze_status(WP_REST_Request $request): WP_REST_Response { $id = (string) $request->get_param('id'); $jwt = $this->auth->access_token(); if (empty($jwt)) { return new WP_REST_Response(['success' => false, 'error' => 'not_authenticated'], 401); } $result = $this->ai->analysis_status($id, $jwt); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_quote(WP_REST_Request $request): WP_REST_Response { $analysis_id = (string) $request->get_param('analysis_id'); $analysis_json = $request->get_param('analysis_json'); $add_ons = (array) ($request->get_param('add_ons') ?? []); $jwt = $this->auth->access_token(); if (empty($jwt)) { return new WP_REST_Response(['success' => false, 'error' => 'not_authenticated'], 401); } $result = $this->ai->quote($analysis_id, $add_ons, $jwt, $analysis_json); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_approve(WP_REST_Request $request): WP_REST_Response { $quote_id = (string) $request->get_param('quote_id'); $jwt = $this->auth->access_token(); if (empty($jwt)) { return new WP_REST_Response(['success' => false, 'error' => 'not_authenticated'], 401); } $result = $this->ai->approve($quote_id, $jwt); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } /** * GET /payment-status — proxies the AI service's read-only final-payment * verification. Used by the wizard's payment-return flow to confirm the * Stripe webhook has landed (status = paid_full) before unlocking Step 10. */ public function handle_payment_status(WP_REST_Request $request): WP_REST_Response { $quote_id = (string) ($request->get_param('quote_id') ?? ''); if (empty($quote_id)) { return new WP_REST_Response(['success' => false, 'error' => 'quote_id required'], 400); } $jwt = $this->auth->access_token(); if (empty($jwt)) { return new WP_REST_Response(['success' => false, 'error' => 'not_authenticated'], 401); } if ($this->auth->is_expired()) { $refresh = $this->auth->refresh(); if (empty($refresh['success'])) { return new WP_REST_Response(['success' => false, 'error' => 'session_expired'], 401); } $jwt = $this->auth->access_token(); } $result = $this->ai->payment_status($quote_id, $jwt); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_checkout(WP_REST_Request $request): WP_REST_Response { $quote_id = (string) $request->get_param('quote_id'); if (empty($quote_id)) { return new WP_REST_Response(['success' => false, 'error' => 'quote_id required'], 400); } $jwt = $this->auth->access_token(); if (empty($jwt)) { return new WP_REST_Response(['success' => false, 'error' => 'not_authenticated'], 401); } $result = $this->ai->start_checkout($quote_id, $jwt); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_schema_generate(WP_REST_Request $request): WP_REST_Response { $jwt = $this->auth->access_token(); if (empty($jwt)) { return new WP_REST_Response(['success' => false, 'error' => 'not_authenticated'], 401); } $payload = $this->extractor->build_analysis_payload(); $result = $this->ai->schema_generate($payload, $jwt); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } // ----------------------------------------------------------------- // MCP hand-off (Phase D5) — Claude/MCP config for migrated Sanity // ----------------------------------------------------------------- public function handle_mcp_token(WP_REST_Request $request): WP_REST_Response { $mcp_token = (string) ($request->get_param('mcp_token') ?? ''); $result = $this->mcp->mint_token($mcp_token); // 404 from Worker (tenant not found) is a real error; 200 only on success. $code = $result['success'] ? 200 : 502; return new WP_REST_Response($result, $code); } public function handle_mcp_config(WP_REST_Request $request): WP_REST_Response { $result = $this->mcp->fetch_config(); // mcp_token_not_set → 404 so the JS can distinguish "mint one" from a hard error. if (!$result['success'] && !empty($result['data']['error']) && $result['data']['error'] === 'mcp_token_not_set') { return new WP_REST_Response($result, 404); } return new WP_REST_Response($result, $result['success'] ? 200 : 502); } public function handle_mcp_revoke(WP_REST_Request $request): WP_REST_Response { $result = $this->mcp->revoke(); return new WP_REST_Response($result, $result['success'] ? 200 : 502); } // ----------------------------------------------------------------- // Auth // ----------------------------------------------------------------- public function handle_auth_login(WP_REST_Request $request): WP_REST_Response { try { $email = sanitize_email($request->get_param('email') ?? ''); $password = (string) ($request->get_param('password') ?? ''); if (empty($email) || empty($password)) { return new WP_REST_Response(['success' => false, 'error' => 'email and password required'], 400); } $result = $this->auth->login($email, $password); return new WP_REST_Response($result, $result['success'] ? 200 : 401); } catch (\Throwable $e) { error_log('[AD] auth/login fatal: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine()); return new WP_REST_Response([ 'success' => false, 'error' => 'server_error: ' . $e->getMessage(), 'file' => basename($e->getFile()), 'line' => $e->getLine(), ], 500); } catch (\Exception $e) { error_log('[AD] auth/login exception: ' . $e->getMessage()); return new WP_REST_Response([ 'success' => false, 'error' => 'server_error: ' . $e->getMessage(), ], 500); } } public function handle_auth_signup(WP_REST_Request $request): WP_REST_Response { $email = sanitize_email($request->get_param('email') ?? ''); $password = (string) ($request->get_param('password') ?? ''); if (empty($email) || empty($password)) { return new WP_REST_Response(['success' => false, 'error' => 'email and password required'], 400); } $result = $this->auth->signup($email, $password); return new WP_REST_Response($result, $result['success'] ? 200 : 400); } public function handle_auth_logout(WP_REST_Request $request): WP_REST_Response { $this->auth->logout(); return new WP_REST_Response(['success' => true], 200); } // ----------------------------------------------------------------- // Connect handshakes // ----------------------------------------------------------------- /** * Single-tenant /connect (Phase A back-compat). */ public function handle_connect(WP_REST_Request $request): WP_REST_Response { $service_url = esc_url_raw($request->get_param('service_url') ?? ''); $admin_token = sanitize_text_field($request->get_param('admin_token') ?? ''); $webhook_secret = sanitize_text_field($request->get_param('webhook_secret') ?? ''); if (empty($service_url) || empty($admin_token)) { return new WP_REST_Response(['success' => false, 'data' => ['error' => 'service_url and admin_token are required']], 400); } if (empty($webhook_secret)) { $webhook_secret = $this->core->setting('webhook_secret', ''); } $endpoint = rtrim($service_url, '/') . '/connect'; $response = wp_remote_post($endpoint, [ 'timeout' => 20, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $admin_token, ], 'body' => wp_json_encode(['webhook_secret' => $webhook_secret]), ]); if (is_wp_error($response)) { return new WP_REST_Response(['success' => false, 'data' => ['error' => $response->get_error_message()]], 502); } $code = wp_remote_retrieve_response_code($response); $decoded = json_decode(wp_remote_retrieve_body($response), true); if ($code !== 200 || !is_array($decoded) || empty($decoded['connected'])) { $err = is_array($decoded) && !empty($decoded['error']) ? $decoded['error'] : 'Worker returned ' . intval($code); return new WP_REST_Response(['success' => false, 'data' => ['error' => $err, 'status' => intval($code)]], 502); } $settings = $this->core->settings(); $settings['service_url'] = $service_url; $settings['admin_token'] = $admin_token; $settings['webhook_url'] = rtrim($service_url, '/') . '/webhook/wp'; if (!empty($webhook_secret)) { $settings['webhook_secret'] = $webhook_secret; } $this->core->update_settings($settings); return new WP_REST_Response(['success' => true, 'data' => $decoded], 200); } /** * Multi-tenant /v2/connect. Customer supplies their own Sanity creds; * Worker provisions a tenant + returns webhook_url + webhook_secret. */ public function handle_connect_v2(WP_REST_Request $request): WP_REST_Response { // Fall back to pre-configured settings when the wizard doesn't pass // service_url/admin_token (they're set by the network admin and // shouldn't be re-entered by the customer). $stored = $this->core->settings(); $service_url = esc_url_raw($request->get_param('service_url') ?? ''); if (empty($service_url)) { $service_url = $stored['service_url'] ?? ''; } $admin_token = sanitize_text_field($request->get_param('admin_token') ?? ''); if (empty($admin_token)) { $admin_token = $stored['admin_token'] ?? ''; } $account_id = sanitize_text_field($request->get_param('account_id') ?? ''); // Fall back to the Supabase JWT sub claim (account_id) server-side // — the customer never sees their JWT and shouldn't have to enter it. if (empty($account_id)) { $jwt = $this->auth->access_token(); if (!empty($jwt)) { $parts = explode('.', $jwt); if (count($parts) === 3) { $payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true); if (is_array($payload) && !empty($payload['sub'])) { $account_id = sanitize_text_field($payload['sub']); } } } } $sanity_project_id = sanitize_text_field($request->get_param('sanity_project_id') ?? ''); $sanity_api_token = (string) ($request->get_param('sanity_api_token') ?? ''); $sanity_dataset = sanitize_text_field($request->get_param('sanity_dataset') ?? 'production'); $nextjs_revalidate_url = esc_url_raw($request->get_param('nextjs_revalidate_url') ?? ''); if (empty($service_url) || empty($admin_token) || empty($sanity_project_id) || empty($sanity_api_token)) { return new WP_REST_Response([ 'success' => false, 'data' => ['error' => 'service_url, admin_token, sanity_project_id, and sanity_api_token are required'], ], 400); } $endpoint = rtrim($service_url, '/') . '/v2/connect'; $response = wp_remote_post($endpoint, [ 'timeout' => 25, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $admin_token, ], 'body' => wp_json_encode([ 'account_id' => $account_id, 'wp_site_url' => home_url(), 'sanity_project_id' => $sanity_project_id, 'sanity_api_token' => $sanity_api_token, 'sanity_dataset' => $sanity_dataset, 'nextjs_revalidate_url' => $nextjs_revalidate_url, ]), ]); if (is_wp_error($response)) { return new WP_REST_Response(['success' => false, 'data' => ['error' => $response->get_error_message()]], 502); } $code = wp_remote_retrieve_response_code($response); $decoded = json_decode(wp_remote_retrieve_body($response), true); if ($code !== 200 || !is_array($decoded) || empty($decoded['tenant_id'])) { $err = is_array($decoded) && !empty($decoded['error']) ? $decoded['error'] : 'Worker returned ' . intval($code); return new WP_REST_Response(['success' => false, 'data' => ['error' => $err, 'status' => intval($code)]], 502); } // Persist tenant-scoped config. webhook_url is tenant-scoped via the // signature path; we still store the bare URL and let the Worker // resolve the tenant from the signature. $settings = $this->core->settings(); $settings['service_url'] = $service_url; $settings['admin_token'] = $admin_token; $settings['webhook_url'] = rtrim($service_url, '/') . '/webhook/wp'; if (!empty($decoded['webhook_secret'])) { $settings['webhook_secret'] = $decoded['webhook_secret']; } $settings['tenant_id'] = $decoded['tenant_id']; $this->core->update_settings($settings); return new WP_REST_Response(['success' => true, 'data' => $decoded], 200); } public function handle_connect_test(WP_REST_Request $request): WP_REST_Response { $service_url = $this->core->setting('service_url', '') ?: esc_url_raw($request->get_param('service_url') ?? ''); if (empty($service_url)) { return new WP_REST_Response(['success' => false, 'data' => ['error' => 'No service_url configured. Connect first.']], 400); } $response = wp_remote_get(rtrim($service_url, '/') . '/health', ['timeout' => 15]); if (is_wp_error($response)) { return new WP_REST_Response(['success' => false, 'data' => ['error' => $response->get_error_message()]], 502); } $code = wp_remote_retrieve_response_code($response); $decoded = json_decode(wp_remote_retrieve_body($response), true); if ($code !== 200 || !is_array($decoded)) { return new WP_REST_Response(['success' => false, 'data' => ['error' => 'Worker /health returned ' . intval($code), 'status' => intval($code)]], 502); } return new WP_REST_Response(['success' => true, 'data' => $decoded], 200); } // ----------------------------------------------------------------- // Wizard state + AJAX settings + paginated logs (unified wizard) // ----------------------------------------------------------------- /** * GET /wizard-state — returns the full state snapshot: stored wizard * state merged with runtime-inferred flags + the current content * settings so the JS can render the content step without a second * round-trip. */ public function handle_wizard_state_get(WP_REST_Request $request): WP_REST_Response { $state = $this->core->get_wizard_state(); $settings = $this->core->settings(); // Runtime-inferred state (computed fresh on each call, not stored). $jwt = $this->auth->access_token(); $logged_in = !empty($jwt) && !$this->auth->is_expired(); $sanity_connected = !empty($settings['tenant_id']) && !empty($settings['service_url']); $has_service = !empty($settings['service_url']) && !empty($settings['admin_token']); // Override stored booleans with runtime truth where possible. $state['logged_in'] = $logged_in; $state['sanity_connected'] = $sanity_connected || $state['sanity_connected']; $state['has_service'] = $has_service; return new WP_REST_Response([ 'success' => true, 'data' => [ 'state' => $state, 'settings' => [ 'post_types' => $settings['post_types'] ?? ['post', 'page'], 'taxonomies' => $settings['taxonomies'] ?? ['category', 'post_tag'], 'include_drafts' => (bool) ($settings['include_drafts'] ?? false), 'auto_sync' => (bool) ($settings['auto_sync'] ?? false), ], ], ], 200); } /** * POST /wizard-state — allowlist-merge a partial update into the * stored wizard state. */ public function handle_wizard_state_update(WP_REST_Request $request): WP_REST_Response { $params = $request->get_json_params() ?: []; if (!is_array($params)) { $params = []; } $this->core->update_wizard_state($params); return new WP_REST_Response([ 'success' => true, 'data' => $this->core->get_wizard_state(), ], 200); } /** * POST /settings — AJAX settings save replacing the Settings API form. * Accepts post_types, taxonomies, include_drafts, auto_sync and merges * into the existing settings, preserving service_url/admin_token/etc. */ public function handle_settings_save(WP_REST_Request $request): WP_REST_Response { $params = $request->get_json_params() ?: []; if (!is_array($params)) { $params = []; } $settings = $this->core->settings(); if (isset($params['post_types']) && is_array($params['post_types'])) { $settings['post_types'] = array_values(array_filter( array_map('sanitize_text_field', $params['post_types']) )); } if (isset($params['taxonomies']) && is_array($params['taxonomies'])) { $settings['taxonomies'] = array_values(array_filter( array_map('sanitize_text_field', $params['taxonomies']) )); } if (isset($params['include_drafts'])) { $settings['include_drafts'] = (bool) $params['include_drafts']; } if (isset($params['auto_sync'])) { $settings['auto_sync'] = (bool) $params['auto_sync']; } $this->core->update_settings($settings); return new WP_REST_Response([ 'success' => true, 'data' => [ 'post_types' => $settings['post_types'], 'taxonomies' => $settings['taxonomies'], 'include_drafts' => $settings['include_drafts'], 'auto_sync' => $settings['auto_sync'], ], ], 200); } /** * GET /logs — paginated migration log for the Next Steps > Migration * Log tab. Reads $wpdb->prefix.'ad_migration_log'. */ public function handle_logs(WP_REST_Request $request): WP_REST_Response { global $wpdb; $table = $wpdb->prefix . 'ad_migration_log'; $paged = max(1, (int) ($request->get_param('paged') ?? 1)); $per_page = 25; $status = sanitize_text_field((string) ($request->get_param('status') ?? '')); $where = '1=1'; if ($status && $status !== 'all') { $where = $wpdb->prepare('status = %s', $status); } $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE {$where}"); $total_pages = (int) ceil($total / $per_page); $offset = ($paged - 1) * $per_page; $logs = $wpdb->get_results( $wpdb->prepare("SELECT * FROM {$table} WHERE {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d", $per_page, $offset), ARRAY_A ); return new WP_REST_Response([ 'success' => true, 'data' => [ 'logs' => $logs ?: [], 'total' => $total, 'total_pages' => $total_pages, 'paged' => $paged, 'per_page' => $per_page, ], ], 200); } }
Save
cmd:
run