From 441b3c56e9dcc1ff5cc86f083dde4be63bf27e57 Mon Sep 17 00:00:00 2001 From: Chuck Date: Thu, 19 Feb 2026 16:58:22 -0500 Subject: [PATCH] fix(starlark): code review fixes - security, robustness, and schema parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Security Fixes - manager.py: Check _update_manifest_safe return values to prevent silent failures - manager.py: Improve temp file cleanup in _save_manifest to prevent leaks - manager.py: Fix uninstall order (manifest → memory → disk) for consistency - api_v3.py: Add path traversal validation in uninstall endpoint - api_v3.py: Implement atomic writes for manifest files with temp + rename - pixlet_renderer.py: Relax config validation to only block dangerous shell metacharacters ## Frontend Robustness - plugins_manager.js: Add safeLocalStorage wrapper for restricted contexts (private browsing) - starlark_config.html: Scope querySelector to container to prevent modal conflicts ## Schema Parsing Improvements - pixlet_renderer.py: Indentation-aware get_schema() extraction (handles nested functions) - pixlet_renderer.py: Handle quoted defaults with commas (e.g., "New York, NY") - tronbyte_repository.py: Validate file_name is string before path traversal checks ## Dependencies - requirements.txt: Update Pillow (10.4.0), PyYAML (6.0.2), requests (2.32.0) ## Documentation - docs/STARLARK_APPS_GUIDE.md: Comprehensive guide explaining: - How Starlark apps work - That apps come from Tronbyte (not LEDMatrix) - Installation, configuration, troubleshooting - Links to upstream projects All changes improve security, reliability, and user experience. Co-Authored-By: Claude Sonnet 4.5 --- docs/STARLARK_APPS_GUIDE.md | 500 ++++++++++++++++++ plugin-repos/starlark-apps/manager.py | 30 +- plugin-repos/starlark-apps/pixlet_renderer.py | 71 ++- plugin-repos/starlark-apps/requirements.txt | 6 +- .../starlark-apps/tronbyte_repository.py | 6 + web_interface/blueprints/api_v3.py | 29 +- web_interface/static/v3/plugins_manager.js | 53 +- .../v3/partials/starlark_config.html | 15 +- 8 files changed, 667 insertions(+), 43 deletions(-) create mode 100644 docs/STARLARK_APPS_GUIDE.md diff --git a/docs/STARLARK_APPS_GUIDE.md b/docs/STARLARK_APPS_GUIDE.md new file mode 100644 index 00000000..59c79c36 --- /dev/null +++ b/docs/STARLARK_APPS_GUIDE.md @@ -0,0 +1,500 @@ +# Starlark Apps Guide + +## Overview + +The Starlark Apps plugin for LEDMatrix enables you to run **Tidbyt/Tronbyte community apps** on your LED matrix display without modification. This integration allows you to access hundreds of pre-built widgets and apps from the vibrant Tidbyt community ecosystem. + +## Important: Third-Party Content + +**⚠️ Apps are NOT managed by the LEDMatrix project** + +- Starlark apps are developed and maintained by the **Tidbyt/Tronbyte community** +- LEDMatrix provides the runtime environment but does **not** create, maintain, or support these apps +- All apps originate from the [Tronbyte Apps Repository](https://github.com/tronbyt/apps) +- App quality, functionality, and security are the responsibility of individual app authors +- LEDMatrix is not affiliated with Tidbyt Inc. or the Tronbyte project + +## What is Starlark? + +[Starlark](https://github.com/bazelbuild/starlark) is a Python-like language originally developed by Google for the Bazel build system. Tidbyt adopted Starlark for building LED display apps because it's: + +- **Sandboxed**: Apps run in a safe, restricted environment +- **Simple**: Python-like syntax that's easy to learn +- **Deterministic**: Apps produce consistent output +- **Fast**: Compiled and optimized for performance + +## How It Works + +### Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ LEDMatrix System │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Starlark Apps Plugin (manager.py) │ │ +│ │ • Manages app lifecycle (install/uninstall) │ │ +│ │ • Handles app configuration │ │ +│ │ • Schedules app rendering │ │ +│ └─────────────────┬──────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────▼──────────────────────────────────┐ │ +│ │ Pixlet Renderer (pixlet_renderer.py) │ │ +│ │ • Executes .star files using Pixlet CLI │ │ +│ │ • Extracts configuration schemas │ │ +│ │ • Outputs WebP animations │ │ +│ └─────────────────┬──────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────▼──────────────────────────────────┐ │ +│ │ Frame Extractor (frame_extractor.py) │ │ +│ │ • Decodes WebP animations into frames │ │ +│ │ • Scales/centers output for display size │ │ +│ │ • Manages frame timing │ │ +│ └─────────────────┬──────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────▼──────────────────────────────────┐ │ +│ │ LED Matrix Display │ │ +│ │ • Renders final output to physical display │ │ +│ └────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + + ▲ + │ + Downloads apps from + │ +┌───────────────────┴─────────────────────────────────────┐ +│ Tronbyte Apps Repository (GitHub) │ +│ • 974+ community-built apps │ +│ • Weather, sports, stocks, games, clocks, etc. │ +│ • https://github.com/tronbyt/apps │ +└──────────────────────────────────────────────────────────┘ +``` + +### Rendering Pipeline + +1. **User installs app** from the Tronbyte repository via web UI +2. **Plugin downloads** the `.star` file (and any assets like images/fonts) +3. **Schema extraction** parses configuration options from the `.star` source +4. **User configures** the app through the web UI (timezone, location, API keys, etc.) +5. **Pixlet renders** the app with user config → produces WebP animation +6. **Frame extraction** decodes WebP → individual PIL Image frames +7. **Display scaling** adapts 64x32 Tidbyt output to your matrix size +8. **Rotation** cycles through your installed apps based on schedule + +## Getting Started + +### 1. Install Pixlet + +Pixlet is the rendering engine that executes Starlark apps. The plugin will attempt to use: + +1. **Bundled binary** (recommended): Downloaded to `bin/pixlet/pixlet-{platform}-{arch}` +2. **System installation**: If `pixlet` is available in your PATH + +#### Auto-Install via Web UI + +Navigate to: **Plugins → Starlark Apps → Status → Install Pixlet** + +This runs the bundled installation script which downloads the appropriate binary for your platform. + +#### Manual Installation + +```bash +cd /path/to/LEDMatrix +bash scripts/download_pixlet.sh +``` + +Verify installation: +```bash +./bin/pixlet/pixlet-linux-amd64 version +# Pixlet 0.50.2 (or later) +``` + +### 2. Enable the Starlark Apps Plugin + +1. Open the web UI +2. Navigate to **Plugins** +3. Find **Starlark Apps** in the installed plugins list +4. Enable the plugin +5. Configure settings: + - **Magnify**: Auto-calculated based on your display size (or set manually) + - **Render Interval**: How often apps re-render (default: 300s) + - **Display Duration**: How long each app shows (default: 15s) + - **Cache Output**: Enable to reduce re-rendering (recommended) + +### 3. Browse and Install Apps + +1. Navigate to **Plugins → Starlark Apps → App Store** +2. Browse available apps (974+ options) +3. Filter by category: Weather, Sports, Finance, Games, Clocks, etc. +4. Click **Install** on desired apps +5. Configure each app: + - Set location/timezone + - Enter API keys if required + - Customize display preferences + +### 4. Configure Apps + +Each app may have different configuration options: + +#### Common Configuration Types + +- **Location** (lat/lng/timezone): For weather, clocks, transit +- **API Keys**: For services like weather, stocks, sports scores +- **Display Preferences**: Colors, units, layouts +- **Dropdown Options**: Team selections, language, themes +- **Toggles**: Enable/disable features + +Configuration is stored in `starlark-apps/{app-id}/config.json` and persists across app updates. + +## App Sources and Categories + +All apps are sourced from the [Tronbyte Apps Repository](https://github.com/tronbyt/apps). Popular categories include: + +### 🌤️ Weather +- Analog Clock (with weather) +- Current Weather +- Weather Forecast +- Air Quality Index + +### 🏈 Sports +- NFL Scores +- NBA Scores +- MLB Scores +- NHL Scores +- Soccer/Football Scores +- Formula 1 Results + +### 💰 Finance +- Stock Tickers +- Cryptocurrency Prices +- Market Indices + +### 🎮 Games & Fun +- Conway's Game of Life +- Pong +- Nyan Cat +- Retro Animations + +### 🕐 Clocks +- Analog Clock +- Fuzzy Clock +- Binary Clock +- Word Clock + +### 📰 Information +- News Headlines +- RSS Feeds +- GitHub Activity +- Reddit Feed + +### 🚌 Transit & Travel +- Transit Arrivals +- Flight Tracker +- Train Schedules + +## Display Size Compatibility + +Tronbyte/Tidbyt apps are designed for **64×32 displays**. LEDMatrix automatically adapts content for different display sizes: + +### Magnification + +The plugin calculates optimal magnification based on your display: + +``` +magnify = floor(min(display_width / 64, display_height / 32)) +``` + +Examples: +- **64×32**: magnify = 1 (native, pixel-perfect) +- **128×64**: magnify = 2 (2x scaling, crisp) +- **192×64**: magnify = 2 (2x + horizontal centering) +- **256×64**: magnify = 2 (2x + centering) + +### Scaling Modes + +**Config → Starlark Apps → Scale Method:** +- `nearest` (default): Sharp pixels, retro look +- `bilinear`: Smooth scaling, slight blur +- `bicubic`: Higher quality smooth scaling +- `lanczos`: Best quality, most processing + +**Center vs Scale:** +- `scale_output=true`: Stretch to fill display (may distort aspect ratio) +- `center_small_output=true`: Center output without stretching (preserves aspect ratio) + +## Configuration Schema Extraction + +LEDMatrix automatically extracts configuration schemas from Starlark apps by parsing the `get_schema()` function in the `.star` source code. + +### Supported Field Types + +| Starlark Type | Web UI Rendering | +|--------------|------------------| +| `schema.Location` | Lat/Lng/Timezone picker | +| `schema.Text` | Text input field | +| `schema.Toggle` | Checkbox/switch | +| `schema.Dropdown` | Select dropdown | +| `schema.Color` | Color picker | +| `schema.DateTime` | Date/time picker | +| `schema.OAuth2` | Warning message (not supported) | +| `schema.PhotoSelect` | Warning message (not supported) | +| `schema.LocationBased` | Text fallback with note | +| `schema.Typeahead` | Text fallback with note | + +### Schema Coverage + +- **90-95%** of apps: Full schema support +- **5%**: Partial extraction (complex/dynamic schemas) +- **<1%**: No schema (apps without configuration) + +Apps without extracted schemas can still run with default settings. + +## File Structure + +``` +LEDMatrix/ +├── plugin-repos/starlark-apps/ # Plugin source code +│ ├── manager.py # Main plugin logic +│ ├── pixlet_renderer.py # Pixlet CLI wrapper +│ ├── frame_extractor.py # WebP decoder +│ ├── tronbyte_repository.py # GitHub API client +│ └── requirements.txt # Python dependencies +│ +├── starlark-apps/ # Installed apps (user data) +│ ├── manifest.json # App registry +│ │ +│ └── analogclock/ # Example app +│ ├── analogclock.star # Starlark source +│ ├── config.json # User configuration +│ ├── schema.json # Extracted schema +│ ├── cached_render.webp # Rendered output cache +│ └── images/ # App assets (if any) +│ ├── hour_hand.png +│ └── minute_hand.png +│ +├── bin/pixlet/ # Pixlet binaries +│ ├── pixlet-linux-amd64 +│ ├── pixlet-linux-arm64 +│ └── pixlet-darwin-arm64 +│ +└── scripts/ + └── download_pixlet.sh # Pixlet installer +``` + +## API Keys and External Services + +Many apps require API keys for external services: + +### Common API Services + +- **Weather**: OpenWeatherMap, Weather.gov, Dark Sky +- **Sports**: ESPN, The Sports DB, SportsData.io +- **Finance**: Alpha Vantage, CoinGecko, Yahoo Finance +- **Transit**: TransitLand, NextBus, local transit APIs +- **News**: NewsAPI, Reddit, RSS feeds + +### Security Note + +- API keys are stored in `config.json` files on disk +- The LEDMatrix web interface does NOT encrypt API keys +- Ensure your Raspberry Pi is on a trusted network +- Use read-only or limited-scope API keys when possible +- **Never commit `starlark-apps/*/config.json` to version control** + +## Troubleshooting + +### Pixlet Not Found + +**Symptom**: "Pixlet binary not found" error + +**Solutions**: +1. Run auto-installer: **Plugins → Starlark Apps → Install Pixlet** +2. Manual install: `bash scripts/download_pixlet.sh` +3. Check permissions: `chmod +x bin/pixlet/pixlet-*` +4. Verify architecture: `uname -m` matches binary name + +### App Fails to Render + +**Symptom**: "Rendering failed" error in logs + +**Solutions**: +1. Check logs: `journalctl -u ledmatrix | grep -i pixlet` +2. Verify config: Ensure all required fields are filled +3. Test manually: `./bin/pixlet/pixlet-linux-amd64 render starlark-apps/{app-id}/{app-id}.star` +4. Missing assets: Some apps need images/fonts that may fail to download +5. API issues: Check API keys and rate limits + +### Schema Not Extracted + +**Symptom**: App installs but shows no configuration options + +**Solutions**: +1. App may not have a `get_schema()` function (normal for some apps) +2. Schema extraction failed: Check logs for parse errors +3. Manual config: Edit `starlark-apps/{app-id}/config.json` directly +4. Report issue: File bug with app details at LEDMatrix GitHub + +### Apps Show Distorted/Wrong Size + +**Symptom**: Content appears stretched, squished, or cropped + +**Solutions**: +1. Check magnify setting: **Plugins → Starlark Apps → Config** +2. Try `center_small_output=true` to preserve aspect ratio +3. Adjust `magnify` manually (1-8) for your display size +4. Some apps assume 64×32 - may not scale perfectly to all sizes + +### App Shows Outdated Data + +**Symptom**: Weather, sports scores, etc. don't update + +**Solutions**: +1. Check render interval: **App Config → Render Interval** (300s default) +2. Force re-render: **Plugins → Starlark Apps → {App} → Render Now** +3. Clear cache: Restart LEDMatrix service +4. API rate limits: Some services throttle requests +5. Check app logs for API errors + +## Performance Considerations + +### Render Intervals + +- Apps re-render on a schedule (default: 300s = 5 minutes) +- Lower intervals = more CPU/API usage +- Recommended minimums: + - Static content (clocks): 30-60s + - Weather: 300s (5min) + - Sports scores: 60-120s + - Stock tickers: 60s + +### Caching + +Enable caching to reduce CPU load: +- `cache_rendered_output=true` (recommended) +- `cache_ttl=300` (5 minutes) + +Cached WebP files are stored in `starlark-apps/{app-id}/cached_render.webp` + +### Display Rotation + +Balance number of enabled apps with display duration: +- 5 apps × 15s = 75s full cycle +- 20 apps × 15s = 300s (5min) cycle + +Long cycles may cause apps to render before being displayed. + +## Limitations + +### Unsupported Features + +- **OAuth2 Authentication**: Apps requiring OAuth login won't work +- **PhotoSelect**: Image upload from mobile device not supported +- **Push Notifications**: Apps can't receive real-time events +- **Background Jobs**: No persistent background tasks + +### API Rate Limits + +Many apps use free API tiers with rate limits: +- Rendering too frequently may exceed limits +- Use appropriate `render_interval` settings +- Consider paid API tiers for heavy usage + +### Display Size Constraints + +Apps designed for 64×32 may not utilize larger displays fully: +- Content may appear small on 128×64+ displays +- Magnification helps but doesn't add detail +- Some apps hard-code 64×32 dimensions + +## Advanced Usage + +### Manual App Installation + +Upload custom `.star` files: +1. Navigate to **Starlark Apps → Upload** +2. Select `.star` file from disk +3. Configure app ID and metadata +4. Set render/display timing + +### Custom App Development + +While LEDMatrix runs Tronbyte apps, you can also create your own: + +1. **Learn Starlark**: [Tidbyt Developer Docs](https://tidbyt.dev/) +2. **Write `.star` file**: Use Pixlet APIs for rendering +3. **Test locally**: `pixlet render myapp.star` +4. **Upload**: Use LEDMatrix web UI to install +5. **Share**: Contribute to [Tronbyte Apps](https://github.com/tronbyt/apps) repo + +### Configuration Reference + +**Plugin Config** (`config/config.json` → `plugins.starlark-apps`): + +```json +{ + "enabled": true, + "magnify": 0, // 0 = auto, 1-8 = manual + "render_timeout": 30, // Max seconds for Pixlet render + "cache_rendered_output": true, // Cache WebP files + "cache_ttl": 300, // Cache duration (seconds) + "scale_output": true, // Scale to display size + "scale_method": "nearest", // nearest|bilinear|bicubic|lanczos + "center_small_output": false, // Center instead of scale + "default_frame_delay": 50, // Frame timing (ms) + "max_frames": null, // Limit frames (null = unlimited) + "auto_refresh_apps": true // Auto re-render on interval +} +``` + +**App Config** (`starlark-apps/{app-id}/config.json`): + +```json +{ + "location": "{\"lat\":\"40.7128\",\"lng\":\"-74.0060\",\"timezone\":\"America/New_York\"}", + "units": "imperial", + "api_key": "your-api-key-here", + "render_interval": 300, // App-specific override + "display_duration": 15 // App-specific override +} +``` + +## Resources + +### Official Documentation + +- **Tidbyt Developer Docs**: https://tidbyt.dev/ +- **Starlark Language**: https://github.com/bazelbuild/starlark +- **Pixlet Repository**: https://github.com/tidbyt/pixlet +- **Tronbyte Apps**: https://github.com/tronbyt/apps + +### LEDMatrix Documentation + +- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) +- [REST API Reference](REST_API_REFERENCE.md) +- [Troubleshooting Guide](TROUBLESHOOTING.md) + +### Community + +- **Tidbyt Community**: https://discuss.tidbyt.com/ +- **Tronbyte Apps Issues**: https://github.com/tronbyt/apps/issues +- **LEDMatrix Issues**: https://github.com/ChuckBuilds/LEDMatrix/issues + +## License and Legal + +- **LEDMatrix**: MIT License (see project root) +- **Starlark Apps Plugin**: MIT License (part of LEDMatrix) +- **Pixlet**: Apache 2.0 License (Tidbyt Inc.) +- **Tronbyte Apps**: Various licenses (see individual app headers) +- **Starlark Language**: Apache 2.0 License (Google/Bazel) + +**Disclaimer**: LEDMatrix is an independent project and is not affiliated with, endorsed by, or sponsored by Tidbyt Inc. The Starlark Apps plugin enables interoperability with Tidbyt's open-source ecosystem but does not imply any official relationship. + +## Support + +For issues with: +- **LEDMatrix integration**: File issues at [LEDMatrix GitHub](https://github.com/ChuckBuilds/LEDMatrix/issues) +- **Specific apps**: File issues at [Tronbyte Apps](https://github.com/tronbyt/apps/issues) +- **Pixlet rendering**: File issues at [Pixlet Repository](https://github.com/tidbyt/pixlet/issues) + +--- + +**Ready to get started?** Install the Starlark Apps plugin and explore 974+ community apps! 🎨 diff --git a/plugin-repos/starlark-apps/manager.py b/plugin-repos/starlark-apps/manager.py index 07513906..a3e71808 100644 --- a/plugin-repos/starlark-apps/manager.py +++ b/plugin-repos/starlark-apps/manager.py @@ -553,6 +553,7 @@ class StarlarkAppsPlugin(BasePlugin): Save apps manifest to file with file locking to prevent race conditions. Uses exclusive lock during write to prevent concurrent modifications. """ + temp_file = None try: # Use atomic write pattern: write to temp file, then rename temp_file = self.manifest_file.with_suffix('.tmp') @@ -573,10 +574,10 @@ class StarlarkAppsPlugin(BasePlugin): except Exception as e: self.logger.error(f"Error saving manifest: {e}") # Clean up temp file if it exists - if temp_file.exists(): + if temp_file and temp_file.exists(): try: temp_file.unlink() - except: + except Exception: pass return False @@ -879,7 +880,9 @@ class StarlarkAppsPlugin(BasePlugin): def update_fn(manifest): manifest["apps"][safe_app_id] = app_manifest - self._update_manifest_safe(update_fn) + if not self._update_manifest_safe(update_fn): + self.logger.error(f"Failed to update manifest for {app_id}") + return False # Create app instance (use safe_app_id for internal key, original for display) app = StarlarkApp(safe_app_id, app_dir, app_manifest) @@ -913,19 +916,24 @@ class StarlarkAppsPlugin(BasePlugin): if self.current_app and self.current_app.app_id == app_id: self.current_app = None - # Remove from apps dict - app = self.apps.pop(app_id) + # Get app reference before removing from dict + app = self.apps.get(app_id) - # Remove directory - if app.app_dir.exists(): - shutil.rmtree(app.app_dir) - - # Update manifest + # Update manifest FIRST (before modifying filesystem) def update_fn(manifest): if app_id in manifest["apps"]: del manifest["apps"][app_id] - self._update_manifest_safe(update_fn) + if not self._update_manifest_safe(update_fn): + self.logger.error(f"Failed to update manifest when uninstalling {app_id}") + return False + + # Remove from apps dict + self.apps.pop(app_id) + + # Remove directory (after manifest update succeeds) + if app and app.app_dir.exists(): + shutil.rmtree(app.app_dir) self.logger.info(f"Uninstalled Starlark app: {app_id}") return True diff --git a/plugin-repos/starlark-apps/pixlet_renderer.py b/plugin-repos/starlark-apps/pixlet_renderer.py index ad66e59a..9efe5ced 100644 --- a/plugin-repos/starlark-apps/pixlet_renderer.py +++ b/plugin-repos/starlark-apps/pixlet_renderer.py @@ -264,10 +264,11 @@ class PixletRenderer: else: value_str = str(value) - # Validate value doesn't contain shell metacharacters - # Allow alphanumeric, spaces, and common safe chars: .-_:/@#, - if not re.match(r'^[a-zA-Z0-9 .\-_:/@#,{}"\[\]]*$', value_str): - logger.warning(f"Skipping config value with unsafe characters for key {key}: {value_str}") + # Validate value doesn't contain dangerous shell metacharacters + # Block: backticks, $(), pipes, redirects, semicolons, ampersands, null bytes + # Allow: most printable chars including spaces, quotes, brackets, braces + if re.search(r'[`$|<>&;\x00]|\$\(', value_str): + logger.warning(f"Skipping config value with unsafe shell characters for key {key}: {value_str}") continue # Add as positional argument (not -c flag) @@ -469,7 +470,7 @@ class PixletRenderer: def _extract_get_schema_body(self, content: str) -> Optional[str]: """ - Extract get_schema() function body. + Extract get_schema() function body using indentation-aware parsing. Args: content: .star file content @@ -477,12 +478,45 @@ class PixletRenderer: Returns: Function body text, or None if not found """ - # Find def get_schema(): - pattern = r'def\s+get_schema\s*\(\s*\)\s*:(.*?)(?=\ndef\s|\Z)' - match = re.search(pattern, content, re.DOTALL) + # Find def get_schema(): line + pattern = r'^(\s*)def\s+get_schema\s*\(\s*\)\s*:' + match = re.search(pattern, content, re.MULTILINE) - if match: - return match.group(1) + if not match: + return None + + # Get the indentation level of the function definition + func_indent = len(match.group(1)) + func_start = match.end() + + # Split content into lines starting after the function definition + lines_after = content[func_start:].split('\n') + body_lines = [] + + for line in lines_after: + # Skip empty lines + if not line.strip(): + body_lines.append(line) + continue + + # Calculate indentation of current line + stripped = line.lstrip() + line_indent = len(line) - len(stripped) + + # If line has same or less indentation than function def, check if it's a top-level def + if line_indent <= func_indent: + # This is a line at the same or outer level - check if it's a function + if re.match(r'def\s+\w+', stripped): + # Found next top-level function, stop here + break + # Otherwise it might be a comment or other top-level code, stop anyway + break + + # Line is indented more than function def, so it's part of the body + body_lines.append(line) + + if body_lines: + return '\n'.join(body_lines) return None def _parse_schema_field(self, field_type: str, params_text: str, var_table: Dict) -> Optional[Dict[str, Any]]: @@ -545,15 +579,24 @@ class PixletRenderer: field_dict['icon'] = icon_match.group(1) # default (can be string, bool, or variable reference) - default_match = re.search(r'default\s*=\s*([^,\)]+)', params_text) + # First try to match quoted strings (which may contain commas) + default_match = re.search(r'default\s*=\s*"([^"]*)"', params_text) + if not default_match: + # Try single quotes + default_match = re.search(r"default\s*=\s*'([^']*)'", params_text) + if not default_match: + # Fall back to unquoted value (stop at comma or closing paren) + default_match = re.search(r'default\s*=\s*([^,\)]+)', params_text) + if default_match: default_value = default_match.group(1).strip() # Handle boolean if default_value in ('True', 'False'): field_dict['default'] = default_value.lower() - # Handle string literal - elif default_value.startswith('"') and default_value.endswith('"'): - field_dict['default'] = default_value.strip('"') + # Handle string literal from first two patterns (already extracted without quotes) + elif re.search(r'default\s*=\s*["\']', params_text): + # This was a quoted string, use the captured content directly + field_dict['default'] = default_value # Handle variable reference (can't resolve, use as-is) else: # Try to extract just the value if it's like options[0].value diff --git a/plugin-repos/starlark-apps/requirements.txt b/plugin-repos/starlark-apps/requirements.txt index 8c3fe577..7c1dfc12 100644 --- a/plugin-repos/starlark-apps/requirements.txt +++ b/plugin-repos/starlark-apps/requirements.txt @@ -1,3 +1,3 @@ -Pillow>=10.0.0 -PyYAML>=6.0 -requests>=2.31.0 +Pillow>=10.4.0 +PyYAML>=6.0.2 +requests>=2.32.0 diff --git a/plugin-repos/starlark-apps/tronbyte_repository.py b/plugin-repos/starlark-apps/tronbyte_repository.py index c15ebd5b..c00de632 100644 --- a/plugin-repos/starlark-apps/tronbyte_repository.py +++ b/plugin-repos/starlark-apps/tronbyte_repository.py @@ -462,6 +462,12 @@ class TronbyteRepository: for file_item in dir_data: if file_item.get('type') == 'file': file_name = file_item.get('name') + + # Ensure file_name is a non-empty string before validation + if not file_name or not isinstance(file_name, str): + logger.warning(f"Skipping file with invalid name in {dir_name}: {file_item}") + continue + # Validate filename for path traversal if '..' in file_name or '/' in file_name or '\\' in file_name: logger.warning(f"Skipping potentially unsafe file: {file_name}") diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 76c90749..16ae721f 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -7078,14 +7078,29 @@ def _read_starlark_manifest() -> dict: def _write_starlark_manifest(manifest: dict) -> bool: - """Write the starlark-apps manifest.json to disk.""" + """Write the starlark-apps manifest.json to disk with atomic write.""" + temp_file = None try: _STARLARK_APPS_DIR.mkdir(parents=True, exist_ok=True) - with open(_STARLARK_MANIFEST_FILE, 'w') as f: + + # Atomic write pattern: write to temp file, then rename + temp_file = _STARLARK_MANIFEST_FILE.with_suffix('.tmp') + with open(temp_file, 'w') as f: json.dump(manifest, f, indent=2) + f.flush() + os.fsync(f.fileno()) # Ensure data is written to disk + + # Atomic rename (overwrites destination) + temp_file.replace(_STARLARK_MANIFEST_FILE) return True except OSError as e: logger.error(f"Error writing starlark manifest: {e}") + # Clean up temp file if it exists + if temp_file and temp_file.exists(): + try: + temp_file.unlink() + except Exception: + pass return False @@ -7398,7 +7413,15 @@ def uninstall_starlark_app(app_id): else: # Standalone: remove app dir and manifest entry import shutil - app_dir = _STARLARK_APPS_DIR / app_id + app_dir = (_STARLARK_APPS_DIR / app_id).resolve() + + # Path traversal check - ensure app_dir is within _STARLARK_APPS_DIR + try: + app_dir.relative_to(_STARLARK_APPS_DIR.resolve()) + except ValueError: + logger.warning(f"Path traversal attempt in uninstall: {app_id}") + return jsonify({'status': 'error', 'message': 'Invalid app_id'}), 400 + if app_dir.exists(): shutil.rmtree(app_dir) manifest = _read_starlark_manifest() diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js index cd290093..570eb130 100644 --- a/web_interface/static/v3/plugins_manager.js +++ b/web_interface/static/v3/plugins_manager.js @@ -1,6 +1,43 @@ +// ─── LocalStorage Safety Wrappers ──────────────────────────────────────────── +// Handles environments where localStorage is unavailable or restricted (private browsing, etc.) +const safeLocalStorage = { + getItem(key) { + try { + if (typeof localStorage !== 'undefined') { + return safeLocalStorage.getItem(key); + } + } catch (e) { + console.warn(`safeLocalStorage.getItem failed for key "${key}":`, e.message); + } + return null; + }, + setItem(key, value) { + try { + if (typeof localStorage !== 'undefined') { + safeLocalStorage.setItem(key, value); + return true; + } + } catch (e) { + console.warn(`safeLocalStorage.setItem failed for key "${key}":`, e.message); + } + return false; + }, + removeItem(key) { + try { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem(key); + return true; + } + } catch (e) { + console.warn(`localStorage.removeItem failed for key "${key}":`, e.message); + } + return false; + } +}; + // Define critical functions immediately so they're available before any HTML is rendered -// Debug logging controlled by localStorage.setItem('pluginDebug', 'true') -const _PLUGIN_DEBUG_EARLY = typeof localStorage !== 'undefined' && localStorage.getItem('pluginDebug') === 'true'; +// Debug logging controlled by safeLocalStorage.setItem('pluginDebug', 'true') +const _PLUGIN_DEBUG_EARLY = safeLocalStorage.getItem('pluginDebug') === 'true'; if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS SCRIPT] Defining configurePlugin and togglePlugin at top level...'); // Expose on-demand functions early as stubs (will be replaced when IIFE runs) @@ -865,7 +902,7 @@ window.currentPluginConfig = null; // Store filter/sort state const storeFilterState = { - sort: localStorage.getItem('storeSort') || 'a-z', + sort: safeLocalStorage.getItem('storeSort') || 'a-z', filterVerified: false, filterNew: false, filterInstalled: null, // null = all, true = installed only, false = not installed only @@ -873,7 +910,7 @@ window.currentPluginConfig = null; filterCategories: [], persist() { - localStorage.setItem('storeSort', this.sort); + safeLocalStorage.setItem('storeSort', this.sort); }, reset() { @@ -898,7 +935,7 @@ window.currentPluginConfig = null; }; // Installed plugins sort state - let installedSort = localStorage.getItem('installedSort') || 'a-z'; + let installedSort = safeLocalStorage.getItem('installedSort') || 'a-z'; // Shared on-demand status store (mirrors Alpine store when available) window.__onDemandStore = window.__onDemandStore || { @@ -1251,8 +1288,8 @@ const pluginLoadCache = { } }; -// Debug flag - set via localStorage.setItem('pluginDebug', 'true') -const PLUGIN_DEBUG = typeof localStorage !== 'undefined' && localStorage.getItem('pluginDebug') === 'true'; +// Debug flag - set via safeLocalStorage.setItem('pluginDebug', 'true') +const PLUGIN_DEBUG = typeof localStorage !== 'undefined' && safeLocalStorage.getItem('pluginDebug') === 'true'; function pluginLog(...args) { if (PLUGIN_DEBUG) console.log(...args); } @@ -5269,7 +5306,7 @@ function setupStoreFilterListeners() { installedSortSelect.value = installedSort; installedSortSelect.addEventListener('change', () => { installedSort = installedSortSelect.value; - localStorage.setItem('installedSort', installedSort); + safeLocalStorage.setItem('installedSort', installedSort); const plugins = window.installedPlugins || []; if (plugins.length > 0) { sortAndRenderInstalledPlugins(plugins); diff --git a/web_interface/templates/v3/partials/starlark_config.html b/web_interface/templates/v3/partials/starlark_config.html index f743f3b3..af50a7a9 100644 --- a/web_interface/templates/v3/partials/starlark_config.html +++ b/web_interface/templates/v3/partials/starlark_config.html @@ -155,13 +155,13 @@ class="w-10 h-10 rounded border border-gray-300 cursor-pointer p-0.5" value="{{ current_val or '#FFFFFF' }}" data-starlark-color-picker="{{ field_id }}" - oninput="document.querySelector('[data-starlark-config={{ field_id }}]').value = this.value"> + oninput="this.closest('.space-y-6').querySelector('[data-starlark-config={{ field_id }}]').value = this.value"> + oninput="var cp = this.closest('.space-y-6').querySelector('[data-starlark-color-picker={{ field_id }}]'); if(cp && this.value.match(/^#[0-9a-fA-F]{6}$/)) cp.value = this.value;"> {% if field_desc %}

{{ field_desc }}

@@ -375,8 +375,15 @@ function toggleStarlarkApp(appId, enabled) { function saveStarlarkConfig(appId) { var config = {}; + // Get container to scope queries (prevents conflicts if multiple modals open) + var container = document.getElementById('plugin-config-starlark:' + appId); + if (!container) { + console.error('Container not found for appId:', appId); + return; + } + // Collect standard inputs (text, number, select, datetime, color text companion) - document.querySelectorAll('[data-starlark-config]').forEach(function(input) { + container.querySelectorAll('[data-starlark-config]').forEach(function(input) { var key = input.getAttribute('data-starlark-config'); var type = input.getAttribute('data-starlark-type'); @@ -390,7 +397,7 @@ function saveStarlarkConfig(appId) { }); // Collect location mini-form groups - document.querySelectorAll('[data-starlark-location-group]').forEach(function(group) { + container.querySelectorAll('[data-starlark-location-group]').forEach(function(group) { var fieldId = group.getAttribute('data-starlark-location-group'); var loc = {}; group.querySelectorAll('[data-starlark-location-field="' + fieldId + '"]').forEach(function(sub) {