fix(csp): replace inline on* handlers with data-on* + JS wiring
Lint & Test / test (push) Successful in 38s

The strict `script-src 'self'` CSP blocks inline onclick/onchange/oninput/
onsubmit attribute evaluation, breaking every button and form in the UI.

- Rename all 53 inline handler attributes in index.html to data-on*
- Add wireInlineHandlers() in app.js that parses each data-on* expression
  on DOMContentLoaded and attaches a proper addEventListener calling the
  matching window-global function. Supports no-arg, string/number/bool/null
  literals, and the `event` token.

CSP stays strict; no unsafe-inline or unsafe-hashes needed.
This commit is contained in:
2026-05-16 18:35:51 +03:00
parent bcc6d40ed7
commit eaeebb64cd
2 changed files with 115 additions and 53 deletions
+53 -53
View File
@@ -26,15 +26,15 @@
</div> </div>
</div> </div>
<div class="mini-controls"> <div class="mini-controls">
<button class="mini-control-btn mini-nav-btn" onclick="previousTrack()" data-i18n-title="player.previous" title="Previous"> <button class="mini-control-btn mini-nav-btn" data-onclick="previousTrack()" data-i18n-title="player.previous" title="Previous">
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg> <svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button> </button>
<button class="mini-control-btn" onclick="togglePlayPause()" id="mini-btn-play-pause" title="Play/Pause"> <button class="mini-control-btn" data-onclick="togglePlayPause()" id="mini-btn-play-pause" title="Play/Pause">
<svg viewBox="0 0 24 24" id="mini-play-pause-icon"> <svg viewBox="0 0 24 24" id="mini-play-pause-icon">
<path d="M8 5v14l11-7z"/> <path d="M8 5v14l11-7z"/>
</svg> </svg>
</button> </button>
<button class="mini-control-btn mini-nav-btn" onclick="nextTrack()" data-i18n-title="player.next" title="Next"> <button class="mini-control-btn mini-nav-btn" data-onclick="nextTrack()" data-i18n-title="player.next" title="Next">
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg> <svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button> </button>
</div> </div>
@@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="mini-volume-container"> <div class="mini-volume-container">
<button class="mini-control-btn" onclick="toggleMute()" id="mini-btn-mute" title="Mute"> <button class="mini-control-btn" data-onclick="toggleMute()" id="mini-btn-mute" title="Mute">
<svg viewBox="0 0 24 24" id="mini-mute-icon"> <svg viewBox="0 0 24 24" id="mini-mute-icon">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/> <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg> </svg>
@@ -67,7 +67,7 @@
<h2 data-i18n="app.title">Media Server</h2> <h2 data-i18n="app.title">Media Server</h2>
<p data-i18n="auth.message">Enter your API token to connect to the media server.</p> <p data-i18n="auth.message">Enter your API token to connect to the media server.</p>
<input type="text" id="token-input" data-i18n-placeholder="auth.placeholder" placeholder="Enter API Token" autocomplete="off"> <input type="text" id="token-input" data-i18n-placeholder="auth.placeholder" placeholder="Enter API Token" autocomplete="off">
<button class="btn-connect" onclick="authenticate()" data-i18n="auth.connect">Connect</button> <button class="btn-connect" data-onclick="authenticate()" data-i18n="auth.connect">Connect</button>
<div class="help-text"> <div class="help-text">
<p data-i18n="auth.help">To get your token, run:</p> <p data-i18n="auth.help">To get your token, run:</p>
<code>media-server --show-token</code> <code>media-server --show-token</code>
@@ -91,23 +91,23 @@
<a class="header-btn" href="/docs" target="_blank" title="API Documentation" aria-label="API Documentation"> <a class="header-btn" href="/docs" target="_blank" title="API Documentation" aria-label="API Documentation">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h5v7h7v9H6zm2-4h8v2H8v-2zm0-3h8v2H8v-2z"/></svg> <svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h5v7h7v9H6zm2-4h8v2H8v-2zm0-3h8v2H8v-2z"/></svg>
</a> </a>
<button class="header-btn" onclick="showAboutDialog()" data-i18n-title="about.button_title" title="About" aria-label="About"> <button class="header-btn" data-onclick="showAboutDialog()" data-i18n-title="about.button_title" title="About" aria-label="About">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 7h2v2h-2V7zm0 4h2v6h-2v-6zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg> <svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 7h2v2h-2V7zm0 4h2v6h-2v-6zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
</button> </button>
<div class="accent-picker"> <div class="accent-picker">
<button class="header-btn" onclick="toggleAccentPicker()" title="Accent color" aria-label="Accent color"> <button class="header-btn" data-onclick="toggleAccentPicker()" title="Accent color" aria-label="Accent color">
<span class="accent-dot" id="accentDot"></span> <span class="accent-dot" id="accentDot"></span>
</button> </button>
<div class="accent-picker-dropdown" id="accentDropdown"></div> <div class="accent-picker-dropdown" id="accentDropdown"></div>
</div> </div>
<button class="header-btn" onclick="toggleDynamicBackground()" data-i18n-title="player.background" title="Dynamic background" aria-label="Dynamic background" id="bgToggle"> <button class="header-btn" data-onclick="toggleDynamicBackground()" data-i18n-title="player.background" title="Dynamic background" aria-label="Dynamic background" id="bgToggle">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg> <svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>
</button> </button>
<button class="header-btn" onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen" title="Fullscreen player" aria-label="Fullscreen player" id="fullscreenToggle"> <button class="header-btn" data-onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen" title="Fullscreen player" aria-label="Fullscreen player" id="fullscreenToggle">
<svg id="fullscreen-icon-enter" viewBox="0 0 24 24"><path fill="currentColor" d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zm0 14v-2h3v-3h2v5h-5zM5 14h2v3h3v2H5v-5z"/></svg> <svg id="fullscreen-icon-enter" viewBox="0 0 24 24"><path fill="currentColor" d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zm0 14v-2h3v-3h2v5h-5zM5 14h2v3h3v2H5v-5z"/></svg>
<svg id="fullscreen-icon-exit" viewBox="0 0 24 24" style="display:none"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg> <svg id="fullscreen-icon-exit" viewBox="0 0 24 24" style="display:none"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg>
</button> </button>
<button class="header-btn" onclick="toggleTheme()" data-i18n-title="player.theme" title="Toggle theme" aria-label="Toggle theme" id="theme-toggle"> <button class="header-btn" data-onclick="toggleTheme()" data-i18n-title="player.theme" title="Toggle theme" aria-label="Toggle theme" id="theme-toggle">
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;"> <svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/> <path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
</svg> </svg>
@@ -115,12 +115,12 @@
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/> <path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
</svg> </svg>
</button> </button>
<select id="locale-select" class="header-locale" onchange="changeLocale()" title="Change language"> <select id="locale-select" class="header-locale" data-onchange="changeLocale()" title="Change language">
<option value="en">EN</option> <option value="en">EN</option>
<option value="ru">RU</option> <option value="ru">RU</option>
</select> </select>
<span class="header-toolbar-sep"></span> <span class="header-toolbar-sep"></span>
<button class="header-btn header-btn-logout" onclick="clearToken()" data-i18n-title="auth.logout.title" title="Clear saved token" aria-label="Logout"> <button class="header-btn header-btn-logout" data-onclick="clearToken()" data-i18n-title="auth.logout.title" title="Clear saved token" aria-label="Logout">
<svg viewBox="0 0 24 24"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg> <svg viewBox="0 0 24 24"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
</button> </button>
</div> </div>
@@ -136,29 +136,29 @@
<!-- Connection Banner --> <!-- Connection Banner -->
<div class="connection-banner hidden" id="connectionBanner"> <div class="connection-banner hidden" id="connectionBanner">
<span id="connectionBannerText"></span> <span id="connectionBannerText"></span>
<button class="connection-banner-btn" id="connectionBannerBtn" onclick="manualReconnect()" style="display: none;" data-i18n="connection.reconnect">Reconnect</button> <button class="connection-banner-btn" id="connectionBannerBtn" data-onclick="manualReconnect()" style="display: none;" data-i18n="connection.reconnect">Reconnect</button>
</div> </div>
<!-- Tab Bar (editorial: numbered, italic active) --> <!-- Tab Bar (editorial: numbered, italic active) -->
<div class="tab-bar" id="tabBar" role="tablist"> <div class="tab-bar" id="tabBar" role="tablist">
<div class="tab-indicator" id="tabIndicator"></div> <div class="tab-indicator" id="tabIndicator"></div>
<button class="tab-btn active" data-tab="player" onclick="switchTab('player')" role="tab" aria-selected="true" aria-controls="panel-player" tabindex="0"> <button class="tab-btn active" data-tab="player" data-onclick="switchTab('player')" role="tab" aria-selected="true" aria-controls="panel-player" tabindex="0">
<span class="tab-num">01</span> <span class="tab-num">01</span>
<span data-i18n="tab.player">Now Spinning</span> <span data-i18n="tab.player">Now Spinning</span>
</button> </button>
<button class="tab-btn" data-tab="display" onclick="switchTab('display')" role="tab" aria-selected="false" aria-controls="panel-display" tabindex="-1"> <button class="tab-btn" data-tab="display" data-onclick="switchTab('display')" role="tab" aria-selected="false" aria-controls="panel-display" tabindex="-1">
<span class="tab-num">02</span> <span class="tab-num">02</span>
<span data-i18n="tab.display">Display</span> <span data-i18n="tab.display">Display</span>
</button> </button>
<button class="tab-btn" data-tab="browser" onclick="switchTab('browser')" role="tab" aria-selected="false" aria-controls="panel-browser" tabindex="-1"> <button class="tab-btn" data-tab="browser" data-onclick="switchTab('browser')" role="tab" aria-selected="false" aria-controls="panel-browser" tabindex="-1">
<span class="tab-num">03</span> <span class="tab-num">03</span>
<span data-i18n="tab.browser">Library</span> <span data-i18n="tab.browser">Library</span>
</button> </button>
<button class="tab-btn" data-tab="quick-actions" onclick="switchTab('quick-actions')" role="tab" aria-selected="false" aria-controls="panel-quick-actions" tabindex="-1"> <button class="tab-btn" data-tab="quick-actions" data-onclick="switchTab('quick-actions')" role="tab" aria-selected="false" aria-controls="panel-quick-actions" tabindex="-1">
<span class="tab-num">04</span> <span class="tab-num">04</span>
<span data-i18n="tab.quick_access">Quick Access</span> <span data-i18n="tab.quick_access">Quick Access</span>
</button> </button>
<button class="tab-btn" data-tab="settings" onclick="switchTab('settings')" role="tab" aria-selected="false" aria-controls="panel-settings" tabindex="-1"> <button class="tab-btn" data-tab="settings" data-onclick="switchTab('settings')" role="tab" aria-selected="false" aria-controls="panel-settings" tabindex="-1">
<span class="tab-num">05</span> <span class="tab-num">05</span>
<span data-i18n="tab.settings">Settings</span> <span data-i18n="tab.settings">Settings</span>
</button> </button>
@@ -172,7 +172,7 @@
<span class="fs-chrome-sep">·</span> <span class="fs-chrome-sep">·</span>
<span class="fs-chrome-kicker" data-i18n="player.kicker">Now Playing</span> <span class="fs-chrome-kicker" data-i18n="player.kicker">Now Playing</span>
</div> </div>
<button class="fs-chrome-exit" onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen.exit" title="Exit fullscreen" aria-label="Exit fullscreen"> <button class="fs-chrome-exit" data-onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen.exit" title="Exit fullscreen" aria-label="Exit fullscreen">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg> <svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg>
<span data-i18n="player.fullscreen.exit_short">Exit</span> <span data-i18n="player.fullscreen.exit_short">Exit</span>
<kbd class="fs-chrome-kbd">ESC</kbd> <kbd class="fs-chrome-kbd">ESC</kbd>
@@ -267,13 +267,13 @@
</div> </div>
<div class="controls"> <div class="controls">
<button class="btn-trans" onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous"> <button class="btn-trans" data-onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg> <svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button> </button>
<button class="btn-trans primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause"> <button class="btn-trans primary" data-onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
<svg viewBox="0 0 24 24" id="play-pause-icon"><path d="M8 5v14l11-7z"/></svg> <svg viewBox="0 0 24 24" id="play-pause-icon"><path d="M8 5v14l11-7z"/></svg>
</button> </button>
<button class="btn-trans" onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next"> <button class="btn-trans" data-onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg> <svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button> </button>
@@ -287,7 +287,7 @@
</div> </div>
<!-- Volume control: mute + slim slider, integrated --> <!-- Volume control: mute + slim slider, integrated -->
<div class="vu-volume"> <div class="vu-volume">
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute"> <button class="mute-btn" data-onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
<svg viewBox="0 0 24 24" id="mute-icon"> <svg viewBox="0 0 24 24" id="mute-icon">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/> <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg> </svg>
@@ -301,7 +301,7 @@
<!-- Hidden but functional: legacy display + visualizer toggle. --> <!-- Hidden but functional: legacy display + visualizer toggle. -->
<div class="visually-hidden"> <div class="visually-hidden">
<div id="volume-display">50%</div> <div id="volume-display">50%</div>
<button onclick="toggleVisualizer()" id="visualizerToggle" data-i18n-title="player.visualizer" title="Audio visualizer" style="display:none"> <button data-onclick="toggleVisualizer()" id="visualizerToggle" data-i18n-title="player.visualizer" title="Audio visualizer" style="display:none">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 18h2v-8H3v8zm4 0h2V6H7v12zm4 0h2V2h-2v16zm4 0h2v-6h-2v6zm4 0h2V9h-2v9z"/></svg> <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 18h2v-8H3v8zm4 0h2V6H7v12zm4 0h2V2h-2v16zm4 0h2v-6h-2v6zm4 0h2V9h-2v9z"/></svg>
</button> </button>
</div> </div>
@@ -318,34 +318,34 @@
<div class="browser-toolbar" id="browserToolbar"> <div class="browser-toolbar" id="browserToolbar">
<div class="browser-toolbar-left"> <div class="browser-toolbar-left">
<div class="view-toggle"> <div class="view-toggle">
<button class="view-toggle-btn active" id="viewGridBtn" onclick="setViewMode('grid')" data-i18n-title="browser.view_grid" title="Grid view"> <button class="view-toggle-btn active" id="viewGridBtn" data-onclick="setViewMode('grid')" data-i18n-title="browser.view_grid" title="Grid view">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 3h8v8H3V3zm0 10h8v8H3v-8zm10-10h8v8h-8V3zm0 10h8v8h-8v-8z"/></svg> <svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 3h8v8H3V3zm0 10h8v8H3v-8zm10-10h8v8h-8V3zm0 10h8v8h-8v-8z"/></svg>
</button> </button>
<button class="view-toggle-btn" id="viewCompactBtn" onclick="setViewMode('compact')" data-i18n-title="browser.view_compact" title="Compact view"> <button class="view-toggle-btn" id="viewCompactBtn" data-onclick="setViewMode('compact')" data-i18n-title="browser.view_compact" title="Compact view">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M2 2h5v5H2V2zm0 8h5v5H2v-5zm0 8h5v5H2v-5zm7-16h5v5H9V2zm0 8h5v5H9v-5zm0 8h5v5H9v-5zm7-16h5v5h-5V2zm0 8h5v5h-5v-5zm0 8h5v5h-5v-5z"/></svg> <svg viewBox="0 0 24 24" width="18" height="18"><path d="M2 2h5v5H2V2zm0 8h5v5H2v-5zm0 8h5v5H2v-5zm7-16h5v5H9V2zm0 8h5v5H9v-5zm0 8h5v5H9v-5zm7-16h5v5h-5V2zm0 8h5v5h-5v-5zm0 8h5v5h-5v-5z"/></svg>
</button> </button>
<button class="view-toggle-btn" id="viewListBtn" onclick="setViewMode('list')" data-i18n-title="browser.view_list" title="List view"> <button class="view-toggle-btn" id="viewListBtn" data-onclick="setViewMode('list')" data-i18n-title="browser.view_list" title="List view">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg> <svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg>
</button> </button>
</div> </div>
<button class="view-toggle-btn browser-refresh-btn" id="refreshBtn" onclick="refreshBrowser()" data-i18n-title="browser.refresh" title="Refresh"> <button class="view-toggle-btn browser-refresh-btn" id="refreshBtn" data-onclick="refreshBrowser()" data-i18n-title="browser.refresh" title="Refresh">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> <svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
</button> </button>
<button class="browser-play-all-btn" id="playAllBtn" onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;"> <button class="browser-play-all-btn" id="playAllBtn" data-onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg> <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
</button> </button>
</div> </div>
<div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;"> <div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;">
<svg class="browser-search-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg> <svg class="browser-search-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="browserSearchInput" class="browser-search-input" data-i18n-placeholder="browser.search" placeholder="Search..." oninput="onBrowserSearch()"> <input type="text" id="browserSearchInput" class="browser-search-input" data-i18n-placeholder="browser.search" placeholder="Search..." data-oninput="onBrowserSearch()">
<button class="browser-search-clear" id="browserSearchClear" onclick="clearBrowserSearch()" style="display: none;"> <button class="browser-search-clear" id="browserSearchClear" data-onclick="clearBrowserSearch()" style="display: none;">
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button> </button>
</div> </div>
<div class="browser-toolbar-right"> <div class="browser-toolbar-right">
<label class="items-per-page-label"> <label class="items-per-page-label">
<span data-i18n="browser.items_per_page">Items per page:</span> <span data-i18n="browser.items_per_page">Items per page:</span>
<select id="itemsPerPageSelect" onchange="onItemsPerPageChanged()"> <select id="itemsPerPageSelect" data-onchange="onItemsPerPageChanged()">
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100" selected>100</option> <option value="100" selected>100</option>
@@ -366,13 +366,13 @@
<!-- Pagination --> <!-- Pagination -->
<div class="pagination" id="browserPagination" style="display: none;"> <div class="pagination" id="browserPagination" style="display: none;">
<button id="prevPage" onclick="previousPage()" data-i18n="browser.previous">Previous</button> <button id="prevPage" data-onclick="previousPage()" data-i18n="browser.previous">Previous</button>
<div class="pagination-center"> <div class="pagination-center">
<span data-i18n="browser.page">Page</span> <span data-i18n="browser.page">Page</span>
<input type="number" id="pageInput" class="page-input" min="1" value="1" onchange="goToPage()"> <input type="number" id="pageInput" class="page-input" min="1" value="1" data-onchange="goToPage()">
<span id="pageTotal">/ 1</span> <span id="pageTotal">/ 1</span>
</div> </div>
<button id="nextPage" onclick="nextPage()" data-i18n="browser.next">Next</button> <button id="nextPage" data-onclick="nextPage()" data-i18n="browser.next">Next</button>
<span class="pagination-showing" id="paginationShowing"></span> <span class="pagination-showing" id="paginationShowing"></span>
</div> </div>
</div> </div>
@@ -398,7 +398,7 @@
<div class="audio-device-selector"> <div class="audio-device-selector">
<label> <label>
<span data-i18n="settings.audio.device">Loopback Device</span> <span data-i18n="settings.audio.device">Loopback Device</span>
<select id="audioDeviceSelect" onchange="onAudioDeviceChanged()"> <select id="audioDeviceSelect" data-onchange="onAudioDeviceChanged()">
<option value="" data-i18n="settings.audio.auto">Auto-detect</option> <option value="" data-i18n="settings.audio.auto">Auto-detect</option>
</select> </select>
</label> </label>
@@ -434,7 +434,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="add-card" onclick="showAddFolderDialog()"> <div class="add-card" data-onclick="showAddFolderDialog()">
<span class="add-card-icon">+</span> <span class="add-card-icon">+</span>
</div> </div>
</div> </div>
@@ -464,7 +464,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="add-card" onclick="showAddScriptDialog()"> <div class="add-card" data-onclick="showAddScriptDialog()">
<span class="add-card-icon">+</span> <span class="add-card-icon">+</span>
</div> </div>
</div> </div>
@@ -496,7 +496,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="add-card" onclick="showAddLinkDialog()"> <div class="add-card" data-onclick="showAddLinkDialog()">
<span class="add-card-icon">+</span> <span class="add-card-icon">+</span>
</div> </div>
</div> </div>
@@ -528,7 +528,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="add-card" onclick="showAddCallbackDialog()"> <div class="add-card" data-onclick="showAddCallbackDialog()">
<span class="add-card-icon">+</span> <span class="add-card-icon">+</span>
</div> </div>
</div> </div>
@@ -551,7 +551,7 @@
<div class="dialog-header"> <div class="dialog-header">
<h3 id="dialogTitle" data-i18n="scripts.dialog.add">Add Script</h3> <h3 id="dialogTitle" data-i18n="scripts.dialog.add">Add Script</h3>
</div> </div>
<form id="scriptForm" onsubmit="saveScript(event)"> <form id="scriptForm" data-onsubmit="saveScript(event)">
<div class="dialog-body"> <div class="dialog-body">
<input type="hidden" id="scriptOriginalName"> <input type="hidden" id="scriptOriginalName">
<input type="hidden" id="scriptIsEdit"> <input type="hidden" id="scriptIsEdit">
@@ -593,13 +593,13 @@
<div class="params-section"> <div class="params-section">
<div class="params-header"> <div class="params-header">
<span data-i18n="scripts.field.parameters">Parameters</span> <span data-i18n="scripts.field.parameters">Parameters</span>
<button type="button" class="btn-small" onclick="addParameterRow()" data-i18n="scripts.params.add">+ Add</button> <button type="button" class="btn-small" data-onclick="addParameterRow()" data-i18n="scripts.params.add">+ Add</button>
</div> </div>
<div id="scriptParamsContainer"></div> <div id="scriptParamsContainer"></div>
</div> </div>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeScriptDialog()" data-i18n="scripts.button.cancel">Cancel</button> <button type="button" class="btn-secondary" data-onclick="closeScriptDialog()" data-i18n="scripts.button.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="scripts.button.save">Save</button> <button type="submit" class="btn-primary" data-i18n="scripts.button.save">Save</button>
</div> </div>
</form> </form>
@@ -610,12 +610,12 @@
<div class="dialog-header"> <div class="dialog-header">
<h3 id="scriptParamsDialogTitle" data-i18n="scripts.params.execute">Execute Script</h3> <h3 id="scriptParamsDialogTitle" data-i18n="scripts.params.execute">Execute Script</h3>
</div> </div>
<form id="scriptParamsForm" onsubmit="submitScriptWithParams(event)"> <form id="scriptParamsForm" data-onsubmit="submitScriptWithParams(event)">
<div class="dialog-body"> <div class="dialog-body">
<div id="scriptParamsInputs"></div> <div id="scriptParamsInputs"></div>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeScriptParamsDialog()" data-i18n="scripts.button.cancel">Cancel</button> <button type="button" class="btn-secondary" data-onclick="closeScriptParamsDialog()" data-i18n="scripts.button.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="scripts.params.execute">Execute</button> <button type="submit" class="btn-primary" data-i18n="scripts.params.execute">Execute</button>
</div> </div>
</form> </form>
@@ -626,7 +626,7 @@
<div class="dialog-header"> <div class="dialog-header">
<h3 id="callbackDialogTitle" data-i18n="callbacks.dialog.add">Add Callback</h3> <h3 id="callbackDialogTitle" data-i18n="callbacks.dialog.add">Add Callback</h3>
</div> </div>
<form id="callbackForm" onsubmit="saveCallback(event)"> <form id="callbackForm" data-onsubmit="saveCallback(event)">
<div class="dialog-body"> <div class="dialog-body">
<input type="hidden" id="callbackIsEdit"> <input type="hidden" id="callbackIsEdit">
@@ -664,7 +664,7 @@
</label> </label>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeCallbackDialog()" data-i18n="callbacks.button.cancel">Cancel</button> <button type="button" class="btn-secondary" data-onclick="closeCallbackDialog()" data-i18n="callbacks.button.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="callbacks.button.save">Save</button> <button type="submit" class="btn-primary" data-i18n="callbacks.button.save">Save</button>
</div> </div>
</form> </form>
@@ -675,7 +675,7 @@
<div class="dialog-header"> <div class="dialog-header">
<h3 id="linkDialogTitle" data-i18n="links.dialog.add">Add Link</h3> <h3 id="linkDialogTitle" data-i18n="links.dialog.add">Add Link</h3>
</div> </div>
<form id="linkForm" onsubmit="saveLink(event)"> <form id="linkForm" data-onsubmit="saveLink(event)">
<div class="dialog-body"> <div class="dialog-body">
<input type="hidden" id="linkOriginalName"> <input type="hidden" id="linkOriginalName">
<input type="hidden" id="linkIsEdit"> <input type="hidden" id="linkIsEdit">
@@ -710,7 +710,7 @@
</label> </label>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeLinkDialog()" data-i18n="links.button.cancel">Cancel</button> <button type="button" class="btn-secondary" data-onclick="closeLinkDialog()" data-i18n="links.button.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="links.button.save">Save</button> <button type="submit" class="btn-primary" data-i18n="links.button.save">Save</button>
</div> </div>
</form> </form>
@@ -737,7 +737,7 @@
</div> </div>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeExecutionDialog()" data-i18n="scripts.execution.close">Close</button> <button type="button" class="btn-secondary" data-onclick="closeExecutionDialog()" data-i18n="scripts.execution.close">Close</button>
</div> </div>
</dialog> </dialog>
@@ -746,7 +746,7 @@
<div class="dialog-header"> <div class="dialog-header">
<h3 id="folderDialogTitle" data-i18n="browser.folder_dialog.title_add">Add Media Folder</h3> <h3 id="folderDialogTitle" data-i18n="browser.folder_dialog.title_add">Add Media Folder</h3>
</div> </div>
<form id="folderForm" onsubmit="saveFolder(event)"> <form id="folderForm" data-onsubmit="saveFolder(event)">
<div class="dialog-body"> <div class="dialog-body">
<input type="hidden" id="folderIsEdit"> <input type="hidden" id="folderIsEdit">
<input type="hidden" id="folderOriginalId"> <input type="hidden" id="folderOriginalId">
@@ -773,7 +773,7 @@
</label> </label>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeFolderDialog()" data-i18n="browser.folder_dialog.cancel">Cancel</button> <button type="button" class="btn-secondary" data-onclick="closeFolderDialog()" data-i18n="browser.folder_dialog.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="browser.folder_dialog.save">Save</button> <button type="submit" class="btn-primary" data-i18n="browser.folder_dialog.save">Save</button>
</div> </div>
</form> </form>
@@ -813,7 +813,7 @@
</ul> </ul>
</div> </div>
<div class="dialog-footer"> <div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeAboutDialog()" data-i18n="dialog.close">Close</button> <button type="button" class="btn-secondary" data-onclick="closeAboutDialog()" data-i18n="dialog.close">Close</button>
</div> </div>
</dialog> </dialog>
+62
View File
@@ -159,10 +159,72 @@ HTMLDialogElement.prototype.showModal = function (...args) {
return result; return result;
}; };
// CSP-safe replacement for inline on* handlers. HTML uses data-onclick,
// data-onchange, data-oninput, data-onsubmit with simple call expressions
// like "fn()", "fn('arg')", "fn(event)". We parse those at startup and
// attach proper addEventListener calls so script-src 'self' stays strict.
const INLINE_HANDLER_EVENTS = {
'data-onclick': 'click',
'data-onchange': 'change',
'data-oninput': 'input',
'data-onsubmit': 'submit',
};
function parseInlineHandlerArg(token) {
const t = token.trim();
if (t === '') return { kind: 'empty' };
if (t === 'event') return { kind: 'event' };
if (t === 'true') return { kind: 'literal', value: true };
if (t === 'false') return { kind: 'literal', value: false };
if (t === 'null') return { kind: 'literal', value: null };
if (/^-?\d+(\.\d+)?$/.test(t)) return { kind: 'literal', value: Number(t) };
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
return { kind: 'literal', value: t.slice(1, -1) };
}
console.warn('inline-handler: unsupported arg token', token);
return { kind: 'literal', value: undefined };
}
function compileInlineHandler(expr) {
const m = expr.match(/^\s*([A-Za-z_$][\w$]*)\s*\((.*)\)\s*;?\s*$/s);
if (!m) {
console.warn('inline-handler: unparsable expression', expr);
return null;
}
const fnName = m[1];
const argsRaw = m[2].trim();
const argTokens = argsRaw === '' ? [] : argsRaw.split(',').map(s => s.trim());
const parsedArgs = argTokens.map(parseInlineHandlerArg);
return function (event) {
const fn = window[fnName];
if (typeof fn !== 'function') {
console.error('inline-handler: missing global function', fnName);
return;
}
const args = parsedArgs.map(a => a.kind === 'event' ? event : a.value);
return fn.apply(this, args);
};
}
function wireInlineHandlers(root) {
for (const [attr, eventName] of Object.entries(INLINE_HANDLER_EVENTS)) {
const nodes = root.querySelectorAll(`[${attr}]`);
for (const el of nodes) {
const expr = el.getAttribute(attr);
const handler = compileInlineHandler(expr);
if (handler) el.addEventListener(eventName, handler);
el.removeAttribute(attr);
}
}
}
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener('DOMContentLoaded', async () => {
// Cache DOM references // Cache DOM references
cacheDom(); cacheDom();
// Wire CSP-safe inline-handler stand-ins from index.html
wireInlineHandlers(document);
// Initialize theme and accent color // Initialize theme and accent color
initTheme(); initTheme();
initAccentColor(); initAccentColor();