From bdc84a3a86d5bd7ad64ff1bb95aec7b9479f40ce Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Mon, 7 Sep 2026 08:38:56 +0000 Subject: [PATCH 1/3] Fix crash bugs found in full repository audit - plugin.py: day_selected and target_date were only assigned inside conditional branches but referenced unconditionally afterward, causing NameError/UnboundLocalError crashes when the API returns fewer days than expected or the hourly forecast is empty. - foreca_svg_map_viewer.py: removed an import of REGION_CENTERS and get_background_for_layer from foreca_map_viewer, which never defined them (ImportError on opening any wind-symbol SVG map). Defined both locally, mapped to background PNGs that actually exist under thumb/. - foreca_map_menu.py: a debug-loop leftover variable was compared instead of the current layer's id when filtering layer 3 from the map menu, so filtering behaved inconsistently depending on API response ordering. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UPpumFb2PP21ATpDwJBYBB --- .../Extensions/Foreca1/foreca_map_menu.py | 15 +++++----- .../Foreca1/foreca_svg_map_viewer.py | 30 +++++++++++++++++-- .../Plugins/Extensions/Foreca1/plugin.py | 2 ++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_menu.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_menu.py index 34e84e6d..83203083 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_menu.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_menu.py @@ -63,14 +63,13 @@ def load_layers(self): self.layers = self.api.get_capabilities() if DEBUG: print(f"[DEBUG] Layers ricevuti ({len(self.layers)}):") - for layer in self.layers: - layer_id = layer['id'] - title = layer.get('title', 'N/A') - layer_type = layer.get('type', 'N/A') - colorschemes = layer.get('colorschemes', []) - if DEBUG: + if DEBUG: + for layer in self.layers: + title = layer.get('title', 'N/A') + layer_type = layer.get('type', 'N/A') + colorschemes = layer.get('colorschemes', []) print( - f" ID: {layer_id}, Title: {title}, Type: {layer_type}, Schemes: {colorschemes}") + f" ID: {layer['id']}, Title: {title}, Type: {layer_type}, Schemes: {colorschemes}") if not self.layers: self["info"].setText(_("Error loading maps. Check connection.")) @@ -81,7 +80,7 @@ def load_layers(self): title = layer.get('title', f"Layer {layer['id']}") if 'wind symbol' in title.lower(): continue - if layer_id == 3: + if layer['id'] == 3: continue items.append((trans(title), layer)) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_svg_map_viewer.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_svg_map_viewer.py index 548e40b6..e1b15074 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_svg_map_viewer.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_svg_map_viewer.py @@ -30,14 +30,40 @@ apply_global_theme, TEMP_DIR ) -from .foreca_map_viewer import REGION_CENTERS, get_background_for_layer - SVG_MAPS_DIR = join(TEMP_DIR, "svgmapviewer") if not exists(SVG_MAPS_DIR): makedirs(SVG_MAPS_DIR) TILE_SIZE = 256 +# Fallback center coordinates per region, used when a layer has no usable extent. +REGION_CENTERS = { + 'eu': (50.0, 10.0), + 'europe': (50.0, 10.0), + 'us': (39.0, -98.0), + 'usa': (39.0, -98.0), + 'africa': (1.0, 20.0), + 'asia': (34.0, 100.0), + 'oceania': (-25.0, 135.0), + 'world': (20.0, 0.0), +} + +# Maps a region to a background PNG that actually exists under thumb/. +_REGION_BACKGROUNDS = { + 'eu': 'europa.png', + 'europe': 'europa.png', + 'us': 'nordamerika.png', + 'usa': 'nordamerika.png', + 'africa': 'africa.png', + 'asia': 'asia_se.png', + 'oceania': 'australia.png', +} + + +def get_background_for_layer(layer_title, region): + """Pick a background PNG for the given region (layer_title currently unused).""" + return _REGION_BACKGROUNDS.get((region or '').lower(), 'world.png') + class ForecaSVGMapViewer(Screen, HelpableScreen): def __init__(self, session, api, layer, unit_system='metric', region='eu'): diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py index 12730ce3..b672433d 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py @@ -1201,6 +1201,7 @@ def _load_favorite(self, fav_index, path_loc, forced_name=None): print( f"[DEBUG] sunrise={daily_all[self.tag].sunrise}, sunset={daily_all[self.tag].sunset}") + day_selected = None if daily_all and len(daily_all) > self.tag: day_selected = daily_all[self.tag] @@ -1269,6 +1270,7 @@ def _load_favorite(self, fav_index, path_loc, forced_name=None): # Hourly forecast (try free, fallback to auth) hourly = None + target_date = None # First try free API try: hourly = self.weather_api.get_hourly_forecast( From 8c3f834aadbe24357d407b663a0b3ddaeb0376e1 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Mon, 7 Sep 2026 08:39:13 +0000 Subject: [PATCH 2/3] Fix security issues found in full repository audit - foreca_map_api.py: replaced hardcoded, real-looking Foreca API credentials (used as a fallback default and written into the generated .example config) with the same placeholder values already used elsewhere in the file. - plugin.py: the saved API credentials file is now chmod 600 (previously inherited the process umask, typically world-readable); dropped the needless execute bit on other saved plain-data files (favorites/color/alpha configs), tightening 0o655 to 0o644. - rain_maps.py: validate the tile-server host returned by api.rainviewer.com's JSON response before using it to build subsequent outbound tile-fetch URLs, instead of trusting it unconditionally. - city_panel.py: URL-encode the user-entered city search term before interpolating it into the request URL, matching the existing correct pattern already used in foreca_weather_api.py. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UPpumFb2PP21ATpDwJBYBB --- .../python/Plugins/Extensions/Foreca1/city_panel.py | 4 +++- .../python/Plugins/Extensions/Foreca1/foreca_map_api.py | 8 ++++---- .../enigma2/python/Plugins/Extensions/Foreca1/plugin.py | 9 +++++---- .../python/Plugins/Extensions/Foreca1/rain_maps.py | 9 ++++++++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/city_panel.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/city_panel.py index c2dee23f..7b7368d4 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/city_panel.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/city_panel.py @@ -5,6 +5,7 @@ # fallback import requests +from urllib.parse import quote_plus from os.path import exists, join from enigma import eListboxPythonMultiContent, gFont, RT_VALIGN_CENTER, eTimer, eListbox @@ -296,7 +297,8 @@ def search_online(self, search_term): """Cerca tramite API Foreca. Ritorna True se ha trovato risultati, False altrimenti.""" current_lang = _get_system_language() try: - url = "%s/locations/search/%s.json" % (BASE_URL, search_term) + url = "%s/locations/search/%s.json" % ( + BASE_URL, quote_plus(search_term)) params = { "limit": 20, "lang": current_lang diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_api.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_api.py index 4fc43d37..ac805daa 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_api.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/foreca_map_api.py @@ -100,8 +100,8 @@ def load_config(self): config_data = default_config # Assign values - self.user = config_data.get("API_USER", "ekekaz") - self.password = config_data.get("API_PASSWORD", "im5issEYcMUG") + self.user = config_data.get("API_USER", "your_username_here") + self.password = config_data.get("API_PASSWORD", "your_password_here") self.token_expire_hours = int( config_data.get( "TOKEN_EXPIRE_HOURS", 720)) @@ -150,10 +150,10 @@ def create_example_config(self): # Rename this file to api_config.txt and fill with your credentials # Your Foreca API username - API_USER=ekekaz + API_USER=your_username_here # Your Foreca API password - API_PASSWORD=im5issEYcMUG + API_PASSWORD=your_password_here # Token expiration in hours (max 720 = 30 days) TOKEN_EXPIRE_HOURS=720 diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py index b672433d..3d005732 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py @@ -344,6 +344,7 @@ def save(self): f"TOKEN_EXPIRE_HOURS={token_expire_hours_int}\n") config_file.write(f"MAP_SERVER={map_server}\n") config_file.write(f"AUTH_SERVER={auth_server}\n") + chmod(CONFIG_FILE, 0o600) except Exception as error: self.session.open( MessageBox, @@ -1397,10 +1398,10 @@ def _save_favorite(self, index, city_id): try: with open(filename, "w") as f: f.write(city_id) - chmod(filename, 0o655) + chmod(filename, 0o644) if DEBUG: print( - f"[Foreca1] Saved {names[index]} = {city_id} (perms 655)") + f"[Foreca1] Saved {names[index]} = {city_id} (perms 644)") except Exception as e: print(f"[Foreca1] Error saving {names[index]}: {e}") @@ -1409,7 +1410,7 @@ def _save_color(self): try: with open(path, "w") as f: f.write(f"{self.rgbmyr} {self.rgbmyg} {self.rgbmyb}") - chmod(path, 0o655) + chmod(path, 0o644) if DEBUG: print( f"[Foreca1] Color saved: {self.rgbmyr} {self.rgbmyg} {self.rgbmyb}") @@ -1421,7 +1422,7 @@ def _save_alpha(self): try: with open(path, "w") as f: f.write(self.alpha) - chmod(path, 0o655) + chmod(path, 0o644) except Exception as e: print("[Foreca1] Error saving alpha:", e) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/rain_maps.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/rain_maps.py index 080f18e0..c022f031 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/rain_maps.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/rain_maps.py @@ -262,7 +262,14 @@ def _fetch_frames(self): _("API error"))) return data = resp.json() - self.host = data['host'] + returned_host = data['host'] + if returned_host.startswith('https://') and returned_host[8:].split( + '/')[0].endswith('.rainviewer.com'): + self.host = returned_host + else: + print( + f"[RainViewer] Unexpected host in API response, ignoring: {returned_host}") + self.host = 'https://tilecache.rainviewer.com' self.frames = [frame['path'] for frame in data['radar']['past']] self.frames.reverse() # oldest to newest self.current_frame = len(self.frames) - 1 # last frame From e6f26bbb563010e7d3f739c5e4d236d737909962 Mon Sep 17 00:00:00 2001 From: pQu4k3r Date: Mon, 7 Sep 2026 08:39:29 +0000 Subject: [PATCH 3/3] Fix UI-thread blocking and missing dependency from full repo audit - plugin.py: _update_station_label ran in a background Thread but called self["station_name"].setText(...) directly from that thread instead of marshaling back to the UI thread, risking GUI corruption if the screen closes/rebuilds mid-fetch. Now wrapped in reactor.callFromThread, matching the pattern already used elsewhere in the file (_moon_api_callback). - meteogram.py: fetch_data ran its network request and HTML/JSON parsing synchronously from onLayoutFinish, freezing the whole Enigma2 UI while it waited. Split into a background-thread worker that fetches/parses data and a UI-thread method that only populates widgets, connected via reactor.callFromThread. - moon_calendar.py: load_calendar performed ~365 trig-heavy lunar position calculations synchronously from onLayoutFinish. Same treatment: computation moved to a background Thread, only the final widget population runs via reactor.callFromThread. - CONTROL/preinst: the opkg (.ipk) install path never installed Pillow even though several plugin modules import it for map rendering, unlike installer.sh which already installs it correctly. Added the missing ${PY}-pillow install. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UPpumFb2PP21ATpDwJBYBB --- CONTROL/preinst | 7 +++++++ .../Plugins/Extensions/Foreca1/meteogram.py | 14 ++++++++++++-- .../Plugins/Extensions/Foreca1/moon_calendar.py | 10 ++++++++++ .../python/Plugins/Extensions/Foreca1/plugin.py | 15 ++++++++++----- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/CONTROL/preinst b/CONTROL/preinst index 424b5da9..6b86c125 100755 --- a/CONTROL/preinst +++ b/CONTROL/preinst @@ -52,6 +52,13 @@ else echo "⚠ Could not install ${PY}-requests, but continuing installation" fi +echo "Installing ${PY}-pillow..." +if opkg install --force-reinstall "${PY}-pillow" > /dev/null 2>&1; then + echo "✓ ${PY}-pillow installed successfully" +else + echo "⚠ Could not install ${PY}-pillow, but continuing installation" +fi + # Additional dependency checks echo "Verifying system compatibility..." if [ -e "/usr/bin/enigma2" ]; then diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/meteogram.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/meteogram.py index fc47828a..14ece036 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/meteogram.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/meteogram.py @@ -8,7 +8,9 @@ from json import loads, JSONDecodeError from os.path import exists, join from os import makedirs, listdir, remove +from threading import Thread import requests +from twisted.internet import reactor from enigma import getDesktop, ePoint @@ -224,11 +226,15 @@ def cleanup_temp_files(self): print(f"[Meteogram] Error cleaning temp files: {e}") def fetch_data(self): - """Download the detailed forecast page and extract JSON data.""" + """Kick off the (blocking) forecast download in a background thread.""" + Thread(target=self._fetch_data_worker).start() + + def _fetch_data_worker(self): + """Download the detailed forecast page and extract JSON data. Runs off the UI thread.""" lang = _get_system_language() place = self.api.get_location_by_id(self.loc_id) if not place: - self.close() + reactor.callFromThread(self.close) return url = f"https://www.foreca.com/{lang}/{self.loc_id}/{place.address}/detailed-forecast" @@ -272,6 +278,10 @@ def fetch_data(self): write_meteogram_debug( f"First element keys: {list(forecast[0].keys())}") + reactor.callFromThread(self._apply_fetched_data, forecast, ranges) + + def _apply_fetched_data(self, forecast, ranges): + """Populate widgets from downloaded forecast data. Runs on the UI thread.""" # Update time (first element's 'updated' field) if forecast and len(forecast) > 1: updated_utc = forecast[1].get('updated', '').replace('Z', '+00:00') diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/moon_calendar.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/moon_calendar.py index 4e189e9b..8e3cd1fa 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/moon_calendar.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/moon_calendar.py @@ -6,6 +6,8 @@ from datetime import datetime, timedelta from os.path import exists, join from collections import defaultdict +from threading import Thread +from twisted.internet import reactor from Screens.Screen import Screen from Screens.HelpMenu import HelpableScreen from Screens.MessageBox import MessageBox @@ -156,6 +158,10 @@ def _jd_to_datetime(self, jd): def load_calendar(self): """Generate the list of lunar phases and special events for the next 12 months.""" self["info"].setText(_("Calculating...")) + Thread(target=self._load_calendar_worker).start() + + def _load_calendar_worker(self): + """Heavy lunar-phase computation. Runs off the UI thread.""" self.phases = [] today = datetime.now() # Start from the first day of next month @@ -253,6 +259,10 @@ def load_calendar(self): # Update current moon info info = self.moon.get_phase_info() + reactor.callFromThread(self._apply_calendar_data, info) + + def _apply_calendar_data(self, info): + """Populate widgets with the computed calendar data. Runs on the UI thread.""" if info["icon_path"] and exists(info["icon_path"]): self["current_phase_icon"].instance.setPixmapFromFile( info["icon_path"]) diff --git a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py index 3d005732..2a99730a 100644 --- a/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py +++ b/usr/lib/enigma2/python/Plugins/Extensions/Foreca1/plugin.py @@ -2215,11 +2215,16 @@ def truncate(text, max_len=25): # Truncate text if too long station_text = truncate(station_text) - # Safely update the widget only if it exists - if "station_name" in self: - self["station_name"].setText(station_text) - if source: - print(f"[Foreca1] Station source: {source}") + from twisted.internet import reactor + + def update_ui(): + # Safely update the widget only if it exists + if "station_name" in self: + self["station_name"].setText(station_text) + if source: + print(f"[Foreca1] Station source: {source}") + + reactor.callFromThread(update_ui) def _update_moon(self, target_date=None): """