api = new Swi_Foot_API();
// Register shortcodes
add_shortcode('swi_foot_match', array($this, 'match_shortcode'));
add_shortcode('swi_foot_match_home_team', array($this, 'match_home_team_shortcode'));
add_shortcode('swi_foot_match_away_team', array($this, 'match_away_team_shortcode'));
add_shortcode('swi_foot_match_date', array($this, 'match_date_shortcode'));
add_shortcode('swi_foot_match_time', array($this, 'match_time_shortcode'));
add_shortcode('swi_foot_match_venue', array($this, 'match_venue_shortcode'));
add_shortcode('swi_foot_match_score', array($this, 'match_score_shortcode'));
add_shortcode('swi_foot_match_status', array($this, 'match_status_shortcode'));
add_shortcode('swi_foot_match_league', array($this, 'match_league_shortcode'));
add_shortcode('swi_foot_match_round', array($this, 'match_round_shortcode'));
add_shortcode('swi_foot_standings', array($this, 'standings_shortcode'));
add_shortcode('swi_foot_roster', array($this, 'roster_shortcode'));
add_shortcode('swi_foot_events', array($this, 'events_shortcode'));
add_shortcode('swi_foot_match_data', array($this, 'match_data_shortcode'));
add_shortcode('swi_foot_match_home_team_logo', array($this, 'match_home_team_logo_shortcode'));
add_shortcode('swi_foot_match_away_team_logo', array($this, 'match_away_team_logo_shortcode'));
add_shortcode('swi_foot_match_home_team_logo_url', array($this, 'match_home_team_logo_url_shortcode'));
add_shortcode('swi_foot_match_away_team_logo_url', array($this, 'match_away_team_logo_url_shortcode'));
// Register query variable for team logo requests and handle image serving
add_filter('query_vars', array($this, 'register_query_vars'));
add_action('template_redirect', array($this, 'handle_team_logo_request'));
}
/**
* Register custom query variables
*/
public function register_query_vars($vars)
{
$vars[] = 'swi_foot_team_logo';
$vars[] = 'position';
return $vars;
}
/**
* Handle internal requests for team logo images
* URL format: /?swi_foot_team_logo=team_id&position=home|away|event
* Uses template_redirect hook which fires after WordPress determines the query
* but before loading the template/theme
*/
public function handle_team_logo_request()
{
$team_logo_id = get_query_var('swi_foot_team_logo');
error_log('Swiss Football: handle_team_logo_request() called, team_logo_id from get_query_var: ' . var_export($team_logo_id, true));
if (empty($team_logo_id)) {
error_log('Swiss Football: team_logo_id is empty, skipping image serving');
return; // Not a team logo request, continue normal WordPress flow
}
error_log('Swiss Football: Processing team logo request for team_id: ' . $team_logo_id);
// This is a team logo request, serve the image and exit
$this->serve_team_logo_image($team_logo_id);
exit; // Never reached, but for clarity
}
/**
* Serve team logo image from API base64 data
*/
private function serve_team_logo_image($team_id)
{
error_log('Swiss Football: serve_team_logo_image() called with team_id: ' . $team_id . ', type: ' . gettype($team_id));
// Validate team_id is numeric
if (!ctype_digit((string)$team_id) || empty($team_id)) {
error_log('Swiss Football: Invalid team ID provided: ' . var_export($team_id, true));
wp_die('Invalid team ID', 'Invalid Request', array('response' => 400));
}
error_log('Swiss Football: team_id validation passed, team_id: ' . $team_id);
error_log('Swiss Football: Attempting to fetch team picture for team_id: ' . $team_id);
// Fetch image from API
$image_data = $this->api->get_team_picture($team_id);
error_log('Swiss Football: API response type: ' . gettype($image_data));
if (is_wp_error($image_data)) {
error_log('Swiss Football: API returned WP_Error: ' . $image_data->get_error_message());
} else {
error_log('Swiss Football: API response length: ' . strlen(var_export($image_data, true)) . ', is_array: ' . (is_array($image_data) ? 'yes' : 'no') . ', is_string: ' . (is_string($image_data) ? 'yes' : 'no'));
if (is_string($image_data)) {
error_log('Swiss Football: String response first 200 chars: ' . substr($image_data, 0, 200));
}
}
if (is_wp_error($image_data)) {
error_log('Swiss Football: API error fetching team picture: ' . $image_data->get_error_message());
wp_die('Image not found', 'Not Found', array('response' => 404));
}
if (empty($image_data)) {
error_log('Swiss Football: Team picture returned empty for team_id: ' . $team_id);
wp_die('Image not found', 'Not Found', array('response' => 404));
}
error_log('Swiss Football: Successfully fetched image data for team_id: ' . $team_id . ', length: ' . strlen($image_data));
// Handle API response that may be an array (if JSON returned a structure)
// or a string (if JSON returned a plain string)
if (is_array($image_data)) {
error_log('Swiss Football: Image data is array, attempting to extract base64 from common fields');
// Try to find the base64 data in common field names
$potential_fields = array('picture', 'logo', 'image', 'data', 'url', 'href');
$found_data = null;
foreach ($potential_fields as $field) {
if (isset($image_data[$field]) && !empty($image_data[$field])) {
$found_data = $image_data[$field];
error_log('Swiss Football: Found image data in array field: ' . $field);
break;
}
}
if (!$found_data) {
error_log('Swiss Football: Could not find image data in array, array keys: ' . implode(', ', array_keys($image_data)));
wp_die('Image not found', 'Not Found', array('response' => 404));
}
$image_data = $found_data;
error_log('Swiss Football: Extracted base64 from array, length: ' . strlen($image_data));
}
// At this point, $image_data should be a string (base64 encoded)
// The API returns base64 encoded image data
// Detect image format from the data or default to PNG
$mime_type = 'image/png';
if (strpos($image_data, 'data:image/') === 0) {
// Already has data URI format, extract mime type
preg_match('/data:([^;]+)/', $image_data, $matches);
if (isset($matches[1])) {
$mime_type = $matches[1];
// Remove data URI prefix to get just base64
$image_data = preg_replace('/^data:[^;]+;base64,/', '', $image_data);
}
} elseif (strpos($image_data, '/') === false && strlen($image_data) > 100) {
// Looks like raw base64, assume PNG
$mime_type = 'image/png';
}
// Decode base64
$decoded = base64_decode(trim($image_data), true);
if ($decoded === false) {
error_log('Swiss Football: Failed to decode base64 image data for team_id: ' . $team_id);
wp_die('Invalid image data', 'Bad Request', array('response' => 400));
}
error_log('Swiss Football: Successfully decoded image, size: ' . strlen($decoded) . ' bytes, mime: ' . $mime_type);
// Set headers for image serving
header('Content-Type: ' . $mime_type);
header('Content-Length: ' . strlen($decoded));
header('Cache-Control: public, max-age=2592000'); // 30 days
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 2592000) . ' GMT');
header('Access-Control-Allow-Origin: *');
// Output image and exit
echo $decoded;
exit;
}
/**
* Generate internal URL for serving team logo image
*
* @param string $team_id The team ID
* @param string $position 'home' or 'away'
* @return string URL to internal team logo endpoint
*/
private function get_team_logo_url($team_id, $position = 'home')
{
return add_query_arg(array(
'swi_foot_team_logo' => $team_id,
'position' => $position
), home_url('/'));
}
public function match_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'team_id' => '',
'show_next' => 'false'
), $atts);
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '';
}
ob_start();
?>
get_match_data($atts);
if (!$match_data || empty($match_data['teams'])) {
return '';
}
foreach ($match_data['teams'] as $team) {
if ($team['isHomeTeam']) {
return '';
}
}
return '';
}
public function match_away_team_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (!$match_data || empty($match_data['teams'])) {
return '';
}
foreach ($match_data['teams'] as $team) {
if (!$team['isHomeTeam']) {
return '';
}
}
return '';
}
public function match_date_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'team_id' => '',
'show_next' => 'false',
'format' => get_option('date_format', 'd.m.Y')
), $atts);
$match_data = $this->get_match_data($atts);
if (!$match_data || empty($match_data['matchDate'])) {
return '';
}
return '';
}
public function match_time_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'team_id' => '',
'show_next' => 'false',
'format' => get_option('time_format', 'H:i')
), $atts);
$match_data = $this->get_match_data($atts);
if (!$match_data || empty($match_data['matchDate'])) {
return '';
}
return '';
}
public function match_venue_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '';
}
$venue = $match_data['stadiumFieldName'] ?? '';
if ($venue) {
return '';
}
return '';
}
public function match_score_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'team_id' => '',
'show_next' => 'false',
'separator' => ':'
), $atts);
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '';
}
if ($match_data['hasMatchStarted'] || $match_data['hasMatchEnded']) {
$score = esc_html($match_data['scoreTeamA']) . ' ' . $atts['separator'] . ' ' . esc_html($match_data['scoreTeamB']);
return '';
}
return '';
}
public function match_status_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '';
}
$status = $match_data['matchStateName'] ?? '';
if ($status) {
return '';
}
return '';
}
public function match_league_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '';
}
$league = $match_data['leagueName'] ?? '';
if ($league) {
return '';
}
return '';
}
public function match_round_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '';
}
$round = $match_data['roundNbr'] ?? '';
if ($round) {
return '';
}
return '';
}
/**
* Generic inline data point shortcode
* Extracts a specific field from match data and displays it inline
* Usage: [swi_foot_match_data data_point="teamNameA"]
* Match ID is automatically picked up from container context if not specified
* Special data points: matchDate_date and matchDate_time (formatted per WordPress locale)
*/
public function match_data_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'team_id' => '',
'data_point' => '',
), $atts);
// If match_id not provided, get it from post context
if (empty($atts['match_id'])) {
$context = function_exists('swi_foot_resolve_context') ? swi_foot_resolve_context(get_the_ID()) : array();
$atts['match_id'] = $context['match_id'] ?? '';
}
if (empty($atts['match_id']) || empty($atts['data_point'])) {
return '';
}
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '';
}
// Normalize match data to ensure consistent field names
$match_data = $this->normalize_shortcode_match_data($match_data);
// Handle special date/time formatting
if ($atts['data_point'] === 'matchDate_date') {
$value = $this->format_match_date($match_data);
} elseif ($atts['data_point'] === 'matchDate_time') {
$value = $this->format_match_time($match_data);
} else {
// Support nested data points like "intermediateResults/scoreTeamA"
$value = $this->get_nested_value($match_data, $atts['data_point']);
}
if ($value === null || $value === '') {
return '';
}
// Convert boolean values to readable text
if (is_bool($value)) {
$value = $value ? __('Ja', 'swi_foot_matchdata') : __('Nein', 'swi_foot_matchdata');
}
return '';
}
/**
* Normalize match data to ensure consistent field names
* Extracts team names from teams array if not present as direct fields
* @param array $match Raw match data
* @return array Normalized match data
*/
private function normalize_shortcode_match_data($match)
{
// If already has teamNameA/teamNameB as direct fields, return as-is
if (!empty($match['teamNameA']) || !empty($match['teamNameB'])) {
return $match;
}
// Extract team names from teams array if not present as direct fields
if (!empty($match['teams']) && is_array($match['teams'])) {
foreach ($match['teams'] as $team) {
if (!empty($team['isHomeTeam']) && !empty($team['teamName'])) {
$match['teamNameA'] = $team['teamName'];
} elseif (empty($team['isHomeTeam']) && !empty($team['teamName'])) {
$match['teamNameB'] = $team['teamName'];
}
}
}
// Normalize stadium field name
if (empty($match['stadiumFieldName']) && !empty($match['stadiumPlaygroundName'])) {
$match['stadiumFieldName'] = $match['stadiumPlaygroundName'];
}
return $match;
}
/**
* Helper to get nested array values using dot/slash notation
* @param array $array
* @param string $path e.g. "intermediateResults/scoreTeamA"
* @return mixed
*/
private function get_nested_value($array, $path)
{
if (!is_array($array) || empty($path)) {
return null;
}
$keys = explode('/', $path);
$current = $array;
foreach ($keys as $key) {
if (!is_array($current) || !isset($current[$key])) {
return null;
}
$current = $current[$key];
}
return $current;
}
/**
* Format match date according to WordPress locale settings
* Extracts date part from matchDate and formats using WordPress date format
* @param array $match Match data array
* @return string|null Formatted date or null if matchDate not available
*/
private function format_match_date($match)
{
if (empty($match['matchDate'])) {
return null;
}
// Parse the matchDate string - handle multiple formats
$timestamp = strtotime($match['matchDate']);
if ($timestamp === false) {
return null;
}
// Use WordPress date format setting
$date_format = get_option('date_format');
return wp_date($date_format, $timestamp);
}
/**
* Format match time according to WordPress locale settings
* Extracts time part from matchDate and formats using WordPress time format
* @param array $match Match data array
* @return string|null Formatted time or null if matchDate not available
*/
private function format_match_time($match)
{
if (empty($match['matchDate'])) {
return null;
}
// Parse the matchDate string - handle multiple formats
$timestamp = strtotime($match['matchDate']);
if ($timestamp === false) {
return null;
}
// Use WordPress time format setting
$time_format = get_option('time_format');
return wp_date($time_format, $timestamp);
}
private function get_match_data($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'team_id' => '',
'show_next' => 'false'
), $atts);
$match_id = $atts['match_id'];
// If no match_id provided explicitly, try to resolve it from the post/page context
if (empty($match_id)) {
if (function_exists('swi_foot_resolve_context')) {
$ctx = swi_foot_resolve_context();
if (!empty($ctx['match_id'])) {
$match_id = $ctx['match_id'];
} elseif (empty($match_id) && !empty($ctx['team_id']) && $atts['show_next'] === 'true') {
// Use schedule to find next match for the team when show_next requested
$schedule = $this->api->get_schedule($ctx['team_id']);
if (!is_wp_error($schedule) && !empty($schedule)) {
foreach ($schedule as $match) {
if (strtotime($match['matchDate']) >= time()) {
$match_id = $match['matchId'];
break;
}
}
}
}
}
}
// If no match_id but show_next is true, try to find next match for team
if (empty($match_id) && $atts['show_next'] === 'true' && !empty($atts['team_id'])) {
$schedule = $this->api->get_schedule($atts['team_id']);
if (!is_wp_error($schedule) && !empty($schedule)) {
// Find next upcoming match
foreach ($schedule as $match) {
if (strtotime($match['matchDate']) >= time()) {
$match_id = $match['matchId'];
break;
}
}
}
}
if (empty($match_id)) {
return null;
}
// Try to get cached data first
$match_data = $this->api->get_cached_match_data($match_id);
// If no cached data, fetch from API
if (!$match_data) {
$match_data = $this->api->get_match_details($match_id);
if (is_wp_error($match_data)) {
return null;
}
}
return $match_data;
}
public function standings_shortcode($atts)
{
$atts = shortcode_atts(array(
'team_id' => ''
), $atts);
if (empty($atts['team_id'])) {
return '';
}
$standings = $this->api->get_standings($atts['team_id']);
if (is_wp_error($standings) || empty($standings)) {
return '';
}
ob_start();
?>
'',
'team_id' => '',
'show_current' => 'false',
'side' => '',
'with_bench' => 'false'
), $atts);
$side = strtolower(trim($atts['side']));
if (!in_array($side, array('home', 'away'), true)) {
return '';
}
$match_id = $atts['match_id'];
if ($atts['show_current'] === 'true' && !empty($atts['team_id'])) {
$current_match = $this->api->get_current_match($atts['team_id']);
if ($current_match) {
$match_id = $current_match['matchId'];
}
}
if (empty($match_id)) {
return '';
}
// First check if we have saved final data
$saved = $this->api->get_finished_match_data($match_id);
if ($saved) {
$players = $saved['roster']['players'] ?? array();
$bench_players = $saved['roster']['bench'] ?? array();
} else {
// Live fetch
$players = $this->api->get_match_players($match_id);
if (is_wp_error($players)) {
return '';
}
$bench_players = array();
if (strtolower($atts['with_bench']) === 'true') {
$bench = $this->api->get_match_bench($match_id);
if (!is_wp_error($bench)) {
$bench_players = $bench;
}
}
// Check match ended & possibly save events + roster together
$match_details = $this->api->get_match_details($match_id);
if (!is_wp_error($match_details) && !empty($match_details['hasMatchEnded'])) {
$events = $this->api->get_match_events($match_id);
if (is_wp_error($events)) {
$events = array();
}
// Only save if we have valid player data (not a WP_Error)
if (is_array($players)) {
$this->api->save_finished_match_data(
$match_id,
array('players' => $players, 'bench' => $bench_players),
$events
);
}
}
}
// Filter roster for side
$filtered_players = array_filter($players, function ($p) use ($side) {
return $side === 'home' ? !empty($p['isHomeTeam']) : empty($p['isHomeTeam']);
});
$filtered_bench = array_filter($bench_players, function ($p) use ($side) {
return $side === 'home' ? !empty($p['isHomeTeam']) : empty($p['isHomeTeam']);
});
ob_start();
?>
'',
'team_id' => '',
'show_current' => 'false',
'refresh_interval' => '30',
'event_order' => 'dynamic'
), $atts);
error_log('[SWI_FOOT_EVENTS_DEBUG] PARSED ATTRIBUTES: ' . json_encode($atts, JSON_UNESCAPED_SLASHES));
error_log('[SWI_FOOT_EVENTS_DEBUG] EVENT_ORDER VALUE: ' . var_export($atts['event_order'], true));
$match_id = $atts['match_id'];
if ($atts['show_current'] === 'true' && !empty($atts['team_id'])) {
$current_match = $this->api->get_current_match($atts['team_id']);
if ($current_match) {
$match_id = $current_match['matchId'];
}
}
if (empty($match_id)) {
return '';
}
// Try saved first
$saved = $this->api->get_finished_match_data($match_id);
if ($saved) {
$events = $saved['events'] ?? array();
} else {
$events = $this->api->get_match_events($match_id);
if (is_wp_error($events)) {
return '';
}
// Check if match ended and store both data sets
$match_details = $this->api->get_match_details($match_id);
if (!is_wp_error($match_details) && !empty($match_details['hasMatchEnded'])) {
$players = $this->api->get_match_players($match_id);
$bench = $this->api->get_match_bench($match_id);
if (is_wp_error($players)) $players = array();
if (is_wp_error($bench)) $bench = array();
$this->api->save_finished_match_data(
$match_id,
array('players' => $players, 'bench' => $bench),
$events
);
}
}
// Determine event sort order based on setting and match status
$event_order = $atts['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
$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_details = $this->api->get_match_details($match_id);
$match_has_ended = !is_wp_error($match_details) && !empty($match_details['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'];
});
}
}
ob_start();
?>
get_match_data($atts);
if (!$match_data || empty($match_data['teams']) || empty($match_data['teams'][0])) {
return '';
}
$home_team_id = $match_data['teams'][0]['teamId'] ?? '';
if (empty($home_team_id)) {
return '';
}
$logo_url = $this->get_team_logo_url($home_team_id, 'home');
$team_name = esc_attr($match_data['teams'][0]['teamName'] ?? 'Home Team');
return '';
}
public function match_away_team_logo_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (!$match_data || empty($match_data['teams']) || empty($match_data['teams'][1])) {
return '';
}
$away_team_id = $match_data['teams'][1]['teamId'] ?? '';
if (empty($away_team_id)) {
return '';
}
$logo_url = $this->get_team_logo_url($away_team_id, 'away');
$team_name = esc_attr($match_data['teams'][1]['teamName'] ?? 'Away Team');
return '';
}
/**
* Shortcode: Display home team logo URL only (for use in image blocks)
* Usage: [swi_foot_match_home_team_logo_url]
* Returns: Internal URL that serves the team logo image
*/
public function match_home_team_logo_url_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (empty($match_data)) {
return '';
}
$home_team_id = $match_data['teams'][0]['teamId'] ?? '';
if (empty($home_team_id)) {
return '';
}
return $this->get_team_logo_url($home_team_id, 'home');
}
/**
* Shortcode: Display away team logo URL only (for use in image blocks)
* Usage: [swi_foot_match_away_team_logo_url]
* Returns: Internal URL that serves the team logo image
*/
public function match_away_team_logo_url_shortcode($atts)
{
$match_data = $this->get_match_data($atts);
if (empty($match_data)) {
return '';
}
$away_team_id = $match_data['teams'][1]['teamId'] ?? '';
if (empty($away_team_id)) {
return '';
}
return $this->get_team_logo_url($away_team_id, 'away');
}
public function enqueue_editor_assets()
{
wp_enqueue_script(
'swi-foot-editor-blocks',
SWI_FOOT_PLUGIN_URL . 'assets/editor-blocks.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data', 'jquery'),
SWI_FOOT_PLUGIN_VERSION,
true
);
wp_localize_script('swi-foot-editor-blocks', 'swiFootEditorData', array(
'rest_url' => esc_url_raw(rest_url('swi-foot/v1')),
'rest_nonce' => wp_create_nonce('wp_rest')
));
}
}
/**
* Replace saved inline shortcode spans with their rendered shortcode output on the front-end.
* Spans are inserted by the editor format and contain a `data-shortcode` attribute.
*/
function swi_foot_render_inline_shortcodes($content)
{
if (stripos($content, 'swi-foot-inline-shortcode') === false) {
return $content;
}
return preg_replace_callback(
'/]*class=["\']?[^"\'>]*swi-foot-inline-shortcode[^"\'>]*["\']?[^>]*data-shortcode=["\']([^"\']+)["\'][^>]*>.*?<\/span>/is',
function ($m) {
$sc = html_entity_decode($m[1]);
return do_shortcode($sc);
},
$content
);
}
add_filter('the_content', 'swi_foot_render_inline_shortcodes', 11);
?>