Fix remaining GitHub CodeQL security alerts

- py/stack-trace-exposure: Remove str(e) and traceback.format_exc() from
  all HTTP responses across api_v3.py, pages_v3.py, and app.py; replace
  with generic messages and logger.error(exc_info=True)
- py/reflective-xss: Escape partial_name via markupsafe.escape in the
  load_partial 404 response
- py/path-injection: Add regex validation of plugin_id before filesystem
  use in _load_plugin_config_partial
- py/incomplete-url-substring-sanitization: Replace 'github.com' in
  substring checks with urlparse hostname comparison in store_manager.py
- py/clear-text-logging-sensitive-data: Remove football-scoreboard debug
  prints and sensitive request-body prints from update endpoint
- js/bad-tag-filter: Replace script-only regex in BaseWidget.sanitizeValue
  with DOM-based textContent stripping that removes all HTML
- js/incomplete-sanitization: Fix escapeAttr to properly encode &, ", ',
  <, > using HTML entities instead of backslash escaping
- js/prototype-pollution-utility: Add __proto__/constructor/prototype
  key guards to deepMerge function in plugins_manager.js
- app.py error handlers: Always return generic messages; remove debug-mode
  branches that could expose tracebacks in production

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-05-23 19:01:06 -04:00
parent 8652aacf37
commit 1d2303e620
6 changed files with 222 additions and 235 deletions

View File

@@ -21,6 +21,8 @@ from pathlib import Path
from typing import List, Dict, Optional, Any, Tuple from typing import List, Dict, Optional, Any, Tuple
import logging import logging
from urllib.parse import urlparse
from src.common.permission_utils import sudo_remove_directory from src.common.permission_utils import sudo_remove_directory
try: try:
@@ -356,7 +358,8 @@ class PluginStoreManager:
# Extract owner/repo from URL # Extract owner/repo from URL
try: try:
# Handle different URL formats # Handle different URL formats
if 'github.com' in repo_url: _parsed_url = urlparse(repo_url)
if _parsed_url.hostname in ('github.com', 'www.github.com'):
parts = repo_url.strip('/').split('/') parts = repo_url.strip('/').split('/')
if len(parts) >= 2: if len(parts) >= 2:
owner = parts[-2] owner = parts[-2]
@@ -518,9 +521,10 @@ class PluginStoreManager:
# Try to find plugins.json in common locations # Try to find plugins.json in common locations
# First try root directory # First try root directory
registry_urls = [] registry_urls = []
# Extract owner/repo from URL # Extract owner/repo from URL
if 'github.com' in repo_url: _parsed_repo_url = urlparse(repo_url)
if _parsed_repo_url.hostname in ('github.com', 'www.github.com'):
parts = repo_url.split('/') parts = repo_url.split('/')
if len(parts) >= 2: if len(parts) >= 2:
owner = parts[-2] owner = parts[-2]
@@ -775,7 +779,8 @@ class PluginStoreManager:
try: try:
# Convert repo URL to raw content URL # Convert repo URL to raw content URL
# https://github.com/user/repo -> https://raw.githubusercontent.com/user/repo/branch/manifest.json # https://github.com/user/repo -> https://raw.githubusercontent.com/user/repo/branch/manifest.json
if 'github.com' in repo_url: _parsed_manifest_url = urlparse(repo_url)
if _parsed_manifest_url.hostname in ('github.com', 'www.github.com'):
# Handle different URL formats # Handle different URL formats
repo_url = repo_url.rstrip('/') repo_url = repo_url.rstrip('/')
if repo_url.endswith('.git'): if repo_url.endswith('.git'):

View File

@@ -204,24 +204,12 @@ def serve_plugin_asset(plugin_id, filename):
# Use send_from_directory to serve the file # Use send_from_directory to serve the file
return send_from_directory(str(assets_dir), filename, mimetype=content_type) return send_from_directory(str(assets_dir), filename, mimetype=content_type)
except Exception as e: except Exception:
# Log the exception with full traceback server-side
import traceback
app.logger.exception('Error serving plugin asset file') app.logger.exception('Error serving plugin asset file')
return jsonify({
# Return generic error message to client (avoid leaking internal details) 'status': 'error',
# Only include detailed error information when in debug mode 'message': 'Internal server error'
if app.debug: }), 500
return jsonify({
'status': 'error',
'message': str(e),
'traceback': traceback.format_exc()
}), 500
else:
return jsonify({
'status': 'error',
'message': 'Internal server error'
}), 500
# Prime psutil CPU measurement once at startup so interval=None returns a real value # Prime psutil CPU measurement once at startup so interval=None returns a real value
try: try:
@@ -342,35 +330,25 @@ def not_found_error(error):
@app.errorhandler(500) @app.errorhandler(500)
def internal_error(error): def internal_error(error):
"""Handle 500 errors.""" """Handle 500 errors."""
import traceback
error_details = traceback.format_exc()
# Log the error
import logging import logging
logger = logging.getLogger('web_interface') logger = logging.getLogger('web_interface')
logger.error(f"Internal server error: {error}", exc_info=True) logger.error("Internal server error", exc_info=True)
# Return user-friendly error (hide internal details in production)
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'error_code': 'INTERNAL_ERROR', 'error_code': 'INTERNAL_ERROR',
'message': 'An internal error occurred', 'message': 'An internal error occurred; see logs for details',
'details': error_details if app.debug else None
}), 500 }), 500
@app.errorhandler(Exception) @app.errorhandler(Exception)
def handle_exception(error): def handle_exception(error):
"""Handle all unhandled exceptions.""" """Handle all unhandled exceptions."""
import traceback
import logging import logging
logger = logging.getLogger('web_interface') logger = logging.getLogger('web_interface')
logger.error(f"Unhandled exception: {error}", exc_info=True) logger.error("Unhandled exception", exc_info=True)
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'error_code': 'UNKNOWN_ERROR', 'error_code': 'UNKNOWN_ERROR',
'message': str(error) if app.debug else 'An error occurred', 'message': 'An error occurred; see logs for details',
'details': traceback.format_exc() if app.debug else None
}), 500 }), 500
# Captive portal redirect middleware # Captive portal redirect middleware
@@ -492,7 +470,8 @@ def system_status_generator():
} }
yield status yield status
except Exception as e: except Exception as e:
yield {'error': str(e)} app.logger.error("SSE generator error", exc_info=True)
yield {'error': 'An error occurred; see server logs'}
time.sleep(10) # Update every 10 seconds (reduced frequency for better performance) time.sleep(10) # Update every 10 seconds (reduced frequency for better performance)
# Display preview generator for SSE # Display preview generator for SSE
@@ -555,7 +534,8 @@ def display_preview_generator():
} }
except Exception as e: except Exception as e:
yield {'error': str(e)} app.logger.error("SSE generator error", exc_info=True)
yield {'error': 'An error occurred; see server logs'}
time.sleep(1.0) # Check once per second — halves PIL encode overhead vs 0.5s time.sleep(1.0) # Check once per second — halves PIL encode overhead vs 0.5s
@@ -598,17 +578,19 @@ def logs_generator():
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
# Timeout - just skip this update # Timeout - just skip this update
pass pass
except Exception as e: except Exception:
app.logger.error("Error running journalctl", exc_info=True)
error_data = { error_data = {
'timestamp': time.time(), 'timestamp': time.time(),
'logs': f'Error running journalctl: {str(e)}' 'logs': 'Error running journalctl; see server logs'
} }
yield error_data yield error_data
except Exception as e: except Exception:
app.logger.error("Unexpected error in logs generator", exc_info=True)
error_data = { error_data = {
'timestamp': time.time(), 'timestamp': time.time(),
'logs': f'Unexpected error in logs generator: {str(e)}' 'logs': 'Unexpected error in logs generator; see server logs'
} }
yield error_data yield error_data

