mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-10 17:16:36 +00:00
fix(store): hide and refuse registry entries that aren't plugins
The skin filters went with the skin system, but a custom registry can still list "type": "skin" entries, and installing one as a plugin would unpack it into the plugins directory. PluginStoreManager.is_plugin_entry() (a missing type means plugin) now hides non-plugin entries from the store and custom-registry listings, and install refuses them, in the route with a clear 400 and in _install_plugin_impl for any other caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,17 @@ class PluginStoreManager:
|
|||||||
# would delete — an empty id resolves to the plugins root itself.
|
# would delete — an empty id resolves to the plugins root itself.
|
||||||
_PLUGIN_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
_PLUGIN_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_plugin_entry(entry) -> bool:
|
||||||
|
"""Whether a registry entry is a plugin core can install.
|
||||||
|
|
||||||
|
A missing ``type`` means plugin. Anything else (registries used to
|
||||||
|
carry ``"type": "skin"`` entries, and a custom registry still can) is
|
||||||
|
hidden from the store and refused at install, rather than being
|
||||||
|
unpacked into the plugins directory as if it were a plugin.
|
||||||
|
"""
|
||||||
|
return isinstance(entry, dict) and (entry.get('type') or 'plugin') == 'plugin'
|
||||||
|
|
||||||
def __init__(self, plugins_dir: str = "plugins",
|
def __init__(self, plugins_dir: str = "plugins",
|
||||||
uninstalled_registry_path: Optional[str] = None):
|
uninstalled_registry_path: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
@@ -1314,6 +1325,10 @@ class PluginStoreManager:
|
|||||||
if not plugin_info:
|
if not plugin_info:
|
||||||
self.logger.error(f"Plugin not found in registry: {plugin_id}")
|
self.logger.error(f"Plugin not found in registry: {plugin_id}")
|
||||||
return False
|
return False
|
||||||
|
if not self.is_plugin_entry(plugin_info):
|
||||||
|
self.logger.error(f"Not installing {plugin_id}: registry entry type "
|
||||||
|
f"{plugin_info.get('type')!r} is not a plugin")
|
||||||
|
return False
|
||||||
|
|
||||||
repo_url = plugin_info.get('repo')
|
repo_url = plugin_info.get('repo')
|
||||||
if not repo_url:
|
if not repo_url:
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Registry entries that aren't plugins are hidden and refused.
|
||||||
|
|
||||||
|
The official registry no longer lists skins, but a custom registry (added
|
||||||
|
from the Plugin Store) can still carry ``"type": "skin"`` entries. With the
|
||||||
|
skin system removed, installing one as a plugin would unpack it into the
|
||||||
|
plugins directory, so the store hides such entries and refuses them at install.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.plugin_system.store_manager import PluginStoreManager
|
||||||
|
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401
|
||||||
|
|
||||||
|
PLUGIN = {"id": "clock", "name": "Clock", "repo": "https://github.com/x/clock"}
|
||||||
|
UNTYPED = {"id": "news", "name": "News", "repo": "https://github.com/x/news"}
|
||||||
|
SKIN = {"id": "retro", "name": "Retro", "type": "skin", "repo": "https://github.com/x/retro"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("entry,expected", [
|
||||||
|
(PLUGIN, True),
|
||||||
|
({**PLUGIN, "type": "plugin"}, True),
|
||||||
|
({**PLUGIN, "type": None}, True),
|
||||||
|
(SKIN, False),
|
||||||
|
({**PLUGIN, "type": "theme"}, False),
|
||||||
|
(None, False),
|
||||||
|
("clock", False),
|
||||||
|
])
|
||||||
|
def test_is_plugin_entry(entry, expected):
|
||||||
|
assert PluginStoreManager.is_plugin_entry(entry) is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_refuses_a_non_plugin_entry(tmp_path):
|
||||||
|
store = PluginStoreManager(plugins_dir=str(tmp_path / "plugins"))
|
||||||
|
store.get_plugin_info = MagicMock(return_value=dict(SKIN))
|
||||||
|
store._install_from_monorepo_zip = MagicMock()
|
||||||
|
store._install_via_git = MagicMock()
|
||||||
|
assert store._install_plugin_impl("retro") is False
|
||||||
|
assert not (tmp_path / "plugins" / "retro").exists()
|
||||||
|
store._install_from_monorepo_zip.assert_not_called()
|
||||||
|
store._install_via_git.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def store(api_v3_module):
|
||||||
|
mock = MagicMock()
|
||||||
|
mock.is_plugin_entry = PluginStoreManager.is_plugin_entry
|
||||||
|
api_v3_module.api_v3.plugin_store_manager = mock
|
||||||
|
return mock
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_registry_listing_hides_non_plugins(api_v3_client, store):
|
||||||
|
store.fetch_registry_from_url.return_value = {"plugins": [PLUGIN, SKIN, UNTYPED]}
|
||||||
|
resp = api_v3_client.post("/api/v3/plugins/registry-from-url",
|
||||||
|
data=json.dumps({"repo_url": "https://github.com/x/registry"}),
|
||||||
|
content_type="application/json")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert [p["id"] for p in resp.get_json()["plugins"]] == ["clock", "news"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_listing_hides_non_plugins(api_v3_client, store):
|
||||||
|
store.search_plugins.return_value = [PLUGIN, SKIN, UNTYPED]
|
||||||
|
resp = api_v3_client.get("/api/v3/plugins/store/list")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.get_json()
|
||||||
|
listed = body.get("data", body).get("plugins", [])
|
||||||
|
assert [p["id"] for p in listed] == ["clock", "news"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_route_refuses_a_non_plugin_entry(api_v3_client, store):
|
||||||
|
store.get_registry_info.return_value = dict(SKIN)
|
||||||
|
resp = api_v3_client.post("/api/v3/plugins/install",
|
||||||
|
data=json.dumps({"plugin_id": "retro"}),
|
||||||
|
content_type="application/json")
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert "not a plugin" in resp.get_json()["message"]
|
||||||
|
store.install_plugin.assert_not_called()
|
||||||
@@ -1296,6 +1296,17 @@ def install_plugin():
|
|||||||
plugin_id = data['plugin_id']
|
plugin_id = data['plugin_id']
|
||||||
branch = data.get('branch') # Optional branch parameter
|
branch = data.get('branch') # Optional branch parameter
|
||||||
|
|
||||||
|
# A registry entry that isn't a plugin (a custom registry can still
|
||||||
|
# list old "type": "skin" entries) gets a clear refusal, not a failed
|
||||||
|
# install.
|
||||||
|
try:
|
||||||
|
registry_entry = api_v3.plugin_store_manager.get_registry_info(plugin_id)
|
||||||
|
except Exception:
|
||||||
|
registry_entry = None
|
||||||
|
if isinstance(registry_entry, dict) and not api_v3.plugin_store_manager.is_plugin_entry(registry_entry):
|
||||||
|
return jsonify({'status': 'error',
|
||||||
|
'message': f"{plugin_id} is a {registry_entry.get('type')!r} entry, not a plugin"}), 400
|
||||||
|
|
||||||
# Install the plugin
|
# Install the plugin
|
||||||
# Log the plugins directory being used for debugging
|
# Log the plugins directory being used for debugging
|
||||||
plugins_dir = api_v3.plugin_store_manager.plugins_dir
|
plugins_dir = api_v3.plugin_store_manager.plugins_dir
|
||||||
@@ -1498,7 +1509,8 @@ def get_registry_from_url():
|
|||||||
if registry:
|
if registry:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'plugins': registry.get('plugins', []),
|
'plugins': [p for p in registry.get('plugins', [])
|
||||||
|
if api_v3.plugin_store_manager.is_plugin_entry(p)],
|
||||||
'registry_url': repo_url
|
'registry_url': repo_url
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
@@ -1613,6 +1625,8 @@ def list_plugin_store():
|
|||||||
# Format plugins for the web interface
|
# Format plugins for the web interface
|
||||||
formatted_plugins = []
|
formatted_plugins = []
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
|
if not api_v3.plugin_store_manager.is_plugin_entry(plugin):
|
||||||
|
continue
|
||||||
formatted_plugins.append({
|
formatted_plugins.append({
|
||||||
'id': plugin.get('id'),
|
'id': plugin.get('id'),
|
||||||
'name': plugin.get('name'),
|
'name': plugin.get('name'),
|
||||||
|
|||||||
Reference in New Issue
Block a user