diff --git a/api/plugins.py b/api/plugins.py index 94aa150e..fc46fcae 100644 --- a/api/plugins.py +++ b/api/plugins.py @@ -24,10 +24,11 @@ logger = logging.getLogger(__name__) # a settings key). Lowercase alnum + - / _, 1-64 chars, must start with a letter. _VALID_PLUGIN_NAME = re.compile(r"^[a-z][a-z0-9_-]{0,63}$") -# Valid tab.path: a clean same-origin absolute path. Must start with '/', then -# only safe path chars — no quotes, whitespace, control chars, query ('?') or -# fragment ('#') so it can't break out of a JS-string nav arg or shadow routes. -_VALID_PLUGIN_TAB_PATH = re.compile(r"^/[A-Za-z0-9._~/-]{0,255}$") +# Valid tab.path: a clean same-origin absolute path. Must start with a single +# '/' (NOT '//' — a leading '//' is a protocol-relative URL that would resolve +# to a remote origin when assigned to iframe.src), then only safe path chars — +# no quotes, whitespace, control chars, query ('?') or fragment ('#'). +_VALID_PLUGIN_TAB_PATH = re.compile(r"^/(?!/)[A-Za-z0-9._~/-]{0,255}$") # plugin_name -> manifest dict (as loaded from manifest.json) PLUGIN_MANIFESTS: dict[str, dict] = {} diff --git a/static/panels.js b/static/panels.js index 41e0b82d..a2444701 100644 --- a/static/panels.js +++ b/static/panels.js @@ -6513,7 +6513,6 @@ const enabled=plugin&&plugin.enabled!==false;
${esc((plugin&&plugin.name)||t('plugins_unnamed'))}
${esc((plugin&&plugin.key)||'plugin')}${version}
- ${isDashboardPlugin?`${enabled?t('plugins_enabled'):t('plugins_disabled')}`:''} ${badgeText}
diff --git a/static/style.css b/static/style.css index 74f3d8ba..162a27ad 100644 --- a/static/style.css +++ b/static/style.css @@ -3492,11 +3492,15 @@ main.main > #mainPlugin{display:none;} .plugin-toggle-switch{position:relative;display:inline-block;width:32px;height:18px;flex-shrink:0;} .plugin-toggle-switch input{opacity:0;width:0;height:0;} .plugin-toggle-slider{position:absolute;cursor:pointer;inset:0;background:var(--border2);border-radius:9px;transition:background .2s;} -.plugin-toggle-slider::before{content:'';position:absolute;height:12px;width:12px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:transform .2s;} +.plugin-toggle-slider::before{content:'';position:absolute;height:12px;width:12px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 2px rgba(0,0,0,.45);} .plugin-toggle-switch input:checked+.plugin-toggle-slider{background:var(--accent);} .plugin-toggle-switch input:checked+.plugin-toggle-slider::before{transform:translateX(14px);} -.plugin-open-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;background:var(--accent);color:var(--accent-text);border:none;border-radius:7px;font-size:13px;font-weight:500;cursor:pointer;text-decoration:none;} -.plugin-open-btn:hover{opacity:.85;} +/* Ghost/outline button: --accent-text is not guaranteed to contrast against + --accent in every theme (e.g. it equals --accent in the default gold theme, + which renders invisible text on a filled block). Use the accent as the text + + border colour on the card's own surface, which is always legible. */ +.plugin-open-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:transparent;color:var(--accent);border:1px solid var(--accent);border-radius:7px;font-size:13px;font-weight:600;cursor:pointer;text-decoration:none;} +.plugin-open-btn:hover{background:var(--accent);color:var(--bg);} /* ── Provider model tags ── */ .provider-card-models{ diff --git a/tests/test_plugins_panel.py b/tests/test_plugins_panel.py index 83992a0c..acb4915a 100644 --- a/tests/test_plugins_panel.py +++ b/tests/test_plugins_panel.py @@ -428,6 +428,7 @@ class TestPluginNameValidation: assert not plugins._VALID_PLUGIN_TAB_PATH.match("/x?y=1") # query assert not plugins._VALID_PLUGIN_TAB_PATH.match("/x#frag") # fragment assert not plugins._VALID_PLUGIN_TAB_PATH.match("/x y") # whitespace + assert not plugins._VALID_PLUGIN_TAB_PATH.match("//evil.example/p") # protocol-relative def test_open_button_and_toggle_use_no_inline_handlers(self): # tab.path / plugin.key must not be interpolated into inline onclick/