diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index d547fbf0..5a774056 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -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'): diff --git a/web_interface/app.py b/web_interface/app.py index c87a7f21..1bccd49c 100644 --- a/web_interface/app.py +++ b/web_interface/app.py @@ -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 diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 3e11050b..2222aa84 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -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/', 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//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/', 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//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/', 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/', 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/', 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//static/', 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 ) diff --git a/web_interface/blueprints/pages_v3.py b/web_interface/blueprints/pages_v3.py index 1215b241..bc3a1e81 100644 --- a/web_interface/blueprints/pages_v3.py +++ b/web_interface/blueprints/pages_v3.py @@ -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/') @@ -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'
Error loading plugin config: {escape(str(e))}
', 500 + except Exception: + logger.error("Error loading plugin config partial for %s", plugin_id, exc_info=True) + return '
Error loading plugin config; see logs for details
', 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 '
Invalid plugin ID
', 400 + try: if not pages_v3.plugin_manager: return '
Plugin manager not available
', 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'
Error loading plugin config: {escape(str(e))}
', 500 + logger.error("Error loading plugin config partial for %s", plugin_id, exc_info=True) + return '
Error loading plugin config; see logs for details
', 500 def _load_starlark_config_partial(app_id): diff --git a/web_interface/static/v3/js/widgets/base-widget.js b/web_interface/static/v3/js/widgets/base-widget.js index 18ec5d40..084c8555 100644 --- a/web_interface/static/v3/js/widgets/base-widget.js +++ b/web_interface/static/v3/js/widgets/base-widget.js @@ -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>/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; } diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index 65bab8ca..c34c45fc 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -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, '>'); }; // 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] = {};