mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-25 13:43:31 +00:00
* fix(web-ui): fix quick actions not firing, add toast feedback, suppress install handler warning - base.html: add htmx:afterSettle listener to set data-loaded on tab containers after HTMX swaps their content, preventing the overview partial from being re-fetched (and handlers lost) on every tab switch - base.html: call htmx.process() in loadOverviewDirect/loadPluginsDirect fallbacks so buttons get HTMX handlers even if HTMX finished its initial body scan before the fallback fetch completed - overview.html + index.html (11 buttons): replace event.detail.xhr.responseJSON (undefined in HTMX 1.9.x) with JSON.parse(event.detail.xhr.responseText) so quick action toast notifications actually fire - plugins_manager.js: add guarded htmx:afterSettle listener that only calls attachInstallButtonHandler when #install-plugin-from-url is in the DOM, eliminating the spurious console warning on non-plugin tab loads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web-ui): ensure quick-action toasts always fire even on xhr/parse failure Replace silent catch(e){} in all 11 hx-on:htmx:after-request handlers with a pattern that sets default message/status before the try block and calls showNotification(m,s) unconditionally after it, so a fallback toast is shown whenever xhr is absent or responseText is not valid JSON. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web-ui): show error toast on non-JSON 4xx/5xx quick-action responses In the catch block of all 11 hx-on:htmx:after-request handlers, check xhr.status >= 400 and downgrade s to 'error' so a failed action that returns an HTML error page (or other non-JSON body) surfaces as an error toast instead of the optimistic 'success'/'info' default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web-ui): guard setTimeout fallback for attachInstallButtonHandler The 500ms fallback setTimeout was calling attachInstallButtonHandler() unconditionally even when the plugins partial wasn't in the DOM, causing a spurious console.warn on every page load. Add the same element-existence check already present on the htmx:afterSettle listener. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix backup API 404s, hardware status 500, and HTMX loading race - Add all backup API routes to api_v3.py: preview, list, export, validate, restore (with plugin reinstall), download, delete - Fix PermissionError on /hardware/status: return graceful 200 instead of 500 when the status file is owned by a different user; also fix root cause by writing the file world-readable (0o644) in display_manager - Fix HTMX race: dispatch htmx:ready window event from HTMX onload callback; loadTabContent now waits for that event instead of immediately falling back to direct fetch (eliminating the "HTMX not available" console warning on initial load) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Cancel HTMX fallback timers when htmx:ready fires The 5-second setTimeout fallbacks for plugins and overview were firing before the htmx:ready event arrived, logging spurious warnings. Each timer now self-cancels via htmx:ready so the fallback only triggers when HTMX genuinely fails to load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address review feedback: error leaks, ok:false, htmx:ready coverage - Backup endpoints: replace raw str(e) in user-facing responses with a generic message; full exception still logged via exc_info=True - hardware/status: change ok:null to ok:false for PermissionError and json.JSONDecodeError so the UI's hw.ok===false check triggers correctly - base.html: dispatch htmx:ready from the fallback load path so any deferred listeners fire on CDN-fallback loads too - loadTabContent: also listen for htmx-load-failed so overview/wifi/plugins fall back to direct fetch when HTMX is completely unavailable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Treat system-managed pip packages as satisfied for dependency marker When a plugin's requirements.txt includes a package installed via the system package manager (dnf/apt), pip fails with 'uninstall-no-record-file' because it can't replace the system-tracked copy. The package is present and functional, but the missing marker caused the install to be retried on every service restart. Detect this specific error pattern: if the only pip failure is uninstall-no-record-file, write the .dependencies_installed marker and log a warning instead of returning False, suppressing the repeated warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix uninstall-no-record-file detection condition The previous check used a string replacement that left 'error:' in the remaining text, causing the condition to always evaluate false. Simplify to a direct substring check: if 'uninstall-no-record-file' appears in pip stderr the affected package is installed at the system level and we write the marker, suppressing the repeated warning on every restart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Resolve CodeQL security findings in backup API Path traversal (CWE-22): - backup_download: switch from send_file(user-tainted-path) to send_from_directory(_BACKUP_EXPORT_DIR, filename); Flask uses werkzeug safe_join internally which CodeQL recognises as a sanitizer - backup_delete: enumerate the export directory and match by name so entry.unlink() operates on a filesystem-derived Path rather than one constructed from user input; _safe_backup_path still guards first Information exposure through exceptions (CWE-209): - backup_validate: err_msg from validate_backup() can embed exception strings containing temp-file paths; log the detail, return a generic 'Invalid or corrupted backup file' to the client - Other backup endpoints: already fixed (str(e) -> generic message); CodeQL alerts will clear on next scan plugin_loader.py:185 (path traversal): false positive — requirements_file is constructed from plugin_dir returned by find_plugin_directory() (a filesystem scan), not from raw HTTP request input; no change needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix pre-existing information exposure in version and action endpoints - get_system_version (alert #218): replaced str(e) with generic message; exception still logged via logger.error(exc_info=True) - execute_system_action (alert #216): removed str(e) and full traceback.format_exc() from the HTTP response — the full stack trace was being sent directly to clients; replaced with generic message and proper logger.error call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 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> * Fix three remaining CodeQL path-injection and info-exposure alerts - plugin_loader.py: resolve plugin_dir with strict=True and validate marker_path with relative_to() before any filesystem writes, giving CodeQL the positive sanitization pattern it requires (py/path-injection) - api_v3.py _safe_backup_path: replace substring negative checks with a strict positive regex (^[a-zA-Z0-9][a-zA-Z0-9._-]{0,200}\.zip$) that CodeQL recognises as sanitising the user-supplied filename (py/path-injection) - api_v3.py backup_validate: whitelist known-safe manifest fields before returning JSON, preventing any exception strings captured inside validate_backup() from reaching the HTTP response (py/stack-trace-exposure) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Resolve 29 open CodeQL security alerts across 5 files py/flask-debug (#214): - debug_web_manual.py: read debug mode from LEDMATRIX_FLASK_DEBUG env var instead of hardcoded True py/stack-trace-exposure (#216, #218): - api_v3.py execute_system_action: remove subprocess stdout/stderr from HTTP responses; log via logger instead - api_v3.py get_git_version: validate output matches safe ref format (^[a-zA-Z0-9._-]+$) before including in response - api_v3.py: remove all remaining traceback.format_exc() dead variables and print() debug calls (replaced with logger.debug/warning) py/reflective-xss (#207, #208, #209, #210, #211, #212): - api_v3.py: remove plugin_id from all error/success response messages (uninstall, install, update, health, not-found responses) - pages_v3.py load_partial: return static "Partial not found" message instead of echoing partial_name - pages_v3.py _load_starlark_config_partial: add app_id regex validation, use static error messages instead of f-strings with app_id py/path-injection (#187–#206): - pages_v3.py _load_plugin_config_partial: resolve plugins_base and validate _plugin_dir with relative_to() before all file operations; same for assets metadata directory - pages_v3.py _load_starlark_config_partial: resolve starlark_base and validate schema_file/config_file paths with relative_to() - plugin_loader.py _find_plugin_directory: resolve plugins_dir and validate strategy-2 candidates with relative_to() - plugin_loader.py install_dependencies: resolve plugin_dir first, then construct requirements_file and marker_path from resolved base - plugin_loader.py load_module: resolve plugin_dir with strict=True and validate entry_file with relative_to() before exec_module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix 15 remaining CodeQL path-injection and stack-trace-exposure alerts Switch from resolve()+relative_to() to os.path.basename() reassignment, which CodeQL recognizes as a path sanitizer that breaks the taint chain. Also remove exception objects from backup_manager validate_backup return strings to eliminate the stack-trace-exposure taint source. Fixes alerts #227, #233, #234, #235, #237, #238, #239, #240, #241, #242, #243, #244, #245, #246, #247. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix broken logger format string and leaked exception in config save error - pages_v3.py: plain string was used instead of %-style substitution, so every manifest-read failure logged the literal "{plugin_id}" - api_v3.py save_main_config: exception message was still leaking through the error response; replace with generic message (consistent with the rest of the CodeQL sweep in this PR) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
6.3 KiB
JavaScript
195 lines
6.3 KiB
JavaScript
/**
|
|
* LEDMatrix Base Widget Class
|
|
*
|
|
* Provides common functionality and utilities for all widgets.
|
|
* Widgets can extend this or use it as a reference for best practices.
|
|
*
|
|
* @module BaseWidget
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
/**
|
|
* Base Widget Class
|
|
* Provides common utilities and patterns for widgets
|
|
*/
|
|
class BaseWidget {
|
|
constructor(name, version) {
|
|
this.name = name;
|
|
this.version = version || '1.0.0';
|
|
}
|
|
|
|
/**
|
|
* Validate widget configuration
|
|
* @param {Object} config - Configuration object from schema
|
|
* @param {Object} schema - Full schema object
|
|
* @returns {Object} Validation result {valid: boolean, errors: Array}
|
|
*/
|
|
validateConfig(config, schema) {
|
|
const errors = [];
|
|
|
|
if (!config) {
|
|
errors.push('Configuration is required');
|
|
return { valid: false, errors };
|
|
}
|
|
|
|
// Add widget-specific validation here
|
|
// This is a base implementation that can be overridden
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sanitize value for storage
|
|
* @param {*} value - Raw value from widget
|
|
* @returns {*} Sanitized value
|
|
*/
|
|
sanitizeValue(value) {
|
|
// Base implementation - widgets should override for specific needs
|
|
if (typeof value === 'string') {
|
|
// Strip all HTML tags via the DOM parser to prevent XSS
|
|
const div = document.createElement('div');
|
|
div.textContent = value;
|
|
return div.textContent;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Get field ID from container or options
|
|
* @param {HTMLElement} container - Container element
|
|
* @param {Object} options - Options object
|
|
* @returns {string} Field ID
|
|
*/
|
|
getFieldId(container, options) {
|
|
if (options && options.fieldId) {
|
|
return options.fieldId;
|
|
}
|
|
if (container && container.id) {
|
|
return container.id.replace(/_widget_container$/, '');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
* @param {HTMLElement} container - Container element
|
|
* @param {string} message - Error message
|
|
*/
|
|
showError(container, message) {
|
|
if (!container) return;
|
|
|
|
// Remove existing error
|
|
const existingError = container.querySelector('.widget-error');
|
|
if (existingError) {
|
|
existingError.remove();
|
|
}
|
|
|
|
// Create error element using DOM APIs to prevent XSS
|
|
const errorEl = document.createElement('div');
|
|
errorEl.className = 'widget-error text-sm text-red-600 mt-2';
|
|
|
|
const icon = document.createElement('i');
|
|
icon.className = 'fas fa-exclamation-circle mr-1';
|
|
errorEl.appendChild(icon);
|
|
|
|
const messageText = document.createTextNode(message);
|
|
errorEl.appendChild(messageText);
|
|
|
|
container.appendChild(errorEl);
|
|
}
|
|
|
|
/**
|
|
* Clear error message
|
|
* @param {HTMLElement} container - Container element
|
|
*/
|
|
clearError(container) {
|
|
if (!container) return;
|
|
const errorEl = container.querySelector('.widget-error');
|
|
if (errorEl) {
|
|
errorEl.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
* Always escapes the input, even for non-strings, by coercing to string first
|
|
* @param {*} text - Text to escape (will be coerced to string)
|
|
* @returns {string} Escaped text
|
|
*/
|
|
escapeHtml(text) {
|
|
// Always coerce to string first, then escape
|
|
const textStr = String(text);
|
|
const div = document.createElement('div');
|
|
div.textContent = textStr;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Sanitize identifier for use in DOM IDs and CSS selectors
|
|
* @param {string} id - Identifier to sanitize
|
|
* @returns {string} Sanitized identifier safe for DOM/CSS
|
|
*/
|
|
sanitizeId(id) {
|
|
if (typeof id !== 'string') {
|
|
id = String(id);
|
|
}
|
|
// Allow only alphanumeric, underscore, and hyphen
|
|
return id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
}
|
|
|
|
/**
|
|
* Trigger widget change event
|
|
* @param {string} fieldId - Field ID
|
|
* @param {*} value - New value
|
|
*/
|
|
triggerChange(fieldId, value) {
|
|
const event = new CustomEvent('widget-change', {
|
|
detail: { fieldId, value },
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
document.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Get notification function (if available)
|
|
* @returns {Function|null} Notification function or null
|
|
*/
|
|
getNotificationFunction() {
|
|
if (typeof window.showNotification === 'function') {
|
|
return window.showNotification;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Show notification
|
|
* @param {string} message - Message to show
|
|
* @param {string} type - Notification type (success, error, info, warning)
|
|
*/
|
|
notify(message, type) {
|
|
// Normalize type to prevent errors when undefined/null
|
|
const normalizedType = type ? String(type) : 'info';
|
|
|
|
const notifyFn = this.getNotificationFunction();
|
|
if (notifyFn) {
|
|
notifyFn(message, normalizedType);
|
|
} else {
|
|
console.log(`[${normalizedType.toUpperCase()}] ${message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export for use in widget implementations
|
|
if (typeof window !== 'undefined') {
|
|
window.BaseWidget = BaseWidget;
|
|
}
|
|
|
|
console.log('[BaseWidget] Base widget class loaded');
|
|
})();
|