mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-25 21:43:32 +00:00
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:
@@ -21,6 +21,8 @@ from pathlib import Path
|
||||
from typing import List, Dict, Optional, Any, Tuple
|
||||
import logging
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from src.common.permission_utils import sudo_remove_directory
|
||||
|
||||
try:
|
||||
@@ -356,7 +358,8 @@ class PluginStoreManager:
|
||||
# Extract owner/repo from URL
|
||||
try:
|
||||
# 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('/')
|
||||
if len(parts) >= 2:
|
||||
owner = parts[-2]
|
||||
@@ -518,9 +521,10 @@ class PluginStoreManager:
|
||||
# Try to find plugins.json in common locations
|
||||
# First try root directory
|
||||
registry_urls = []
|
||||
|
||||
|
||||
# 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('/')
|
||||
if len(parts) >= 2:
|
||||
owner = parts[-2]
|
||||
@@ -775,7 +779,8 @@ class PluginStoreManager:
|
||||
try:
|
||||
# Convert repo URL to raw content URL
|
||||
# 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
|
||||
repo_url = repo_url.rstrip('/')
|
||||
if repo_url.endswith('.git'):
|
||||
|
||||
@@ -204,24 +204,12 @@ def serve_plugin_asset(plugin_id, filename):
|
||||
# Use send_from_directory to serve the file
|
||||
return send_from_directory(str(assets_dir), filename, mimetype=content_type)
|
||||
|
||||
except Exception as e:
|
||||
# Log the exception with full traceback server-side
|
||||
import traceback
|
||||
except Exception:
|
||||
app.logger.exception('Error serving plugin asset file')
|
||||
|
||||
# Return generic error message to client (avoid leaking internal details)
|
||||
# Only include detailed error information when in debug mode
|
||||
if app.debug:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e),
|
||||
'traceback': traceback.format_exc()
|
||||
}), 500
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Internal server error'
|
||||
}), 500
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Internal server error'
|
||||
}), 500
|
||||
|
||||
# Prime psutil CPU measurement once at startup so interval=None returns a real value
|
||||
try:
|
||||
@@ -342,35 +330,25 @@ def not_found_error(error):
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
"""Handle 500 errors."""
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
|
||||
# Log the error
|
||||
import logging
|
||||
logger = logging.getLogger('web_interface')
|
||||
logger.error(f"Internal server error: {error}", exc_info=True)
|
||||
|
||||
# Return user-friendly error (hide internal details in production)
|
||||
logger.error("Internal server error", exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'error_code': 'INTERNAL_ERROR',
|
||||
'message': 'An internal error occurred',
|
||||
'details': error_details if app.debug else None
|
||||
'message': 'An internal error occurred; see logs for details',
|
||||
}), 500
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_exception(error):
|
||||
"""Handle all unhandled exceptions."""
|
||||
import traceback
|
||||
import logging
|
||||
logger = logging.getLogger('web_interface')
|
||||
logger.error(f"Unhandled exception: {error}", exc_info=True)
|
||||
|
||||
logger.error("Unhandled exception", exc_info=True)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'error_code': 'UNKNOWN_ERROR',
|
||||
'message': str(error) if app.debug else 'An error occurred',
|
||||
'details': traceback.format_exc() if app.debug else None
|
||||
'message': 'An error occurred; see logs for details',
|
||||
}), 500
|
||||
|
||||
# Captive portal redirect middleware
|
||||
@@ -492,7 +470,8 @@ def system_status_generator():
|
||||
}
|
||||
yield status
|
||||
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)
|
||||
|
||||
# Display preview generator for SSE
|
||||
@@ -555,7 +534,8 @@ def display_preview_generator():
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -598,17 +578,19 @@ def logs_generator():
|
||||
except subprocess.TimeoutExpired:
|
||||
# Timeout - just skip this update
|
||||
pass
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
app.logger.error("Error running journalctl", exc_info=True)
|
||||
error_data = {
|
||||
'timestamp': time.time(),
|
||||
'logs': f'Error running journalctl: {str(e)}'
|
||||
'logs': 'Error running journalctl; see server logs'
|
||||
}
|
||||
yield error_data
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
app.logger.error("Unexpected error in logs generator", exc_info=True)
|
||||
error_data = {
|
||||
'timestamp': time.time(),
|
||||
'logs': f'Unexpected error in logs generator: {str(e)}'
|
||||
'logs': 'Unexpected error in logs generator; see server logs'
|
||||
}
|
||||
yield error_data
|
||||
|
||||
|
||||
@@ -211,7 +211,8 @@ def get_main_config():
|
||||
config = api_v3.config_manager.load_config()
|
||||
return jsonify({'status': 'success', 'data': config})
|
||||
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'])
|
||||
def get_schedule_config():
|
||||
@@ -231,7 +232,7 @@ def get_schedule_config():
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_LOAD_FAILED,
|
||||
f"Error loading schedule configuration: {str(e)}",
|
||||
"An error occurred; see logs for details",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -406,8 +407,8 @@ def save_schedule_config():
|
||||
logging.error(error_msg)
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_SAVE_FAILED,
|
||||
f"Error saving schedule configuration: {str(e)}",
|
||||
details=traceback.format_exc(),
|
||||
"An error occurred; see logs for details",
|
||||
|
||||
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)
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_LOAD_FAILED,
|
||||
f"Error reading configuration file: {str(e)}",
|
||||
"An error occurred; see logs for details",
|
||||
status_code=500
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"[DIM SCHEDULE] Unexpected error loading config: {e}", exc_info=True)
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_LOAD_FAILED,
|
||||
f"Unexpected error loading dim schedule configuration: {str(e)}",
|
||||
"An error occurred; see logs for details",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -627,8 +628,8 @@ def save_dim_schedule_config():
|
||||
logging.error(error_msg)
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_SAVE_FAILED,
|
||||
f"Error saving dim schedule configuration: {str(e)}",
|
||||
details=traceback.format_exc(),
|
||||
"An error occurred; see logs for details",
|
||||
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -1049,7 +1050,7 @@ def save_main_config():
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_SAVE_FAILED,
|
||||
f"Error saving configuration: {e}",
|
||||
details=traceback.format_exc(),
|
||||
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -1063,7 +1064,8 @@ def get_secrets_config():
|
||||
config = api_v3.config_manager.get_raw_file_content('secrets')
|
||||
return jsonify({'status': 'success', 'data': config})
|
||||
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'])
|
||||
def save_raw_main_config():
|
||||
@@ -1082,7 +1084,8 @@ def save_raw_main_config():
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Main configuration saved successfully'})
|
||||
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:
|
||||
import logging
|
||||
import traceback
|
||||
@@ -1094,22 +1097,22 @@ def save_raw_main_config():
|
||||
|
||||
# Extract more specific error message if it's a 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:
|
||||
error_message = f"{error_message} (config_path: {e.config_path})"
|
||||
return error_response(
|
||||
ErrorCode.CONFIG_SAVE_FAILED,
|
||||
error_message,
|
||||
details=traceback.format_exc(),
|
||||
|
||||
context={'config_path': e.config_path} if hasattr(e, 'config_path') and e.config_path else None,
|
||||
status_code=500
|
||||
)
|
||||
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(
|
||||
ErrorCode.UNKNOWN_ERROR,
|
||||
error_message,
|
||||
details=traceback.format_exc(),
|
||||
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -1133,7 +1136,8 @@ def save_raw_secrets_config():
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Secrets configuration saved successfully'})
|
||||
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:
|
||||
import logging
|
||||
import traceback
|
||||
@@ -1146,11 +1150,11 @@ def save_raw_secrets_config():
|
||||
# Extract more specific error message if it's a ConfigError
|
||||
if isinstance(e, ConfigError):
|
||||
# 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:
|
||||
error_message = f"{error_message} (config_path: {e.config_path})"
|
||||
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
|
||||
|
||||
@@ -1239,7 +1243,8 @@ def get_system_status():
|
||||
|
||||
return jsonify({'status': 'success', 'data': status})
|
||||
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'])
|
||||
def get_health():
|
||||
@@ -1283,7 +1288,7 @@ def get_health():
|
||||
health_status['checks']['config_file'] = {
|
||||
'status': 'error',
|
||||
'readable': False,
|
||||
'error': str(e)
|
||||
'error': 'see logs for details'
|
||||
}
|
||||
|
||||
# Check plugin system
|
||||
@@ -1302,7 +1307,7 @@ def get_health():
|
||||
except Exception as e:
|
||||
health_status['checks']['plugin_system'] = {
|
||||
'status': 'error',
|
||||
'error': str(e)
|
||||
'error': 'see logs for details'
|
||||
}
|
||||
|
||||
# Check hardware connectivity (if display manager available)
|
||||
@@ -1324,7 +1329,7 @@ def get_health():
|
||||
except Exception as e:
|
||||
health_status['checks']['hardware'] = {
|
||||
'status': 'unknown',
|
||||
'error': str(e)
|
||||
'error': 'see logs for details'
|
||||
}
|
||||
|
||||
# Determine overall health
|
||||
@@ -1340,7 +1345,7 @@ def get_health():
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e),
|
||||
'message': 'An error occurred; see logs for details',
|
||||
'data': {'status': 'unhealthy'}
|
||||
}), 500
|
||||
|
||||
@@ -1658,7 +1663,8 @@ def get_display_current():
|
||||
}
|
||||
return jsonify({'status': 'success', 'data': display_data})
|
||||
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'])
|
||||
def get_on_demand_status():
|
||||
@@ -1683,9 +1689,8 @@ def get_on_demand_status():
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_on_demand_status: {exc}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(exc)}), 500
|
||||
logger.error('Error in get_on_demand_status', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/display/on-demand/start', methods=['POST'])
|
||||
def start_on_demand_display():
|
||||
@@ -1789,9 +1794,8 @@ def start_on_demand_display():
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in start_on_demand_display: {exc}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(exc)}), 500
|
||||
logger.error('Error in start_on_demand_display', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/display/on-demand/stop', methods=['POST'])
|
||||
def stop_on_demand_display():
|
||||
@@ -1828,9 +1832,8 @@ def stop_on_demand_display():
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in stop_on_demand_display: {exc}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(exc)}), 500
|
||||
logger.error('Error in stop_on_demand_display', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/installed', methods=['GET'])
|
||||
def get_installed_plugins():
|
||||
@@ -1960,9 +1963,8 @@ def get_installed_plugins():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_installed_plugins: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e), 'details': error_details}), 500
|
||||
logger.error('Error in get_installed_plugins', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/health', methods=['GET'])
|
||||
def get_plugin_health():
|
||||
@@ -1989,9 +1991,8 @@ def get_plugin_health():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_plugin_health: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_plugin_health', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/health/<plugin_id>', methods=['GET'])
|
||||
def get_plugin_health_single(plugin_id):
|
||||
@@ -2017,9 +2018,8 @@ def get_plugin_health_single(plugin_id):
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_plugin_health_single: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_plugin_health_single', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/health/<plugin_id>/reset', methods=['POST'])
|
||||
def reset_plugin_health(plugin_id):
|
||||
@@ -2045,9 +2045,8 @@ def reset_plugin_health(plugin_id):
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in reset_plugin_health: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in reset_plugin_health', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/metrics', methods=['GET'])
|
||||
def get_plugin_metrics():
|
||||
@@ -2074,9 +2073,8 @@ def get_plugin_metrics():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_plugin_metrics: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_plugin_metrics', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/metrics/<plugin_id>', methods=['GET'])
|
||||
def get_plugin_metrics_single(plugin_id):
|
||||
@@ -2102,9 +2100,8 @@ def get_plugin_metrics_single(plugin_id):
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_plugin_metrics_single: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_plugin_metrics_single', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/metrics/<plugin_id>/reset', methods=['POST'])
|
||||
def reset_plugin_metrics(plugin_id):
|
||||
@@ -2130,9 +2127,8 @@ def reset_plugin_metrics(plugin_id):
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in reset_plugin_metrics: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in reset_plugin_metrics', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/limits/<plugin_id>', methods=['GET', 'POST'])
|
||||
def manage_plugin_limits(plugin_id):
|
||||
@@ -2188,9 +2184,8 @@ def manage_plugin_limits(plugin_id):
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in manage_plugin_limits: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in manage_plugin_limits', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/toggle', methods=['POST'])
|
||||
def toggle_plugin():
|
||||
@@ -2674,18 +2669,13 @@ def update_plugin():
|
||||
# JSON request
|
||||
data, error = validate_request_json(['plugin_id'])
|
||||
if error:
|
||||
# Log what we received for debugging
|
||||
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()}")
|
||||
logger.debug("[UPDATE] JSON validation failed. Content-Type: %s", content_type)
|
||||
return error
|
||||
else:
|
||||
# Form data or query string
|
||||
plugin_id = request.args.get('plugin_id') or request.form.get('plugin_id')
|
||||
if not plugin_id:
|
||||
print(f"[UPDATE] Missing plugin_id. Content-Type: {content_type}")
|
||||
print(f"[UPDATE] Query args: {request.args.to_dict()}")
|
||||
print(f"[UPDATE] Form data: {request.form.to_dict()}")
|
||||
logger.debug("[UPDATE] Missing plugin_id. Content-Type: %s", content_type)
|
||||
return error_response(
|
||||
ErrorCode.INVALID_INPUT,
|
||||
'plugin_id required',
|
||||
@@ -2859,7 +2849,7 @@ def update_plugin():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
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}")
|
||||
|
||||
from src.web_interface.errors import WebInterfaceError
|
||||
@@ -3158,9 +3148,8 @@ def install_plugin():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in install_plugin: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in install_plugin', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/install-from-url', methods=['POST'])
|
||||
def install_plugin_from_url():
|
||||
@@ -3216,9 +3205,8 @@ def install_plugin_from_url():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in install_plugin_from_url: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in install_plugin_from_url', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/registry-from-url', methods=['POST'])
|
||||
def get_registry_from_url():
|
||||
@@ -3251,9 +3239,8 @@ def get_registry_from_url():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_registry_from_url: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_registry_from_url', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/saved-repositories', methods=['GET'])
|
||||
def get_saved_repositories():
|
||||
@@ -3267,9 +3254,8 @@ def get_saved_repositories():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_saved_repositories: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_saved_repositories', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/saved-repositories', methods=['POST'])
|
||||
def add_saved_repository():
|
||||
@@ -3301,9 +3287,8 @@ def add_saved_repository():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in add_saved_repository: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in add_saved_repository', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/saved-repositories', methods=['DELETE'])
|
||||
def remove_saved_repository():
|
||||
@@ -3334,9 +3319,8 @@ def remove_saved_repository():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in remove_saved_repository: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in remove_saved_repository', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/store/list', methods=['GET'])
|
||||
def list_plugin_store():
|
||||
@@ -3390,9 +3374,8 @@ def list_plugin_store():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in list_plugin_store: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in list_plugin_store', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/store/github-status', methods=['GET'])
|
||||
def get_github_auth_status():
|
||||
@@ -3444,9 +3427,8 @@ def get_github_auth_status():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_github_auth_status: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_github_auth_status', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/store/refresh', methods=['POST'])
|
||||
def refresh_plugin_store():
|
||||
@@ -3474,9 +3456,8 @@ def refresh_plugin_store():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in refresh_plugin_store: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in refresh_plugin_store', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
def deep_merge(base_dict, update_dict):
|
||||
"""
|
||||
@@ -4760,18 +4741,8 @@ def save_plugin_config():
|
||||
if plugin_id not in current_config:
|
||||
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)
|
||||
|
||||
# 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
|
||||
if secrets_config:
|
||||
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
|
||||
import os
|
||||
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(
|
||||
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
|
||||
)
|
||||
|
||||
@@ -4927,9 +4898,8 @@ def get_plugin_schema():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in get_plugin_schema: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in get_plugin_schema', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/config/reset', methods=['POST'])
|
||||
def reset_plugin_config():
|
||||
@@ -5041,9 +5011,8 @@ def reset_plugin_config():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in reset_plugin_config: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in reset_plugin_config', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/action', methods=['POST'])
|
||||
def execute_plugin_action():
|
||||
@@ -5058,10 +5027,8 @@ def execute_plugin_action():
|
||||
logger.error(f"Error parsing JSON in execute_plugin_action: {e}")
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid JSON in request: {str(e)}',
|
||||
'content_type': request.content_type,
|
||||
'data': request.data.decode('utf-8', errors='ignore')[:200]
|
||||
}), 400
|
||||
'message': 'Invalid JSON in request body',
|
||||
'content_type': request.content_type }), 400
|
||||
|
||||
plugin_id = data.get('plugin_id')
|
||||
action_id = data.get('action_id')
|
||||
@@ -5305,10 +5272,9 @@ sys.exit(proc.returncode)
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error executing action step 1: {e}")
|
||||
print(error_details)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error executing action: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
else:
|
||||
# Simple script execution
|
||||
@@ -5359,9 +5325,8 @@ sys.exit(proc.returncode)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in execute_plugin_action: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in execute_plugin_action', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/authenticate/spotify', methods=['POST'])
|
||||
def authenticate_spotify():
|
||||
@@ -5494,18 +5459,16 @@ sys.exit(proc.returncode)
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error getting Spotify auth URL: {e}")
|
||||
print(error_details)
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error generating authorization URL: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in authenticate_spotify: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in authenticate_spotify', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/authenticate/ytm', methods=['POST'])
|
||||
def authenticate_ytm():
|
||||
@@ -5556,9 +5519,8 @@ def authenticate_ytm():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in authenticate_ytm: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in authenticate_ytm', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/fonts/catalog', methods=['GET'])
|
||||
def get_fonts_catalog():
|
||||
@@ -5653,7 +5615,7 @@ def get_fonts_catalog():
|
||||
|
||||
return jsonify({'status': 'success', 'data': {'catalog': catalog}})
|
||||
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'])
|
||||
def get_font_tokens():
|
||||
@@ -5671,7 +5633,8 @@ def get_font_tokens():
|
||||
}
|
||||
return jsonify({'status': 'success', 'data': {'tokens': tokens}})
|
||||
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'])
|
||||
def get_fonts_overrides():
|
||||
@@ -5682,7 +5645,8 @@ def get_fonts_overrides():
|
||||
overrides = {}
|
||||
return jsonify({'status': 'success', 'data': {'overrides': overrides}})
|
||||
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'])
|
||||
def save_fonts_overrides():
|
||||
@@ -5695,7 +5659,8 @@ def save_fonts_overrides():
|
||||
# This would integrate with the actual font system
|
||||
return jsonify({'status': 'success', 'message': 'Font overrides saved'})
|
||||
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'])
|
||||
def delete_font_override(element_key):
|
||||
@@ -5704,7 +5669,8 @@ def delete_font_override(element_key):
|
||||
# This would integrate with the actual font system
|
||||
return jsonify({'status': 'success', 'message': f'Font override for {element_key} deleted'})
|
||||
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'])
|
||||
def upload_font():
|
||||
@@ -5768,7 +5734,8 @@ def upload_font():
|
||||
'path': f'assets/fonts/{safe_filename}'
|
||||
})
|
||||
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'])
|
||||
@@ -5912,7 +5879,8 @@ def get_font_preview() -> tuple[Response, int] | Response:
|
||||
}
|
||||
})
|
||||
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'])
|
||||
@@ -5999,7 +5967,8 @@ def delete_font(font_family: str) -> tuple[Response, int] | Response:
|
||||
'message': f'Font {deleted_filename} deleted successfully'
|
||||
})
|
||||
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'])
|
||||
@@ -6147,7 +6116,8 @@ def upload_plugin_asset():
|
||||
|
||||
except Exception as e:
|
||||
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'])
|
||||
def upload_of_the_day_json():
|
||||
@@ -6206,7 +6176,7 @@ def upload_of_the_day_json():
|
||||
except json.JSONDecodeError as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid JSON in {file.filename}: {str(e)}'
|
||||
'message': 'Invalid JSON in request body'
|
||||
}), 400
|
||||
except UnicodeDecodeError:
|
||||
return jsonify({
|
||||
@@ -6297,7 +6267,8 @@ def upload_of_the_day_json():
|
||||
|
||||
except Exception as e:
|
||||
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'])
|
||||
def delete_of_the_day_json():
|
||||
@@ -6344,7 +6315,8 @@ def delete_of_the_day_json():
|
||||
|
||||
except Exception as e:
|
||||
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'])
|
||||
def serve_plugin_static(plugin_id, file_path):
|
||||
@@ -6390,7 +6362,8 @@ def serve_plugin_static(plugin_id, file_path):
|
||||
|
||||
except Exception as e:
|
||||
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'])
|
||||
@@ -6473,9 +6446,8 @@ def upload_calendar_credentials():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in upload_calendar_credentials: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in upload_calendar_credentials', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/plugins/assets/delete', methods=['POST'])
|
||||
def delete_plugin_asset():
|
||||
@@ -6518,7 +6490,8 @@ def delete_plugin_asset():
|
||||
|
||||
except Exception as e:
|
||||
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'])
|
||||
def list_plugin_assets():
|
||||
@@ -6546,7 +6519,8 @@ def list_plugin_assets():
|
||||
|
||||
except Exception as e:
|
||||
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'])
|
||||
def get_logs():
|
||||
@@ -6582,7 +6556,7 @@ def get_logs():
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error fetching logs: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
# Multi-Display Sync Endpoints
|
||||
@@ -6649,7 +6623,7 @@ def get_wifi_status():
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error getting WiFi status: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/scan', methods=['GET'])
|
||||
@@ -6697,7 +6671,8 @@ def scan_wifi_networks():
|
||||
|
||||
return jsonify(response_data)
|
||||
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
|
||||
error_str = str(e).lower()
|
||||
@@ -6773,7 +6748,7 @@ def connect_wifi():
|
||||
logger.error(f"Error connecting to WiFi: {e}\n{traceback.format_exc()}")
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error connecting to WiFi: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
@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()}")
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error disconnecting from WiFi: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/ap/enable', methods=['POST'])
|
||||
@@ -6827,7 +6802,7 @@ def enable_ap_mode():
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error enabling AP mode: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/ap/disable', methods=['POST'])
|
||||
@@ -6852,7 +6827,7 @@ def disable_ap_mode():
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error disabling AP mode: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/ap/auto-enable', methods=['GET'])
|
||||
@@ -6873,7 +6848,7 @@ def get_auto_enable_ap_mode():
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error getting auto-enable setting: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/wifi/ap/auto-enable', methods=['POST'])
|
||||
@@ -6905,7 +6880,7 @@ def set_auto_enable_ap_mode():
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error setting auto-enable: {str(e)}'
|
||||
'message': 'An error occurred; see logs for details'
|
||||
}), 500
|
||||
|
||||
@api_v3.route('/cache/list', methods=['GET'])
|
||||
@@ -6931,9 +6906,8 @@ def list_cache_files():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in list_cache_files: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in list_cache_files', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
@api_v3.route('/cache/delete', methods=['POST'])
|
||||
def delete_cache_file():
|
||||
@@ -6960,9 +6934,8 @@ def delete_cache_file():
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"Error in delete_cache_file: {str(e)}")
|
||||
print(error_details)
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
logger.error('Error in delete_cache_file', exc_info=True)
|
||||
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -6985,7 +6958,6 @@ def get_error_summary():
|
||||
return error_response(
|
||||
error_code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Failed to retrieve error summary",
|
||||
details=str(e),
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -7009,7 +6981,6 @@ def get_plugin_errors(plugin_id):
|
||||
return error_response(
|
||||
error_code=ErrorCode.SYSTEM_ERROR,
|
||||
message=f"Failed to retrieve health for plugin {plugin_id}",
|
||||
details=str(e),
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@@ -7063,7 +7034,6 @@ def clear_old_errors():
|
||||
return error_response(
|
||||
error_code=ErrorCode.SYSTEM_ERROR,
|
||||
message="Failed to clear old errors",
|
||||
details=str(e),
|
||||
status_code=500
|
||||
)
|
||||
|
||||
|
||||
@@ -84,10 +84,11 @@ def load_partial(partial_name):
|
||||
elif partial_name == 'operation-history':
|
||||
return _load_operation_history_partial()
|
||||
else:
|
||||
return f"Partial '{partial_name}' not found", 404
|
||||
return f"Partial '{escape(partial_name)}' not found", 404
|
||||
|
||||
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>')
|
||||
@@ -95,8 +96,9 @@ def load_plugin_config_partial(plugin_id):
|
||||
"""Load plugin configuration partial via HTMX - server-side rendered form"""
|
||||
try:
|
||||
return _load_plugin_config_partial(plugin_id)
|
||||
except Exception as e:
|
||||
return f'<div class="text-red-500 p-4">Error loading plugin config: {escape(str(e))}</div>', 500
|
||||
except Exception:
|
||||
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():
|
||||
"""Load overview partial with system stats"""
|
||||
@@ -107,7 +109,8 @@ def _load_overview_partial():
|
||||
return render_template('v3/partials/overview.html',
|
||||
main_config=main_config)
|
||||
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():
|
||||
"""Load general settings partial"""
|
||||
@@ -117,7 +120,8 @@ def _load_general_partial():
|
||||
return render_template('v3/partials/general.html',
|
||||
main_config=main_config)
|
||||
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():
|
||||
"""Load display settings partial"""
|
||||
@@ -127,7 +131,8 @@ def _load_display_partial():
|
||||
return render_template('v3/partials/display.html',
|
||||
main_config=main_config)
|
||||
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():
|
||||
"""Load display durations partial"""
|
||||
@@ -137,7 +142,8 @@ def _load_durations_partial():
|
||||
return render_template('v3/partials/durations.html',
|
||||
main_config=main_config)
|
||||
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():
|
||||
"""Load schedule settings partial"""
|
||||
@@ -153,7 +159,8 @@ def _load_schedule_partial():
|
||||
dim_schedule_config=dim_schedule_config,
|
||||
normal_brightness=normal_brightness)
|
||||
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():
|
||||
@@ -164,7 +171,8 @@ def _load_weather_partial():
|
||||
return render_template('v3/partials/weather.html',
|
||||
main_config=main_config)
|
||||
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():
|
||||
"""Load stocks configuration partial"""
|
||||
@@ -174,7 +182,8 @@ def _load_stocks_partial():
|
||||
return render_template('v3/partials/stocks.html',
|
||||
main_config=main_config)
|
||||
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():
|
||||
"""Load plugins management partial"""
|
||||
@@ -208,7 +217,7 @@ def _load_plugins_partial():
|
||||
plugin_info.update(fresh_manifest)
|
||||
except Exception as e:
|
||||
# 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)
|
||||
# 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
|
||||
})
|
||||
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',
|
||||
plugins=plugins_data)
|
||||
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():
|
||||
"""Load fonts management partial"""
|
||||
@@ -271,14 +281,16 @@ def _load_fonts_partial():
|
||||
return render_template('v3/partials/fonts.html',
|
||||
fonts=fonts_data)
|
||||
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():
|
||||
"""Load logs viewer partial"""
|
||||
try:
|
||||
return render_template('v3/partials/logs.html')
|
||||
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():
|
||||
"""Load raw JSON editor partial"""
|
||||
@@ -295,14 +307,16 @@ def _load_raw_json_partial():
|
||||
main_config_path=pages_v3.config_manager.get_config_path(),
|
||||
secrets_config_path=pages_v3.config_manager.get_secrets_path())
|
||||
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():
|
||||
"""Load backup & restore partial."""
|
||||
try:
|
||||
return render_template('v3/partials/backup_restore.html')
|
||||
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')
|
||||
def captive_setup():
|
||||
@@ -314,21 +328,24 @@ def _load_wifi_partial():
|
||||
try:
|
||||
return render_template('v3/partials/wifi.html')
|
||||
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():
|
||||
"""Load cache management partial"""
|
||||
try:
|
||||
return render_template('v3/partials/cache.html')
|
||||
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():
|
||||
"""Load operation history partial"""
|
||||
try:
|
||||
return render_template('v3/partials/operation_history.html')
|
||||
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):
|
||||
@@ -336,6 +353,11 @@ def _load_plugin_config_partial(plugin_id):
|
||||
Load plugin configuration partial - server-side rendered form.
|
||||
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:
|
||||
if not pages_v3.plugin_manager:
|
||||
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:
|
||||
config['images'] = config.get('images', []) + new_images
|
||||
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
|
||||
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:
|
||||
schema = json.load(f)
|
||||
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
|
||||
web_ui_actions = []
|
||||
@@ -417,7 +439,7 @@ def _load_plugin_config_partial(plugin_id):
|
||||
manifest = json.load(f)
|
||||
web_ui_actions = manifest.get('web_ui_actions', [])
|
||||
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)
|
||||
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:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
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_starlark_config_partial(app_id):
|
||||
|
||||
@@ -51,8 +51,10 @@
|
||||
sanitizeValue(value) {
|
||||
// Base implementation - widgets should override for specific needs
|
||||
if (typeof value === 'string') {
|
||||
// Basic XSS prevention
|
||||
return value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||||
// Strip all HTML tags via the DOM parser to prevent XSS
|
||||
const div = document.createElement('div');
|
||||
div.textContent = value;
|
||||
return div.textContent;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1442,9 +1442,14 @@ function renderInstalledPlugins(plugins) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper function to escape attributes for use in HTML
|
||||
// Helper function to escape values for use in HTML attributes
|
||||
const escapeAttr = (text) => {
|
||||
return (text || '').replace(/'/g, "\\'").replace(/"/g, '"');
|
||||
return (text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
};
|
||||
|
||||
// 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
|
||||
function deepMerge(target, 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 (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
|
||||
target[key] = {};
|
||||
|
||||
Reference in New Issue
Block a user