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:
Chuck
2025-12-30 19:04:21 -05:00
committed by GitHub
parent cca495f306
commit a5c10d6f78
7 changed files with 346 additions and 15 deletions

View File

@@ -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():
"""