View File

@@ -211,7 +211,8 @@ def get_main_config():
config = api_v3.config_manager.load_config() config = api_v3.config_manager.load_config()
return jsonify({'status': 'success', 'data': config}) return jsonify({'status': 'success', 'data': config})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/config/schedule', methods=['GET']) @api_v3.route('/config/schedule', methods=['GET'])
def get_schedule_config(): def get_schedule_config():
@@ -231,7 +232,7 @@ def get_schedule_config():
except Exception as e: except Exception as e:
return error_response( return error_response(
ErrorCode.CONFIG_LOAD_FAILED, ErrorCode.CONFIG_LOAD_FAILED,
f"Error loading schedule configuration: {str(e)}", "An error occurred; see logs for details",
status_code=500 status_code=500
) )
@@ -406,8 +407,8 @@ def save_schedule_config():
logging.error(error_msg) logging.error(error_msg)
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
f"Error saving schedule configuration: {str(e)}", "An error occurred; see logs for details",
details=traceback.format_exc(),
status_code=500 status_code=500
) )
@@ -455,14 +456,14 @@ def get_dim_schedule_config():
logging.error(f"[DIM SCHEDULE] Error reading config file: {e}", exc_info=True) logging.error(f"[DIM SCHEDULE] Error reading config file: {e}", exc_info=True)
return error_response( return error_response(
ErrorCode.CONFIG_LOAD_FAILED, ErrorCode.CONFIG_LOAD_FAILED,
f"Error reading configuration file: {str(e)}", "An error occurred; see logs for details",
status_code=500 status_code=500
) )
except Exception as e: except Exception as e:
logging.error(f"[DIM SCHEDULE] Unexpected error loading config: {e}", exc_info=True) logging.error(f"[DIM SCHEDULE] Unexpected error loading config: {e}", exc_info=True)
return error_response( return error_response(
ErrorCode.CONFIG_LOAD_FAILED, ErrorCode.CONFIG_LOAD_FAILED,
f"Unexpected error loading dim schedule configuration: {str(e)}", "An error occurred; see logs for details",
status_code=500 status_code=500
) )
@@ -627,8 +628,8 @@ def save_dim_schedule_config():
logging.error(error_msg) logging.error(error_msg)
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
f"Error saving dim schedule configuration: {str(e)}", "An error occurred; see logs for details",
details=traceback.format_exc(),
status_code=500 status_code=500
) )
@@ -1049,7 +1050,7 @@ def save_main_config():
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
f"Error saving configuration: {e}", f"Error saving configuration: {e}",
details=traceback.format_exc(),
status_code=500 status_code=500
) )
@@ -1063,7 +1064,8 @@ def get_secrets_config():
config = api_v3.config_manager.get_raw_file_content('secrets') config = api_v3.config_manager.get_raw_file_content('secrets')
return jsonify({'status': 'success', 'data': config}) return jsonify({'status': 'success', 'data': config})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/config/raw/main', methods=['POST']) @api_v3.route('/config/raw/main', methods=['POST'])
def save_raw_main_config(): def save_raw_main_config():
@@ -1082,7 +1084,8 @@ def save_raw_main_config():
return jsonify({'status': 'success', 'message': 'Main configuration saved successfully'}) return jsonify({'status': 'success', 'message': 'Main configuration saved successfully'})
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400 logger.error('Invalid JSON', exc_info=True)
return jsonify({'status': 'error', 'message': 'Invalid JSON in request body'}), 400
except Exception as e: except Exception as e:
import logging import logging
import traceback import traceback
@@ -1094,22 +1097,22 @@ def save_raw_main_config():
# Extract more specific error message if it's a ConfigError # Extract more specific error message if it's a ConfigError
if isinstance(e, ConfigError): if isinstance(e, ConfigError):
error_message = str(e) error_message = 'An error occurred; see logs for details'
if hasattr(e, 'config_path') and e.config_path: if hasattr(e, 'config_path') and e.config_path:
error_message = f"{error_message} (config_path: {e.config_path})" error_message = f"{error_message} (config_path: {e.config_path})"
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
error_message, error_message,
details=traceback.format_exc(),
context={'config_path': e.config_path} if hasattr(e, 'config_path') and e.config_path else None, context={'config_path': e.config_path} if hasattr(e, 'config_path') and e.config_path else None,
status_code=500 status_code=500
) )
else: else:
error_message = str(e) if str(e) else "An unexpected error occurred while saving the configuration" error_message = 'An error occurred; see logs for details'
return error_response( return error_response(
ErrorCode.UNKNOWN_ERROR, ErrorCode.UNKNOWN_ERROR,
error_message, error_message,
details=traceback.format_exc(),
status_code=500 status_code=500
) )
@@ -1133,7 +1136,8 @@ def save_raw_secrets_config():
return jsonify({'status': 'success', 'message': 'Secrets configuration saved successfully'}) return jsonify({'status': 'success', 'message': 'Secrets configuration saved successfully'})
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return jsonify({'status': 'error', 'message': f'Invalid JSON: {str(e)}'}), 400 logger.error('Invalid JSON', exc_info=True)
return jsonify({'status': 'error', 'message': 'Invalid JSON in request body'}), 400
except Exception as e: except Exception as e:
import logging import logging
import traceback import traceback
@@ -1146,11 +1150,11 @@ def save_raw_secrets_config():
# Extract more specific error message if it's a ConfigError # Extract more specific error message if it's a ConfigError
if isinstance(e, ConfigError): if isinstance(e, ConfigError):
# ConfigError has a message attribute and may have context # ConfigError has a message attribute and may have context
error_message = str(e) error_message = 'An error occurred; see logs for details'
if hasattr(e, 'config_path') and e.config_path: if hasattr(e, 'config_path') and e.config_path:
error_message = f"{error_message} (config_path: {e.config_path})" error_message = f"{error_message} (config_path: {e.config_path})"
else: else:
error_message = str(e) if str(e) else "An unexpected error occurred while saving the configuration" error_message = 'An error occurred; see logs for details'
return jsonify({'status': 'error', 'message': error_message}), 500 return jsonify({'status': 'error', 'message': error_message}), 500
@@ -1239,7 +1243,8 @@ def get_system_status():
return jsonify({'status': 'success', 'data': status}) return jsonify({'status': 'success', 'data': status})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/health', methods=['GET']) @api_v3.route('/health', methods=['GET'])
def get_health(): def get_health():
@@ -1283,7 +1288,7 @@ def get_health():
health_status['checks']['config_file'] = { health_status['checks']['config_file'] = {
'status': 'error', 'status': 'error',
'readable': False, 'readable': False,
'error': str(e) 'error': 'see logs for details'
} }
# Check plugin system # Check plugin system
@@ -1302,7 +1307,7 @@ def get_health():
except Exception as e: except Exception as e:
health_status['checks']['plugin_system'] = { health_status['checks']['plugin_system'] = {
'status': 'error', 'status': 'error',
'error': str(e) 'error': 'see logs for details'
} }
# Check hardware connectivity (if display manager available) # Check hardware connectivity (if display manager available)
@@ -1324,7 +1329,7 @@ def get_health():
except Exception as e: except Exception as e:
health_status['checks']['hardware'] = { health_status['checks']['hardware'] = {
'status': 'unknown', 'status': 'unknown',
'error': str(e) 'error': 'see logs for details'
} }
# Determine overall health # Determine overall health
@@ -1340,7 +1345,7 @@ def get_health():
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': str(e), 'message': 'An error occurred; see logs for details',
'data': {'status': 'unhealthy'} 'data': {'status': 'unhealthy'}
}), 500 }), 500
@@ -1658,7 +1663,8 @@ def get_display_current():
} }
return jsonify({'status': 'success', 'data': display_data}) return jsonify({'status': 'success', 'data': display_data})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/display/on-demand/status', methods=['GET']) @api_v3.route('/display/on-demand/status', methods=['GET'])
def get_on_demand_status(): def get_on_demand_status():
@@ -1683,9 +1689,8 @@ def get_on_demand_status():
except Exception as exc: except Exception as exc:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_on_demand_status: {exc}") logger.error('Error in get_on_demand_status', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(exc)}), 500
@api_v3.route('/display/on-demand/start', methods=['POST']) @api_v3.route('/display/on-demand/start', methods=['POST'])
def start_on_demand_display(): def start_on_demand_display():
@@ -1789,9 +1794,8 @@ def start_on_demand_display():
except Exception as exc: except Exception as exc:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in start_on_demand_display: {exc}") logger.error('Error in start_on_demand_display', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(exc)}), 500
@api_v3.route('/display/on-demand/stop', methods=['POST']) @api_v3.route('/display/on-demand/stop', methods=['POST'])
def stop_on_demand_display(): def stop_on_demand_display():
@@ -1828,9 +1832,8 @@ def stop_on_demand_display():
except Exception as exc: except Exception as exc:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in stop_on_demand_display: {exc}") logger.error('Error in stop_on_demand_display', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(exc)}), 500
@api_v3.route('/plugins/installed', methods=['GET']) @api_v3.route('/plugins/installed', methods=['GET'])
def get_installed_plugins(): def get_installed_plugins():
@@ -1960,9 +1963,8 @@ def get_installed_plugins():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_installed_plugins: {str(e)}") logger.error('Error in get_installed_plugins', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e), 'details': error_details}), 500
@api_v3.route('/plugins/health', methods=['GET']) @api_v3.route('/plugins/health', methods=['GET'])
def get_plugin_health(): def get_plugin_health():
@@ -1989,9 +1991,8 @@ def get_plugin_health():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_plugin_health: {str(e)}") logger.error('Error in get_plugin_health', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/health/<plugin_id>', methods=['GET']) @api_v3.route('/plugins/health/<plugin_id>', methods=['GET'])
def get_plugin_health_single(plugin_id): def get_plugin_health_single(plugin_id):
@@ -2017,9 +2018,8 @@ def get_plugin_health_single(plugin_id):
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_plugin_health_single: {str(e)}") logger.error('Error in get_plugin_health_single', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/health/<plugin_id>/reset', methods=['POST']) @api_v3.route('/plugins/health/<plugin_id>/reset', methods=['POST'])
def reset_plugin_health(plugin_id): def reset_plugin_health(plugin_id):
@@ -2045,9 +2045,8 @@ def reset_plugin_health(plugin_id):
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in reset_plugin_health: {str(e)}") logger.error('Error in reset_plugin_health', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/metrics', methods=['GET']) @api_v3.route('/plugins/metrics', methods=['GET'])
def get_plugin_metrics(): def get_plugin_metrics():
@@ -2074,9 +2073,8 @@ def get_plugin_metrics():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_plugin_metrics: {str(e)}") logger.error('Error in get_plugin_metrics', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/metrics/<plugin_id>', methods=['GET']) @api_v3.route('/plugins/metrics/<plugin_id>', methods=['GET'])
def get_plugin_metrics_single(plugin_id): def get_plugin_metrics_single(plugin_id):
@@ -2102,9 +2100,8 @@ def get_plugin_metrics_single(plugin_id):
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_plugin_metrics_single: {str(e)}") logger.error('Error in get_plugin_metrics_single', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/metrics/<plugin_id>/reset', methods=['POST']) @api_v3.route('/plugins/metrics/<plugin_id>/reset', methods=['POST'])
def reset_plugin_metrics(plugin_id): def reset_plugin_metrics(plugin_id):
@@ -2130,9 +2127,8 @@ def reset_plugin_metrics(plugin_id):
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in reset_plugin_metrics: {str(e)}") logger.error('Error in reset_plugin_metrics', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/limits/<plugin_id>', methods=['GET', 'POST']) @api_v3.route('/plugins/limits/<plugin_id>', methods=['GET', 'POST'])
def manage_plugin_limits(plugin_id): def manage_plugin_limits(plugin_id):
@@ -2188,9 +2184,8 @@ def manage_plugin_limits(plugin_id):
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in manage_plugin_limits: {str(e)}") logger.error('Error in manage_plugin_limits', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/toggle', methods=['POST']) @api_v3.route('/plugins/toggle', methods=['POST'])
def toggle_plugin(): def toggle_plugin():
@@ -2674,18 +2669,13 @@ def update_plugin():
# JSON request # JSON request
data, error = validate_request_json(['plugin_id']) data, error = validate_request_json(['plugin_id'])
if error: if error:
# Log what we received for debugging logger.debug("[UPDATE] JSON validation failed. Content-Type: %s", content_type)
print(f"[UPDATE] JSON validation failed. Content-Type: {content_type}")
print(f"[UPDATE] Request data: {request.data}")
print(f"[UPDATE] Request form: {request.form.to_dict()}")
return error return error
else: else:
# Form data or query string # Form data or query string
plugin_id = request.args.get('plugin_id') or request.form.get('plugin_id') plugin_id = request.args.get('plugin_id') or request.form.get('plugin_id')
if not plugin_id: if not plugin_id:
print(f"[UPDATE] Missing plugin_id. Content-Type: {content_type}") logger.debug("[UPDATE] Missing plugin_id. Content-Type: %s", content_type)
print(f"[UPDATE] Query args: {request.args.to_dict()}")
print(f"[UPDATE] Form data: {request.form.to_dict()}")
return error_response( return error_response(
ErrorCode.INVALID_INPUT, ErrorCode.INVALID_INPUT,
'plugin_id required', 'plugin_id required',
@@ -2859,7 +2849,7 @@ def update_plugin():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"[UPDATE] Exception in update_plugin endpoint: {str(e)}") logger.error("Unhandled exception in update endpoint", exc_info=True)
print(f"[UPDATE] Traceback: {error_details}") print(f"[UPDATE] Traceback: {error_details}")
from src.web_interface.errors import WebInterfaceError from src.web_interface.errors import WebInterfaceError
@@ -3158,9 +3148,8 @@ def install_plugin():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in install_plugin: {str(e)}") logger.error('Error in install_plugin', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/install-from-url', methods=['POST']) @api_v3.route('/plugins/install-from-url', methods=['POST'])
def install_plugin_from_url(): def install_plugin_from_url():
@@ -3216,9 +3205,8 @@ def install_plugin_from_url():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in install_plugin_from_url: {str(e)}") logger.error('Error in install_plugin_from_url', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/registry-from-url', methods=['POST']) @api_v3.route('/plugins/registry-from-url', methods=['POST'])
def get_registry_from_url(): def get_registry_from_url():
@@ -3251,9 +3239,8 @@ def get_registry_from_url():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_registry_from_url: {str(e)}") logger.error('Error in get_registry_from_url', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/saved-repositories', methods=['GET']) @api_v3.route('/plugins/saved-repositories', methods=['GET'])
def get_saved_repositories(): def get_saved_repositories():
@@ -3267,9 +3254,8 @@ def get_saved_repositories():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_saved_repositories: {str(e)}") logger.error('Error in get_saved_repositories', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/saved-repositories', methods=['POST']) @api_v3.route('/plugins/saved-repositories', methods=['POST'])
def add_saved_repository(): def add_saved_repository():
@@ -3301,9 +3287,8 @@ def add_saved_repository():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in add_saved_repository: {str(e)}") logger.error('Error in add_saved_repository', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/saved-repositories', methods=['DELETE']) @api_v3.route('/plugins/saved-repositories', methods=['DELETE'])
def remove_saved_repository(): def remove_saved_repository():
@@ -3334,9 +3319,8 @@ def remove_saved_repository():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in remove_saved_repository: {str(e)}") logger.error('Error in remove_saved_repository', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/store/list', methods=['GET']) @api_v3.route('/plugins/store/list', methods=['GET'])
def list_plugin_store(): def list_plugin_store():
@@ -3390,9 +3374,8 @@ def list_plugin_store():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in list_plugin_store: {str(e)}") logger.error('Error in list_plugin_store', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/store/github-status', methods=['GET']) @api_v3.route('/plugins/store/github-status', methods=['GET'])
def get_github_auth_status(): def get_github_auth_status():
@@ -3444,9 +3427,8 @@ def get_github_auth_status():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_github_auth_status: {str(e)}") logger.error('Error in get_github_auth_status', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/store/refresh', methods=['POST']) @api_v3.route('/plugins/store/refresh', methods=['POST'])
def refresh_plugin_store(): def refresh_plugin_store():
@@ -3474,9 +3456,8 @@ def refresh_plugin_store():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in refresh_plugin_store: {str(e)}") logger.error('Error in refresh_plugin_store', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
def deep_merge(base_dict, update_dict): def deep_merge(base_dict, update_dict):
""" """
@@ -4760,18 +4741,8 @@ def save_plugin_config():
if plugin_id not in current_config: if plugin_id not in current_config:
current_config[plugin_id] = {} current_config[plugin_id] = {}
# Debug logging for live_priority before merge
if plugin_id == 'football-scoreboard':
print(f"[DEBUG] Before merge - current NFL live_priority: {current_config[plugin_id].get('nfl', {}).get('live_priority')}")
print(f"[DEBUG] Before merge - regular_config NFL live_priority: {regular_config.get('nfl', {}).get('live_priority')}")
current_config[plugin_id] = deep_merge(current_config[plugin_id], regular_config) current_config[plugin_id] = deep_merge(current_config[plugin_id], regular_config)
# Debug logging for live_priority after merge
if plugin_id == 'football-scoreboard':
print(f"[DEBUG] After merge - NFL live_priority: {current_config[plugin_id].get('nfl', {}).get('live_priority')}")
print(f"[DEBUG] After merge - NCAA FB live_priority: {current_config[plugin_id].get('ncaa_fb', {}).get('live_priority')}")
# Deep merge plugin secrets in secrets config # Deep merge plugin secrets in secrets config
if secrets_config: if secrets_config:
if plugin_id not in current_secrets: if plugin_id not in current_secrets:
@@ -4807,11 +4778,11 @@ def save_plugin_config():
# Log the error but don't fail the entire config save # Log the error but don't fail the entire config save
import os import os
secrets_path = api_v3.config_manager.secrets_path secrets_path = api_v3.config_manager.secrets_path
logger.error(f"Error saving secrets config for {plugin_id}: {e}", exc_info=True) logger.error("Error saving secrets config for %s (path=%s)", plugin_id, secrets_path, exc_info=True)
# Return error response with more context # Return error response with more context
return error_response( return error_response(
ErrorCode.CONFIG_SAVE_FAILED, ErrorCode.CONFIG_SAVE_FAILED,
f"Failed to save secrets configuration: {str(e)} (config_path={secrets_path})", "Failed to save secrets configuration; see logs for details",
status_code=500 status_code=500
) )
@@ -4927,9 +4898,8 @@ def get_plugin_schema():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in get_plugin_schema: {str(e)}") logger.error('Error in get_plugin_schema', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/config/reset', methods=['POST']) @api_v3.route('/plugins/config/reset', methods=['POST'])
def reset_plugin_config(): def reset_plugin_config():
@@ -5041,9 +5011,8 @@ def reset_plugin_config():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in reset_plugin_config: {str(e)}") logger.error('Error in reset_plugin_config', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/action', methods=['POST']) @api_v3.route('/plugins/action', methods=['POST'])
def execute_plugin_action(): def execute_plugin_action():
@@ -5058,10 +5027,8 @@ def execute_plugin_action():
logger.error(f"Error parsing JSON in execute_plugin_action: {e}") logger.error(f"Error parsing JSON in execute_plugin_action: {e}")
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Invalid JSON in request: {str(e)}', 'message': 'Invalid JSON in request body',
'content_type': request.content_type, 'content_type': request.content_type }), 400
'data': request.data.decode('utf-8', errors='ignore')[:200]
}), 400
plugin_id = data.get('plugin_id') plugin_id = data.get('plugin_id')
action_id = data.get('action_id') action_id = data.get('action_id')
@@ -5305,10 +5272,9 @@ sys.exit(proc.returncode)
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error executing action step 1: {e}") print(f"Error executing action step 1: {e}")
print(error_details)
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error executing action: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
else: else:
# Simple script execution # Simple script execution
@@ -5359,9 +5325,8 @@ sys.exit(proc.returncode)
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in execute_plugin_action: {str(e)}") logger.error('Error in execute_plugin_action', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/authenticate/spotify', methods=['POST']) @api_v3.route('/plugins/authenticate/spotify', methods=['POST'])
def authenticate_spotify(): def authenticate_spotify():
@@ -5494,18 +5459,16 @@ sys.exit(proc.returncode)
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error getting Spotify auth URL: {e}") print(f"Error getting Spotify auth URL: {e}")
print(error_details)
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error generating authorization URL: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in authenticate_spotify: {str(e)}") logger.error('Error in authenticate_spotify', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/authenticate/ytm', methods=['POST']) @api_v3.route('/plugins/authenticate/ytm', methods=['POST'])
def authenticate_ytm(): def authenticate_ytm():
@@ -5556,9 +5519,8 @@ def authenticate_ytm():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in authenticate_ytm: {str(e)}") logger.error('Error in authenticate_ytm', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/fonts/catalog', methods=['GET']) @api_v3.route('/fonts/catalog', methods=['GET'])
def get_fonts_catalog(): def get_fonts_catalog():
@@ -5653,7 +5615,7 @@ def get_fonts_catalog():
return jsonify({'status': 'success', 'data': {'catalog': catalog}}) return jsonify({'status': 'success', 'data': {'catalog': catalog}})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/fonts/tokens', methods=['GET']) @api_v3.route('/fonts/tokens', methods=['GET'])
def get_font_tokens(): def get_font_tokens():
@@ -5671,7 +5633,8 @@ def get_font_tokens():
} }
return jsonify({'status': 'success', 'data': {'tokens': tokens}}) return jsonify({'status': 'success', 'data': {'tokens': tokens}})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/fonts/overrides', methods=['GET']) @api_v3.route('/fonts/overrides', methods=['GET'])
def get_fonts_overrides(): def get_fonts_overrides():
@@ -5682,7 +5645,8 @@ def get_fonts_overrides():
overrides = {} overrides = {}
return jsonify({'status': 'success', 'data': {'overrides': overrides}}) return jsonify({'status': 'success', 'data': {'overrides': overrides}})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/fonts/overrides', methods=['POST']) @api_v3.route('/fonts/overrides', methods=['POST'])
def save_fonts_overrides(): def save_fonts_overrides():
@@ -5695,7 +5659,8 @@ def save_fonts_overrides():
# This would integrate with the actual font system # This would integrate with the actual font system
return jsonify({'status': 'success', 'message': 'Font overrides saved'}) return jsonify({'status': 'success', 'message': 'Font overrides saved'})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/fonts/overrides/<element_key>', methods=['DELETE']) @api_v3.route('/fonts/overrides/<element_key>', methods=['DELETE'])
def delete_font_override(element_key): def delete_font_override(element_key):
@@ -5704,7 +5669,8 @@ def delete_font_override(element_key):
# This would integrate with the actual font system # This would integrate with the actual font system
return jsonify({'status': 'success', 'message': f'Font override for {element_key} deleted'}) return jsonify({'status': 'success', 'message': f'Font override for {element_key} deleted'})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/fonts/upload', methods=['POST']) @api_v3.route('/fonts/upload', methods=['POST'])
def upload_font(): def upload_font():
@@ -5768,7 +5734,8 @@ def upload_font():
'path': f'assets/fonts/{safe_filename}' 'path': f'assets/fonts/{safe_filename}'
}) })
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/fonts/preview', methods=['GET']) @api_v3.route('/fonts/preview', methods=['GET'])
@@ -5912,7 +5879,8 @@ def get_font_preview() -> tuple[Response, int] | Response:
} }
}) })
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/fonts/<font_family>', methods=['DELETE']) @api_v3.route('/fonts/<font_family>', methods=['DELETE'])
@@ -5999,7 +5967,8 @@ def delete_font(font_family: str) -> tuple[Response, int] | Response:
'message': f'Font {deleted_filename} deleted successfully' 'message': f'Font {deleted_filename} deleted successfully'
}) })
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/plugins/assets/upload', methods=['POST']) @api_v3.route('/plugins/assets/upload', methods=['POST'])
@@ -6147,7 +6116,8 @@ def upload_plugin_asset():
except Exception as e: except Exception as e:
import traceback import traceback
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/plugins/of-the-day/json/upload', methods=['POST']) @api_v3.route('/plugins/of-the-day/json/upload', methods=['POST'])
def upload_of_the_day_json(): def upload_of_the_day_json():
@@ -6206,7 +6176,7 @@ def upload_of_the_day_json():
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Invalid JSON in {file.filename}: {str(e)}' 'message': 'Invalid JSON in request body'
}), 400 }), 400
except UnicodeDecodeError: except UnicodeDecodeError:
return jsonify({ return jsonify({
@@ -6297,7 +6267,8 @@ def upload_of_the_day_json():
except Exception as e: except Exception as e:
import traceback import traceback
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/plugins/of-the-day/json/delete', methods=['POST']) @api_v3.route('/plugins/of-the-day/json/delete', methods=['POST'])
def delete_of_the_day_json(): def delete_of_the_day_json():
@@ -6344,7 +6315,8 @@ def delete_of_the_day_json():
except Exception as e: except Exception as e:
import traceback import traceback
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/plugins/<plugin_id>/static/<path:file_path>', methods=['GET']) @api_v3.route('/plugins/<plugin_id>/static/<path:file_path>', methods=['GET'])
def serve_plugin_static(plugin_id, file_path): def serve_plugin_static(plugin_id, file_path):
@@ -6390,7 +6362,8 @@ def serve_plugin_static(plugin_id, file_path):
except Exception as e: except Exception as e:
import traceback import traceback
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/plugins/calendar/upload-credentials', methods=['POST']) @api_v3.route('/plugins/calendar/upload-credentials', methods=['POST'])
@@ -6473,9 +6446,8 @@ def upload_calendar_credentials():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in upload_calendar_credentials: {str(e)}") logger.error('Error in upload_calendar_credentials', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/plugins/assets/delete', methods=['POST']) @api_v3.route('/plugins/assets/delete', methods=['POST'])
def delete_plugin_asset(): def delete_plugin_asset():
@@ -6518,7 +6490,8 @@ def delete_plugin_asset():
except Exception as e: except Exception as e:
import traceback import traceback
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/plugins/assets/list', methods=['GET']) @api_v3.route('/plugins/assets/list', methods=['GET'])
def list_plugin_assets(): def list_plugin_assets():
@@ -6546,7 +6519,8 @@ def list_plugin_assets():
except Exception as e: except Exception as e:
import traceback import traceback
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500 logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
@api_v3.route('/logs', methods=['GET']) @api_v3.route('/logs', methods=['GET'])
def get_logs(): def get_logs():
@@ -6582,7 +6556,7 @@ def get_logs():
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error fetching logs: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
# Multi-Display Sync Endpoints # Multi-Display Sync Endpoints
@@ -6649,7 +6623,7 @@ def get_wifi_status():
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error getting WiFi status: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
@api_v3.route('/wifi/scan', methods=['GET']) @api_v3.route('/wifi/scan', methods=['GET'])
@@ -6697,7 +6671,8 @@ def scan_wifi_networks():
return jsonify(response_data) return jsonify(response_data)
except Exception as e: except Exception as e:
error_message = f'Error scanning WiFi networks: {str(e)}' logger.error("Error scanning WiFi networks", exc_info=True)
error_message = 'An error occurred while scanning WiFi networks; see logs for details'
# Provide more specific error messages for common issues # Provide more specific error messages for common issues
error_str = str(e).lower() error_str = str(e).lower()
@@ -6773,7 +6748,7 @@ def connect_wifi():
logger.error(f"Error connecting to WiFi: {e}\n{traceback.format_exc()}") logger.error(f"Error connecting to WiFi: {e}\n{traceback.format_exc()}")
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error connecting to WiFi: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
@api_v3.route('/wifi/disconnect', methods=['POST']) @api_v3.route('/wifi/disconnect', methods=['POST'])
@@ -6802,7 +6777,7 @@ def disconnect_wifi():
logger.error(f"Error disconnecting from WiFi: {e}\n{traceback.format_exc()}") logger.error(f"Error disconnecting from WiFi: {e}\n{traceback.format_exc()}")
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error disconnecting from WiFi: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
@api_v3.route('/wifi/ap/enable', methods=['POST']) @api_v3.route('/wifi/ap/enable', methods=['POST'])
@@ -6827,7 +6802,7 @@ def enable_ap_mode():
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error enabling AP mode: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
@api_v3.route('/wifi/ap/disable', methods=['POST']) @api_v3.route('/wifi/ap/disable', methods=['POST'])
@@ -6852,7 +6827,7 @@ def disable_ap_mode():
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error disabling AP mode: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
@api_v3.route('/wifi/ap/auto-enable', methods=['GET']) @api_v3.route('/wifi/ap/auto-enable', methods=['GET'])
@@ -6873,7 +6848,7 @@ def get_auto_enable_ap_mode():
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error getting auto-enable setting: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
@api_v3.route('/wifi/ap/auto-enable', methods=['POST']) @api_v3.route('/wifi/ap/auto-enable', methods=['POST'])
@@ -6905,7 +6880,7 @@ def set_auto_enable_ap_mode():
except Exception as e: except Exception as e:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': f'Error setting auto-enable: {str(e)}' 'message': 'An error occurred; see logs for details'
}), 500 }), 500
@api_v3.route('/cache/list', methods=['GET']) @api_v3.route('/cache/list', methods=['GET'])
@@ -6931,9 +6906,8 @@ def list_cache_files():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in list_cache_files: {str(e)}") logger.error('Error in list_cache_files', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
@api_v3.route('/cache/delete', methods=['POST']) @api_v3.route('/cache/delete', methods=['POST'])
def delete_cache_file(): def delete_cache_file():
@@ -6960,9 +6934,8 @@ def delete_cache_file():
except Exception as e: except Exception as e:
import traceback import traceback
error_details = traceback.format_exc() error_details = traceback.format_exc()
print(f"Error in delete_cache_file: {str(e)}") logger.error('Error in delete_cache_file', exc_info=True)
print(error_details) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
return jsonify({'status': 'error', 'message': str(e)}), 500
# ============================================================================= # =============================================================================
@@ -6985,7 +6958,6 @@ def get_error_summary():
return error_response( return error_response(
error_code=ErrorCode.SYSTEM_ERROR, error_code=ErrorCode.SYSTEM_ERROR,
message="Failed to retrieve error summary", message="Failed to retrieve error summary",
details=str(e),
status_code=500 status_code=500
) )
@@ -7009,7 +6981,6 @@ def get_plugin_errors(plugin_id):
return error_response( return error_response(
error_code=ErrorCode.SYSTEM_ERROR, error_code=ErrorCode.SYSTEM_ERROR,
message=f"Failed to retrieve health for plugin {plugin_id}", message=f"Failed to retrieve health for plugin {plugin_id}",
details=str(e),
status_code=500 status_code=500
) )
@@ -7063,7 +7034,6 @@ def clear_old_errors():
return error_response( return error_response(
error_code=ErrorCode.SYSTEM_ERROR, error_code=ErrorCode.SYSTEM_ERROR,
message="Failed to clear old errors", message="Failed to clear old errors",
details=str(e),
status_code=500 status_code=500
) )

