analytics_manager = $analytics_manager ?? new Analytics_Manager(); } /** * Register API routes * Following ThinkRank endpoint registration patterns * * @since 1.0.0 */ public function register_routes(): void { // Test Google API connections register_rest_route( $this->namespace, '/' . $this->rest_base . '/test-connections', [ [ 'methods' => 'GET', 'callback' => [$this, 'test_connections'], 'permission_callback' => [$this, 'check_permissions'], ] ] ); // Get dashboard data register_rest_route( $this->namespace, '/' . $this->rest_base . '/dashboard', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_dashboard_data'], 'permission_callback' => [$this, 'check_permissions'], 'args' => $this->get_dashboard_args() ] ] ); // Get SEO opportunities register_rest_route( $this->namespace, '/' . $this->rest_base . '/opportunities', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_seo_opportunities'], 'permission_callback' => [$this, 'check_permissions'], 'args' => $this->get_opportunities_args() ] ] ); // Setup Search Console verification register_rest_route( $this->namespace, '/' . $this->rest_base . '/setup/search-console', [ [ 'methods' => 'POST', 'callback' => [$this, 'setup_search_console'], 'permission_callback' => [$this, 'check_permissions'], 'args' => $this->get_setup_args() ] ] ); // Get indexing status register_rest_route( $this->namespace, '/' . $this->rest_base . '/indexing-status', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_indexing_status'], 'permission_callback' => [$this, 'check_permissions'], ] ] ); // Refresh cached data register_rest_route( $this->namespace, '/' . $this->rest_base . '/refresh', [ [ 'methods' => 'POST', 'callback' => [$this, 'refresh_data'], 'permission_callback' => [$this, 'check_permissions'], ] ] ); // Get client status (for debugging) register_rest_route( $this->namespace, '/' . $this->rest_base . '/status', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_client_status'], 'permission_callback' => [$this, 'check_permissions'], ] ] ); // ======================================== // SEO Intelligence Enhancement Endpoints // ======================================== // Get intelligent dashboard data with trends and insights register_rest_route( $this->namespace, '/' . $this->rest_base . '/intelligent-dashboard', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_intelligent_dashboard'], 'permission_callback' => [$this, 'check_permissions'], 'args' => $this->get_dashboard_args() ] ] ); // Get intelligent SEO opportunities with prioritization register_rest_route( $this->namespace, '/' . $this->rest_base . '/intelligent-opportunities', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_intelligent_opportunities'], 'permission_callback' => [$this, 'check_permissions'], 'args' => $this->get_opportunities_args() ] ] ); // Get SEO insights register_rest_route( $this->namespace, '/' . $this->rest_base . '/insights', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_seo_insights'], 'permission_callback' => [$this, 'check_permissions'], 'args' => $this->get_dashboard_args() ] ] ); // Get Search Console totals for custom date range register_rest_route( $this->namespace, '/' . $this->rest_base . '/search-totals', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_search_totals'], 'permission_callback' => [$this, 'check_permissions'], 'args' => [ 'start_date' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'description' => 'Start date (Y-m-d)', ], 'end_date' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'description' => 'End date (Y-m-d)', ], ], ] ] ); // Get daily Search Console data (by date dimension) for chart rendering register_rest_route( $this->namespace, '/' . $this->rest_base . '/search-daily', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_search_daily'], 'permission_callback' => [$this, 'check_permissions'], 'args' => [ 'date_range' => [ 'required' => false, 'type' => 'string', 'default' => '30d', 'sanitize_callback' => 'sanitize_text_field', 'description' => 'Date range: 7d, 30d, or 90d', ], ], ] ] ); // Get branded vs non-branded breakdown from Search Console register_rest_route( $this->namespace, '/' . $this->rest_base . '/branded', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_branded'], 'permission_callback' => [$this, 'check_permissions'], 'args' => [ 'date_range' => [ 'required' => false, 'type' => 'string', 'default' => '30d', 'sanitize_callback' => 'sanitize_text_field', ], 'brand_name' => [ 'required' => false, 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], ], ] ] ); // Get top countries from Search Console register_rest_route( $this->namespace, '/' . $this->rest_base . '/countries', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_countries'], 'permission_callback' => [$this, 'check_permissions'], 'args' => [ 'date_range' => [ 'required' => false, 'type' => 'string', 'default' => '30d', 'sanitize_callback' => 'sanitize_text_field', 'description' => 'Date range: 7d, 30d, or 90d', ], ], ] ] ); } /** * Test Google API connections * Following ThinkRank response patterns * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function test_connections(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $connection_results = $this->analytics_manager->test_connections(); return new WP_REST_Response([ 'success' => true, 'data' => $connection_results, 'message' => 'Connection tests completed' ], 200); } catch (\Exception $e) { return new WP_Error( 'connection_test_failed', 'Connection test failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get analytics dashboard data * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_dashboard_data(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range'); $dashboard_data = $this->analytics_manager->get_dashboard_data($date_range); return new WP_REST_Response([ 'success' => true, 'data' => $dashboard_data, 'message' => 'Dashboard data retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'dashboard_data_failed', 'Failed to retrieve dashboard data: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get SEO opportunities * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_seo_opportunities(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range'); $opportunities = $this->analytics_manager->get_seo_opportunities($date_range); return new WP_REST_Response([ 'success' => true, 'data' => $opportunities, 'message' => 'SEO opportunities retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'opportunities_failed', 'Failed to retrieve SEO opportunities: ' . $e->getMessage(), ['status' => 500] ); } } /** * Setup Search Console verification * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function setup_search_console(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $site_url = $request->get_param('site_url'); $setup_result = $this->analytics_manager->setup_search_console_verification($site_url); return new WP_REST_Response([ 'success' => $setup_result['success'], 'data' => $setup_result, 'message' => $setup_result['message'] ], $setup_result['success'] ? 200 : 400); } catch (\Exception $e) { return new WP_Error( 'setup_failed', 'Search Console setup failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get indexing status * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_indexing_status(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $indexing_status = $this->analytics_manager->get_indexing_status(); return new WP_REST_Response([ 'success' => true, 'data' => $indexing_status, 'message' => 'Indexing status retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'indexing_status_failed', 'Failed to retrieve indexing status: ' . $e->getMessage(), ['status' => 500] ); } } /** * Refresh cached analytics data * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function refresh_data(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $refresh_result = $this->analytics_manager->refresh_data(); return new WP_REST_Response([ 'success' => $refresh_result['success'], 'data' => $refresh_result, 'message' => $refresh_result['message'] ], 200); } catch (\Exception $e) { return new WP_Error( 'refresh_failed', 'Failed to refresh data: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get client status for debugging * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_client_status(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $client_status = $this->analytics_manager->get_client_status(); return new WP_REST_Response([ 'success' => true, 'data' => $client_status, 'message' => 'Client status retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'status_failed', 'Failed to retrieve client status: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get dashboard endpoint arguments * Following ThinkRank argument validation patterns * * @return array Endpoint arguments */ private function get_dashboard_args(): array { return [ 'date_range' => [ 'type' => 'string', 'default' => '30d', 'enum' => ['7d', '30d', '90d'], 'sanitize_callback' => 'sanitize_key', 'description' => 'Date range for analytics data' ] ]; } /** * Get opportunities endpoint arguments * * @return array Endpoint arguments */ private function get_opportunities_args(): array { return [ 'date_range' => [ 'type' => 'string', 'default' => '30d', 'enum' => ['7d', '30d', '90d'], 'sanitize_callback' => 'sanitize_key', 'description' => 'Date range for opportunities analysis' ] ]; } /** * Get setup endpoint arguments * * @return array Endpoint arguments */ private function get_setup_args(): array { return [ 'site_url' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'validate_callback' => [$this, 'validate_site_url'], 'description' => 'Site URL to verify in Search Console' ] ]; } /** * Validate site URL parameter * Following ThinkRank validation patterns * * @param string $site_url Site URL to validate * @return bool|WP_Error Validation result */ public function validate_site_url(string $site_url): bool|WP_Error { if (empty($site_url)) { return new WP_Error( 'invalid_site_url', 'Site URL is required', ['status' => 400] ); } if (!filter_var($site_url, FILTER_VALIDATE_URL)) { return new WP_Error( 'invalid_site_url', 'Site URL must be a valid URL', ['status' => 400] ); } return true; } /** * Get Search Console totals for a custom date range * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_search_totals(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $start_date = $request->get_param('start_date'); $end_date = $request->get_param('end_date'); // Validate date format and actual calendar validity $start_dt = \DateTime::createFromFormat('Y-m-d', $start_date); $end_dt = \DateTime::createFromFormat('Y-m-d', $end_date); if ( !$start_dt || $start_dt->format('Y-m-d') !== $start_date || !$end_dt || $end_dt->format('Y-m-d') !== $end_date ) { return new WP_Error('invalid_dates', 'Dates must be valid calendar dates in Y-m-d format', ['status' => 400]); } if ($start_dt > $end_dt) { return new WP_Error('invalid_dates', 'start_date must not be after end_date', ['status' => 400]); } // Use Analytics Manager to access the initialized client with decrypted credentials $search_console = $this->analytics_manager->get_search_console_client(); $site_url = $this->analytics_manager->get_property_url(); if (!$search_console) { return new WP_Error('no_client', 'Search Console client not available', ['status' => 500]); } $totals = $search_console->get_search_totals_by_dates($site_url, $start_date, $end_date); return new WP_REST_Response([ 'success' => true, 'data' => $totals, 'message' => 'Search totals retrieved', ], 200); } catch (\Exception $e) { return new WP_Error( 'search_totals_failed', 'Failed to retrieve search totals: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get daily Search Console data grouped by date for chart rendering. * * Returns rows sorted ascending by date, each containing: * clicks, impressions, ctr (as %), position. * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_search_daily(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range') ?: '30d'; $days = (int) preg_replace('/[^0-9]/', '', $date_range); if ($days <= 0 || $days > 90) { $days = 30; } // Window = exactly $days back from today (inclusive of today). // 7d → today-6 ... today // 30d → today-29 ... today // 90d → today-89 ... today $end_date = gmdate('Y-m-d'); $start_date = gmdate('Y-m-d', strtotime('-' . ($days - 1) . ' days')); $search_console = $this->analytics_manager->get_search_console_client(); $site_url = $this->analytics_manager->get_property_url(); if (!$search_console) { return new WP_Error('no_client', 'Search Console client not available', ['status' => 500]); } $raw_rows = $search_console->get_search_performance_by_dates( $site_url, $start_date, $end_date, $days + 5, ['date'] ); // Index GSC rows by date so we can pad missing days (GSC's lag means // the most recent few days often have no data yet). $by_date = []; foreach ($raw_rows as $row) { $date = $row['keys'][0] ?? ''; if (!$date) { continue; } $by_date[$date] = [ 'clicks' => (int) ($row['clicks'] ?? 0), 'impressions' => (int) ($row['impressions'] ?? 0), 'ctr' => round(($row['ctr'] ?? 0) * 100, 2), 'position' => round($row['position'] ?? 0, 1), ]; } // Build a contiguous N-day series from $start_date → $end_date. // Days GSC has no data for (today minus 2-4 days, typically) come // through as zeros so the chart x-axis always spans the full window. $rows = []; $cursor = strtotime($start_date); $end_ts = strtotime($end_date); while ($cursor <= $end_ts) { $date = gmdate('Y-m-d', $cursor); $rows[] = array_merge( ['date' => $date], $by_date[$date] ?? ['clicks' => 0, 'impressions' => 0, 'ctr' => 0, 'position' => 0] ); $cursor = strtotime('+1 day', $cursor); } return new WP_REST_Response([ 'success' => true, 'data' => [ 'rows' => $rows, 'start_date' => $start_date, 'end_date' => $end_date, ], 'message' => 'Daily search data retrieved', ], 200); } catch (\Exception $e) { return new WP_Error( 'search_daily_failed', 'Failed to retrieve daily search data: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get branded vs non-branded query breakdown from Search Console. * * Accepts optional `brand_name` param (comma-separated keywords). * When omitted the brand is auto-derived from the registered domain. * Also returns the equivalent previous-period data so the frontend can * compute trend arrows without a second round-trip. * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error */ public function get_branded(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range') ?: '30d'; $brand_name = $request->get_param('brand_name') ?: ''; $search_console = $this->analytics_manager->get_search_console_client(); $site_url = $this->analytics_manager->get_property_url(); if (!$search_console) { return new WP_Error('no_client', 'Search Console client not available', ['status' => 500]); } $data = $search_console->get_branded_performance($site_url, $date_range, $brand_name); return new WP_REST_Response([ 'success' => true, 'data' => $data, 'message' => 'Branded data retrieved', ], 200); } catch (\Exception $e) { return new WP_Error( 'branded_failed', 'Failed to retrieve branded data: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get top countries from Search Console (country dimension). * * Returns up to 10 countries sorted by clicks descending, each with * clicks, impressions, ctr, position, and a percentage share of total clicks. * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_countries(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range') ?: '30d'; $search_console = $this->analytics_manager->get_search_console_client(); $site_url = $this->analytics_manager->get_property_url(); if (!$search_console) { return new WP_Error('no_client', 'Search Console client not available', ['status' => 500]); } $raw_rows = $search_console->get_country_performance($site_url, $date_range); return new WP_REST_Response([ 'success' => true, 'data' => $raw_rows, 'message' => 'Country data retrieved', ], 200); } catch (\Exception $e) { return new WP_Error( 'countries_failed', 'Failed to retrieve country data: ' . $e->getMessage(), ['status' => 500] ); } } /** * Check permissions for API access * Following ThinkRank permission patterns * * @return bool Permission status */ public function check_permissions(): bool { return current_user_can('manage_options'); } // ======================================== // SEO Intelligence Enhancement Endpoints // ======================================== /** * Get intelligent dashboard data with trends and insights * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_intelligent_dashboard(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range'); $intelligent_data = $this->analytics_manager->get_intelligent_dashboard_data($date_range); $success = isset($intelligent_data['success']) ? $intelligent_data['success'] : false; return new WP_REST_Response([ 'success' => $success, 'data' => $intelligent_data['data'] ?? null, 'message' => $intelligent_data['message'] ?? 'Intelligent dashboard data retrieved', 'timestamp' => current_time('mysql') ], 200); // Always return 200 for successful API calls, even when no data available } catch (Exception $e) { return new WP_Error( 'intelligent_dashboard_error', 'Failed to retrieve intelligent dashboard data: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get intelligent SEO opportunities with prioritization * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_intelligent_opportunities(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range'); $intelligent_opportunities = $this->analytics_manager->get_intelligent_seo_opportunities($date_range); $success = isset($intelligent_opportunities['success']) ? $intelligent_opportunities['success'] : false; return new WP_REST_Response([ 'success' => $success, 'data' => $intelligent_opportunities['data'] ?? null, 'message' => $intelligent_opportunities['message'] ?? 'Intelligent opportunities retrieved', 'timestamp' => current_time('mysql') ], 200); // Always return 200 for successful API calls, even when no data available } catch (Exception $e) { return new WP_Error( 'intelligent_opportunities_error', 'Failed to retrieve intelligent opportunities: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get SEO insights * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object */ public function get_seo_insights(WP_REST_Request $request): WP_REST_Response|WP_Error { try { $date_range = $request->get_param('date_range'); $insights = $this->analytics_manager->get_seo_insights($date_range); $success = isset($insights['success']) ? $insights['success'] : false; return new WP_REST_Response([ 'success' => $success, 'data' => $insights['data'] ?? null, 'cached' => $insights['cached'] ?? false, 'message' => $insights['message'] ?? 'SEO insights retrieved', 'timestamp' => current_time('mysql') ], 200); // Always return 200 for successful API calls, even when no data available } catch (Exception $e) { return new WP_Error( 'seo_insights_error', 'Failed to retrieve SEO insights: ' . $e->getMessage(), ['status' => 500] ); } } }