From 79941529d2587d4055e327a090e15d0025b8af51 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 17 May 2025 11:14:13 +0200 Subject: [PATCH] simplify plugin logic: remove install/uninstall buttons external plugins are enabled iff authorized --- electrum/gui/qt/plugins_dialog.py | 111 ++++++++++-------------------- electrum/plugin.py | 33 ++++----- 2 files changed, 52 insertions(+), 92 deletions(-) diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index e752c333b..b29466d22 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -65,35 +65,32 @@ class PluginDialog(WindowModalDialog): close_button = CloseButton(self) close_button.setText(_('Close')) buttons = [close_button] - if not self.plugins.is_installed(name): - install_button = QPushButton(_('Install...')) - install_button.clicked.connect(self.accept) - buttons.insert(0, install_button) - elif self.plugins.get_metadata(name).get('zip_hash_sha256') != zip_hash: - update_button = QPushButton(_('Update...')) - update_button.clicked.connect(self.accept) - buttons.insert(0, update_button) - else: - remove_button = QPushButton(_('Uninstall')) - remove_button.clicked.connect(self.do_remove) - buttons.insert(0, remove_button) - if not self.plugins.is_authorized(name): - auth_button = QPushButton(_('Authorize...')) + p = self.plugins.get(name) + is_enabled = p and p.is_enabled() + is_external = self.plugins.is_external(name) + if is_external: + is_authorized = self.plugins.is_authorized(name) + if status_button is not None: + # status_button is None when called from add_external_plugin + remove_button = QPushButton('') + remove_button.clicked.connect(self.do_remove) + remove_button.setText(_('Remove')) + buttons.insert(0, remove_button) + if not is_authorized: + auth_button = QPushButton('Authorize') auth_button.clicked.connect(self.do_authorize) buttons.insert(0, auth_button) - elif not self.plugins.is_auto_loaded(name): - toggle_button = QPushButton('') - p = self.plugins.get(name) - is_enabled = p and p.is_enabled() - toggle_button.setText(_('Disable') if is_enabled else _('Enable')) - toggle_button.clicked.connect(self.do_toggle) - buttons.insert(0, toggle_button) - # add settings button - if p and p.requires_settings() and p.is_enabled(): - settings_button = EnterButton( - _('Settings'), - partial(p.settings_dialog, self)) - buttons.insert(1, settings_button) + else: + toggle_button = QPushButton('') + toggle_button.setText(_('Disable') if is_enabled else _('Enable')) + toggle_button.clicked.connect(self.do_toggle) + buttons.insert(0, toggle_button) + # add settings button + if p and p.requires_settings() and p.is_enabled(): + settings_button = EnterButton( + _('Settings'), + partial(p.settings_dialog, self)) + buttons.insert(1, settings_button) # add buttons vbox.addLayout(Buttons(*buttons)) @@ -112,9 +109,10 @@ class PluginDialog(WindowModalDialog): return filename = self.plugins.zip_plugin_path(self.name) self.window.plugins.authorize_plugin(self.name, filename, privkey) + self.window.plugins.enable(self.name) if self.status_button: self.status_button.update() - self.close() + self.accept() class PluginStatusButton(QPushButton): @@ -136,21 +134,12 @@ class PluginStatusButton(QPushButton): from .util import ColorScheme p = self.plugins.get(self.name) plugin_is_loaded = p is not None - enabled = ( - not plugin_is_loaded - or plugin_is_loaded and p.can_user_disable() - ) + enabled = not plugin_is_loaded or (plugin_is_loaded and p.can_user_disable()) self.setEnabled(enabled) - if not self.window.plugins.is_authorized(self.name): - text, color = _('Unauthorized'), ColorScheme.RED + if p is not None and p.is_enabled(): + text, color = _('Enabled'), ColorScheme.BLUE else: - if self.window.plugins.is_auto_loaded(self.name): - text, color = _('Auto-loaded'), ColorScheme.DEFAULT - else: - if p is not None and p.is_enabled(): - text, color = _('Enabled'), ColorScheme.BLUE - else: - text, color = _('Disabled'), ColorScheme.DEFAULT + text, color = _('Disabled'), ColorScheme.RED self.setStyleSheet(color.as_stylesheet()) self.setText(text) @@ -158,7 +147,7 @@ class PluginStatusButton(QPushButton): class PluginsDialog(WindowModalDialog, MessageBoxMixin): _logger = get_logger(__name__) - def __init__(self, config: 'SimpleConfig', plugins:'Plugins', *, gui_object: Optional['ElectrumGui'] = None): + def __init__(self, config: 'SimpleConfig', plugins: 'Plugins', *, gui_object: Optional['ElectrumGui'] = None): WindowModalDialog.__init__(self, None, _('Electrum Plugins')) self.gui_object = gui_object self.config = config @@ -177,17 +166,8 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): add_button = QPushButton(_('Add')) add_button.setMinimumWidth(40) # looks better on windows, no difference on linux menu = QMenu() - for name, item in self.plugins.internal_plugin_metadata.items(): - fullname = item['fullname'] - if not fullname: - continue - if self.plugins.is_auto_loaded(name): - continue - menu.addAction(fullname, partial(self.add_internal_plugin, name)) - menu.addSeparator() - m3 = menu.addMenu('Third-party plugin') - m3.addAction(_('Local ZIP file'), self.add_plugin_dialog) - m3.addAction(_('Download ZIP file'), self.download_plugin_dialog) + menu.addAction(_('Local ZIP file'), self.add_plugin_dialog) + menu.addAction(_('Download ZIP file'), self.download_plugin_dialog) add_button.setMenu(menu) vbox.addLayout(Buttons(add_button, CloseButton(self))) self.show_list() @@ -343,26 +323,16 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): def add_external_plugin(self, path): manifest = self.plugins.read_manifest(path) name = manifest['name'] + self.plugins.external_plugin_metadata[name] = manifest d = PluginDialog(name, manifest, None, self) if not d.exec(): + self.plugins.external_plugin_metadata.pop(name) return False - # ask password once user has approved - privkey = self.get_plugins_privkey() - if not privkey: - return False - self.plugins.install_external_plugin(name, path, privkey, manifest) + if self.gui_object: + self.gui_object.reload_windows() self.show_list() return True - def add_internal_plugin(self, name): - """ simply set the config """ - manifest = self.plugins.internal_plugin_metadata[name] - d = PluginDialog(name, manifest, None, self) - if not d.exec(): - return False - self.plugins.install_internal_plugin(name) - self.show_list() - def show_list(self): descriptions = self.plugins.descriptions descriptions = sorted(descriptions.items()) @@ -376,8 +346,6 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): i += 1 if self.plugins.is_internal(name) and self.plugins.is_auto_loaded(name): continue - if not self.plugins.is_installed(name): - continue display_name = metadata.get('fullname') if not display_name: continue @@ -393,11 +361,6 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin): grid.setRowStretch(i + 1, 1) def do_toggle(self, name, status_button): - if not self.plugins.is_authorized(name): - #self.show_plugin_dialog(name, status_button) - return - if self.plugins.is_auto_loaded(name): - return p = self.plugins.get(name) is_enabled = p and p.is_enabled() if is_enabled: diff --git a/electrum/plugin.py b/electrum/plugin.py index 9705b57ce..ddc4b8383 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -587,14 +587,13 @@ class Plugins(DaemonThread): def maybe_load_plugin_init_method(self, name: str) -> None: """Loads the __init__.py module of the plugin if it is not already loaded.""" - is_external = name in self.external_plugin_metadata - base_name = ('electrum_external_plugins.' if is_external else 'electrum.plugins.') + name + base_name = ('electrum_external_plugins.' if self.is_external(name) else 'electrum.plugins.') + name if base_name not in sys.modules: metadata = self.get_metadata(name) is_zip = metadata.get('is_zip', False) # if the plugin was not enabled on startup the init module hasn't been loaded yet if not is_zip: - if is_external: + if self.is_external(name): # this branch is deprecated: external plugins are always zip files path = os.path.join(metadata['path'], '__init__.py') init_spec = importlib.util.spec_from_file_location(base_name, path) @@ -612,7 +611,7 @@ class Plugins(DaemonThread): return self.plugins[name] # if the plugin was not enabled on startup the init module hasn't been loaded yet self.maybe_load_plugin_init_method(name) - is_external = name in self.external_plugin_metadata + is_external = self.is_external(name) if is_external and not self.is_authorized(name): self.logger.info(f'plugin not authorized {name}') return @@ -642,15 +641,6 @@ class Plugins(DaemonThread): secret = pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations=10**5) return ECPrivkey(secret) - def install_internal_plugin(self, name): - self.config.set_key(f'plugins.{name}.enabled', []) - - def install_external_plugin(self, name, path, privkey, manifest): - # uninstall old version first to get rid of old zip files when updating plugin - self.uninstall(name) - self.external_plugin_metadata[name] = manifest - self.authorize_plugin(name, path, privkey) - def uninstall(self, name: str): if self.config.get(f'plugins.{name}'): self.config.set_key(f'plugins.{name}', None) @@ -662,14 +652,16 @@ class Plugins(DaemonThread): def is_internal(self, name) -> bool: return name in self.internal_plugin_metadata + def is_external(self, name) -> bool: + return name in self.external_plugin_metadata + def is_auto_loaded(self, name): metadata = self.external_plugin_metadata.get(name) or self.internal_plugin_metadata.get(name) return metadata and (metadata.get('registers_keystore') or metadata.get('registers_wallet_type')) def is_installed(self, name) -> bool: """an external plugin may be installed but not authorized """ - return (name in self.internal_plugin_metadata and self.config.get(f'plugins.{name}'))\ - or name in self.external_plugin_metadata + return (name in self.internal_plugin_metadata or name in self.external_plugin_metadata) def is_authorized(self, name) -> bool: if name in self.internal_plugin_metadata: @@ -695,7 +687,8 @@ class Plugins(DaemonThread): plugin_hash = get_file_hash256(filename) sig = privkey.ecdsa_sign(plugin_hash) value = sig.hex() - self.config.set_key(f'plugins.{name}.authorized', value, save=True) + self.config.set_key(f'plugins.{name}.authorized', value) + self.config.set_key(f'plugins.{name}.enabled', True) def enable(self, name: str) -> 'BasePlugin': self.config.enable_plugin(name) @@ -802,11 +795,13 @@ class Plugins(DaemonThread): with zipfile_lib.ZipFile(plugin_filename) as myzip: with myzip.open(os.path.join(dirname, filename)) as myfile: return myfile.read() - else: - assert name in self.internal_plugin_metadata + elif name in self.internal_plugin_metadata: path = os.path.join(os.path.dirname(__file__), 'plugins', name, filename) with open(path, 'rb') as myfile: return myfile.read() + else: + # no icon + return None def get_file_hash256(path: str) -> bytes: '''Get the sha256 hash of a file, similar to `sha256sum`.''' @@ -881,6 +876,8 @@ class BasePlugin(Logger): def is_enabled(self): if not self.is_available(): return False + if not self.parent.is_authorized(self.name): + return False return self.config.is_plugin_enabled(self.name) def is_available(self):