PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.10.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / api / class-integrations-endpoint.php

class-integrations-endpoint.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.10.0, at includes/api/class-integrations-endpoint.php

1,090 lines 38.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Integrations API Endpoints Class
5 *
6 * Provides REST API endpoints for Google API integrations management
7 * following the same pattern as Site Identity and Social Media endpoints.
8 *
9 * @package ThinkRank\API
10 * @since 1.0.0
11 */
12
13 declare(strict_types=1);
14
15 namespace ThinkRank\API;
16
17 use WP_REST_Controller;
18 use WP_REST_Server;
19 use WP_REST_Request;
20 use WP_REST_Response;
21 use WP_Error;
22 use ThinkRank\Core\Settings;
23
24 // Prevent direct access
25 if (!defined('ABSPATH')) {
26 exit;
27 }
28
29 /**
30 * Integrations API Endpoints Class
31 *
32 * Handles Google API keys and integration settings management
33 * following ThinkRank patterns from working endpoints.
34 *
35 * @since 1.0.0
36 */
37 class Integrations_Endpoint extends WP_REST_Controller {
38
39 /**
40 * API namespace
41 *
42 * @since 1.0.0
43 * @var string
44 */
45 protected $namespace = 'thinkrank/v1';
46
47 /**
48 * REST base
49 *
50 * @since 1.0.0
51 * @var string
52 */
53 protected $rest_base = 'integrations';
54
55 /**
56 * Settings instance
57 *
58 * @since 1.0.0
59 * @var Settings
60 */
61 private Settings $settings;
62
63 /**
64 * Constructor
65 *
66 * @since 1.0.0
67 */
68 public function __construct() {
69 $this->settings = new Settings();
70 }
71
72 /**
73 * Register API routes
74 *
75 * @since 1.0.0
76 */
77 public function register_routes(): void {
78 // Integrations settings management
79 register_rest_route(
80 $this->namespace,
81 '/' . $this->rest_base . '/settings',
82 [
83 [
84 'methods' => 'GET',
85 'callback' => [$this, 'get_settings'],
86 'permission_callback' => [$this, 'check_read_permissions']
87 ],
88 [
89 'methods' => 'POST',
90 'callback' => [$this, 'update_settings'],
91 'permission_callback' => [$this, 'check_manage_permissions'],
92 'args' => $this->get_settings_args()
93 ]
94 ]
95 );
96
97 // Test Google API connections
98 register_rest_route(
99 $this->namespace,
100 '/' . $this->rest_base . '/test-connections',
101 [
102 [
103 'methods' => 'POST',
104 'callback' => [$this, 'test_connections'],
105 'permission_callback' => [$this, 'check_manage_permissions']
106 ]
107 ]
108 );
109
110 // Verify GA4 tracking
111 register_rest_route(
112 $this->namespace,
113 '/' . $this->rest_base . '/verify-ga4-tracking',
114 [
115 [
116 'methods' => 'POST',
117 'callback' => [$this, 'verify_ga4_tracking'],
118 'permission_callback' => [$this, 'check_manage_permissions'],
119 'args' => [
120 'measurement_id' => [
121 'required' => true,
122 'type' => 'string',
123 'pattern' => '/^G-[A-Z0-9]{10}$/',
124 'sanitize_callback' => 'sanitize_text_field',
125 'description' => 'GA4 Measurement ID in format G-XXXXXXXXXX'
126 ]
127 ]
128 ]
129 ]
130 );
131
132 // Detect GA4 conflicts
133 register_rest_route(
134 $this->namespace,
135 '/' . $this->rest_base . '/detect-ga4-conflicts',
136 [
137 [
138 'methods' => 'GET',
139 'callback' => [$this, 'detect_ga4_conflicts'],
140 'permission_callback' => [$this, 'check_read_permissions']
141 ]
142 ]
143 );
144
145 // Get Search Console Sites
146 register_rest_route(
147 $this->namespace,
148 '/' . $this->rest_base . '/search-console/sites',
149 [
150 [
151 'methods' => 'GET',
152 'callback' => [$this, 'get_search_console_sites'],
153 'permission_callback' => [$this, 'check_manage_permissions']
154 ]
155 ]
156 );
157
158 // Disconnect Google Account
159 register_rest_route($this->namespace, '/integrations/google/disconnect', [
160 'methods' => WP_REST_Server::CREATABLE,
161 'callback' => [$this, 'disconnect_google_account'],
162 'permission_callback' => [$this, 'check_manage_permissions'] // Changed to check_manage_permissions for consistency
163 ]);
164
165 // Save Google OAuth Tokens
166 register_rest_route(
167 $this->namespace,
168 '/' . $this->rest_base . '/save-google-token',
169 [
170 [
171 'methods' => 'POST',
172 'callback' => [$this, 'save_google_token'],
173 'permission_callback' => [$this, 'check_manage_permissions'],
174 'args' => [
175 'access_token' => [
176 'required' => true,
177 'type' => 'string',
178 'sanitize_callback' => 'sanitize_text_field',
179 ],
180 'refresh_token' => [
181 'required' => false,
182 'type' => 'string',
183 'sanitize_callback' => 'sanitize_text_field',
184 ],
185 'expires_in' => [
186 'required' => false,
187 'type' => 'integer',
188 ],
189 'created' => [
190 'required' => false,
191 'type' => 'integer',
192 ],
193 ]
194 ]
195 ]
196 );
197
198 // Get Search Console Sites
199 register_rest_route(
200 $this->namespace,
201 '/' . $this->rest_base . '/search-console/sites',
202 [
203 [
204 'methods' => 'GET',
205 'callback' => [$this, 'get_search_console_sites'],
206 'permission_callback' => [$this, 'check_manage_permissions']
207 ]
208 ]
209 );
210 }
211
212 /**
213 * Get integrations settings
214 *
215 * @since 1.0.0
216 *
217 * @param WP_REST_Request $request Request object
218 * @return WP_REST_Response Response object
219 */
220 public function get_settings(WP_REST_Request $request): WP_REST_Response {
221 try {
222 $settings = $this->get_integrations_settings();
223
224 return new WP_REST_Response([
225 'success' => true,
226 'data' => [
227 'settings' => $settings
228 ],
229 'message' => 'Integrations settings retrieved successfully'
230 ], 200);
231 } catch (\Exception $e) {
232 return new WP_REST_Response([
233 'success' => false,
234 'message' => 'Failed to retrieve integrations settings: ' . $e->getMessage()
235 ], 500);
236 }
237 }
238
239 /**
240 * Update integrations settings
241 *
242 * @since 1.0.0
243 *
244 * @param WP_REST_Request $request Request object
245 * @return WP_REST_Response Response object
246 */
247 public function update_settings(WP_REST_Request $request): WP_REST_Response {
248 try {
249 $settings = $request->get_param('settings');
250
251 if (empty($settings) || !is_array($settings)) {
252 return new WP_REST_Response([
253 'success' => false,
254 'message' => 'Invalid settings data provided'
255 ], 400);
256 }
257
258 // Sanitize and save settings
259 $sanitized_settings = $this->sanitize_settings($settings);
260 $success = $this->save_integrations_settings($sanitized_settings);
261
262 if ($success) {
263 return new WP_REST_Response([
264 'success' => true,
265 'data' => [
266 'settings' => $this->get_integrations_settings()
267 ],
268 'message' => 'Integrations settings saved successfully'
269 ], 200);
270 } else {
271 return new WP_REST_Response([
272 'success' => false,
273 'message' => 'Failed to save integrations settings'
274 ], 500);
275 }
276 } catch (\Exception $e) {
277 return new WP_REST_Response([
278 'success' => false,
279 'message' => 'Failed to update integrations settings: ' . $e->getMessage()
280 ], 500);
281 }
282 }
283
284 /**
285 * Test Google API connections
286 *
287 * @since 1.0.0
288 *
289 * @param WP_REST_Request $request Request object
290 * @return WP_REST_Response Response object
291 */
292 public function test_connections(WP_REST_Request $request): WP_REST_Response {
293 try {
294 // Get raw, unmasked API keys directly from Settings class for testing
295 $analytics_key = $this->settings->get('google_analytics_api_key');
296 $search_console_key = $this->settings->get('google_search_console_api_key');
297 $pagespeed_key = $this->settings->get('google_pagespeed_api_key');
298
299 $results = [];
300
301 // Test Google Analytics API
302 if (!empty($analytics_key)) {
303 $results['google_analytics'] = $this->test_google_analytics($analytics_key);
304 } else {
305 $results['google_analytics'] = ['status' => 'not_configured', 'message' => 'API key not configured'];
306 }
307
308 // Test Search Console API
309 if (!empty($search_console_key)) {
310 $results['search_console'] = $this->test_search_console($search_console_key);
311 } else {
312 $results['search_console'] = ['status' => 'not_configured', 'message' => 'API key not configured'];
313 }
314
315 // Test PageSpeed API (now uses OAuth access token)
316 $access_token = $this->settings->get('google_access_token');
317 if (!empty($access_token)) {
318 $results['pagespeed'] = $this->test_pagespeed_oauth($access_token);
319 } else {
320 $results['pagespeed'] = ['status' => 'not_configured', 'message' => 'Google account not connected'];
321 }
322
323 return new WP_REST_Response([
324 'success' => true,
325 'data' => $results,
326 'message' => 'Connection tests completed'
327 ], 200);
328 } catch (\Exception $e) {
329 return new WP_REST_Response([
330 'success' => false,
331 'message' => 'Connection test failed: ' . $e->getMessage()
332 ], 500);
333 }
334 }
335
336 /**
337 * Get integrations settings from Settings class (with encryption)
338 *
339 * @since 1.0.0
340 * @return array Settings array
341 */
342 private function get_integrations_settings(): array {
343 $settings = [];
344
345 // Get encrypted API keys
346 $settings['google_analytics_api_key'] = $this->settings->get('google_analytics_api_key');
347 $settings['google_search_console_api_key'] = $this->settings->get('google_search_console_api_key');
348 $settings['google_pagespeed_api_key'] = $this->settings->get('google_pagespeed_api_key');
349
350 // Get GA4 tracking settings (let Settings class handle defaults)
351 $settings['ga4_measurement_id'] = $this->settings->get('ga4_measurement_id');
352 $settings['ga4_auto_inject'] = $this->settings->get('ga4_auto_inject');
353 $settings['ga4_anonymize_ip'] = $this->settings->get('ga4_anonymize_ip');
354 $settings['ga4_exclude_admin'] = $this->settings->get('ga4_exclude_admin');
355 $settings['ga4_tracking_verified'] = $this->settings->get('ga4_tracking_verified');
356 $settings['ga4_last_verification'] = $this->settings->get('ga4_last_verification');
357
358 // Get other integration settings (let Settings class handle defaults)
359 $settings['api_timeout'] = $this->settings->get('api_timeout');
360 $settings['enable_rate_limiting'] = $this->settings->get('enable_rate_limiting');
361 $settings['cache_duration'] = $this->settings->get('cache_duration');
362 $settings['auto_test_connections'] = $this->settings->get('auto_test_connections');
363 $settings['auto_test_connections'] = $this->settings->get('auto_test_connections');
364 $settings['retry_failed_requests'] = $this->settings->get('retry_failed_requests');
365 $settings['google_account_connected'] = $this->settings->get('google_account_connected');
366
367 // Mask API keys for security (like OpenAI/Claude keys)
368 $settings['google_analytics_api_key'] = $this->mask_api_key($settings['google_analytics_api_key']);
369 $settings['google_search_console_api_key'] = $this->mask_api_key($settings['google_search_console_api_key']);
370 $settings['google_pagespeed_api_key'] = $this->mask_api_key($settings['google_pagespeed_api_key']);
371
372 return $settings;
373 }
374
375 /**
376 * Save integrations settings using Settings class (with encryption)
377 *
378 * @since 1.0.0
379 * @param array $settings Settings to save
380 * @return bool Success status
381 */
382 private function save_integrations_settings(array $settings): bool {
383 $success = true;
384
385 // Save each setting individually using the Settings class
386 // This ensures proper encryption for API keys
387 foreach ($settings as $key => $value) {
388 if (!$this->settings->set($key, $value)) {
389 $success = false;
390 // Setting save failed - error details available through settings manager
391 }
392 }
393
394 return $success;
395 }
396
397 /**
398 * Sanitize settings data
399 *
400 * @since 1.0.0
401 * @param array $settings Raw settings
402 * @return array Sanitized settings
403 */
404 private function sanitize_settings(array $settings): array {
405 $sanitized = [];
406
407 // Sanitize API keys (only if not empty - don't overwrite with empty values)
408 if (!empty($settings['google_analytics_api_key'])) {
409 $sanitized['google_analytics_api_key'] = sanitize_text_field($settings['google_analytics_api_key']);
410 }
411 if (!empty($settings['google_search_console_api_key'])) {
412 $sanitized['google_search_console_api_key'] = sanitize_text_field($settings['google_search_console_api_key']);
413 }
414 if (!empty($settings['google_pagespeed_api_key'])) {
415 $sanitized['google_pagespeed_api_key'] = sanitize_text_field($settings['google_pagespeed_api_key']);
416 }
417
418 // Sanitize numeric settings
419 $sanitized['api_timeout'] = absint($settings['api_timeout'] ?? 30);
420 $sanitized['cache_duration'] = absint($settings['cache_duration'] ?? 3600);
421
422 // Sanitize GA4 tracking settings
423 $sanitized['ga4_measurement_id'] = sanitize_text_field($settings['ga4_measurement_id'] ?? '');
424 $sanitized['ga4_auto_inject'] = isset($settings['ga4_auto_inject']) ? (bool) $settings['ga4_auto_inject'] : false;
425 $sanitized['ga4_anonymize_ip'] = isset($settings['ga4_anonymize_ip']) ? (bool) $settings['ga4_anonymize_ip'] : false;
426 $sanitized['ga4_exclude_admin'] = isset($settings['ga4_exclude_admin']) ? (bool) $settings['ga4_exclude_admin'] : false;
427 $sanitized['ga4_tracking_verified'] = isset($settings['ga4_tracking_verified']) ? (bool) $settings['ga4_tracking_verified'] : false;
428 $sanitized['ga4_last_verification'] = sanitize_text_field($settings['ga4_last_verification'] ?? '');
429
430 // Sanitize boolean settings
431 $sanitized['enable_rate_limiting'] = isset($settings['enable_rate_limiting']) ? (bool) $settings['enable_rate_limiting'] : true;
432 $sanitized['auto_test_connections'] = isset($settings['auto_test_connections']) ? (bool) $settings['auto_test_connections'] : true;
433 $sanitized['retry_failed_requests'] = isset($settings['retry_failed_requests']) ? (bool) $settings['retry_failed_requests'] : true;
434
435 return $sanitized;
436 }
437
438 /**
439 * Mask API key for security display (XXX pattern)
440 *
441 * @since 1.0.0
442 * @param string $api_key API key to mask
443 * @return string Masked API key or empty string
444 */
445 private function mask_api_key(string $api_key): string {
446 if (empty($api_key)) {
447 return '';
448 }
449
450 // Show first 6 characters + XXXX suffix (consistent with placeholders)
451 if (strlen($api_key) > 10) {
452 return substr($api_key, 0, 6) . 'XXXX';
453 }
454
455 return 'XXXX';
456 }
457
458 /**
459 * Test Google Analytics API connection
460 *
461 * Makes a real API call to Google's PageSpeed Insights API to verify
462 * that the API key is valid and has proper permissions.
463 *
464 * @since 1.0.0
465 * @param string $api_key API key to test
466 * @return array Test result with status and message
467 */
468 private function test_google_analytics(string $api_key): array {
469 // Basic format validation
470 if (empty($api_key) || strlen($api_key) < 30 || !str_starts_with($api_key, 'AIza')) {
471 return [
472 'status' => 'error',
473 'message' => 'Invalid Google API key format. Key should start with "AIza" and be at least 30 characters.'
474 ];
475 }
476
477 // Make a real API call to test the key
478 // Using PageSpeed Insights API as it uses the same API key and has a simple test endpoint
479 $test_url = add_query_arg([
480 'url' => 'https://example.com/',
481 'key' => $api_key,
482 'category' => 'performance',
483 'strategy' => 'mobile'
484 ], 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed');
485
486 // Get timeout setting from settings or use default
487 $timeout = absint($this->settings->get('api_timeout') ?? 30);
488
489 $response = wp_remote_get($test_url, [
490 'timeout' => $timeout,
491 'headers' => [
492 'Accept' => 'application/json'
493 ],
494 'sslverify' => true
495 ]);
496
497 // Handle network/connection errors
498 if (is_wp_error($response)) {
499 return [
500 'status' => 'error',
501 'message' => 'Connection failed: ' . $response->get_error_message()
502 ];
503 }
504
505 $response_code = wp_remote_retrieve_response_code($response);
506 $response_body = wp_remote_retrieve_body($response);
507
508 // Handle HTTP errors
509 if ($response_code === 400) {
510 $error_data = json_decode($response_body, true);
511 $error_message = $error_data['error']['message'] ?? 'Bad request';
512
513 return [
514 'status' => 'error',
515 'message' => 'API key validation failed: ' . $error_message
516 ];
517 }
518
519 if ($response_code === 403) {
520 $error_data = json_decode($response_body, true);
521 $error_message = $error_data['error']['message'] ?? 'Access forbidden';
522
523 // Check if it's an API key issue
524 if (stripos($error_message, 'API key') !== false || stripos($error_message, 'invalid') !== false) {
525 return [
526 'status' => 'error',
527 'message' => 'Invalid API key or insufficient permissions. Please verify your Google API key.'
528 ];
529 }
530
531 return [
532 'status' => 'error',
533 'message' => 'Access denied: ' . $error_message
534 ];
535 }
536
537 if ($response_code === 429) {
538 return [
539 'status' => 'configured',
540 'message' => 'API rate limit exceeded. The key is valid but you\'ve reached the quota limit.'
541 ];
542 }
543
544 if ($response_code !== 200) {
545 return [
546 'status' => 'error',
547 'message' => 'API request failed with status code: ' . $response_code
548 ];
549 }
550
551 // Validate response body
552 $data = json_decode($response_body, true);
553
554 if (json_last_error() !== JSON_ERROR_NONE) {
555 return [
556 'status' => 'error',
557 'message' => 'Invalid API response format'
558 ];
559 }
560
561 // Check if response has expected structure
562 if (!isset($data['lighthouseResult']) && !isset($data['loadingExperience'])) {
563 return [
564 'status' => 'error',
565 'message' => 'Unexpected API response structure'
566 ];
567 }
568
569 // Success - API key is valid and working
570 return [
571 'status' => 'configured',
572 'message' => 'Google API key is valid and working correctly'
573 ];
574 }
575
576 /**
577 * Test Google Search Console API connection
578 *
579 * Makes a real API call to Google's PageSpeed Insights API to verify
580 * that the API key is valid and has proper permissions.
581 *
582 * @since 1.0.0
583 * @param string $api_key API key to test
584 * @return array Test result with status and message
585 */
586 private function test_search_console(string $api_key): array {
587 // Basic format validation
588 if (empty($api_key) || strlen($api_key) < 30 || !str_starts_with($api_key, 'AIza')) {
589 return [
590 'status' => 'error',
591 'message' => 'Invalid Google API key format. Key should start with "AIza" and be at least 30 characters.'
592 ];
593 }
594
595 // Make a real API call to test the key
596 // Using PageSpeed Insights API as it uses the same API key format
597 $test_url = add_query_arg([
598 'url' => 'https://example.com/',
599 'key' => $api_key,
600 'category' => 'seo',
601 'strategy' => 'desktop'
602 ], 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed');
603
604 // Get timeout setting from settings or use default
605 $timeout = absint($this->settings->get('api_timeout') ?? 30);
606
607 $response = wp_remote_get($test_url, [
608 'timeout' => $timeout,
609 'headers' => [
610 'Accept' => 'application/json'
611 ],
612 'sslverify' => true
613 ]);
614
615 // Handle network/connection errors
616 if (is_wp_error($response)) {
617 return [
618 'status' => 'error',
619 'message' => 'Connection failed: ' . $response->get_error_message()
620 ];
621 }
622
623 $response_code = wp_remote_retrieve_response_code($response);
624 $response_body = wp_remote_retrieve_body($response);
625
626 // Handle HTTP errors
627 if ($response_code === 400) {
628 $error_data = json_decode($response_body, true);
629 $error_message = $error_data['error']['message'] ?? 'Bad request';
630
631 return [
632 'status' => 'error',
633 'message' => 'API key validation failed: ' . $error_message
634 ];
635 }
636
637 if ($response_code === 403) {
638 $error_data = json_decode($response_body, true);
639 $error_message = $error_data['error']['message'] ?? 'Access forbidden';
640
641 // Check if it's an API key issue
642 if (stripos($error_message, 'API key') !== false || stripos($error_message, 'invalid') !== false) {
643 return [
644 'status' => 'error',
645 'message' => 'Invalid API key or insufficient permissions. Please verify your Google API key.'
646 ];
647 }
648
649 return [
650 'status' => 'error',
651 'message' => 'Access denied: ' . $error_message
652 ];
653 }
654
655 if ($response_code === 429) {
656 return [
657 'status' => 'configured',
658 'message' => 'API rate limit exceeded. The key is valid but you\'ve reached the quota limit.'
659 ];
660 }
661
662 if ($response_code !== 200) {
663 return [
664 'status' => 'error',
665 'message' => 'API request failed with status code: ' . $response_code
666 ];
667 }
668
669 // Validate response body
670 $data = json_decode($response_body, true);
671
672 if (json_last_error() !== JSON_ERROR_NONE) {
673 return [
674 'status' => 'error',
675 'message' => 'Invalid API response format'
676 ];
677 }
678
679 // Check if response has expected structure
680 if (!isset($data['lighthouseResult']) && !isset($data['loadingExperience'])) {
681 return [
682 'status' => 'error',
683 'message' => 'Unexpected API response structure'
684 ];
685 }
686
687 // Success - API key is valid and working
688 return [
689 'status' => 'configured',
690 'message' => 'Google API key is valid and working correctly'
691 ];
692 }
693
694 /**
695 * Test Google PageSpeed API connection using OAuth token
696 *
697 * Makes a real API call to Google's PageSpeed Insights API using OAuth
698 * Bearer token to verify that the account is properly connected.
699 *
700 * @since 1.0.0
701 * @param string $access_token OAuth access token
702 * @return array Test result with status and message
703 */
704 private function test_pagespeed_oauth(string $access_token): array {
705 if (empty($access_token)) {
706 return [
707 'status' => 'error',
708 'message' => 'Google account not connected. Please connect your Google account to use PageSpeed Insights.'
709 ];
710 }
711
712 // Make a real API call to test the OAuth token with PageSpeed API
713 $test_url = add_query_arg([
714 'url' => 'https://example.com/',
715 'category' => 'performance',
716 'strategy' => 'mobile'
717 ], 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed');
718
719 // Get timeout setting from settings or use default
720 $timeout = absint($this->settings->get('api_timeout') ?? 30);
721
722 $response = wp_remote_get($test_url, [
723 'timeout' => $timeout,
724 'headers' => [
725 'Accept' => 'application/json',
726 'Authorization' => 'Bearer ' . $access_token
727 ],
728 'sslverify' => true
729 ]);
730
731 // Handle network/connection errors
732 if (is_wp_error($response)) {
733 return [
734 'status' => 'error',
735 'message' => 'Connection failed: ' . $response->get_error_message()
736 ];
737 }
738
739 $response_code = wp_remote_retrieve_response_code($response);
740 $response_body = wp_remote_retrieve_body($response);
741
742 // Handle HTTP errors
743 if ($response_code === 401) {
744 return [
745 'status' => 'error',
746 'message' => 'Google account token is expired or invalid. Please reconnect your Google account.'
747 ];
748 }
749
750 if ($response_code === 403) {
751 $error_data = json_decode($response_body, true);
752 $error_message = $error_data['error']['message'] ?? 'Access forbidden';
753 return [
754 'status' => 'error',
755 'message' => 'Access denied: ' . $error_message
756 ];
757 }
758
759 if ($response_code === 429) {
760 return [
761 'status' => 'configured',
762 'message' => 'API rate limit exceeded. The account is valid but you\'ve reached the quota limit.'
763 ];
764 }
765
766 if ($response_code !== 200) {
767 return [
768 'status' => 'error',
769 'message' => 'API request failed with status code: ' . $response_code
770 ];
771 }
772
773 // Validate response body
774 $data = json_decode($response_body, true);
775
776 if (json_last_error() !== JSON_ERROR_NONE) {
777 return [
778 'status' => 'error',
779 'message' => 'Invalid API response format'
780 ];
781 }
782
783 // Check if response has expected structure
784 if (!isset($data['lighthouseResult']) && !isset($data['loadingExperience'])) {
785 return [
786 'status' => 'error',
787 'message' => 'Unexpected API response structure'
788 ];
789 }
790
791 // Success - OAuth token is valid and working with PageSpeed API
792 return [
793 'status' => 'configured',
794 'message' => 'Google PageSpeed Insights connected successfully via Google OAuth'
795 ];
796 }
797
798 /**
799 * Save Google OAuth Token
800 *
801 * @since 1.0.0
802 * @param WP_REST_Request $request Request object
803 * @return WP_REST_Response|WP_Error Response object
804 */
805 public function save_google_token(WP_REST_Request $request): WP_REST_Response|WP_Error {
806 try {
807 $access_token = $request->get_param('access_token');
808 $refresh_token = $request->get_param('refresh_token');
809 $expires_in = $request->get_param('expires_in');
810 $created = $request->get_param('created') ?: time();
811
812 // Save to settings
813 $this->settings->set('google_access_token', $access_token);
814 if (!empty($refresh_token)) {
815 $this->settings->set('google_refresh_token', $refresh_token);
816 }
817 if (!empty($expires_in)) {
818 $this->settings->set('google_token_expires_in', $expires_in);
819 }
820 $this->settings->set('google_token_created', $created);
821
822 // Mark as connected
823 $this->settings->set('google_account_connected', true);
824
825 return new WP_REST_Response([
826 'success' => true,
827 'message' => 'Google tokens saved successfully'
828 ], 200);
829 } catch (\Exception $e) {
830 return new WP_Error(
831 'save_token_failed',
832 'Failed to save Google tokens: ' . $e->getMessage(),
833 ['status' => 500]
834 );
835 }
836 }
837
838 /**
839 * Disconnect Google Account
840 *
841 * @since 1.0.0
842 * @param WP_REST_Request $request Request object
843 * @return WP_REST_Response|WP_Error Response object
844 */
845 public function disconnect_google_account(WP_REST_Request $request): WP_REST_Response|WP_Error {
846 try {
847 // Clear all Google-related settings
848 $this->settings->set('google_access_token', '');
849 $this->settings->set('google_refresh_token', '');
850 $this->settings->set('google_token_expires_in', '');
851 $this->settings->set('google_token_created', '');
852 $this->settings->set('google_account_connected', false);
853 // Also clear site selection
854 $this->settings->set('google_search_console_site', '');
855
856 return new WP_REST_Response([
857 'success' => true,
858 'message' => 'Google account disconnected successfully'
859 ], 200);
860 } catch (\Exception $e) {
861 return new WP_Error(
862 'disconnect_failed',
863 'Failed to disconnect Google account: ' . $e->getMessage(),
864 ['status' => 500]
865 );
866 }
867 }
868
869
870 /**
871 * Get settings arguments for REST API
872 *
873 * @since 1.0.0
874 * @return array Settings arguments
875 */
876 private function get_settings_args(): array {
877 return [
878 'settings' => [
879 'required' => true,
880 'type' => 'object',
881 'description' => 'Integrations settings object'
882 ]
883 ];
884 }
885
886 /**
887 * Check read permissions
888 *
889 * @since 1.0.0
890 * @return bool Permission status
891 */
892 public function check_read_permissions(): bool {
893 return current_user_can('manage_options');
894 }
895
896 /**
897 * Check manage permissions
898 *
899 * @since 1.0.0
900 * @return bool Permission status
901 */
902 public function check_manage_permissions(): bool {
903 return current_user_can('manage_options');
904 }
905
906 /**
907 * Verify GA4 tracking
908 * Following ThinkRank API response patterns
909 *
910 * @since 1.0.0
911 * @param WP_REST_Request $request Request object
912 * @return WP_REST_Response|WP_Error Response object
913 */
914 public function verify_ga4_tracking(WP_REST_Request $request): WP_REST_Response|WP_Error {
915 try {
916 $measurement_id = $request->get_param('measurement_id');
917
918 if (empty($measurement_id)) {
919 return new WP_Error(
920 'missing_measurement_id',
921 'Measurement ID is required',
922 ['status' => 400]
923 );
924 }
925
926 // Load tracking manager
927 if (!class_exists('ThinkRank\\Frontend\\Google_Analytics_Tracking_Manager')) {
928 require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/class-google-analytics-tracking-manager.php';
929 }
930
931 $tracking_manager = new \ThinkRank\Frontend\Google_Analytics_Tracking_Manager();
932 $verification_result = $tracking_manager->verify_tracking($measurement_id);
933
934 return new WP_REST_Response([
935 'success' => true,
936 'data' => $verification_result,
937 'message' => 'Tracking verification completed'
938 ], 200);
939 } catch (\Exception $e) {
940 return new WP_Error(
941 'verification_failed',
942 'Tracking verification failed: ' . $e->getMessage(),
943 ['status' => 500]
944 );
945 }
946 }
947
948 /**
949 * Detect GA4 conflicts
950 * Following ThinkRank API response patterns
951 *
952 * @since 1.0.0
953 * @param WP_REST_Request $request Request object
954 * @return WP_REST_Response|WP_Error Response object
955 */
956 public function detect_ga4_conflicts(WP_REST_Request $request): WP_REST_Response|WP_Error {
957 try {
958 // Load tracking manager
959 if (!class_exists('ThinkRank\\Frontend\\Google_Analytics_Tracking_Manager')) {
960 require_once THINKRANK_PLUGIN_DIR . 'includes/frontend/class-google-analytics-tracking-manager.php';
961 }
962
963 $tracking_manager = new \ThinkRank\Frontend\Google_Analytics_Tracking_Manager();
964 $conflicts = $tracking_manager->detect_existing_tracking();
965
966 return new WP_REST_Response([
967 'success' => true,
968 'data' => [
969 'conflicts' => $conflicts,
970 'has_conflicts' => !empty($conflicts)
971 ],
972 'message' => 'Conflict detection completed'
973 ], 200);
974 } catch (\Exception $e) {
975 return new WP_Error(
976 'conflict_detection_failed',
977 'Conflict detection failed: ' . $e->getMessage(),
978 ['status' => 500]
979 );
980 }
981 }
982 /**
983 * Get Search Console Sites
984 *
985 * @since 1.0.0
986 * @param WP_REST_Request $request Request object
987 * @return WP_REST_Response Response object
988 */
989 public function get_search_console_sites(WP_REST_Request $request): WP_REST_Response {
990 try {
991 // Ensure Analytics_Manager is loaded for proactive token refresh
992 if (!class_exists('ThinkRank\\SEO\\Analytics_Manager')) {
993 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-analytics-manager.php';
994 }
995
996 // Proactively refresh token if expired — prevents 401 errors on initial load
997 \ThinkRank\SEO\Analytics_Manager::ensure_fresh_token();
998
999 // Get access token (now guaranteed fresh if refresh_token is available)
1000 $access_token = $this->settings->get('google_access_token');
1001 $api_key = $this->settings->get('google_search_console_api_key');
1002
1003 if (empty($access_token)) {
1004 return new WP_REST_Response([
1005 'success' => false,
1006 'message' => 'Google account not connected'
1007 ], 401);
1008 }
1009
1010 // Initialize Search Console Client
1011 if (!class_exists('ThinkRank\\Integrations\\Google_Search_Console_Client')) {
1012 require_once THINKRANK_PLUGIN_DIR . 'includes/integrations/class-google-search-console-client.php';
1013 }
1014
1015 // Ensure Analytics_Manager is loaded for token refresh
1016 if (!class_exists('ThinkRank\\SEO\\Analytics_Manager')) {
1017 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-analytics-manager.php';
1018 }
1019
1020 // We need a client that can use the access token
1021 $client = new \ThinkRank\Integrations\Google_Search_Console_Client(
1022 $api_key ?: '',
1023 30,
1024 $access_token
1025 );
1026
1027 $max_retries = 1;
1028 $retry_count = 0;
1029 $sites_data = [];
1030
1031 while ($retry_count <= $max_retries) {
1032 try {
1033 // Fetch sites
1034 $sites_data = $client->list_sites();
1035 break; // Success
1036 } catch (\Exception $e) {
1037 // Check for 401 error
1038 if ($e->getCode() === 401 && $retry_count < $max_retries) {
1039 error_log('ThinkRank: 401 detected in get_search_console_sites. Forcing token refresh...');
1040
1041 // Initialize Analytics Manager to handle token refresh
1042 $analytics_manager = new \ThinkRank\SEO\Analytics_Manager();
1043 $analytics_manager->refresh_access_token(true); // Force refresh
1044
1045 // Get new token
1046 $new_access_token = $this->settings->get('google_access_token');
1047 // Update client with new token
1048 $client = new \ThinkRank\Integrations\Google_Search_Console_Client(
1049 $api_key ?: '',
1050 30,
1051 $new_access_token
1052 );
1053
1054 $retry_count++;
1055 continue;
1056 }
1057
1058 throw $e;
1059 }
1060 }
1061
1062 $sites = [];
1063 if (isset($sites_data['siteEntry']) && is_array($sites_data['siteEntry'])) {
1064 foreach ($sites_data['siteEntry'] as $site) {
1065 $sites[] = [
1066 'siteUrl' => $site['siteUrl'] ?? '',
1067 'permissionLevel' => $site['permissionLevel'] ?? 'siteOwner'
1068 ];
1069 }
1070 }
1071
1072 return new WP_REST_Response([
1073 'success' => true,
1074 'data' => [
1075 'sites' => $sites
1076 ],
1077 'message' => 'Search Console sites retrieved successfully'
1078 ], 200);
1079 } catch (\Exception $e) {
1080 $code = $e->getCode();
1081 $status = ($code >= 400 && $code < 600) ? $code : 500;
1082
1083 return new WP_REST_Response([
1084 'success' => false,
1085 'message' => 'Failed to retrieve Search Console sites: ' . $e->getMessage()
1086 ], $status);
1087 }
1088 }
1089 }
1090