false, 'message' => __('Unauthorized: Insufficient user permissions.', 'zbridge'), 'code' => 403, ], 403); exit; } } /** * Tests Cloudflare API connection. */ public function handleTestConnection(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); $isProActive = class_exists('\WPZaraz\Licensing\LicenseManager') && \WPZaraz\Licensing\LicenseManager::isPro(); if (!$isProActive || !class_exists('\WPZaraz\Cloudflare\ApiClient')) { wp_send_json_error([ 'success' => false, 'message' => __('Connecting to the Cloudflare API requires an active WP Zbridge Pro license. In the free edition, download the container and import it in Cloudflare.', 'zbridge'), 'code' => 403, ], 403); return; } $token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : ''; $zoneId = isset($_POST['zone_id']) ? sanitize_text_field(wp_unslash($_POST['zone_id'])) : ''; $client = new ApiClient($token, $zoneId); $result = $client->verifyZone(); if ($result['success']) { if (!empty($token)) { Options::set('cf_api_token', $token); } if (!empty($zoneId)) { Options::set('cf_zone_id', $zoneId); } wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * Synchronizes container directly to Cloudflare Zaraz at edge. */ public function handleSyncConfig(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); $isProActive = class_exists('\WPZaraz\Licensing\LicenseManager') && \WPZaraz\Licensing\LicenseManager::isPro(); if (!$isProActive || !class_exists('\WPZaraz\Cloudflare\ApiClient')) { wp_send_json_error([ 'success' => false, 'message' => __('Sync to Zaraz requires an active WP Zbridge Pro license. In the free edition, download the container and import it in Cloudflare.', 'zbridge'), 'code' => 403, ], 403); return; } try { $token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : ''; $zoneId = isset($_POST['zone_id']) ? sanitize_text_field(wp_unslash($_POST['zone_id'])) : ''; // 1. If settings payload is passed, persist settings first if (!empty($_POST['settings']) && is_array($_POST['settings'])) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized recursively with sanitize_text_field $rawSettings = function_exists('map_deep') ? map_deep(wp_unslash($_POST['settings']), 'sanitize_text_field') : array_map('sanitize_text_field', wp_unslash($_POST['settings'])); $sanitized = Options::sanitizeSubmitted((array) $rawSettings); Options::updateMany($sanitized); } if (!empty($token)) { Options::set('cf_api_token', $token); } else { $token = (string) Options::get('cf_api_token', ''); } if (!empty($zoneId)) { Options::set('cf_zone_id', $zoneId); } else { $zoneId = (string) Options::get('cf_zone_id', ''); } $client = new ApiClient($token, $zoneId); $registry = new RecipeRegistry(); $builder = new ContainerBuilder($registry); $credentials = Options::getAll(); $credentials['cf_api_token'] = $token; $credentials['cf_zone_id'] = $zoneId; // Only tools with user-provided IDs are deployed; never push empty tools to the live config $activeToolIds = self::activeToolIds($registry, $credentials); if (empty($activeToolIds) && !Options::get('instagram_embeds', false)) { wp_send_json_error([ 'success' => false, 'message' => __('Enter at least one tool ID (for example your GA4 Measurement ID) before syncing.', 'zbridge'), ]); return; } $container = $builder->build($activeToolIds, $credentials); $result = $client->syncConfig($container, true); if ($result['success']) { HealthCheck::recordContainer($container, true); delete_transient('wp_zaraz_live_config'); Logger::info('Zaraz container synchronized successfully', ['active_tools' => $activeToolIds]); wp_send_json_success($result); } else { Logger::error('Zaraz container sync failed', ['message' => $result['message'] ?? 'Failed']); wp_send_json_error($result); } } catch (\Throwable $e) { Logger::error('Zaraz container sync exception: ' . $e->getMessage()); wp_send_json_error([ 'success' => false, 'message' => 'Error: ' . $e->getMessage(), ]); } } /** * Puts back the Zaraz configuration saved just before the last sync (Pro). */ public function handleRollback(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); if (!class_exists('\WPZaraz\Cloudflare\ApiClient')) { wp_send_json_error(['message' => __('Rolling back requires WP Zbridge Pro.', 'zbridge')], 403); return; } $result = (new ApiClient())->rollbackLastSync(); if (empty($result['success'])) { wp_send_json_error($result); return; } // Zaraz no longer has the container built from the current settings Options::set(HealthCheck::CONTAINER_HASH_OPTION, 'rolled-back'); delete_transient('wp_zaraz_live_config'); wp_send_json_success($result); } /** * Returns the Setup Health items and the Zaraz Activity card, so the dashboard can refresh * them without a page load. * With fresh=1 the Zaraz activity and live configuration are fetched again. */ public function handleHealth(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); $fresh = !empty($_POST['fresh']); $live = HealthCheck::liveData($fresh); $health = (new HealthCheck())->run($live['activity'], $live['live_config']); wp_send_json_success([ 'html' => \WPZaraz\Admin\Tabs\DashboardTab::healthItemsHtml($health), 'steps_html' => \WPZaraz\Admin\Tabs\DashboardTab::setupStepsHtml($health), 'activity_html' => is_array($live['activity']) ? \WPZaraz\Admin\Tabs\DashboardTab::activityHtml($live['activity'], $live['live_config']) : null, ]); } /** * Recipes whose required ID field (the first field) has a value. * * @param RecipeRegistry $registry * @param array $credentials * @return array */ public static function activeToolIds(RecipeRegistry $registry, array $credentials): array { $active = []; foreach ($registry->getRecipes() as $rId => $recipe) { $fields = $recipe->getFields(); $primary = reset($fields); $optKey = is_array($primary) ? ($primary['option_key'] ?? key($fields)) : ''; if ($optKey === '' || empty($credentials[$optKey])) { continue; } // Recipes that need more than their first field say so (e.g. Pinterest Conversions API) if (method_exists($recipe, 'isConfigured') && !$recipe::isConfigured($credentials)) { continue; } $active[] = $rId; } return $active; } /** * Exports container as downloadable JSON. */ public function handleExportJson(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); $registry = new RecipeRegistry(); $builder = new ContainerBuilder($registry); $credentials = Options::getAll(); $activeToolIds = self::activeToolIds($registry, $credentials); if (empty($activeToolIds) && !Options::get('instagram_embeds', false)) { wp_send_json_error(['message' => __('Enter at least one tool ID (for example your GA4 Measurement ID) before exporting.', 'zbridge')]); return; } $container = $builder->build($activeToolIds, $credentials); $json = (string) wp_json_encode($container, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); HealthCheck::recordContainer($container); wp_send_json_success(['json' => $json]); } /** * Activates a license key. */ public function handleActivateLicense(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); if (!class_exists('\WPZaraz\Licensing\LicenseManager')) { wp_send_json_error(['message' => __('License activation is available in WP Zbridge Pro.', 'zbridge')]); return; } $key = isset($_POST['key']) ? sanitize_text_field(wp_unslash($_POST['key'])) : ''; $manager = new LicenseManager(); $result = $manager->activate($key); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * Deactivates the license key and frees the edge site slot. */ public function handleDeactivateLicense(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); if (!class_exists('\WPZaraz\Licensing\LicenseManager')) { wp_send_json_error(['message' => __('License deactivation is available in WP Zbridge Pro.', 'zbridge')]); return; } $manager = new LicenseManager(); $result = $manager->deactivate(); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * Re-validates license against the remote edge registry. */ public function handleCheckLicense(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); if (!class_exists('\WPZaraz\Licensing\LicenseManager')) { wp_send_json_error(['message' => __('License verification is available in WP Zbridge Pro.', 'zbridge')]); return; } $result = LicenseManager::validateRemote(true); if ($result['valid']) { wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * Dispatches simulated purchase to Zaraz HTTP events. */ public function handleSendTestEvent(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); $isProActive = class_exists('\WPZaraz\Licensing\LicenseManager') && \WPZaraz\Licensing\LicenseManager::isPro(); if (!$isProActive || !class_exists('\WPZaraz\Tracking\ServerDispatch')) { wp_send_json_error(['message' => __('Live server dispatch test requires an active WP Zbridge Pro license.', 'zbridge'), 'code' => 403], 403); return; } $dispatcher = new ServerDispatch(); $result = $dispatcher->dispatchTestEvent(); if ($result['success']) { wp_send_json_success(['message' => $result['message'], 'code' => $result['code'] ?? 200]); } else { wp_send_json_error(['message' => $result['message'], 'code' => $result['code'] ?? 0]); } } /** * Dispatches simulated refund to Zaraz HTTP events. */ public function handleSendTestRefund(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); $isProActive = class_exists('\WPZaraz\Licensing\LicenseManager') && \WPZaraz\Licensing\LicenseManager::isPro(); if (!$isProActive || !class_exists('\WPZaraz\Tracking\ServerDispatch')) { wp_send_json_error(['message' => __('Live server refund test requires an active WP Zbridge Pro license.', 'zbridge'), 'code' => 403], 403); return; } $dispatcher = new ServerDispatch(); $result = $dispatcher->dispatchTestRefund(); if ($result['success']) { wp_send_json_success(['message' => $result['message'], 'code' => $result['code'] ?? 200]); } else { wp_send_json_error(['message' => $result['message'], 'code' => $result['code'] ?? 0]); } } /** * Saves settings from admin form. */ public function handleSaveSettings(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); if (empty($_POST['settings']) || !is_array($_POST['settings'])) { wp_send_json_error(['message' => __('Invalid settings payload', 'zbridge')]); return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized recursively with sanitize_text_field $data = function_exists('map_deep') ? map_deep(wp_unslash($_POST['settings']), 'sanitize_text_field') : array_map('sanitize_text_field', wp_unslash($_POST['settings'])); $sanitized = Options::sanitizeSubmitted((array) $data); $isProActive = class_exists('\WPZaraz\Licensing\LicenseManager') && \WPZaraz\Licensing\LicenseManager::isPro(); if (!$isProActive) { $sanitized['connection_mode'] = 'manual'; } Options::updateMany($sanitized); wp_send_json_success(['message' => __('Settings saved successfully!', 'zbridge')]); } /** * Executes automated cookie and tracker scan. */ public function handleScanCookies(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); // What the admin's browser saw on the homepage (names and hosts only, no values) $browser = []; if (isset($_POST['browser']) && is_string($_POST['browser'])) { $decoded = json_decode(wp_unslash($_POST['browser']), true); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each value is sanitized below if (is_array($decoded)) { foreach (['cookies', 'local', 'session', 'hosts'] as $list) { $values = array_slice(array_filter((array) ($decoded[$list] ?? []), 'is_string'), 0, 300); $browser[$list] = array_values(array_map(static function ($value) { return substr(sanitize_text_field($value), 0, 200); }, $values)); } $browser['consent'] = sanitize_key((string) ($decoded['consent'] ?? '')); } } $scanner = new \WPZaraz\Privacy\CookieScanner(); $results = $scanner->scan($browser); if ($results['success']) { wp_send_json_success($results); } else { wp_send_json_error($results); } } /** * Executes real-time non-destructive Edge Handshake ping to Cloudflare Zaraz. */ public function handleEdgeHandshake(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); if (!class_exists('\WPZaraz\Tracking\ServerDispatch')) { wp_send_json_error([ 'latency' => 0, 'colo' => 'N/A', 'message' => __('Edge handshake diagnostic ping is available in WP Zbridge Pro.', 'zbridge'), ]); return; } $startTime = microtime(true); $dispatcher = new ServerDispatch(); $result = $dispatcher->dispatchTestEvent([ 'id' => time(), 'total' => 0.01, 'test_handshake' => true, ]); $latencyMs = round((microtime(true) - $startTime) * 1000); $colo = 'Edge'; if (preg_match('/\[CF-Ray:\s*([a-f0-9]+)-([A-Z]{3})\]/i', $result['message'], $matches)) { $colo = $matches[2]; } if ($result['success']) { wp_send_json_success([ 'success' => true, 'latency_ms' => $latencyMs, 'colo' => $colo, 'message' => "Edge Handshake verified with Cloudflare Anycast edge in {$colo} ({$latencyMs}ms roundtrip).", ]); } else { wp_send_json_error([ 'success' => false, 'latency_ms' => $latencyMs, 'message' => $result['message'], ]); } } /** * Handles manual dismissal of the onboarding / setup wizard banner. */ public function handleDismissWizardBanner(): void { check_ajax_referer('wp_zaraz_admin_nonce', 'nonce'); $this->checkAdminPermission(); \WPZaraz\Support\Options::updateMany([ 'wizard_banner_dismissed' => true, ]); wp_send_json_success(['message' => __('Banner dismissed.', 'zbridge')]); } }