View File

@@ -84,10 +84,11 @@ def load_partial(partial_name):
elif partial_name == 'operation-history': elif partial_name == 'operation-history':
return _load_operation_history_partial() return _load_operation_history_partial()
else: else:
return f"Partial '{partial_name}' not found", 404 return f"Partial '{escape(partial_name)}' not found", 404
except Exception as e: except Exception as e:
return f"Error loading partial '{partial_name}': {str(e)}", 500 logger.error("Error loading partial %s", partial_name, exc_info=True)
return "Error loading partial", 500
@pages_v3.route('/partials/plugin-config/<plugin_id>') @pages_v3.route('/partials/plugin-config/<plugin_id>')
@@ -95,8 +96,9 @@ def load_plugin_config_partial(plugin_id):
"""Load plugin configuration partial via HTMX - server-side rendered form""" """Load plugin configuration partial via HTMX - server-side rendered form"""
try: try:
return _load_plugin_config_partial(plugin_id) return _load_plugin_config_partial(plugin_id)
except Exception as e: except Exception:
return f'<div class="text-red-500 p-4">Error loading plugin config: {escape(str(e))}</div>', 500 logger.error("Error loading plugin config partial for %s", plugin_id, exc_info=True)
return '<div class="text-red-500 p-4">Error loading plugin config; see logs for details</div>', 500
def _load_overview_partial(): def _load_overview_partial():
"""Load overview partial with system stats""" """Load overview partial with system stats"""
@@ -107,7 +109,8 @@ def _load_overview_partial():
return render_template('v3/partials/overview.html', return render_template('v3/partials/overview.html',
main_config=main_config) main_config=main_config)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_general_partial(): def _load_general_partial():
"""Load general settings partial""" """Load general settings partial"""
@@ -117,7 +120,8 @@ def _load_general_partial():
return render_template('v3/partials/general.html', return render_template('v3/partials/general.html',
main_config=main_config) main_config=main_config)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_display_partial(): def _load_display_partial():
"""Load display settings partial""" """Load display settings partial"""
@@ -127,7 +131,8 @@ def _load_display_partial():
return render_template('v3/partials/display.html', return render_template('v3/partials/display.html',
main_config=main_config) main_config=main_config)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_durations_partial(): def _load_durations_partial():
"""Load display durations partial""" """Load display durations partial"""
@@ -137,7 +142,8 @@ def _load_durations_partial():
return render_template('v3/partials/durations.html', return render_template('v3/partials/durations.html',
main_config=main_config) main_config=main_config)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_schedule_partial(): def _load_schedule_partial():
"""Load schedule settings partial""" """Load schedule settings partial"""
@@ -153,7 +159,8 @@ def _load_schedule_partial():
dim_schedule_config=dim_schedule_config, dim_schedule_config=dim_schedule_config,
normal_brightness=normal_brightness) normal_brightness=normal_brightness)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_weather_partial(): def _load_weather_partial():
@@ -164,7 +171,8 @@ def _load_weather_partial():
return render_template('v3/partials/weather.html', return render_template('v3/partials/weather.html',
main_config=main_config) main_config=main_config)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_stocks_partial(): def _load_stocks_partial():
"""Load stocks configuration partial""" """Load stocks configuration partial"""
@@ -174,7 +182,8 @@ def _load_stocks_partial():
return render_template('v3/partials/stocks.html', return render_template('v3/partials/stocks.html',
main_config=main_config) main_config=main_config)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_plugins_partial(): def _load_plugins_partial():
"""Load plugins management partial""" """Load plugins management partial"""
@@ -208,7 +217,7 @@ def _load_plugins_partial():
plugin_info.update(fresh_manifest) plugin_info.update(fresh_manifest)
except Exception as e: except Exception as e:
# If we can't read the fresh manifest, use the cached one # If we can't read the fresh manifest, use the cached one
print(f"Warning: Could not read fresh manifest for {plugin_id}: {e}") logger.warning("Could not read fresh manifest for {plugin_id}")
# Get enabled status from config (source of truth) # Get enabled status from config (source of truth)
# Read from config file first, fall back to plugin instance if config doesn't have the key # Read from config file first, fall back to plugin instance if config doesn't have the key
@@ -256,12 +265,13 @@ def _load_plugins_partial():
'branch': branch 'branch': branch
}) })
except Exception as e: except Exception as e:
print(f"Error loading plugin data: {e}") logger.error("Error loading plugin data", exc_info=True)
return render_template('v3/partials/plugins.html', return render_template('v3/partials/plugins.html',
plugins=plugins_data) plugins=plugins_data)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_fonts_partial(): def _load_fonts_partial():
"""Load fonts management partial""" """Load fonts management partial"""
@@ -271,14 +281,16 @@ def _load_fonts_partial():
return render_template('v3/partials/fonts.html', return render_template('v3/partials/fonts.html',
fonts=fonts_data) fonts=fonts_data)
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_logs_partial(): def _load_logs_partial():
"""Load logs viewer partial""" """Load logs viewer partial"""
try: try:
return render_template('v3/partials/logs.html') return render_template('v3/partials/logs.html')
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_raw_json_partial(): def _load_raw_json_partial():
"""Load raw JSON editor partial""" """Load raw JSON editor partial"""
@@ -295,14 +307,16 @@ def _load_raw_json_partial():
main_config_path=pages_v3.config_manager.get_config_path(), main_config_path=pages_v3.config_manager.get_config_path(),
secrets_config_path=pages_v3.config_manager.get_secrets_path()) secrets_config_path=pages_v3.config_manager.get_secrets_path())
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_backup_restore_partial(): def _load_backup_restore_partial():
"""Load backup & restore partial.""" """Load backup & restore partial."""
try: try:
return render_template('v3/partials/backup_restore.html') return render_template('v3/partials/backup_restore.html')
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
@pages_v3.route('/setup') @pages_v3.route('/setup')
def captive_setup(): def captive_setup():
@@ -314,21 +328,24 @@ def _load_wifi_partial():
try: try:
return render_template('v3/partials/wifi.html') return render_template('v3/partials/wifi.html')
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_cache_partial(): def _load_cache_partial():
"""Load cache management partial""" """Load cache management partial"""
try: try:
return render_template('v3/partials/cache.html') return render_template('v3/partials/cache.html')
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_operation_history_partial(): def _load_operation_history_partial():
"""Load operation history partial""" """Load operation history partial"""
try: try:
return render_template('v3/partials/operation_history.html') return render_template('v3/partials/operation_history.html')
except Exception as e: except Exception as e:
return f"Error: {str(e)}", 500 logger.error("Error loading partial", exc_info=True)
return "Error loading partial", 500
def _load_plugin_config_partial(plugin_id): def _load_plugin_config_partial(plugin_id):
@@ -336,6 +353,11 @@ def _load_plugin_config_partial(plugin_id):
Load plugin configuration partial - server-side rendered form. Load plugin configuration partial - server-side rendered form.
This replaces the client-side generateConfigForm() JavaScript. This replaces the client-side generateConfigForm() JavaScript.
""" """
import re as _re
# Reject plugin IDs containing path-traversal characters before any filesystem use
if not _re.match(r'^[a-zA-Z0-9_\-.:]+$', plugin_id or ''):
return '<div class="text-red-500 p-4">Invalid plugin ID</div>', 400
try: try:
if not pages_v3.plugin_manager: if not pages_v3.plugin_manager:
return '<div class="text-red-500 p-4">Plugin manager not available</div>', 500 return '<div class="text-red-500 p-4">Plugin manager not available</div>', 500
@@ -394,7 +416,7 @@ def _load_plugin_config_partial(plugin_id):
if new_images: if new_images:
config['images'] = config.get('images', []) + new_images config['images'] = config.get('images', []) + new_images
except Exception as e: except Exception as e:
print(f"Warning: Could not load metadata for {plugin_id}: {e}") logger.warning("Could not load metadata for {plugin_id}")
except Exception as e: # nosec B110 - metadata pre-load is optional; schema loads fully below except Exception as e: # nosec B110 - metadata pre-load is optional; schema loads fully below
logger.debug("Metadata pre-load skipped for plugin %s: %s", plugin_id, e) logger.debug("Metadata pre-load skipped for plugin %s: %s", plugin_id, e)
@@ -406,7 +428,7 @@ def _load_plugin_config_partial(plugin_id):
with open(schema_path, 'r', encoding='utf-8') as f: with open(schema_path, 'r', encoding='utf-8') as f:
schema = json.load(f) schema = json.load(f)
except Exception as e: except Exception as e:
print(f"Warning: Could not load schema for {plugin_id}: {e}") logger.warning("Could not load schema for {plugin_id}")
# Get web UI actions from plugin manifest # Get web UI actions from plugin manifest
web_ui_actions = [] web_ui_actions = []
@@ -417,7 +439,7 @@ def _load_plugin_config_partial(plugin_id):
manifest = json.load(f) manifest = json.load(f)
web_ui_actions = manifest.get('web_ui_actions', []) web_ui_actions = manifest.get('web_ui_actions', [])
except Exception as e: except Exception as e:
print(f"Warning: Could not load manifest for {plugin_id}: {e}") logger.warning("Could not load manifest for {plugin_id}")
# Mask secret fields before rendering template (fail closed — never leak secrets) # Mask secret fields before rendering template (fail closed — never leak secrets)
schema_properties = schema.get('properties') if isinstance(schema, dict) else None schema_properties = schema.get('properties') if isinstance(schema, dict) else None
@@ -453,9 +475,8 @@ def _load_plugin_config_partial(plugin_id):
) )
except Exception as e: except Exception as e:
import traceback logger.error("Error loading plugin config partial for %s", plugin_id, exc_info=True)
traceback.print_exc() return '<div class="text-red-500 p-4">Error loading plugin config; see logs for details</div>', 500
return f'<div class="text-red-500 p-4">Error loading plugin config: {escape(str(e))}</div>', 500
def _load_starlark_config_partial(app_id): def _load_starlark_config_partial(app_id):

