settings = new Settings(); } /** * Register API routes * * @since 1.0.0 */ public function register_routes(): void { // Integrations settings management register_rest_route( $this->namespace, '/' . $this->rest_base . '/settings', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_settings'], 'permission_callback' => [$this, 'check_read_permissions'] ], [ 'methods' => 'POST', 'callback' => [$this, 'update_settings'], 'permission_callback' => [$this, 'check_manage_permissions'], 'args' => $this->get_settings_args() ] ] ); // Test Google API connections register_rest_route( $this->namespace, '/' . $this->rest_base . '/test-connections', [ [ 'methods' => 'POST', 'callback' => [$this, 'test_connections'], 'permission_callback' => [$this, 'check_manage_permissions'] ] ] ); // Verify GA4 tracking register_rest_route( $this->namespace, '/' . $this->rest_base . '/verify-ga4-tracking', [ [ 'methods' => 'POST', 'callback' => [$this, 'verify_ga4_tracking'], 'permission_callback' => [$this, 'check_manage_permissions'], 'args' => [ 'measurement_id' => [ 'required' => true, 'type' => 'string', 'pattern' => '/^G-[A-Z0-9]{10}$/', 'sanitize_callback' => 'sanitize_text_field', 'description' => 'GA4 Measurement ID in format G-XXXXXXXXXX' ] ] ] ] ); // Detect GA4 conflicts register_rest_route( $this->namespace, '/' . $this->rest_base . '/detect-ga4-conflicts', [ [ 'methods' => 'GET', 'callback' => [$this, 'detect_ga4_conflicts'], 'permission_callback' => [$this, 'check_read_permissions'] ] ] ); } /** * Get integrations settings * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response Response object */ public function get_settings(WP_REST_Request $request): WP_REST_Response { try { $settings = $this->get_integrations_settings(); return new WP_REST_Response([ 'success' => true, 'data' => [ 'settings' => $settings ], 'message' => 'Integrations settings retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_REST_Response([ 'success' => false, 'message' => 'Failed to retrieve integrations settings: ' . $e->getMessage() ], 500); } } /** * Update integrations settings * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response Response object */ public function update_settings(WP_REST_Request $request): WP_REST_Response { try { $settings = $request->get_param('settings'); if (empty($settings) || !is_array($settings)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invalid settings data provided' ], 400); } // Sanitize and save settings $sanitized_settings = $this->sanitize_settings($settings); $success = $this->save_integrations_settings($sanitized_settings); if ($success) { return new WP_REST_Response([ 'success' => true, 'data' => [ 'settings' => $this->get_integrations_settings() ], 'message' => 'Integrations settings saved successfully' ], 200); } else { return new WP_REST_Response([ 'success' => false, 'message' => 'Failed to save integrations settings' ], 500); } } catch (\Exception $e) { return new WP_REST_Response([ 'success' => false, 'message' => 'Failed to update integrations settings: ' . $e->getMessage() ], 500); } } /** * Test Google API connections * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response Response object */ public function test_connections(WP_REST_Request $request): WP_REST_Response { try { // Get raw, unmasked API keys directly from Settings class for testing $analytics_key = $this->settings->get('google_analytics_api_key'); $search_console_key = $this->settings->get('google_search_console_api_key'); $pagespeed_key = $this->settings->get('google_pagespeed_api_key'); $results = []; // Test Google Analytics API if (!empty($analytics_key)) { $results['google_analytics'] = $this->test_google_analytics($analytics_key); } else { $results['google_analytics'] = ['status' => 'not_configured', 'message' => 'API key not configured']; } // Test Search Console API if (!empty($search_console_key)) { $results['search_console'] = $this->test_search_console($search_console_key); } else { $results['search_console'] = ['status' => 'not_configured', 'message' => 'API key not configured']; } // Test PageSpeed API if (!empty($pagespeed_key)) { $results['pagespeed'] = $this->test_pagespeed($pagespeed_key); } else { $results['pagespeed'] = ['status' => 'not_configured', 'message' => 'API key not configured']; } return new WP_REST_Response([ 'success' => true, 'data' => $results, 'message' => 'Connection tests completed' ], 200); } catch (\Exception $e) { return new WP_REST_Response([ 'success' => false, 'message' => 'Connection test failed: ' . $e->getMessage() ], 500); } } /** * Get integrations settings from Settings class (with encryption) * * @since 1.0.0 * @return array Settings array */ private function get_integrations_settings(): array { $settings = []; // Get encrypted API keys $settings['google_analytics_api_key'] = $this->settings->get('google_analytics_api_key'); $settings['google_search_console_api_key'] = $this->settings->get('google_search_console_api_key'); $settings['google_pagespeed_api_key'] = $this->settings->get('google_pagespeed_api_key'); // Get GA4 tracking settings (let Settings class handle defaults) $settings['ga4_measurement_id'] = $this->settings->get('ga4_measurement_id'); $settings['ga4_auto_inject'] = $this->settings->get('ga4_auto_inject'); $settings['ga4_anonymize_ip'] = $this->settings->get('ga4_anonymize_ip'); $settings['ga4_exclude_admin'] = $this->settings->get('ga4_exclude_admin'); $settings['ga4_tracking_verified'] = $this->settings->get('ga4_tracking_verified'); $settings['ga4_last_verification'] = $this->settings->get('ga4_last_verification'); // Get other integration settings (let Settings class handle defaults) $settings['api_timeout'] = $this->settings->get('api_timeout'); $settings['enable_rate_limiting'] = $this->settings->get('enable_rate_limiting'); $settings['cache_duration'] = $this->settings->get('cache_duration'); $settings['auto_test_connections'] = $this->settings->get('auto_test_connections'); $settings['retry_failed_requests'] = $this->settings->get('retry_failed_requests'); // Mask API keys for security (like OpenAI/Claude keys) $settings['google_analytics_api_key'] = $this->mask_api_key($settings['google_analytics_api_key']); $settings['google_search_console_api_key'] = $this->mask_api_key($settings['google_search_console_api_key']); $settings['google_pagespeed_api_key'] = $this->mask_api_key($settings['google_pagespeed_api_key']); return $settings; } /** * Save integrations settings using Settings class (with encryption) * * @since 1.0.0 * @param array $settings Settings to save * @return bool Success status */ private function save_integrations_settings(array $settings): bool { $success = true; // Save each setting individually using the Settings class // This ensures proper encryption for API keys foreach ($settings as $key => $value) { if (!$this->settings->set($key, $value)) { $success = false; // Setting save failed - error details available through settings manager } } return $success; } /** * Sanitize settings data * * @since 1.0.0 * @param array $settings Raw settings * @return array Sanitized settings */ private function sanitize_settings(array $settings): array { $sanitized = []; // Sanitize API keys (only if not empty - don't overwrite with empty values) if (!empty($settings['google_analytics_api_key'])) { $sanitized['google_analytics_api_key'] = sanitize_text_field($settings['google_analytics_api_key']); } if (!empty($settings['google_search_console_api_key'])) { $sanitized['google_search_console_api_key'] = sanitize_text_field($settings['google_search_console_api_key']); } if (!empty($settings['google_pagespeed_api_key'])) { $sanitized['google_pagespeed_api_key'] = sanitize_text_field($settings['google_pagespeed_api_key']); } // Sanitize numeric settings $sanitized['api_timeout'] = absint($settings['api_timeout'] ?? 30); $sanitized['cache_duration'] = absint($settings['cache_duration'] ?? 3600); // Sanitize GA4 tracking settings $sanitized['ga4_measurement_id'] = sanitize_text_field($settings['ga4_measurement_id'] ?? ''); $sanitized['ga4_auto_inject'] = isset($settings['ga4_auto_inject']) ? (bool) $settings['ga4_auto_inject'] : false; $sanitized['ga4_anonymize_ip'] = isset($settings['ga4_anonymize_ip']) ? (bool) $settings['ga4_anonymize_ip'] : false; $sanitized['ga4_exclude_admin'] = isset($settings['ga4_exclude_admin']) ? (bool) $settings['ga4_exclude_admin'] : false; $sanitized['ga4_tracking_verified'] = isset($settings['ga4_tracking_verified']) ? (bool) $settings['ga4_tracking_verified'] : false; $sanitized['ga4_last_verification'] = sanitize_text_field($settings['ga4_last_verification'] ?? ''); // Sanitize boolean settings $sanitized['enable_rate_limiting'] = isset($settings['enable_rate_limiting']) ? (bool) $settings['enable_rate_limiting'] : true; $sanitized['auto_test_connections'] = isset($settings['auto_test_connections']) ? (bool) $settings['auto_test_connections'] : true; $sanitized['retry_failed_requests'] = isset($settings['retry_failed_requests']) ? (bool) $settings['retry_failed_requests'] : true; return $sanitized; } /** * Mask API key for security display (XXX pattern) * * @since 1.0.0 * @param string $api_key API key to mask * @return string Masked API key or empty string */ private function mask_api_key(string $api_key): string { if (empty($api_key)) { return ''; } // Show first 6 characters + XXXX suffix (consistent with placeholders) if (strlen($api_key) > 10) { return substr($api_key, 0, 6) . 'XXXX'; } return 'XXXX'; } /** * Test Google Analytics API connection * * Makes a real API call to Google's PageSpeed Insights API to verify * that the API key is valid and has proper permissions. * * @since 1.0.0 * @param string $api_key API key to test * @return array Test result with status and message */ private function test_google_analytics(string $api_key): array { // Basic format validation if (empty($api_key) || strlen($api_key) < 30 || !str_starts_with($api_key, 'AIza')) { return [ 'status' => 'error', 'message' => 'Invalid Google API key format. Key should start with "AIza" and be at least 30 characters.' ]; } // Make a real API call to test the key // Using PageSpeed Insights API as it uses the same API key and has a simple test endpoint $test_url = add_query_arg([ 'url' => 'https://example.com/', 'key' => $api_key, 'category' => 'performance', 'strategy' => 'mobile' ], 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'); // Get timeout setting from settings or use default $timeout = absint($this->settings->get('api_timeout') ?? 30); $response = wp_remote_get($test_url, [ 'timeout' => $timeout, 'headers' => [ 'Accept' => 'application/json' ], 'sslverify' => true ]); // Handle network/connection errors if (is_wp_error($response)) { return [ 'status' => 'error', 'message' => 'Connection failed: ' . $response->get_error_message() ]; } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); // Handle HTTP errors if ($response_code === 400) { $error_data = json_decode($response_body, true); $error_message = $error_data['error']['message'] ?? 'Bad request'; return [ 'status' => 'error', 'message' => 'API key validation failed: ' . $error_message ]; } if ($response_code === 403) { $error_data = json_decode($response_body, true); $error_message = $error_data['error']['message'] ?? 'Access forbidden'; // Check if it's an API key issue if (stripos($error_message, 'API key') !== false || stripos($error_message, 'invalid') !== false) { return [ 'status' => 'error', 'message' => 'Invalid API key or insufficient permissions. Please verify your Google API key.' ]; } return [ 'status' => 'error', 'message' => 'Access denied: ' . $error_message ]; } if ($response_code === 429) { return [ 'status' => 'configured', 'message' => 'API rate limit exceeded. The key is valid but you\'ve reached the quota limit.' ]; } if ($response_code !== 200) { return [ 'status' => 'error', 'message' => 'API request failed with status code: ' . $response_code ]; } // Validate response body $data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { return [ 'status' => 'error', 'message' => 'Invalid API response format' ]; } // Check if response has expected structure if (!isset($data['lighthouseResult']) && !isset($data['loadingExperience'])) { return [ 'status' => 'error', 'message' => 'Unexpected API response structure' ]; } // Success - API key is valid and working return [ 'status' => 'configured', 'message' => 'Google API key is valid and working correctly' ]; } /** * Test Google Search Console API connection * * Makes a real API call to Google's PageSpeed Insights API to verify * that the API key is valid and has proper permissions. * * @since 1.0.0 * @param string $api_key API key to test * @return array Test result with status and message */ private function test_search_console(string $api_key): array { // Basic format validation if (empty($api_key) || strlen($api_key) < 30 || !str_starts_with($api_key, 'AIza')) { return [ 'status' => 'error', 'message' => 'Invalid Google API key format. Key should start with "AIza" and be at least 30 characters.' ]; } // Make a real API call to test the key // Using PageSpeed Insights API as it uses the same API key format $test_url = add_query_arg([ 'url' => 'https://example.com/', 'key' => $api_key, 'category' => 'seo', 'strategy' => 'desktop' ], 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'); // Get timeout setting from settings or use default $timeout = absint($this->settings->get('api_timeout') ?? 30); $response = wp_remote_get($test_url, [ 'timeout' => $timeout, 'headers' => [ 'Accept' => 'application/json' ], 'sslverify' => true ]); // Handle network/connection errors if (is_wp_error($response)) { return [ 'status' => 'error', 'message' => 'Connection failed: ' . $response->get_error_message() ]; } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); // Handle HTTP errors if ($response_code === 400) { $error_data = json_decode($response_body, true); $error_message = $error_data['error']['message'] ?? 'Bad request'; return [ 'status' => 'error', 'message' => 'API key validation failed: ' . $error_message ]; } if ($response_code === 403) { $error_data = json_decode($response_body, true); $error_message = $error_data['error']['message'] ?? 'Access forbidden'; // Check if it's an API key issue if (stripos($error_message, 'API key') !== false || stripos($error_message, 'invalid') !== false) { return [ 'status' => 'error', 'message' => 'Invalid API key or insufficient permissions. Please verify your Google API key.' ]; } return [ 'status' => 'error', 'message' => 'Access denied: ' . $error_message ]; } if ($response_code === 429) { return [ 'status' => 'configured', 'message' => 'API rate limit exceeded. The key is valid but you\'ve reached the quota limit.' ]; } if ($response_code !== 200) { return [ 'status' => 'error', 'message' => 'API request failed with status code: ' . $response_code ]; } // Validate response body $data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { return [ 'status' => 'error', 'message' => 'Invalid API response format' ]; } // Check if response has expected structure if (!isset($data['lighthouseResult']) && !isset($data['loadingExperience'])) { return [ 'status' => 'error', 'message' => 'Unexpected API response structure' ]; } // Success - API key is valid and working return [ 'status' => 'configured', 'message' => 'Google API key is valid and working correctly' ]; } /** * Test Google PageSpeed API connection * * Makes a real API call to Google's PageSpeed Insights API to verify * that the API key is valid and has proper permissions. * * @since 1.0.0 * @param string $api_key API key to test * @return array Test result with status and message */ private function test_pagespeed(string $api_key): array { // Basic format validation if (empty($api_key) || strlen($api_key) < 30 || !str_starts_with($api_key, 'AIza')) { return [ 'status' => 'error', 'message' => 'Invalid Google API key format. Key should start with "AIza" and be at least 30 characters.' ]; } // Make a real API call to test the key $test_url = add_query_arg([ 'url' => 'https://example.com/', 'key' => $api_key, 'category' => 'performance', 'strategy' => 'mobile' ], 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'); // Get timeout setting from settings or use default $timeout = absint($this->settings->get('api_timeout') ?? 30); $response = wp_remote_get($test_url, [ 'timeout' => $timeout, 'headers' => [ 'Accept' => 'application/json' ], 'sslverify' => true ]); // Handle network/connection errors if (is_wp_error($response)) { return [ 'status' => 'error', 'message' => 'Connection failed: ' . $response->get_error_message() ]; } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); // Handle HTTP errors if ($response_code === 400) { $error_data = json_decode($response_body, true); $error_message = $error_data['error']['message'] ?? 'Bad request'; return [ 'status' => 'error', 'message' => 'API key validation failed: ' . $error_message ]; } if ($response_code === 403) { $error_data = json_decode($response_body, true); $error_message = $error_data['error']['message'] ?? 'Access forbidden'; // Check if it's an API key issue if (stripos($error_message, 'API key') !== false || stripos($error_message, 'invalid') !== false) { return [ 'status' => 'error', 'message' => 'Invalid API key or insufficient permissions. Please verify your Google API key.' ]; } return [ 'status' => 'error', 'message' => 'Access denied: ' . $error_message ]; } if ($response_code === 429) { return [ 'status' => 'configured', 'message' => 'API rate limit exceeded. The key is valid but you\'ve reached the quota limit.' ]; } if ($response_code !== 200) { return [ 'status' => 'error', 'message' => 'API request failed with status code: ' . $response_code ]; } // Validate response body $data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { return [ 'status' => 'error', 'message' => 'Invalid API response format' ]; } // Check if response has expected structure if (!isset($data['lighthouseResult']) && !isset($data['loadingExperience'])) { return [ 'status' => 'error', 'message' => 'Unexpected API response structure' ]; } // Success - API key is valid and working return [ 'status' => 'configured', 'message' => 'Google API key is valid and working correctly' ]; } /** * Get settings arguments for REST API * * @since 1.0.0 * @return array Settings arguments */ private function get_settings_args(): array { return [ 'settings' => [ 'required' => true, 'type' => 'object', 'description' => 'Integrations settings object' ] ]; } /** * Check read permissions * * @since 1.0.0 * @return bool Permission status */ public function check_read_permissions(): bool { return current_user_can('manage_options'); } /** * Check manage permissions * * @since 1.0.0 * @return bool Permission status */ public function check_manage_permissions(): bool { return current_user_can('manage_options'); } /** * Verify GA4 tracking * Following ThinkRank API response patterns * * @since 1.0.0 * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function verify_ga4_tracking(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $measurement_id = $request->get_param('measurement_id'); if (empty($measurement_id)) { return new WP_Error( 'missing_measurement_id', 'Measurement ID is required', ['status' => 400] ); } // Load tracking manager if (!class_exists('ThinkRank\\Frontend\\Google_Analytics_Tracking_Manager')) { require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/class-google-analytics-tracking-manager.php'; } $tracking_manager = new \ThinkRank\Frontend\Google_Analytics_Tracking_Manager(); $verification_result = $tracking_manager->verify_tracking($measurement_id); return new WP_REST_Response([ 'success' => true, 'data' => $verification_result, 'message' => 'Tracking verification completed' ], 200); } catch (\Exception $e) { return new WP_Error( 'verification_failed', 'Tracking verification failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Detect GA4 conflicts * Following ThinkRank API response patterns * * @since 1.0.0 * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function detect_ga4_conflicts(WP_REST_Request $request): WP_REST_Response|WP_Error { try { // Load tracking manager if (!class_exists('ThinkRank\\Frontend\\Google_Analytics_Tracking_Manager')) { require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/class-google-analytics-tracking-manager.php'; } $tracking_manager = new \ThinkRank\Frontend\Google_Analytics_Tracking_Manager(); $conflicts = $tracking_manager->detect_existing_tracking(); return new WP_REST_Response([ 'success' => true, 'data' => [ 'conflicts' => $conflicts, 'has_conflicts' => !empty($conflicts) ], 'message' => 'Conflict detection completed' ], 200); } catch (\Exception $e) { return new WP_Error( 'conflict_detection_failed', 'Conflict detection failed: ' . $e->getMessage(), ['status' => 500] ); } } }