From 3e7d4749cf5399f524e02428c810526d6af9b5b0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 6 Apr 2024 11:35:57 +0200 Subject: [PATCH 1/6] turn classmethod 'find_all_plugins' into an instance method. change 'use_' prefix to 'enable_plugin_' --- electrum/plugin.py | 100 ++++++++++++------------ electrum/plugins/swapserver/__init__.py | 2 +- tests/regtest.py | 2 +- 3 files changed, 53 insertions(+), 51 deletions(-) diff --git a/electrum/plugin.py b/electrum/plugin.py index aad9bb896..32ff34757 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -57,7 +57,6 @@ class Plugins(DaemonThread): LOGGING_SHORTCUT = 'p' pkgpath = os.path.dirname(plugins.__file__) - _all_found_plugins = None # type: Optional[Dict[str, dict]] @profiler def __init__(self, config: SimpleConfig, gui_name): @@ -66,48 +65,44 @@ class Plugins(DaemonThread): self.config = config self.hw_wallets = {} self.plugins = {} # type: Dict[str, BasePlugin] + self.internal_plugin_metadata = {} self.gui_name = gui_name - self.descriptions = {} self.device_manager = DeviceMgr(config) + self.find_internal_plugins() self.load_plugins() self.add_jobs(self.device_manager.thread_jobs()) self.start() - @classmethod - def find_all_plugins(cls) -> Mapping[str, dict]: - """Return a map of all found plugins: name -> description. - Note that plugins not available for the current GUI are also included. - """ - if cls._all_found_plugins is None: - cls._all_found_plugins = dict() - iter_modules = list(pkgutil.iter_modules([cls.pkgpath])) - for loader, name, ispkg in iter_modules: - # FIXME pyinstaller binaries are packaging each built-in plugin twice: - # once as data and once as code. To honor the "no duplicates" rule below, - # we exclude the ones packaged as *code*, here: - if loader.__class__.__qualname__ == "FrozenImporter": - continue - full_name = f'electrum.plugins.{name}' - spec = importlib.util.find_spec(full_name) - if spec is None: # pkgutil found it but importlib can't ?! - raise Exception(f"Error pre-loading {full_name}: no spec") - try: - module = importlib.util.module_from_spec(spec) - # sys.modules needs to be modified for relative imports to work - # see https://stackoverflow.com/a/50395128 - sys.modules[spec.name] = module - spec.loader.exec_module(module) - except Exception as e: - raise Exception(f"Error pre-loading {full_name}: {repr(e)}") from e - d = module.__dict__ - if name in cls._all_found_plugins: - _logger.info(f"Found the following plugin modules: {iter_modules=}") - raise Exception(f"duplicate plugins? for {name=}") - cls._all_found_plugins[name] = d - return cls._all_found_plugins + @property + def descriptions(self): + return dict(list(self.internal_plugin_metadata.items())) - def load_plugins(self): - for name, d in self.find_all_plugins().items(): + def find_internal_plugins(self) -> Mapping[str, dict]: + """Populates self.internal_plugin_metadata + """ + iter_modules = list(pkgutil.iter_modules([self.pkgpath])) + for loader, name, ispkg in iter_modules: + # FIXME pyinstaller binaries are packaging each built-in plugin twice: + # once as data and once as code. To honor the "no duplicates" rule below, + # we exclude the ones packaged as *code*, here: + if loader.__class__.__qualname__ == "FrozenImporter": + continue + full_name = f'electrum.plugins.{name}' + spec = importlib.util.find_spec(full_name) + if spec is None: # pkgutil found it but importlib can't ?! + raise Exception(f"Error pre-loading {full_name}: no spec") + try: + module = importlib.util.module_from_spec(spec) + # sys.modules needs to be modified for relative imports to work + # see https://stackoverflow.com/a/50395128 + sys.modules[spec.name] = module + spec.loader.exec_module(module) + except Exception as e: + raise Exception(f"Error pre-loading {full_name}: {repr(e)}") from e + d = module.__dict__ + if 'fullname' not in d: + continue + d['display_name'] = d['fullname'] gui_good = self.gui_name in d.get('available_for', []) if not gui_good: continue @@ -117,8 +112,20 @@ class Plugins(DaemonThread): details = d.get('registers_keystore') if details: self.register_keystore(name, gui_good, details) - self.descriptions[name] = d - if not d.get('requires_wallet_type') and self.config.get('use_' + name): + if d.get('requires_wallet_type'): + # trustedcoin will not be added to list + continue + if name in self.internal_plugin_metadata: + _logger.info(f"Found the following plugin modules: {iter_modules=}") + raise Exception(f"duplicate plugins? for {name=}") + self.internal_plugin_metadata[name] = d + + def load_plugins(self): + self.load_internal_plugins() + + def load_internal_plugins(self): + for name, d in self.internal_plugin_metadata.items(): + if self.config.get('enable_plugin_' + name) is True: try: self.load_plugin(name) except BaseException as e: @@ -156,14 +163,14 @@ class Plugins(DaemonThread): self.remove_jobs(plugin.thread_jobs()) def enable(self, name: str) -> 'BasePlugin': - self.config.set_key('use_' + name, True, save=True) + self.config.set_key('enable_plugin_' + name, True, save=True) p = self.get(name) if p: return p return self.load_plugin(name) def disable(self, name: str) -> None: - self.config.set_key('use_' + name, False, save=True) + self.config.set_key('enable_plugin_' + name, False, save=True) p = self.get(name) if not p: return @@ -173,12 +180,7 @@ class Plugins(DaemonThread): @classmethod def is_plugin_enabler_config_key(cls, key: str) -> bool: - if not key.startswith('use_'): - return False - # note: the 'use_' prefix is not sufficient to check, there are - # non-plugin-related config keys that also have it... hence: - name = key[4:] - return name in cls.find_all_plugins() + return key.startswith('enable_plugin_') def toggle(self, name: str) -> Optional['BasePlugin']: p = self.get(name) @@ -204,7 +206,7 @@ class Plugins(DaemonThread): if gui_good: try: p = self.get_plugin(name) - if p.is_enabled(): + if p.is_available(): out.append(HardwarePluginToScan(name=name, description=details[2], plugin=p, @@ -276,7 +278,7 @@ class BasePlugin(Logger): self.parent = parent # type: Plugins # The plugins object self.name = name self.config = config - self.wallet = None + self.wallet = None # fixme: this field should not exist Logger.__init__(self) # add self to hooks for k in dir(self): @@ -313,7 +315,7 @@ class BasePlugin(Logger): return [] def is_enabled(self): - return self.is_available() and self.config.get('use_'+self.name) is True + return self.is_available() and self.config.get('enable_plugin_'+self.name) is True def is_available(self): return True diff --git a/electrum/plugins/swapserver/__init__.py b/electrum/plugins/swapserver/__init__.py index 1a7d3547b..ff236f6ef 100644 --- a/electrum/plugins/swapserver/__init__.py +++ b/electrum/plugins/swapserver/__init__.py @@ -6,7 +6,7 @@ Submarine swap server for an Electrum daemon. Example setup: - electrum -o setconfig use_swapserver True + electrum -o setconfig enable_plugin_swapserver True electrum -o setconfig swapserver_port 5455 electrum daemon -v diff --git a/tests/regtest.py b/tests/regtest.py index 68327fd4a..b7a94d64b 100644 --- a/tests/regtest.py +++ b/tests/regtest.py @@ -82,7 +82,7 @@ class TestLightningSwapserver(TestLightning): }, 'bob': { 'lightning_listen': 'localhost:9735', - 'use_swapserver': 'true', + 'enable_plugin_swapserver': 'true', } } From 858d999d313f3828d100e96d40416e391ed2d729 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 29 Nov 2023 12:13:34 +0100 Subject: [PATCH 2/6] Allow external plugins - borrows code brom ElectronCash - external plugins are imported as zip files - check hash from plugins.json file --- electrum/gui/qt/__init__.py | 2 +- electrum/gui/qt/plugins_dialog.py | 10 +-- electrum/plugin.py | 101 ++++++++++++++++++++++++++++-- electrum/simple_config.py | 11 ++-- 4 files changed, 109 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 7f5729d3e..a358980d7 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -150,7 +150,7 @@ class ElectrumGui(BaseElectrumGui, Logger): self.reload_app_stylesheet() # always load 2fa - self.plugins.load_plugin('trustedcoin') + self.plugins.load_internal_plugin('trustedcoin') run_hook('init_qt', self) diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index 21f7070c0..09cb4888b 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -63,15 +63,15 @@ class PluginsDialog(WindowModalDialog): run_hook('init_qt', self.window.gui_object) def show_list(self): - descriptions = self.plugins.descriptions.values() - for i, descr in enumerate(descriptions): - full_name = descr['__name__'] - prefix, _separator, name = full_name.rpartition('.') + descriptions = sorted(self.plugins.descriptions.items()) + i = 0 + for name, descr in descriptions: + i += 1 p = self.plugins.get(name) if descr.get('registers_keystore'): continue try: - cb = QCheckBox(descr['fullname']) + cb = QCheckBox(descr['display_name']) plugin_is_loaded = p is not None cb_enabled = (not plugin_is_loaded and self.plugins.is_available(name, self.wallet) or plugin_is_loaded and p.can_user_disable()) diff --git a/electrum/plugin.py b/electrum/plugin.py index 32ff34757..ed1717844 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -27,12 +27,18 @@ import pkgutil import importlib.util import time import threading +import traceback import sys +import json from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple, Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping, Set) import concurrent +import zipimport from concurrent import futures from functools import wraps, partial +from enum import IntEnum +from packaging.version import parse as parse_version +from electrum.version import ELECTRUM_VERSION from .i18n import _ from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException) @@ -53,6 +59,7 @@ hook_names = set() hooks = {} + class Plugins(DaemonThread): LOGGING_SHORTCUT = 'p' @@ -66,16 +73,21 @@ class Plugins(DaemonThread): self.hw_wallets = {} self.plugins = {} # type: Dict[str, BasePlugin] self.internal_plugin_metadata = {} + self.external_plugin_metadata = {} self.gui_name = gui_name self.device_manager = DeviceMgr(config) + self.user_pkgpath = os.path.join(self.config.electrum_path_root(), 'plugins') + if not os.path.exists(self.user_pkgpath): + os.mkdir(self.user_pkgpath) self.find_internal_plugins() + self.find_external_plugins() self.load_plugins() self.add_jobs(self.device_manager.thread_jobs()) self.start() @property def descriptions(self): - return dict(list(self.internal_plugin_metadata.items())) + return dict(list(self.internal_plugin_metadata.items()) + list(self.external_plugin_metadata.items())) def find_internal_plugins(self) -> Mapping[str, dict]: """Populates self.internal_plugin_metadata @@ -122,15 +134,88 @@ class Plugins(DaemonThread): def load_plugins(self): self.load_internal_plugins() + self.load_external_plugins() def load_internal_plugins(self): for name, d in self.internal_plugin_metadata.items(): - if self.config.get('enable_plugin_' + name) is True: + if not d.get('requires_wallet_type') and self.config.get('enable_plugin_' + name): try: - self.load_plugin(name) + self.load_internal_plugin(name) except BaseException as e: self.logger.exception(f"cannot initialize plugin {name}: {e}") + def load_external_plugin(self, name): + if name in self.plugins: + return self.plugins[name] + # If we do not have the metadata, it was not detected by `load_external_plugins` + # on startup, or added by manual user installation after that point. + metadata = self.external_plugin_metadata.get(name, None) + if metadata is None: + self.logger.exception("attempted to load unknown external plugin %s" % name) + return + + from .crypto import sha256 + external_plugin_dir = self.get_external_plugin_dir() + plugin_file_path = os.path.join(external_plugin_dir, name + '.zip') + if not os.path.exists(plugin_file_path): + return + with open(plugin_file_path, 'rb') as f: + s = f.read() + if sha256(s).hex() != metadata['hash']: + self.logger.exception("wrong hash for plugin '%s'" % plugin_file_path) + return + + try: + zipfile = zipimport.zipimporter(plugin_file_path) + except zipimport.ZipImportError: + self.logger.exception("unable to load zip plugin '%s'" % plugin_file_path) + return + try: + module = zipfile.load_module(name) + except zipimport.ZipImportError as e: + self.logger.exception(f"unable to load zip plugin '{plugin_file_path}' package '{name}'") + return + sys.modules['electrum_external_plugins.'+ name] = module + full_name = f'electrum_external_plugins.{name}.{self.gui_name}' + spec = importlib.util.find_spec(full_name) + if spec is None: + raise RuntimeError("%s implementation for %s plugin not found" + % (self.gui_name, name)) + module = importlib.util.module_from_spec(spec) + self._register_module(spec, module) + if sys.version_info >= (3, 10): + spec.loader.exec_module(module) + else: + module = spec.loader.load_module(full_name) + plugin = module.Plugin(self, self.config, name) + self.add_jobs(plugin.thread_jobs()) + self.plugins[name] = plugin + self.logger.info(f"loaded external plugin {name}") + return plugin + + @staticmethod + def _register_module(spec, module): + # sys.modules needs to be modified for relative imports to work + # see https://stackoverflow.com/a/50395128 + sys.modules[spec.name] = module + + def get_external_plugin_dir(self): + return self.user_pkgpath + + def find_external_plugins(self): + """ read json file """ + from .constants import read_json + self.external_plugin_metadata = read_json('plugins.json', {}) + + def load_external_plugins(self): + for name, d in self.external_plugin_metadata.items(): + if not d.get('requires_wallet_type') and self.config.get('use_' + name): + try: + self.load_external_plugin(name) + except BaseException as e: + traceback.print_exc(file=sys.stdout) # shouldn't this be... suppressed unless -v? + self.logger.exception(f"cannot initialize plugin {name} {e!r}") + def get(self, name): return self.plugins.get(name) @@ -141,9 +226,17 @@ class Plugins(DaemonThread): """Imports the code of the given plugin. note: can be called from any thread. """ + if name in self.internal_plugin_metadata: + return self.load_internal_plugin(name) + elif name in self.external_plugin_metadata: + return self.load_external_plugin(name) + else: + raise Exception() + + def load_internal_plugin(self, name) -> 'BasePlugin': if name in self.plugins: return self.plugins[name] - full_name = f'electrum.plugins.{name}.{self.gui_name}' + full_name = f'electrum.plugins.{name}' + f'.{self.gui_name}' spec = importlib.util.find_spec(full_name) if spec is None: raise RuntimeError("%s implementation for %s plugin not found" diff --git a/electrum/simple_config.py b/electrum/simple_config.py index a8edefc7f..3b85373d5 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -242,14 +242,15 @@ class SimpleConfig(Logger): self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP - def electrum_path(self): + def electrum_path_root(self): # Read electrum_path from command line # Otherwise use the user's default data directory. - path = self.get('electrum_path') - if path is None: - path = self.user_dir() - + path = self.get('electrum_path') or self.user_dir() make_dir(path, allow_symlink=False) + return path + + def electrum_path(self): + path = self.electrum_path_root() if self.get('testnet'): path = os.path.join(path, 'testnet') make_dir(path, allow_symlink=False) From ad774a49be5ae168de3da22cb895808ff6f7be72 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 7 Apr 2024 11:17:22 +0200 Subject: [PATCH 3/6] external plugins: add methods to read image files --- electrum/gui/qt/util.py | 4 ++++ electrum/plugin.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 531bb7a34..28392fa23 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1237,6 +1237,10 @@ def icon_path(icon_basename: str): def read_QIcon(icon_basename: str) -> QIcon: return QIcon(icon_path(icon_basename)) +def read_QIcon_from_bytes(b: bytes) -> QIcon: + qp = QPixmap() + qp.loadFromData(b) + return QIcon(qp) class IconLabel(QWidget): HorizontalSpacing = 2 diff --git a/electrum/plugin.py b/electrum/plugin.py index ed1717844..ac84ad231 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -422,6 +422,14 @@ class BasePlugin(Logger): def settings_dialog(self, window): raise NotImplementedError() + def read_file(self, filename: str) -> bytes: + """ note: only for external plugins """ + import zipfile + plugin_file_path = os.path.join(self.parent.get_external_plugin_dir(), self.name + '.zip') + with zipfile.ZipFile(plugin_file_path) as myzip: + with myzip.open(os.path.join(self.name, filename)) as myfile: + s = myfile.read() + return s class DeviceUnpairableError(UserFacingException): pass class HardwarePluginLibraryUnavailable(Exception): pass From f959b53222291dcb971ac97758cec0066b1aa2e7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 7 Apr 2024 13:15:38 +0200 Subject: [PATCH 4/6] add contrib/make_plugin --- contrib/make_plugin | 69 +++++++++++++++++++++++++++++++++++++++++++ electrum/plugins.json | 2 ++ 2 files changed, 71 insertions(+) create mode 100755 contrib/make_plugin create mode 100644 electrum/plugins.json diff --git a/contrib/make_plugin b/contrib/make_plugin new file mode 100755 index 000000000..ea0b0be24 --- /dev/null +++ b/contrib/make_plugin @@ -0,0 +1,69 @@ +#!/usr/bin/python3 +import os +import sys +import hashlib +import json +import zipfile +import zipimport + +# todo: use version number + +if len(sys.argv) != 2: + print(f"usage: {os.path.basename(__file__)} ", file=sys.stderr) + sys.exit(1) + +source_dir = sys.argv[1] # where the plugin source code is + +plugin_name = os.path.basename(source_dir) +dest_dir = os.path.dirname(source_dir) +zip_path = os.path.join(dest_dir, plugin_name + '.zip') + +# remove old zipfile +if os.path.exists(zip_path): + os.unlink(zip_path) +# create zipfile +print('creating', zip_path) +with zipfile.ZipFile(zip_path, 'w') as zip_object: + for folder_name, sub_folders, file_names in os.walk(source_dir): + for filename in file_names: + file_path = os.path.join(folder_name, filename) + dest_path = os.path.join(plugin_name, os.path.relpath(folder_name, source_dir), os.path.basename(file_path)) + zip_object.write(file_path, dest_path) + print('added', dest_path) + +# read hash +with open(zip_path, 'rb') as f: + s = f.read() +_hash = bytes(hashlib.sha256(s).digest()).hex() + +# read metadata +zip_file = zipimport.zipimporter(zip_path) +module = zip_file.load_module(plugin_name) +plugin_metadata = { + 'hash': _hash, + 'description': module.description, + 'display_name': module.fullname, + 'available_for': module.available_for, + 'download_url': module.download_url, + 'author': module.author, + 'licence': module.licence, + 'version': module.version, +} +print(json.dumps(plugin_metadata, indent=4)) + +# update plugins.json file +json_path = os.path.join(os.path.dirname(__file__), '..', 'electrum', 'plugins.json') +with open(json_path, 'r') as f: + s = f.read() +try: + metadata = json.loads(s) +except: + metadata = {} +metadata[plugin_name] = plugin_metadata +with open(json_path, 'w') as f: + f.write(json.dumps(metadata, indent=4)) + +# rename zip file +if module.version: + zip_path_with_version = os.path.join(dest_dir, plugin_name + '-' + module.version + '.zip') + os.rename(zip_path, zip_path_with_version) diff --git a/electrum/plugins.json b/electrum/plugins.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/electrum/plugins.json @@ -0,0 +1,2 @@ +{ +} From c9820aeca03b46d739acdbcedb52f0bd44181800 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 8 Apr 2024 09:54:48 +0200 Subject: [PATCH 5/6] Qt: add download_plugin_dialog --- electrum/gui/qt/plugins_dialog.py | 13 ++++++ electrum/plugin.py | 67 +++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index 09cb4888b..ca5d76102 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -56,12 +56,25 @@ class PluginsDialog(WindowModalDialog): self.grid.addWidget(widget, i, 1) def do_toggle(self, cb, name, i): + if self.plugins.requires_download(name): + cb.setChecked(False) + self.download_plugin_dialog(cb, name, i) + return p = self.plugins.toggle(name) cb.setChecked(bool(p)) self.enable_settings_widget(p, name, i) # note: all enabled plugins will receive this hook: run_hook('init_qt', self.window.gui_object) + def download_plugin_dialog(self, cb, name, i): + import asyncio + if not self.window.question("Download plugin '%s'?"%name): + return + coro = self.plugins.download_external_plugin(name) + def on_success(x): + self.do_toggle(cb, name, i) + self.window.run_coroutine_dialog(coro, "Downloading '%s' "%name, on_result=on_success, on_cancelled=None) + def show_list(self): descriptions = sorted(self.plugins.descriptions.items()) i = 0 diff --git a/electrum/plugin.py b/electrum/plugin.py index ac84ad231..c2b7c9b00 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -144,36 +144,68 @@ class Plugins(DaemonThread): except BaseException as e: self.logger.exception(f"cannot initialize plugin {name}: {e}") + def requires_download(self, name): + metadata = self.external_plugin_metadata.get(name) + if not metadata: + return False + if os.path.exists(self.external_plugin_path(name)): + return False + return True + + def check_plugin_hash(self, name: str)-> bool: + from .crypto import sha256 + metadata = self.external_plugin_metadata.get(name) + filename = self.external_plugin_path(name) + if not os.path.exists(filename): + return False + with open(filename, 'rb') as f: + s = f.read() + if sha256(s).hex() != metadata['hash']: + return False + return True + + async def download_external_plugin(self, name): + import aiohttp + metadata = self.external_plugin_metadata.get(name) + if metadata is None: + raise Exception("unknown external plugin %s" % name) + url = metadata['download_url'] + filename = self.external_plugin_path(name) + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + if resp.status == 200: + with open(filename, 'wb') as fd: + async for chunk in resp.content.iter_chunked(10): + fd.write(chunk) + if not self.check_plugin_hash(name): + os.unlink(filename) + raise Exception("wrong plugin hash %s" % name) + def load_external_plugin(self, name): if name in self.plugins: return self.plugins[name] # If we do not have the metadata, it was not detected by `load_external_plugins` # on startup, or added by manual user installation after that point. - metadata = self.external_plugin_metadata.get(name, None) + metadata = self.external_plugin_metadata.get(name) if metadata is None: self.logger.exception("attempted to load unknown external plugin %s" % name) return - - from .crypto import sha256 - external_plugin_dir = self.get_external_plugin_dir() - plugin_file_path = os.path.join(external_plugin_dir, name + '.zip') - if not os.path.exists(plugin_file_path): + filename = self.external_plugin_path(name) + if not os.path.exists(filename): return - with open(plugin_file_path, 'rb') as f: - s = f.read() - if sha256(s).hex() != metadata['hash']: - self.logger.exception("wrong hash for plugin '%s'" % plugin_file_path) + if not self.check_plugin_hash(name): + self.logger.exception("wrong hash for plugin '%s'" % name) + os.unlink(filename) return - try: - zipfile = zipimport.zipimporter(plugin_file_path) + zipfile = zipimport.zipimporter(filename) except zipimport.ZipImportError: - self.logger.exception("unable to load zip plugin '%s'" % plugin_file_path) + self.logger.exception("unable to load zip plugin '%s'" % filename) return try: module = zipfile.load_module(name) except zipimport.ZipImportError as e: - self.logger.exception(f"unable to load zip plugin '{plugin_file_path}' package '{name}'") + self.logger.exception(f"unable to load zip plugin '{filename}' package '{name}'") return sys.modules['electrum_external_plugins.'+ name] = module full_name = f'electrum_external_plugins.{name}.{self.gui_name}' @@ -202,6 +234,9 @@ class Plugins(DaemonThread): def get_external_plugin_dir(self): return self.user_pkgpath + def external_plugin_path(self, name): + return os.path.join(self.get_external_plugin_dir(), name + '.zip') + def find_external_plugins(self): """ read json file """ from .constants import read_json @@ -425,8 +460,8 @@ class BasePlugin(Logger): def read_file(self, filename: str) -> bytes: """ note: only for external plugins """ import zipfile - plugin_file_path = os.path.join(self.parent.get_external_plugin_dir(), self.name + '.zip') - with zipfile.ZipFile(plugin_file_path) as myzip: + plugin_filename = self.parent.external_plugin_path(self.name) + with zipfile.ZipFile(plugin_filename) as myzip: with myzip.open(os.path.join(self.name, filename)) as myfile: s = myfile.read() return s From ff07a77cfc9544e06976b6e3f8fbd65dd934dea9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 12 Apr 2024 17:42:01 +0200 Subject: [PATCH 6/6] move virtualkeyboard plugin to other repo add plugin metadata to electrum/plugins.json --- electrum/plugins.json | 14 +++- electrum/plugins/virtualkeyboard/__init__.py | 5 -- electrum/plugins/virtualkeyboard/qt.py | 67 -------------------- 3 files changed, 13 insertions(+), 73 deletions(-) delete mode 100644 electrum/plugins/virtualkeyboard/__init__.py delete mode 100644 electrum/plugins/virtualkeyboard/qt.py diff --git a/electrum/plugins.json b/electrum/plugins.json index 2c63c0851..600eadcb0 100644 --- a/electrum/plugins.json +++ b/electrum/plugins.json @@ -1,2 +1,14 @@ { -} + "virtualkeyboard": { + "hash": "4ab551ec5226e7bb26e21991b33c75387e8de66bd9c5bc402e940e6f8a17050b", + "description": "Add an optional virtual keyboard to the password dialog.\nWarning: do not use this if it makes you pick a weaker password.", + "display_name": "Virtual Keyboard", + "available_for": [ + "qt" + ], + "download_url": "https://raw.githubusercontent.com/spesmilo/electrum-plugins/master/virtualkeyboard-0.0.1.zip", + "author": "The Electrum developers", + "licence": "MIT", + "version": "0.0.1" + } +} \ No newline at end of file diff --git a/electrum/plugins/virtualkeyboard/__init__.py b/electrum/plugins/virtualkeyboard/__init__.py deleted file mode 100644 index c24f19b1a..000000000 --- a/electrum/plugins/virtualkeyboard/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from electrum.i18n import _ - -fullname = 'Virtual Keyboard' -description = '%s\n%s' % (_("Add an optional virtual keyboard to the password dialog."), _("Warning: do not use this if it makes you pick a weaker password.")) -available_for = ['qt'] diff --git a/electrum/plugins/virtualkeyboard/qt.py b/electrum/plugins/virtualkeyboard/qt.py deleted file mode 100644 index a234462af..000000000 --- a/electrum/plugins/virtualkeyboard/qt.py +++ /dev/null @@ -1,67 +0,0 @@ -import random - -from PyQt5.QtWidgets import (QVBoxLayout, QGridLayout, QPushButton) -from PyQt5.QtGui import QFontMetrics - -from electrum.plugin import BasePlugin, hook -from electrum.i18n import _ - - -class Plugin(BasePlugin): - vkb = None - vkb_index = 0 - - @hook - def password_dialog(self, pw, grid, pos): - vkb_button = QPushButton("+") - font_height = QFontMetrics(vkb_button.font()).height() - vkb_button.setFixedWidth(round(1.7 * font_height)) - vkb_button.clicked.connect(lambda: self.toggle_vkb(grid, pw)) - grid.addWidget(vkb_button, pos, 2) - self.kb_pos = 2 - self.vkb = None - - def toggle_vkb(self, grid, pw): - if self.vkb: - grid.removeItem(self.vkb) - self.vkb = self.virtual_keyboard(self.vkb_index, pw) - grid.addLayout(self.vkb, self.kb_pos, 0, 1, 3) - self.vkb_index += 1 - - def virtual_keyboard(self, i, pw): - i = i % 3 - if i == 0: - chars = 'abcdefghijklmnopqrstuvwxyz ' - elif i == 1: - chars = 'ABCDEFGHIJKLMNOPQRTSUVWXYZ ' - elif i == 2: - chars = '1234567890!?.,;:/%&()[]{}+-' - - n = len(chars) - s = [] - for i in range(n): - while True: - k = random.randint(0, n - 1) - if k not in s: - s.append(k) - break - - def add_target(t): - return lambda: pw.setText(str(pw.text()) + t) - - font_height = QFontMetrics(QPushButton().font()).height() - btn_size = max(25, round(1.7 * font_height)) - - vbox = QVBoxLayout() - grid = QGridLayout() - grid.setSpacing(2) - for i in range(n): - l_button = QPushButton(chars[s[i]]) - l_button.setFixedWidth(btn_size) - l_button.setFixedHeight(btn_size) - l_button.clicked.connect(add_target(chars[s[i]])) - grid.addWidget(l_button, i // 6, i % 6) - - vbox.addLayout(grid) - - return vbox