mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
fix(logos): support logo downloads for custom soccer leagues (#262)
* fix(logos): support logo downloads for custom soccer leagues LogoDownloader.fetch_teams_data() and fetch_single_team() only had hardcoded API endpoints for predefined soccer leagues. Custom leagues (e.g., por.1, mex.1) would silently fail when the ESPN game data didn't include a direct logo URL. Now dynamically constructs the ESPN teams API URL for any soccer_* league not in the predefined map. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(logos): address PR review — directory, bulk download, and dedup - get_logo_directory: custom soccer leagues now resolve to shared assets/sports/soccer_logos/ instead of creating per-league dirs - download_all_missing_logos: use _resolve_api_url so custom soccer leagues are no longer silently skipped - Extract _resolve_api_url helper to deduplicate dynamic URL construction between fetch_teams_data and fetch_single_team Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(web): preserve array item properties in _set_nested_value When saving config with array-of-objects fields (e.g., custom_leagues), _set_nested_value would replace existing list objects with dicts when navigating dot-notation paths like "custom_leagues.0.name". This destroyed any properties on array items that weren't submitted in the form (e.g., display_modes, game_limits, filtering). Now properly indexes into existing lists when encountering numeric path segments, preserving all non-submitted properties on array items. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): address PR #262 code review security findings - logo_downloader: validate league name against allowlist before constructing filesystem paths in get_logo_directory to prevent path traversal (reject anything not matching ^[a-z0-9_-]+$) - logo_downloader: validate league_code against allowlist before interpolating into ESPN API URL in _resolve_api_url to prevent URL path injection; return None on invalid input - api_v3: add MAX_LIST_EXPANSION=1000 cap to _set_nested_value list expansion; raise ValueError for out-of-bounds indices; replace silent break fallback with TypeError for unexpected traversal types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ with special support for FCS teams and other NCAA divisions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
@@ -146,6 +147,9 @@ class LogoDownloader:
|
||||
|
||||
return variations
|
||||
|
||||
# Allowlist for league names used in filesystem paths: alphanumerics, underscores, dashes only
|
||||
_SAFE_LEAGUE_RE = re.compile(r'^[a-z0-9_-]+$')
|
||||
|
||||
def get_logo_directory(self, league: str) -> str:
|
||||
"""Get the logo directory for a given league."""
|
||||
directory = LogoDownloader.LOGO_DIRECTORIES.get(league)
|
||||
@@ -154,6 +158,10 @@ class LogoDownloader:
|
||||
if league.startswith('soccer_'):
|
||||
directory = 'assets/sports/soccer_logos'
|
||||
else:
|
||||
# Validate league before using it in a filesystem path
|
||||
if not self._SAFE_LEAGUE_RE.match(league):
|
||||
logger.warning(f"Rejecting unsafe league name for directory construction: {league!r}")
|
||||
raise ValueError(f"Unsafe league name: {league!r}")
|
||||
directory = f'assets/sports/{league}_logos'
|
||||
path = Path(directory)
|
||||
if not path.is_absolute():
|
||||
@@ -244,11 +252,17 @@ class LogoDownloader:
|
||||
logger.error(f"Unexpected error downloading logo for {team_abbreviation}: {e}")
|
||||
return False
|
||||
|
||||
# Allowlist for the league_code segment interpolated into ESPN API URLs
|
||||
_SAFE_LEAGUE_CODE_RE = re.compile(r'^[a-z0-9_-]+$')
|
||||
|
||||
def _resolve_api_url(self, league: str) -> Optional[str]:
|
||||
"""Resolve the ESPN API teams URL for a league, with dynamic fallback for custom soccer leagues."""
|
||||
api_url = self.API_ENDPOINTS.get(league)
|
||||
if not api_url and league.startswith('soccer_'):
|
||||
league_code = league[len('soccer_'):]
|
||||
if not self._SAFE_LEAGUE_CODE_RE.match(league_code):
|
||||
logger.warning(f"Rejecting unsafe league_code for ESPN URL construction: {league_code!r}")
|
||||
return None
|
||||
api_url = f'https://site.api.espn.com/apis/site/v2/sports/soccer/{league_code}/teams'
|
||||
logger.info(f"Using dynamic ESPN endpoint for custom soccer league: {league}")
|
||||
return api_url
|
||||
|
||||
@@ -3697,6 +3697,9 @@ def _parse_form_value_with_schema(value, key_path, schema):
|
||||
return value
|
||||
|
||||
|
||||
MAX_LIST_EXPANSION = 1000
|
||||
|
||||
|
||||
def _set_nested_value(config, key_path, value):
|
||||
"""
|
||||
Set a value in a nested dict using dot notation path.
|
||||
@@ -3723,6 +3726,10 @@ def _set_nested_value(config, key_path, value):
|
||||
# Navigate/create intermediate dicts, greedily matching dotted keys.
|
||||
# We stop before the final part so we can set it as the leaf value.
|
||||
while i < len(parts) - 1:
|
||||
if not isinstance(current, dict):
|
||||
raise TypeError(
|
||||
f"Unexpected type {type(current).__name__!r} at path segment {parts[i]!r} in key_path {key_path!r}"
|
||||
)
|
||||
# Try progressively longer candidate keys (longest first) to match
|
||||
# dict keys that contain dots themselves (e.g. "eng.1").
|
||||
# Never consume the very last part (that's the leaf value key).
|
||||
@@ -3745,6 +3752,10 @@ def _set_nested_value(config, key_path, value):
|
||||
i += 1
|
||||
|
||||
# The remaining parts form the final key (may itself be dotted, e.g. "eng.1")
|
||||
if not isinstance(current, dict):
|
||||
raise TypeError(
|
||||
f"Cannot set key at end of key_path {key_path!r}: expected dict, got {type(current).__name__!r}"
|
||||
)
|
||||
final_key = '.'.join(parts[i:])
|
||||
if value is not None or final_key not in current:
|
||||
current[final_key] = value
|
||||
|
||||
Reference in New Issue
Block a user