mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
fix(web-ui): Fix file upload widget and plugin action buttons (#165)
* fix(plugins): Resolve plugin ID determination error in action buttons
- Fix server-side template parameter order for executePluginAction
- Add data-plugin-id attributes to action buttons in all templates
- Enhance executePluginAction with comprehensive fallback logic
- Support retrieving pluginId from DOM, Alpine context, and config state
- Fixes 'Unable to determine plugin ID' error for Spotify/YouTube auth
* fix(plugins): Add missing button IDs and status divs in server-side action template
- Add action-{id}-{index} IDs to action buttons
- Add action-status-{id}-{index} status divs for each action
- Match client-side template structure for consistency
- Fixes 'Action elements not found' error
* fix(api): Fix indentation error in execute_plugin_action function
- Fix incorrect else block indentation that caused 500 errors
- Correct indentation for OAuth flow and simple script execution paths
- Resolves syntax error preventing plugin actions from executing
* fix(api): Improve error handling for plugin actions and config saves
- Add better JSON parsing error handling with request details
- Add detailed permission error messages for secrets file saves
- Include file path and permission status in error responses
- Helps diagnose 400 errors on action execution and 500 errors on config saves
* fix(api): Add detailed permission error handling for secrets config saves
- Add PermissionError-specific handling with permission checks
- Include directory and file permission status in error logs
- Provide more helpful error messages with file paths
- Helps diagnose permission issues when saving config_secrets.json
* fix(config): Add permission check and actionable error message for config saves
- Check file writability before attempting write
- Show file owner and current permissions in error message
- Provide exact command to fix permissions (chown + chmod)
- Helps diagnose and resolve permission issues with config_secrets.json
* fix(config): Preserve detailed permission error messages
- Handle PermissionError separately to preserve detailed error messages
- Ensure actionable permission fix commands are included in error response
- Prevents detailed error messages from being lost in exception chain
* fix(config): Remove overly strict pre-write permission check
- Remove pre-write file existence/writability check that was blocking valid writes
- Let actual file write operation determine success/failure
- Provide detailed error messages only when write actually fails
- Fixes regression where config_secrets.json saves were blocked unnecessarily
* fix(config): Use atomic writes for config_secrets.json to handle permission issues
- Write to temp file first, then atomically move to final location
- Works even when existing file isn't writable (as long as directory is writable)
- Matches pattern used elsewhere in codebase (disk_cache, atomic_manager)
- Fixes permission errors when saving secrets configuration
* chore: Update music plugin submodule to include live_priority fix
* fix(plugins): Improve plugin ID determination in dynamic button generation
- Update generateFormFromSchema to pass currentPluginConfig?.pluginId and add data attributes
- Update generateSimpleConfigForm to pass currentPluginConfig?.pluginId and add data attributes
- Scope fallback 6 DOM lookup to button context instead of document-wide search
- Ensures correct plugin tab selection when multiple plugins are present
- Maintains existing try/catch error handling and logging
* chore: Update music plugin submodule to fix has_live_priority enabled attribute
* chore: Update music plugin submodule - remove redundant music_priority_mode
* fix(web-ui): Fix file upload widget detection for nested plugin properties
- Added helper function to get schema properties by full key path
- Enhanced x-widget detection to check both property object and schema directly
- Improved upload config retrieval with fallback to schema
- Added debug logging for file-upload widget detection
- Fixes issue where static-image plugin file upload widget was not rendering
The file upload widget was not being detected for nested properties like
image_config.images because the x-widget attribute wasn't being checked
in the schema directly. This fix ensures the widget is properly detected
and rendered even when nested deep in the configuration structure.
* fix(web-ui): Improve file upload widget detection with direct schema fallback
- Fixed getSchemaProperty helper function to correctly navigate nested paths
- Added direct schema lookup fallback for image_config.images path
- Enhanced debug logging to diagnose widget detection issues
- Simplified widget detection logic while maintaining robustness
* fix(web-ui): Add aggressive schema lookup for file-upload widget detection
- Always try direct schema navigation for image_config.images
- Added general direct lookup fallback if getSchemaProperty fails
- Enhanced debug logging with schema existence checks
- Prioritize schema lookup over prop object for x-widget detection
* fix(web-ui): Add direct check for top-level images field in file upload detection
- Added specific check for top-level 'images' field (flattened schema)
- Enhanced debug logging to show all x-widget detection attempts
- Improved widget detection to check prop object more thoroughly
* fix(web-ui): Prioritize prop object for x-widget detection
- Check prop object first (should have x-widget from schema)
- Then fall back to schema lookup
- Enhanced debug logging to show all detection attempts
* fix(web-ui): Add aggressive direct detection for images file upload widget
- Added direct check for 'images' field in schema.properties.images
- Multiple fallback detection methods (direct, prop object, schema lookup)
- Simplified logic to explicitly check for file-upload widget
- Enhanced debug logging to show detection path
* fix(web-ui): Add file upload widget support to server-side Jinja2 template
- Added check for x-widget: file-upload in array field rendering
- Renders file upload drop zone with drag-and-drop support
- Displays uploaded images list with delete and schedule buttons
- Falls back to comma-separated text input for regular arrays
- Fixes file upload widget not appearing in static-image plugin
* feat(web-ui): Add route to serve plugin asset files from assets directory
- Added /assets/plugins/<plugin_id>/uploads/<filename> route
- Serves uploaded images and other assets with proper content types
- Includes security checks to prevent directory traversal
- Fixes 404 errors when displaying uploaded plugin images
* fix(web-ui): Fix import for send_from_directory in plugin assets route
* feat(web-ui): Load uploaded images from metadata file when rendering config form
- Populates images field from .metadata.json if not in config
- Ensures uploaded images appear in form even before config is saved
- Merges metadata images with existing config images to avoid duplicates
* fix(web-ui): Fix PROJECT_ROOT reference in image metadata loading
* docs(web-ui): Add reminder to save configuration after file upload
- Added informational note below upload widget
- Reminds users to save config after uploading files
- Uses amber color and info icon for visibility
* fix(web-ui): Move plugin asset serving route to main app
- Moved /assets/plugins/... route from api_v3 blueprint to main app
- Blueprint has /api/v3 prefix, but route needs to be at /assets/...
- Fixes 404 errors when trying to display uploaded images
- Route must be on main app for correct URL path
* security(web-ui): Fix path containment check in plugin asset serving
- Replace string startswith() with proper path resolution using os.path.commonpath()
- Prevents prefix-based directory traversal bypasses
- Uses resolved absolute paths to ensure true path containment
- Handles ValueError for cross-drive paths (Windows compatibility)
* security(web-ui): Remove traceback exposure from plugin asset serving errors
- Return generic error message instead of full traceback in production
- Log exceptions server-side using app.logger.exception()
- Only include detailed error information when app.debug is True
- Prevents leaking internal implementation details to clients
* fix(web-ui): Assign currentPluginConfig to window for template access
- Assign currentPluginConfig to window.currentPluginConfig when building the object
- Fixes empty pluginId in template interpolation for plugin action buttons
- Ensures window.currentPluginConfig?.pluginId is available in onclick handlers
- Prevents executePluginAction from receiving empty pluginId parameter
* chore: Update music plugin submodule to include .gitignore
---------
Co-authored-by: Chuck <chuck@example.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from flask import Flask, Blueprint, render_template, request, redirect, url_for, flash, jsonify, Response
|
||||
from flask import Flask, Blueprint, render_template, request, redirect, url_for, flash, jsonify, Response, send_from_directory
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
@@ -141,6 +141,89 @@ api_v3.cache_manager = CacheManager()
|
||||
app.register_blueprint(pages_v3, url_prefix='/v3')
|
||||
app.register_blueprint(api_v3, url_prefix='/api/v3')
|
||||
|
||||
# Route to serve plugin asset files (registered on main app, not blueprint, for /assets/... path)
|
||||
@app.route('/assets/plugins/<plugin_id>/uploads/<path:filename>', methods=['GET'])
|
||||
def serve_plugin_asset(plugin_id, filename):
|
||||
"""Serve uploaded asset files from assets/plugins/{plugin_id}/uploads/"""
|
||||
try:
|
||||
# Build the asset directory path
|
||||
assets_dir = project_root / 'assets' / 'plugins' / plugin_id / 'uploads'
|
||||
assets_dir = assets_dir.resolve()
|
||||
|
||||
# Security check: ensure the assets directory exists and is within project_root
|
||||
if not assets_dir.exists() or not assets_dir.is_dir():
|
||||
return jsonify({'status': 'error', 'message': 'Asset directory not found'}), 404
|
||||
|
||||
# Ensure we're serving from within the assets directory (prevent directory traversal)
|
||||
# Use proper path resolution instead of string prefix matching to prevent bypasses
|
||||
assets_dir_resolved = assets_dir.resolve()
|
||||
project_root_resolved = project_root.resolve()
|
||||
|
||||
# Check that assets_dir is actually within project_root using commonpath
|
||||
try:
|
||||
common_path = os.path.commonpath([str(assets_dir_resolved), str(project_root_resolved)])
|
||||
if common_path != str(project_root_resolved):
|
||||
return jsonify({'status': 'error', 'message': 'Invalid asset path'}), 403
|
||||
except ValueError:
|
||||
# commonpath raises ValueError if paths are on different drives (Windows)
|
||||
return jsonify({'status': 'error', 'message': 'Invalid asset path'}), 403
|
||||
|
||||
# Resolve the requested file path
|
||||
requested_file = (assets_dir / filename).resolve()
|
||||
|
||||
# Security check: ensure file is within the assets directory using proper path comparison
|
||||
# Use commonpath to ensure assets_dir is a true parent of requested_file
|
||||
try:
|
||||
common_path = os.path.commonpath([str(requested_file), str(assets_dir_resolved)])
|
||||
if common_path != str(assets_dir_resolved):
|
||||
return jsonify({'status': 'error', 'message': 'Invalid file path'}), 403
|
||||
except ValueError:
|
||||
# commonpath raises ValueError if paths are on different drives (Windows)
|
||||
return jsonify({'status': 'error', 'message': 'Invalid file path'}), 403
|
||||
|
||||
# Check if file exists
|
||||
if not requested_file.exists() or not requested_file.is_file():
|
||||
return jsonify({'status': 'error', 'message': 'File not found'}), 404
|
||||
|
||||
# Determine content type based on file extension
|
||||
content_type = 'application/octet-stream'
|
||||
if filename.lower().endswith(('.png', '.jpg', '.jpeg')):
|
||||
content_type = 'image/jpeg' if filename.lower().endswith(('.jpg', '.jpeg')) else 'image/png'
|
||||
elif filename.lower().endswith('.gif'):
|
||||
content_type = 'image/gif'
|
||||
elif filename.lower().endswith('.bmp'):
|
||||
content_type = 'image/bmp'
|
||||
elif filename.lower().endswith('.webp'):
|
||||
content_type = 'image/webp'
|
||||
elif filename.lower().endswith('.svg'):
|
||||
content_type = 'image/svg+xml'
|
||||
elif filename.lower().endswith('.json'):
|
||||
content_type = 'application/json'
|
||||
elif filename.lower().endswith('.txt'):
|
||||
content_type = 'text/plain'
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# Helper function to check if AP mode is active
|
||||
def is_ap_mode_active():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user