base_url = get_option('swi_foot_api_base_url', 'https://stg-club-api-services.football.ch'); $this->username = get_option('swi_foot_api_username'); $this->password = get_option('swi_foot_api_password'); $this->verein_id = get_option('swi_foot_verein_id'); $this->season_id = get_option('swi_foot_season_id', date('Y')); $this->cache_duration = get_option('swi_foot_match_cache_duration', 30); $this->language = get_option('swi_foot_api_language', '1'); // AJAX actions were migrated to REST endpoints (see includes/class-swi-foot-rest.php) // Hook to update match cache on page load add_action('wp', array($this, 'maybe_update_match_cache')); } /** * Build a full URL for the given endpoint, avoiding duplicate /api segments */ private function build_url($endpoint) { $base = rtrim($this->base_url, '/'); // If base already contains '/api' and endpoint starts with '/api', strip the endpoint prefix if (strpos($base, '/api') !== false && strpos($endpoint, '/api') === 0) { $endpoint = preg_replace('#^/api#', '', $endpoint); } return $base . '/' . ltrim($endpoint, '/'); } public function maybe_update_match_cache() { // Only run on single posts/pages if (!is_single() && !is_page()) { return; } global $post; if (!$post) { return; } // Check if content has match shortcodes if ( !has_shortcode($post->post_content, 'swi_foot_match') && !has_shortcode($post->post_content, 'swi_foot_match_home_team') && !has_shortcode($post->post_content, 'swi_foot_match_away_team') && !has_shortcode($post->post_content, 'swi_foot_match_date') && !has_shortcode($post->post_content, 'swi_foot_match_time') && !has_shortcode($post->post_content, 'swi_foot_match_venue') && !has_shortcode($post->post_content, 'swi_foot_match_score') && !has_shortcode($post->post_content, 'swi_foot_match_status') && !has_shortcode($post->post_content, 'swi_foot_match_league') && !has_shortcode($post->post_content, 'swi_foot_match_round') ) { return; } // Extract match IDs from shortcodes $match_ids = array(); $patterns = array( '/\[swi_foot_match[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_home_team[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_away_team[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_date[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_time[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_venue[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_score[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_status[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_league[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/', '/\[swi_foot_match_round[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/' ); foreach ($patterns as $pattern) { preg_match_all($pattern, $post->post_content, $matches); if (!empty($matches[1])) { $match_ids = array_merge($match_ids, $matches[1]); } } // Update cache for found match IDs $match_ids = array_unique($match_ids); foreach ($match_ids as $match_id) { $this->get_match_details($match_id, false); // Don't force refresh unless needed } } private function get_access_token() { $token = get_transient('swi_foot_access_token'); // Transient handles expiration; return if present if ($token) { return $token; } // Fetch a new token return $this->refresh_access_token(); } private function refresh_access_token() { if (empty($this->username) || empty($this->password)) { return false; } $response = wp_remote_post($this->build_url('/api/token'), array( 'body' => json_encode(array( 'applicationKey' => $this->username, 'applicationPass' => $this->password )), 'headers' => array( 'Content-Type' => 'application/json' ), 'timeout' => 30 )); if (is_wp_error($response)) { error_log('Swiss Football API: Token request failed - ' . $response->get_error_message()); return false; } $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 200) { $body_debug = wp_remote_retrieve_body($response); error_log('Swiss Football API: Token request returned ' . $response_code . ' - body: ' . substr($body_debug, 0, 1000)); return false; } $body = wp_remote_retrieve_body($response); // Try JSON decode first, fallback to trimmed string $maybe_json = json_decode($body, true); if (is_string($maybe_json) && $maybe_json !== '') { $token = $maybe_json; } elseif (is_string($body) && $body !== '') { $token = trim($body, '"'); } else { $token = false; } if ($token) { // Store token in transient (30 minutes). WP constant MINUTE_IN_SECONDS available. set_transient('swi_foot_access_token', $token, 30 * MINUTE_IN_SECONDS); return $token; } return false; } private function api_request($endpoint, $params = array(), $retry_on_401 = true) { $token = $this->get_access_token(); if (!$token) { return new WP_Error('auth_failed', 'Failed to authenticate with Swiss Football API'); } $url = $this->build_url($endpoint); if (!empty($params)) { $url .= '?' . http_build_query($params); } $response = wp_remote_get($url, array( 'headers' => array( 'X-User-Token' => $token, 'X-User-Language' => $this->language, 'Content-Type' => 'application/json' ), 'timeout' => 30 )); if (is_wp_error($response)) { return $response; } $response_code = wp_remote_retrieve_response_code($response); // Handle 401 Unauthorized - token may have expired if ($response_code === 401 && $retry_on_401) { error_log('Swiss Football API: Received 401 Unauthorized, attempting to refresh token'); // Clear the expired token delete_transient('swi_foot_access_token'); // Refresh the token $new_token = $this->refresh_access_token(); if ($new_token) { // Retry the request with the new token (prevent infinite recursion with retry_on_401 = false) return $this->api_request($endpoint, $params, false); } return new WP_Error('auth_failed', 'Authentication failed: Token refresh unsuccessful'); } if ($response_code !== 200) { $body_debug = wp_remote_retrieve_body($response); error_log('Swiss Football API: Request to ' . $url . ' returned ' . $response_code . ' - body: ' . substr($body_debug, 0, 1000)); // Handle 406 Not Acceptable - data not yet available if ($response_code === 406) { return new WP_Error('data_not_available', 'Data not yet available', array('status_code' => 406)); } return new WP_Error('api_error', 'API request failed with code ' . $response_code, array('status_code' => $response_code)); } $body = wp_remote_retrieve_body($response); return json_decode($body, true); } public function get_teams() { // Prefer transient-based caching for teams (24 hours) $teams = get_transient('swi_foot_teams'); if ($teams !== false) { return $teams; } if (empty($this->verein_id)) { return new WP_Error('no_verein_id', 'Verein ID not configured'); } $teams = $this->api_request('/api/team/list', array( 'ClubId' => $this->verein_id, 'SeasonId' => $this->season_id )); if (!is_wp_error($teams) && is_array($teams)) { set_transient('swi_foot_teams', $teams, 24 * HOUR_IN_SECONDS); } return $teams; } public function get_standings($team_id) { if (empty($this->verein_id)) { return new WP_Error('no_verein_id', 'Verein ID not configured'); } return $this->api_request('/api/club/ranking', array( 'ClubId' => $this->verein_id, 'SeasonId' => $this->season_id, 'TeamId' => $team_id )); } public function get_schedule($team_id) { if (empty($this->verein_id)) { return new WP_Error('no_verein_id', 'Verein ID not configured'); } return $this->api_request('/api/club/schedule', array( 'ClubId' => $this->verein_id, 'SeasonId' => $this->season_id, 'TeamId' => $team_id )); } public function get_match_details($match_id, $force_refresh = false) { // If we've permanently saved finished match data, return it (do not call API) $finished = $this->get_finished_match_data($match_id); if (!$force_refresh && $finished && isset($finished['match'])) { return $finished['match']; } $transient_key = 'swi_foot_match_' . $match_id; if (!$force_refresh) { $cached = get_transient($transient_key); if ($cached !== false) { return isset($cached['data']) ? $cached['data'] : $cached; } } // Fetch fresh data from API $match_data = $this->api_request('/api/match/' . $match_id); if (!is_wp_error($match_data)) { // Cache the data as transient for configured duration $cache_payload = array( 'data' => $match_data, 'cached_at' => time() ); set_transient($transient_key, $cache_payload, (int) $this->cache_duration); // Maintain index of cached match ids for management / clearing $keys = get_transient('swi_foot_match_keys'); if (!is_array($keys)) { $keys = array(); } if (!in_array($match_id, $keys, true)) { $keys[] = $match_id; // No expiration for keys index so we can clear caches reliably set_transient('swi_foot_match_keys', $keys, 0); } // If match finished, persist it permanently (store match details inside finished data) if (!empty($match_data['hasMatchEnded'])) { // Ensure finished entry exists and include match details $saved = $this->get_finished_match_data($match_id) ?: array(); $saved['match'] = $match_data; // Keep any existing roster/events if present if (!isset($saved['roster'])) $saved['roster'] = array(); if (!isset($saved['events'])) $saved['events'] = array(); $all = get_option('swi_foot_finished_matches', array()); $all[$match_id] = array_merge($all[$match_id] ?? array(), $saved, array('saved_at' => time())); update_option('swi_foot_finished_matches', $all); // Also delete transient cache to force reads from permanent store delete_transient($transient_key); } } return $match_data; } public function get_cached_match_data($match_id) { // Prefer finished permanent store $finished = $this->get_finished_match_data($match_id); if ($finished && isset($finished['match'])) { return $finished['match']; } $transient_key = 'swi_foot_match_' . $match_id; $cached = get_transient($transient_key); if ($cached === false) return null; return isset($cached['data']) ? $cached['data'] : $cached; } public function get_match_players($match_id) { return $this->api_request('/api/match/' . $match_id . '/players'); } public function get_match_bench($match_id) { return $this->api_request('/api/match/' . $match_id . '/bench'); } public function get_match_events($match_id) { return $this->api_request('/api/match/' . $match_id . '/events'); } public function get_match_referees($match_id) { return $this->api_request('/api/match/' . $match_id . '/referees'); } public function get_team_picture($team_id) { // Special handling for team picture endpoint which returns 200 with data or 204 No Content $token = $this->get_access_token(); if (!$token) { error_log('Swiss Football API: get_team_picture - Failed to get access token'); return null; } $url = $this->build_url('/api/team/picture/' . $team_id); error_log('Swiss Football API: get_team_picture - Requesting URL: ' . $url); $response = wp_remote_get($url, array( 'headers' => array( 'X-User-Token' => $token, 'X-User-Language' => $this->language, 'Content-Type' => 'application/json' ), 'timeout' => 30 )); if (is_wp_error($response)) { error_log('Swiss Football API: get_team_picture - wp_remote_get error: ' . $response->get_error_message()); return null; } $response_code = wp_remote_retrieve_response_code($response); error_log('Swiss Football API: get_team_picture - Response code: ' . $response_code); // Handle 401 Unauthorized - token may have expired if ($response_code === 401) { error_log('Swiss Football API: get_team_picture - Received 401, refreshing token and retrying'); delete_transient('swi_foot_access_token'); $new_token = $this->refresh_access_token(); if ($new_token) { // Recursively retry with new token return $this->get_team_picture($team_id); } error_log('Swiss Football API: get_team_picture - Token refresh failed'); return null; } // Handle 204 No Content - team has no picture if ($response_code === 204) { error_log('Swiss Football API: get_team_picture - Team ' . $team_id . ' has no picture (204)'); return null; } // Handle other error responses if ($response_code !== 200) { $body = wp_remote_retrieve_body($response); error_log('Swiss Football API: get_team_picture - Request failed with code ' . $response_code . ' - body: ' . substr($body, 0, 500)); return null; } // Success - return the base64 image data $body = wp_remote_retrieve_body($response); error_log('Swiss Football API: get_team_picture - Successfully retrieved image, size: ' . strlen($body) . ' bytes'); return $body; } public function get_common_ids($params = array()) { return $this->api_request('/api/common/ids', $params); } /** * Debug helper — return token and resolved endpoint info for admin debugging. * Note: only for admin use; not called on public endpoints. */ public function debug_get_token_info() { $token = $this->get_access_token(); $team_list_url = $this->build_url('/api/team/list') . '?' . http_build_query(array( 'ClubId' => $this->verein_id, 'SeasonId' => $this->season_id )); return array( 'token_present' => ($token !== false && !empty($token)), 'token_preview' => is_string($token) ? substr($token, 0, 32) : null, 'base_url' => $this->base_url, 'team_list_url' => $team_list_url ); } /** * Public helper to test whether we can obtain a valid access token * Returns true on success, false otherwise. */ public function test_connection() { // First, check if credentials are configured if (empty($this->username) || empty($this->password)) { return array( 'success' => false, 'error' => 'API credentials not configured', 'details' => 'Please configure API Username and Password in Settings → Swiss Football' ); } // Check if Verein ID is configured if (empty($this->verein_id)) { return array( 'success' => false, 'error' => 'Verein ID (Club ID) not configured', 'details' => 'Please configure Verein ID in Settings → Swiss Football' ); } // Test connection using the /api/common/ids endpoint with required parameters $result = $this->get_common_ids(array( 'ClubId' => $this->verein_id, 'Language' => 1 // 1 = German )); if (is_wp_error($result)) { $error_msg = $result->get_error_message(); error_log('Swiss Football API: Connection test failed - ' . $error_msg); return array( 'success' => false, 'error' => 'API connection failed', 'details' => $error_msg ); } if (!is_array($result)) { return array( 'success' => false, 'error' => 'Invalid API response', 'details' => 'The API returned unexpected data format' ); } return array( 'success' => true, 'error' => null, 'details' => 'Connection successful!' ); } public function get_current_match($team_id) { $schedule = $this->get_schedule($team_id); if (is_wp_error($schedule) || empty($schedule)) { return null; } $current_time = time(); $current_match = null; // Look for upcoming match within 5 days or recent match within 2 days foreach ($schedule as $match) { $match_time = strtotime($match['matchDate']); // Upcoming within 5 days if ($match_time > $current_time && ($match_time - $current_time) <= (5 * 24 * 60 * 60)) { $current_match = $match; break; } // Recent within 2 days if ($match_time <= $current_time && ($current_time - $match_time) <= (2 * 24 * 60 * 60)) { $current_match = $match; } } return $current_match; } /** * Save finished match data permanently (roster + events in one structure) */ public function save_finished_match_data($match_id, $roster_data, $events_data) { if (empty($match_id)) { return false; } $saved_matches = get_option('swi_foot_finished_matches', array()); $existing = isset($saved_matches[$match_id]) ? $saved_matches[$match_id] : array(); $existing['roster'] = $roster_data; $existing['events'] = $events_data; $existing['saved_at'] = time(); // If match details are cached as transient, try to move them into permanent record $transient_key = 'swi_foot_match_' . $match_id; $match_details = get_transient($transient_key); if ($match_details !== false) { $existing['match'] = $match_details; delete_transient($transient_key); } $saved_matches[$match_id] = $existing; update_option('swi_foot_finished_matches', $saved_matches); return true; } /** * Retrieve saved finished match data if available */ public function get_finished_match_data($match_id) { $saved_matches = get_option('swi_foot_finished_matches', array()); return $saved_matches[$match_id] ?? null; } } /** * Resolve effective context for a post: season, team_id, match_id. * Priority: post meta 'swi_foot_context' -> plugin option defaults */ function swi_foot_resolve_context($post_id = null) { if (empty($post_id)) { $post_id = get_the_ID(); } $stored = get_post_meta($post_id, 'swi_foot_context', true); $season = null; $team_id = null; $match_id = null; if (is_array($stored)) { $season = !empty($stored['season']) ? $stored['season'] : null; $team_id = !empty($stored['team_id']) ? $stored['team_id'] : null; $match_id = !empty($stored['match_id']) ? $stored['match_id'] : null; } if (empty($season)) { $season = get_option('swi_foot_season_id', date('Y')); } return array( 'season' => $season, 'team_id' => $team_id, 'match_id' => $match_id ); }