URI: 
       tFix issue #6201: - Pass a proper callback to WalletDialog (we used to call load_wallet_by_name recursively) - Do not cache PasswordDialog instances - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 88bb5309c4d74adb7ba1ac24e5bcb05ef34b2bf9
   DIR parent dc6dbe5bfba448dba739b33bff8041f9ccfa0d62
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Thu,  4 Jun 2020 07:51:14 +0200
       
       Fix issue #6201:
        - Pass a proper callback to WalletDialog
          (we used to call load_wallet_by_name recursively)
        - Do not cache PasswordDialog instances
       
       Diffstat:
         M electrum/gui/kivy/main_window.py    |      93 ++++++++++---------------------
         M electrum/gui/kivy/uix/dialogs/inst… |       3 +--
         M electrum/gui/kivy/uix/dialogs/pass… |      92 +++++++++++++++++++++++++++++--
         M electrum/gui/kivy/uix/dialogs/wall… |      24 +++++++++---------------
       
       4 files changed, 125 insertions(+), 87 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -34,7 +34,7 @@ from kivy.clock import Clock
        from kivy.factory import Factory
        from kivy.metrics import inch
        from kivy.lang import Builder
       -from .uix.dialogs.password_dialog import PasswordDialog, PincodeDialog
       +from .uix.dialogs.password_dialog import OpenWalletDialog, ChangePasswordDialog, PincodeDialog
        
        ## lazy imports for factory so that widgets can be used in kv
        #Factory.register('InstallWizard', module='electrum.gui.kivy.uix.dialogs.installwizard')
       t@@ -379,8 +379,6 @@ class ElectrumWindow(App):
        
                # cached dialogs
                self._settings_dialog = None
       -        self._pincode_dialog = None
       -        self._password_dialog = None
                self._channels_dialog = None
                self._addresses_dialog = None
                self.fee_status = self.electrum_config.get_fee_status()
       t@@ -635,43 +633,10 @@ class ElectrumWindow(App):
                    return
                if self.wallet and self.wallet.storage.path == path:
                    return
       -        wallet = self.daemon.load_wallet(path, None)
       -        if wallet:
       -            if wallet.has_password():
       -                def on_success(x):
       -                    # save password in memory
       -                    self.password = x
       -                    self.load_wallet(wallet)
       -                self.password_dialog(
       -                    basename = wallet.basename(),
       -                    check_password=wallet.check_password,
       -                    on_success=on_success,
       -                    on_failure=self.stop)
       -            else:
       -                self.load_wallet(wallet)
                else:
                    def launch_wizard():
       -                storage = WalletStorage(path)
       -                if not storage.file_exists():
       -                    wizard = Factory.InstallWizard(self.electrum_config, self.plugins)
       -                    wizard.path = path
       -                    wizard.bind(on_wizard_complete=self.on_wizard_complete)
       -                    wizard.run('new')
       -                else:
       -                    if storage.is_encrypted():
       -                        if not storage.is_encrypted_with_user_pw():
       -                            raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
       -                        def on_password(pw):
       -                            self.password = pw
       -                            storage.decrypt(pw)
       -                            self._on_decrypted_storage(storage)
       -                        self.password_dialog(
       -                            basename = storage.basename(),
       -                            check_password=storage.check_password,
       -                            on_success=on_password,
       -                            on_failure=self.stop)
       -                        return
       -                    self._on_decrypted_storage(storage)
       +                d = OpenWalletDialog(self, path, self.on_open_wallet)
       +                d.open()
                    if not ask_if_wizard:
                        launch_wizard()
                    else:
       t@@ -685,6 +650,21 @@ class ElectrumWindow(App):
                        d = Question(_('Do you want to launch the wizard again?'), handle_answer)
                        d.open()
        
       +    def on_open_wallet(self, pw, storage):
       +        if not storage.file_exists():
       +            wizard = Factory.InstallWizard(self.electrum_config, self.plugins)
       +            wizard.path = storage.path
       +            wizard.bind(on_wizard_complete=self.on_wizard_complete)
       +            wizard.run('new')
       +        else:
       +            try:
       +                storage.decrypt(pw)
       +            except StorageReadWriteError:
       +                app.show_error(_("R/W error accessing path"))
       +                return
       +            self.password = pw
       +            self._on_decrypted_storage(storage)
       +
            def on_stop(self):
                Logger.info('on_stop')
                self.stop_wallet()
       t@@ -749,8 +729,8 @@ class ElectrumWindow(App):
        
            def wallets_dialog(self):
                from .uix.dialogs.wallets import WalletDialog
       -        d = WalletDialog()
       -        d.path = os.path.dirname(self.electrum_config.get_wallet_path())
       +        dirname = os.path.dirname(self.electrum_config.get_wallet_path())
       +        d = WalletDialog(dirname, self.load_wallet_by_name)
                d.open()
        
            def popup_dialog(self, name):
       t@@ -969,7 +949,8 @@ class ElectrumWindow(App):
            def on_resume(self):
                now = time.time()
                if self.wallet and self.wallet.has_password() and now - self.pause_time > 5*60:
       -            self.pincode_dialog(check_password=self.check_pin_code, on_success=None, on_failure=self.stop)
       +            d = PincodeDialog(check_password=self.check_pin_code, on_success=None, on_failure=self.stop)
       +            d.open()
                if self.nfcscanner:
                    self.nfcscanner.nfc_enable()
        
       t@@ -1150,11 +1131,12 @@ class ElectrumWindow(App):
                if self.electrum_config.get('pin_code'):
                    msg += "\n" + _("Enter your PIN code to proceed")
                    on_success = lambda pw: f(*args, self.password)
       -            self.pincode_dialog(
       +            d = PincodeDialog(
                        message = msg,
                        check_password=self.check_pin_code,
                        on_success=on_success,
                        on_failure=lambda: None)
       +            d.open()
                else:
                    d = Question(
                        msg,
       t@@ -1242,45 +1224,28 @@ class ElectrumWindow(App):
                if pin != self.electrum_config.get('pin_code'):
                    raise InvalidPassword
        
       -    def password_dialog(self, **kwargs):
       -        if self._password_dialog is None:
       -            self._password_dialog = PasswordDialog()
       -        self._password_dialog.init(self, **kwargs)
       -        self._password_dialog.open()
       -
       -    def pincode_dialog(self, **kwargs):
       -        if self._pincode_dialog is None:
       -            self._pincode_dialog = PincodeDialog()
       -        self._pincode_dialog.init(self, **kwargs)
       -        self._pincode_dialog.open()
       -
            def change_password(self, cb):
                def on_success(old_password, new_password):
                    self.wallet.update_password(old_password, new_password)
                    self.password = new_password
                    self.show_info(_("Your password was updated"))
                on_failure = lambda: self.show_error(_("Password not updated"))
       -        self.password_dialog(
       -            basename = self.wallet.basename(),
       -            check_password = self.wallet.check_password,
       -            on_success=on_success, on_failure=on_failure,
       -            is_change=True,
       -            has_password=self.wallet.has_password())
       +        d = ChangePasswordDialog(self, self.wallet, on_success, on_failure)
       +        d.open()
        
            def change_pin_code(self, cb):
       -        if self._pincode_dialog is None:
       -            self._pincode_dialog = PincodeDialog()
                def on_success(old_password, new_password):
                    self.electrum_config.set_key('pin_code', new_password)
                    cb()
                    self.show_info(_("PIN updated") if new_password else _('PIN disabled'))
                on_failure = lambda: self.show_error(_("PIN not updated"))
       -        self._pincode_dialog.init(
       +
       +        d = PincodeDialog(
                    self, check_password=self.check_pin_code,
                    on_success=on_success, on_failure=on_failure,
                    is_change=True,
                    has_password = self.has_pin_code())
       -        self._pincode_dialog.open()
       +        d.open()
        
            def save_backup(self):
                if platform != 'android':
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py
       t@@ -1168,9 +1168,8 @@ class InstallWizard(BaseWizard, Widget):
                def on_failure():
                    self.show_error(_('Password mismatch'))
                    self.run('request_password', run_next)
       -        popup = PasswordDialog()
                app = App.get_running_app()
       -        popup.init(
       +        popup = PasswordDialog(
                    app,
                    check_password=lambda x:True,
                    on_success=on_success,
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/password_dialog.py b/electrum/gui/kivy/uix/dialogs/password_dialog.py
       t@@ -1,4 +1,5 @@
        from typing import Callable, TYPE_CHECKING, Optional, Union
       +import os
        
        from kivy.app import App
        from kivy.factory import Factory
       t@@ -8,8 +9,11 @@ from decimal import Decimal
        from kivy.clock import Clock
        
        from electrum.util import InvalidPassword
       +from electrum.wallet import WalletStorage
        from electrum.gui.kivy.i18n import _
        
       +from .wallets import WalletDialog
       +
        if TYPE_CHECKING:
            from ...main_window import ElectrumWindow
            from electrum.wallet import Abstract_Wallet
       t@@ -23,6 +27,7 @@ Builder.load_string('''
            message: ''
            basename:''
            is_change: False
       +    require_password: True
            BoxLayout:
                size_hint: 1, 1
                orientation: 'vertical'
       t@@ -57,6 +62,8 @@ Builder.load_string('''
                BoxLayout:
                    orientation: 'horizontal'
                    id: box_generic_password
       +            disabled: not root.require_password
       +            opacity: int(root.require_password)
                    size_hint_y: 0.05
                    height: '40dp'
                    TextInput:
       t@@ -79,6 +86,20 @@ Builder.load_string('''
                            textinput_generic_password.password = False if textinput_generic_password.password else True
                Widget:
                    size_hint: 1, 1
       +        BoxLayout:
       +            orientation: 'horizontal'
       +            size_hint: 1, 0.5
       +            Button:
       +                text: 'Cancel'
       +                size_hint: 0.5, None
       +                height: '48dp'
       +                on_release: popup.dismiss()
       +            Button:
       +                text: 'Next'
       +                size_hint: 0.5, None
       +                height: '48dp'
       +                on_release:
       +                    popup.on_password(textinput_generic_password.text)
        
        
        <PincodeDialog@Popup>
       t@@ -142,9 +163,9 @@ Builder.load_string('''
        ''')
        
        
       -class AbstractPasswordDialog:
       +class AbstractPasswordDialog(Factory.Popup):
        
       -    def init(self, app: 'ElectrumWindow', *,
       +    def __init__(self, app: 'ElectrumWindow', *,
                     check_password = None,
                     on_success: Callable = None, on_failure: Callable = None,
                     is_change: bool = False,
       t@@ -152,6 +173,7 @@ class AbstractPasswordDialog:
                     has_password: bool = False,
                     message: str = '',
                     basename:str=''):
       +        Factory.Popup.__init__(self)
                self.app = app
                self.pw_check = check_password
                self.message = message
       t@@ -234,17 +256,26 @@ class AbstractPasswordDialog:
                    self.clear_password()
        
        
       -class PasswordDialog(AbstractPasswordDialog, Factory.Popup):
       +class PasswordDialog(AbstractPasswordDialog):
            enter_pw_message = _('Enter your password')
            enter_new_pw_message = _('Enter new password')
            confirm_new_pw_message = _('Confirm new password')
            wrong_password_message = _('Wrong password')
            allow_disable = False
        
       +    def __init__(self, app, **kwargs):
       +        AbstractPasswordDialog.__init__(self, app, **kwargs)
       +
            def clear_password(self):
                self.ids.textinput_generic_password.text = ''
        
            def on_password(self, pw: str):
       +        #
       +        if not self.require_password:
       +            self.success = True
       +            self.message = _('Please wait...')
       +            self.dismiss()
       +            return
                # if setting new generic password, enforce min length
                if self.level > 0:
                    if len(pw) < 6:
       t@@ -253,17 +284,18 @@ class PasswordDialog(AbstractPasswordDialog, Factory.Popup):
                # don't enforce minimum length on existing
                self.do_check(pw)
        
       -    def select_file(self):
       -        self.app.wallets_dialog()
        
        
       -class PincodeDialog(AbstractPasswordDialog, Factory.Popup):
       +class PincodeDialog(AbstractPasswordDialog):
            enter_pw_message = _('Enter your PIN')
            enter_new_pw_message = _('Enter new PIN')
            confirm_new_pw_message = _('Confirm new PIN')
            wrong_password_message = _('Wrong PIN')
            allow_disable = True
        
       +    def __init__(self, app, **kwargs):
       +        AbstractPasswordDialog.__init__(self, app, **kwargs)
       +
            def clear_password(self):
                self.ids.kb.password = ''
        
       t@@ -271,3 +303,51 @@ class PincodeDialog(AbstractPasswordDialog, Factory.Popup):
                # PIN codes are exactly 6 chars
                if len(pw) >= 6:
                    self.do_check(pw)
       +
       +
       +class ChangePasswordDialog(PasswordDialog):
       +
       +    def __init__(self, app, wallet, on_success, on_failure):
       +        PasswordDialog.__init__(self, app,
       +            basename = wallet.basename(),
       +            check_password = wallet.check_password,
       +            on_success=on_success,
       +            on_failure=on_failure,
       +            is_change=True,
       +            has_password=wallet.has_password())
       +
       +
       +class OpenWalletDialog(PasswordDialog):
       +
       +    def __init__(self, app, path, callback):
       +        self.app = app
       +        self.callback = callback
       +        PasswordDialog.__init__(self, app,
       +            on_success=lambda pw: self.callback(pw, self.storage),
       +            on_failure=self.app.stop)
       +        self.init_storage_from_path(path)
       +
       +    def select_file(self):
       +        dirname = os.path.dirname(self.app.electrum_config.get_wallet_path())
       +        d = WalletDialog(dirname, self.init_storage_from_path)
       +        d.open()
       +
       +    def init_storage_from_path(self, path):
       +        self.storage = WalletStorage(path)
       +        self.basename = self.storage.basename()
       +        if not self.storage.file_exists():
       +            self.require_password = False
       +            self.message = _('Press Next to create')
       +        elif self.storage.is_encrypted():
       +            if not self.storage.is_encrypted_with_user_pw():
       +                raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
       +            self.require_password = True
       +            self.pw_check = self.storage.check_password
       +            self.message = self.enter_pw_message
       +        else:
       +            # it is a bit wasteful load the wallet here and load it again in main_window,
       +            # but that is fine, because we are progressively enforcing storage encryption.
       +            wallet = self.app.daemon.load_wallet(path, None)
       +            self.require_password = wallet.has_password()
       +            self.pw_check = wallet.check_password
       +            self.message = self.enter_pw_message if self.require_password else _('Wallet not encrypted')
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/wallets.py b/electrum/gui/kivy/uix/dialogs/wallets.py
       t@@ -12,7 +12,6 @@ from ...i18n import _
        from .label_dialog import LabelDialog
        
        Builder.load_string('''
       -#:import os os
        <WalletDialog@Popup>:
            title: _('Wallets')
            id: popup
       t@@ -40,7 +39,7 @@ Builder.load_string('''
                        text: _('New')
                        on_release:
                            popup.dismiss()
       -                    root.new_wallet(app, wallet_selector.path)
       +                    root.new_wallet(wallet_selector.path)
                    Button:
                        id: open_button
                        size_hint: 0.1, None
       t@@ -49,26 +48,21 @@ Builder.load_string('''
                        disabled: not wallet_selector.selection
                        on_release:
                            popup.dismiss()
       -                    root.open_wallet(app)
       +                    root.callback(wallet_selector.selection[0])
        ''')
        
        class WalletDialog(Factory.Popup):
        
       -    def new_wallet(self, app, dirname):
       +    def __init__(self, path, callback):
       +        Factory.Popup.__init__(self)
       +        self.path = path
       +        self.callback = callback
       +
       +    def new_wallet(self, dirname):
                def cb(filename):
                    if not filename:
                        return
                    # FIXME? "filename" might contain ".." (etc) and hence sketchy path traversals are possible
       -            try:
       -                app.load_wallet_by_name(os.path.join(dirname, filename))
       -            except StorageReadWriteError:
       -                app.show_error(_("R/W error accessing path"))
       +            self.callback(os.path.join(dirname, filename))
                d = LabelDialog(_('Enter wallet name'), '', cb)
                d.open()
       -
       -    def open_wallet(self, app):
       -        try:
       -            app.load_wallet_by_name(self.ids.wallet_selector.selection[0])
       -        except StorageReadWriteError:
       -            app.show_error(_("R/W error accessing path"))
       -