wp-plugin-swiss-football-ma.../includes/class-swi-foot-api.php
Reindl David (IT-PTR-CEN2-SL10) fbd595aa36 initial state
2026-03-18 19:58:24 +01:00

560 lines
19 KiB
PHP

<?php
class Swi_Foot_API
{
private $base_url;
private $username;
private $password;
private $verein_id;
private $season_id;
private $cache_duration;
public function __construct()
{
$this->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);
// 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,
'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));
return new WP_Error('api_error', 'API request failed with 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_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,
'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_commons_ids()
{
return $this->api_request('/api/commons/ids');
}
/**
* 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()
{
// Actually test the connection by making an API request
$result = $this->api_request('/api/teams', array('vereinId' => $this->verein_id));
// Check if the request was successful (not an error)
if (is_wp_error($result)) {
error_log('Swiss Football API: Connection test failed - ' . $result->get_error_message());
return false;
}
// Verify we got a response (array means success)
return is_array($result);
}
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
);
}