isset($team['teamId']) ? (string)$team['teamId'] : (isset($team['id']) ? (string)$team['id'] : ''), 'teamName' => isset($team['teamName']) ? $team['teamName'] : (isset($team['name']) ? $team['name'] : ''), 'teamLeagueName' => isset($team['teamLeagueName']) ? $team['teamLeagueName'] : (isset($team['league']) ? $team['league'] : null), 'icon' => isset($team['icon']) ? $team['icon'] : null, ); } /** * Normalize match data for consistent API responses * @param array $match Raw match data from API * @return array Normalized match object */ private function normalize_match($match) { // Extract team names from teams array if not present as direct fields $teamNameA = ''; $teamNameB = ''; if (!empty($match['teams']) && is_array($match['teams'])) { foreach ($match['teams'] as $team) { if (!empty($team['isHomeTeam']) && !empty($team['teamName'])) { $teamNameA = $team['teamName']; } elseif (empty($team['isHomeTeam']) && !empty($team['teamName'])) { $teamNameB = $team['teamName']; } } } // Use direct fields if available, fallback to extracted values, then fallback field variations return array( 'matchId' => isset($match['matchId']) ? (string)$match['matchId'] : (isset($match['id']) ? (string)$match['id'] : ''), 'matchDate' => isset($match['matchDate']) ? $match['matchDate'] : (isset($match['date']) ? $match['date'] : null), 'teamAId' => isset($match['teamAId']) ? (string)$match['teamAId'] : (isset($match['homeTeamId']) ? (string)$match['homeTeamId'] : ''), 'teamBId' => isset($match['teamBId']) ? (string)$match['teamBId'] : (isset($match['awayTeamId']) ? (string)$match['awayTeamId'] : ''), 'teamNameA' => isset($match['teamNameA']) ? $match['teamNameA'] : ($teamNameA ?: (isset($match['homeTeamName']) ? $match['homeTeamName'] : '')), 'teamNameB' => isset($match['teamNameB']) ? $match['teamNameB'] : ($teamNameB ?: (isset($match['awayTeamName']) ? $match['awayTeamName'] : '')), 'scoreTeamA' => isset($match['scoreTeamA']) ? $match['scoreTeamA'] : (isset($match['homeScore']) ? $match['homeScore'] : null), 'scoreTeamB' => isset($match['scoreTeamB']) ? $match['scoreTeamB'] : (isset($match['awayScore']) ? $match['awayScore'] : null), 'matchStateName' => isset($match['matchStateName']) ? $match['matchStateName'] : (isset($match['status']) ? $match['status'] : null), 'stadiumFieldName' => isset($match['stadiumFieldName']) ? $match['stadiumFieldName'] : (isset($match['stadiumPlaygroundName']) ? $match['stadiumPlaygroundName'] : null), 'leagueName' => isset($match['leagueName']) ? $match['leagueName'] : null, 'divisionName' => isset($match['divisionName']) ? $match['divisionName'] : null, 'roundNbr' => isset($match['roundNbr']) ? $match['roundNbr'] : null, 'matchTypeName' => isset($match['matchTypeName']) ? $match['matchTypeName'] : null, 'hasMatchStarted' => isset($match['hasMatchStarted']) ? $match['hasMatchStarted'] : false, 'isMatchPause' => isset($match['isMatchPause']) ? $match['isMatchPause'] : false, 'hasMatchEnded' => isset($match['hasMatchEnded']) ? $match['hasMatchEnded'] : false, 'intermediateResults' => isset($match['intermediateResults']) ? $match['intermediateResults'] : null, 'teams' => isset($match['teams']) ? $match['teams'] : array(), ); } public function register_routes() { register_rest_route('swi-foot/v1', '/teams', array( 'methods' => 'GET', 'callback' => array($this, 'get_teams'), 'permission_callback' => '__return_true' )); register_rest_route('swi-foot/v1', '/matches', array( 'methods' => 'GET', 'callback' => array($this, 'get_matches_for_team'), 'permission_callback' => '__return_true', 'args' => array( 'team_id' => array( 'validate_callback' => function($param, $request, $key) { return is_numeric($param); } ) ) )); // Also allow team_id as a path parameter: /wp-json/swi-foot/v1/matches/123 register_rest_route('swi-foot/v1', '/matches/(?P\d+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_matches_for_team'), 'permission_callback' => '__return_true', 'args' => array( 'team_id' => array( 'validate_callback' => function($param, $request, $key) { return is_numeric($param); } ) ) )); // Single match details: /wp-json/swi-foot/v1/match/ register_rest_route('swi-foot/v1', '/match/(?P[^/]+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_match_details'), 'permission_callback' => '__return_true', 'args' => array( 'match_id' => array( 'validate_callback' => function($param, $request, $key) { return is_string($param) && strlen($param) > 0; } ) ) )); // Match events with status: /wp-json/swi-foot/v1/events/ // Requires authentication to prevent abuse from non-logged-in users register_rest_route('swi-foot/v1', '/events/(?P[^/]+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_match_events'), 'permission_callback' => function() { return is_user_logged_in() || apply_filters('swi_foot_rest_events_public', false); }, 'args' => array( 'match_id' => array( 'validate_callback' => function($param, $request, $key) { return is_string($param) && strlen($param) > 0; } ) ) )); register_rest_route('swi-foot/v1', '/commons-ids', array( 'methods' => 'GET', 'callback' => array($this, 'get_commons_ids'), 'permission_callback' => '__return_true' )); // Admin actions register_rest_route('swi-foot/v1', '/admin/refresh-teams', array( 'methods' => 'POST', 'callback' => array($this, 'admin_refresh_teams'), 'permission_callback' => function() { return current_user_can('manage_options'); } )); register_rest_route('swi-foot/v1', '/admin/clear-cache', array( 'methods' => 'POST', 'callback' => array($this, 'admin_clear_cache'), 'permission_callback' => function() { return current_user_can('manage_options'); } )); register_rest_route('swi-foot/v1', '/admin/test-connection', array( 'methods' => 'POST', 'callback' => array($this, 'admin_test_connection'), 'permission_callback' => function() { return current_user_can('manage_options'); } )); register_rest_route('swi-foot/v1', '/admin/finished/(?P[^/]+)', array( 'methods' => 'DELETE', 'callback' => array($this, 'admin_delete_finished_match'), 'permission_callback' => function() { return current_user_can('manage_options'); }, 'args' => array( 'match_id' => array( 'validate_callback' => function($param, $request, $key) { return is_string($param) && strlen($param) > 0; } ) ) )); register_rest_route('swi-foot/v1', '/admin/finished', array( 'methods' => 'DELETE', 'callback' => array($this, 'admin_delete_all_finished_matches'), 'permission_callback' => function() { return current_user_can('manage_options'); } )); } public function get_teams($request) { $api = new Swi_Foot_API(); $teams = $api->get_teams(); if (is_wp_error($teams)) { return new WP_REST_Response(array('error' => $teams->get_error_message()), 400); } // Normalize team data for consistent responses $normalized = array(); if (is_array($teams)) { foreach ($teams as $team) { $normalized[] = $this->normalize_team($team); } } return rest_ensure_response($normalized); } public function get_matches_for_team($request) { $team_id = $request->get_param('team_id'); if (empty($team_id)) { return new WP_REST_Response(array('error' => 'team_id required'), 400); } $api = new Swi_Foot_API(); $schedule = $api->get_schedule($team_id); if (is_wp_error($schedule)) { return new WP_REST_Response(array('error' => $schedule->get_error_message()), 400); } // Normalize matches for consistent responses $normalized = array(); if (is_array($schedule)) { foreach ($schedule as $match) { $normalized[] = $this->normalize_match($match); } } return rest_ensure_response($normalized); } public function get_commons_ids($request) { $api = new Swi_Foot_API(); $commons = $api->get_commons_ids(); if (is_wp_error($commons)) { return new WP_REST_Response(array('error' => $commons->get_error_message()), 400); } return rest_ensure_response($commons); } /** * Return full match details for a given match_id * GET /wp-json/swi-foot/v1/match/ */ public function get_match_details($request) { $match_id = $request->get_param('match_id'); if (empty($match_id)) { return new WP_REST_Response(array('error' => 'match_id required'), 400); } $api = new Swi_Foot_API(); $match = $api->get_match_details($match_id); if (is_wp_error($match)) { return new WP_REST_Response(array('error' => $match->get_error_message()), 400); } // Normalize match data for consistent API responses $normalized = $this->normalize_match($match); return rest_ensure_response($normalized); } /** * Get match events with current match status (for frontend refresh polling) * * Endpoint: GET /wp-json/swi-foot/v1/events/ * * Returns the latest match events (sorted newest first) along with critical match * status information (hasMatchStarted, hasMatchEnded). Frontend polls this endpoint * at configurable intervals to update live event displays. * * Permission: Requires logged-in user (can be overridden with swi_foot_rest_events_public filter) * * @param WP_REST_Request $request The REST request object containing match_id parameter * @return WP_REST_Response Array with keys: * - matchId (string): The match ID * - events (array): Array of event objects with matchMinute, eventTypeName, playerName, teamName, timestamp * - hasMatchStarted (bool): Whether the match has begun * - hasMatchEnded (bool): Whether the match has concluded * - matchStateName (string|null): Current match status label */ public function get_match_events($request) { $match_id = $request->get_param('match_id'); if (empty($match_id)) { return new WP_REST_Response(array('error' => 'match_id required'), 400); } $api = new Swi_Foot_API(); // Get match details for status $match = $api->get_match_details($match_id); if (is_wp_error($match)) { return new WP_REST_Response(array('error' => $match->get_error_message()), 400); } // Try saved finished match data first $events = array(); $saved = $api->get_finished_match_data($match_id); if ($saved) { $events = $saved['events'] ?? array(); } else { // Fetch live events $events = $api->get_match_events($match_id); if (is_wp_error($events)) { $events = array(); } } // Get event ordering preference from query parameter $event_order = $request->get_param('event_order') ?? 'dynamic'; // Convert exactEventTime to timestamps for sorting if (!empty($events)) { foreach ($events as &$event) { // Convert ISO 8601 format (2026-03-14T17:03:50.437) to timestamp // Strip milliseconds if present, keep only date and time part $event['_timestamp'] = strtotime(substr($event['exactEventTime'], 0, 19)); } unset($event); // Sort by timestamp if ($event_order === 'dynamic') { // Dynamic: newest first while match is live, chronological after match ends $match_has_ended = !empty($match['hasMatchEnded']); usort($events, function ($a, $b) use ($match_has_ended) { if ($match_has_ended) { // Chronological (ascending): oldest first return $a['_timestamp'] - $b['_timestamp']; } else { // Descending: newest first return $b['_timestamp'] - $a['_timestamp']; } }); } elseif ($event_order === 'newest_first') { // Always newest first (descending) usort($events, function ($a, $b) { return $b['_timestamp'] - $a['_timestamp']; }); } elseif ($event_order === 'oldest_first') { // Always oldest first (ascending) usort($events, function ($a, $b) { return $a['_timestamp'] - $b['_timestamp']; }); } } return rest_ensure_response(array( 'matchId' => $match_id, 'events' => (array)$events, 'hasMatchStarted' => $match['hasMatchStarted'] ?? false, 'hasMatchEnded' => $match['hasMatchEnded'] ?? false, 'matchStateName' => $match['matchStateName'] ?? null, )); } public function admin_refresh_teams($request) { $api = new Swi_Foot_API(); // Collect debug information to assist troubleshooting 401 responses $debug = array(); if (method_exists($api, 'debug_get_token_info')) { $debug = $api->debug_get_token_info(); error_log('Swiss Football Debug: refresh-teams token_present=' . ($debug['token_present'] ? '1' : '0') . ' base=' . $debug['base_url'] . ' url=' . $debug['team_list_url']); } delete_transient('swi_foot_teams'); $teams = $api->get_teams(); if (is_wp_error($teams)) { // Try to include debug information in response for admin callers $err = $teams->get_error_message(); error_log('Swiss Football API: refresh-teams failed - ' . $err); return new WP_REST_Response(array('error' => $err, 'debug' => $debug), 400); } return rest_ensure_response(array('success' => true, 'data' => $teams, 'debug' => $debug)); } public function admin_clear_cache($request) { $keys = get_transient('swi_foot_match_keys'); if (is_array($keys)) { foreach ($keys as $mid) { delete_transient('swi_foot_match_' . $mid); } } delete_transient('swi_foot_match_keys'); delete_transient('swi_foot_teams'); return rest_ensure_response(array('success' => true)); } public function admin_test_connection($request) { $api = new Swi_Foot_API(); if (method_exists($api, 'test_connection')) { $ok = $api->test_connection(); } else { $ok = false; } if ($ok) return rest_ensure_response(array('success' => true)); return new WP_REST_Response(array('error' => 'Connection failed'), 400); } public function admin_delete_finished_match($request) { $match_id = $request->get_param('match_id'); if (empty($match_id)) return new WP_REST_Response(array('error' => 'match_id required'), 400); $data = get_option('swi_foot_finished_matches', array()); if (isset($data[$match_id])) { unset($data[$match_id]); update_option('swi_foot_finished_matches', $data); return rest_ensure_response(array('success' => true)); } return new WP_REST_Response(array('error' => 'not found'), 404); } public function admin_delete_all_finished_matches($request) { delete_option('swi_foot_finished_matches'); return rest_ensure_response(array('success' => true)); } } new Swi_Foot_REST();