from flask import Blueprint, Response, render_template, jsonify, url_for from jinja2 import TemplateNotFound from html.parser import HTMLParser import json import logging import re from pathlib import Path # Strict allowlists for URL-derived values used in path and script operations. _SAFE_PLUGIN_ID_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}$') _SAFE_WEB_UI_FILE_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}\.html$') _SAFE_WIDGET_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}$') _SAFE_WIDGET_SCRIPT_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}\.js$') from src.web_interface.secret_helpers import mask_secret_fields from src.plugin_system.schema_manager import plugin_config_defaults, prepare_plugin_config from src.common.path_safety import resolve_under, safe_path_component from src.pi5_matrix_support import is_raspberry_pi_5 from web_interface import widget_bundle logger = logging.getLogger(__name__) # The managers live on the blueprint object: app.py sets # pages_v3.config_manager, pages_v3.plugin_manager and the rest. pages_v3 = Blueprint('pages_v3', __name__) @pages_v3.route('/assets/widgets.js') def widgets_bundle(): """Every widget script in one request (see web_interface/widget_bundle.py). The individual files stay served from /static for plugin-loader.js and debugging; this saves the Pi ~30 round trips on each first page load. Cached as immutable by app.py because the URL carries the version. """ body, version = widget_bundle.build_bundle() response = Response(body, mimetype='application/javascript') response.headers['X-Widget-Bundle-Version'] = str(version) return response @pages_v3.app_context_processor def inject_widget_bundle_url(): """`widgets_bundle_url()` for templates, versioned by file mtime.""" return { 'widgets_bundle_url': lambda: url_for( 'pages_v3.widgets_bundle', v=widget_bundle.bundle_version() ) } class _SettingsIndexParser(HTMLParser): """Extract searchable settings fields from a rendered partial's HTML. Captures one entry per ``