View File

@@ -51,8 +51,10 @@
sanitizeValue(value) { sanitizeValue(value) {
// Base implementation - widgets should override for specific needs // Base implementation - widgets should override for specific needs
if (typeof value === 'string') { if (typeof value === 'string') {
// Basic XSS prevention // Strip all HTML tags via the DOM parser to prevent XSS
return value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); const div = document.createElement('div');
div.textContent = value;
return div.textContent;
} }
return value; return value;
} }

View File

@@ -1442,9 +1442,14 @@ function renderInstalledPlugins(plugins) {
return; return;
} }
// Helper function to escape attributes for use in HTML // Helper function to escape values for use in HTML attributes
const escapeAttr = (text) => { const escapeAttr = (text) => {
return (text || '').replace(/'/g, "\\'").replace(/"/g, '&quot;'); return (text || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}; };
// Helper function to escape for JavaScript strings (use JSON.stringify for proper escaping) // Helper function to escape for JavaScript strings (use JSON.stringify for proper escaping)
@@ -4507,6 +4512,8 @@ function syncFormToJson() {
// Deep merge with existing config to preserve nested structures // Deep merge with existing config to preserve nested structures
function deepMerge(target, source) { function deepMerge(target, source) {
for (const key in source) { for (const key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) { if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
target[key] = {}; target[key] = {};