simplify plugin logic: remove install/uninstall buttons
external plugins are enabled iff authorized
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user