diff --git a/src/sakia/gui/dialogs/certification/controller.py b/src/sakia/gui/dialogs/certification/controller.py index f1f3de2cd8a42c602d8d82e8b5f572bb0ad87162..135ce09145539cb0cdcdfe1541c19c9e00b9fdd3 100644 --- a/src/sakia/gui/dialogs/certification/controller.py +++ b/src/sakia/gui/dialogs/certification/controller.py @@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QApplication from sakia.decorators import asyncify from sakia.gui.sub.search_user.controller import SearchUserController from sakia.gui.sub.user_information.controller import UserInformationController -from sakia.gui.password_asker import PasswordAskerDialog +from sakia.gui.sub.password_input import PasswordInputController from .model import CertificationModel from .view import CertificationView import attr @@ -109,7 +109,7 @@ class CertificationController(QObject): Validate the dialog """ self.view.button_box.setDisabled(True) - password = await PasswordAskerDialog(self.model.connection).async_exec() + password = await PasswordInputController.open_dialog(self, self.model.connection) if not password: self.view.button_box.setEnabled(True) return diff --git a/src/sakia/gui/dialogs/connection_cfg/controller.py b/src/sakia/gui/dialogs/connection_cfg/controller.py index b9165a025c0883a7ed9cae2de10e57d8b5dc85c7..4c26d0d3eed5f6f3e9522fe3af7478b9e306441b 100644 --- a/src/sakia/gui/dialogs/connection_cfg/controller.py +++ b/src/sakia/gui/dialogs/connection_cfg/controller.py @@ -1,17 +1,18 @@ import asyncio import logging +from PyQt5.QtCore import QObject from aiohttp.errors import DisconnectedError, ClientError, TimeoutError -from duniterpy.documents import MalformedDocumentError + from duniterpy.api.errors import DuniterError -from sakia.errors import NoPeerAvailable -from sakia.decorators import asyncify -from sakia.data.processors import IdentitiesProcessor, NodesProcessor +from duniterpy.documents import MalformedDocumentError from sakia.data.connectors import BmaConnector -from sakia.gui.password_asker import PasswordAskerDialog, detect_non_printable +from sakia.data.processors import IdentitiesProcessor, NodesProcessor +from sakia.decorators import asyncify +from sakia.errors import NoPeerAvailable +from sakia.helpers import detect_non_printable from .model import ConnectionConfigModel from .view import ConnectionConfigView -from PyQt5.QtCore import QObject class ConnectionConfigController(QObject): @@ -42,7 +43,6 @@ class ConnectionConfigController(QObject): lambda: self.step_node.set_result(ConnectionConfigController.REGISTER)) self.view.button_wallet.clicked.connect( lambda: self.step_node.set_result(ConnectionConfigController.WALLET)) - self.password_asker = None self.view.values_changed.connect(lambda: self.view.button_next.setEnabled(self.check_key())) self.view.values_changed.connect(lambda: self.view.button_generate.setEnabled(self.check_key())) self._logger = logging.getLogger('sakia') @@ -112,7 +112,6 @@ class ConnectionConfigController(QObject): self.view.button_connect.setEnabled(False) self.view.button_register.setEnabled(False) await self.model.create_connection() - self.password_asker = PasswordAskerDialog(self.model.connection) except (DisconnectedError, ClientError, MalformedDocumentError, ValueError, TimeoutError) as e: self._logger.debug(str(e)) self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port : <br/>" diff --git a/src/sakia/gui/dialogs/transfer/controller.py b/src/sakia/gui/dialogs/transfer/controller.py index 8aef7ee5f62a678d8f1b181c26ddb5e100926ef2..02a2cf28d5c119edb57a9d6130ead42ccb99ce80 100644 --- a/src/sakia/gui/dialogs/transfer/controller.py +++ b/src/sakia/gui/dialogs/transfer/controller.py @@ -4,12 +4,12 @@ import logging from PyQt5.QtCore import Qt, QObject from PyQt5.QtWidgets import QApplication -from sakia.money import Quantitative from sakia.data.processors import ConnectionsProcessor from sakia.decorators import asyncify -from sakia.gui.password_asker import PasswordAskerDialog +from sakia.gui.sub.password_input import PasswordInputController from sakia.gui.sub.search_user.controller import SearchUserController from sakia.gui.sub.user_information.controller import UserInformationController +from sakia.money import Quantitative from .model import TransferModel from .view import TransferView @@ -19,7 +19,7 @@ class TransferController(QObject): The transfer component controller """ - def __init__(self, view, model, search_user, user_information): + def __init__(self, view, model, search_user, user_information, password_input): """ Constructor of the transfer component @@ -31,6 +31,9 @@ class TransferController(QObject): self.model = model self.search_user = search_user self.user_information = user_information + self.password_input = password_input + self.password_input.set_info_visible(False) + self.password_input.password_changed.connect(self.refresh) self.view.button_box.accepted.connect(self.accept) self.view.button_box.rejected.connect(self.reject) self.view.combo_connections.currentIndexChanged.connect(self.change_current_connection) @@ -46,15 +49,14 @@ class TransferController(QObject): :return: a new Transfer controller :rtype: TransferController """ - view = TransferView(parent.view if parent else None, None, None) - model = TransferModel(app) - transfer = cls(view, model, None, None) + search_user = SearchUserController.create(None, app, "") + user_information = UserInformationController.create(None, app, "", None) + password_input = PasswordInputController.create(None, None) - search_user = SearchUserController.create(transfer, app, "") - transfer.set_search_user(search_user) - - user_information = UserInformationController.create(transfer, app, "", None) - transfer.set_user_information(user_information) + view = TransferView(parent.view if parent else None, + search_user.view, user_information.view, password_input.view) + model = TransferModel(app) + transfer = cls(view, model, search_user, user_information, password_input) search_user.identity_selected.connect(user_information.search_identity) @@ -108,24 +110,6 @@ class TransferController(QObject): return dialog.exec() - def set_search_user(self, search_user): - """ - - :param search_user: - :return: - """ - self.search_user = search_user - self.view.set_search_user(search_user.view) - - def set_user_information(self, user_information): - """ - - :param user_information: - :return: - """ - self.user_information = user_information - self.view.set_user_information(user_information.view) - def selected_pubkey(self): """ Get selected pubkey in the widgets of the window @@ -156,10 +140,7 @@ class TransferController(QObject): amount_base = self.model.current_base() logging.debug("Showing password dialog...") - password = await PasswordAskerDialog(self.model.connection).async_exec() - if password == "": - self.view.button_box.setEnabled(True) - return + password = self.password_input.get_password() logging.debug("Setting cursor...") QApplication.setOverrideCursor(Qt.WaitCursor) @@ -194,8 +175,10 @@ class TransferController(QObject): if amount == 0: self.view.set_button_box(TransferView.ButtonBoxState.NO_AMOUNT) - else: + elif self.password_input.valid(): self.view.set_button_box(TransferView.ButtonBoxState.OK) + else: + self.view.set_button_box(TransferView.ButtonBoxState.WRONG_PASSWORD) max_relative = self.model.quant_to_rel(current_base_amount/100) self.view.spinbox_amount.setSuffix(Quantitative.base_str(current_base)) @@ -205,15 +188,18 @@ class TransferController(QObject): def handle_amount_change(self, value): relative = self.model.quant_to_rel(value) self.view.change_relative_amount(relative) + self.refresh() def handle_relative_change(self, value): amount = self.model.rel_to_quant(value) self.view.change_quantitative_amount(amount) + self.refresh() def change_current_connection(self, index): self.model.set_connection(index) self.search_user.set_currency(self.model.connection.currency) self.user_information.set_currency(self.model.connection.currency) + self.password_input.set_connection(self.model.connection) self.refresh() def async_exec(self): diff --git a/src/sakia/gui/dialogs/transfer/transfer.ui b/src/sakia/gui/dialogs/transfer/transfer.ui index 4e43b10f8372229b0bb59c521022cde60d0ddb8f..ce4d74b534a52dd21728fbaf38ef66d06e91e613 100644 --- a/src/sakia/gui/dialogs/transfer/transfer.ui +++ b/src/sakia/gui/dialogs/transfer/transfer.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>566</width> - <height>450</height> + <height>540</height> </rect> </property> <property name="windowTitle"> @@ -246,6 +246,18 @@ </layout> </widget> </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Password</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QHBoxLayout" name="layout_password_input"/> + </item> + </layout> + </widget> + </item> <item> <widget class="QDialogButtonBox" name="button_box"> <property name="orientation"> diff --git a/src/sakia/gui/dialogs/transfer/view.py b/src/sakia/gui/dialogs/transfer/view.py index ebb087c6a40c473d2f5360eb9b7b20e91dd8e014..4f9bdffe3e45586abbc0c9ce05c2294b7dfeb506 100644 --- a/src/sakia/gui/dialogs/transfer/view.py +++ b/src/sakia/gui/dialogs/transfer/view.py @@ -15,6 +15,7 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): class ButtonBoxState(Enum): NO_AMOUNT = 0 OK = 1 + WRONG_PASSWORD = 2 class RecipientMode(Enum): PUBKEY = 1 @@ -24,15 +25,17 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): _button_box_values = { ButtonBoxState.NO_AMOUNT: (False, QT_TRANSLATE_NOOP("TransferView", "No amount. Please give the transfer amount")), - ButtonBoxState.OK: (True, QT_TRANSLATE_NOOP("CertificationView", "&Ok")) + ButtonBoxState.OK: (True, QT_TRANSLATE_NOOP("CertificationView", "&Ok")), + ButtonBoxState.WRONG_PASSWORD: (False, QT_TRANSLATE_NOOP("TransferView", "Please enter correct password")) } - def __init__(self, parent, search_user_view, user_information_view): + def __init__(self, parent, search_user_view, user_information_view, password_input_view): """ :param parent: - :param sakia.gui.search_user.view.SearchUserView search_user_view: - :param sakia.gui.user_information.view.UserInformationView user_information_view: + :param sakia.gui.sub.search_user.view.SearchUserView search_user_view: + :param sakia.gui.sub.user_information.view.UserInformationView user_information_view: + :param sakia.gui.sub.password_input.view.PasswordInputView password_input_view: """ super().__init__(parent) self.setupUi(self) @@ -41,13 +44,18 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): self.radio_search.toggled.connect(lambda c, radio=TransferView.RecipientMode.SEARCH: self.recipient_mode_changed(radio)) self.radio_local_key.toggled.connect(lambda c, radio=TransferView.RecipientMode.LOCAL_KEY: self.recipient_mode_changed(radio)) - regexp = QRegExp('^([ a-zA-Z0-9-_:/;*?\[\]\(\)\\\?!^+=@&~#{}|<>%.]{0,255})$') validator = QRegExpValidator(regexp) self.edit_message.setValidator(validator) self.search_user = search_user_view + self.layout_search_user.addWidget(search_user_view) + self.search_user.button_reset.hide() self.user_information_view = user_information_view + self.group_box_recipient.layout().addWidget(user_information_view) + self.password_input_view = password_input_view + self.layout_password_input.addWidget(password_input_view) + self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) self._amount_base = 0 self._currency = "" @@ -74,12 +82,6 @@ class TransferView(QDialog, Ui_TransferMoneyDialog): :return: """ self.search_user = search_user_view - self.layout_search_user.addWidget(search_user_view) - self.search_user.button_reset.hide() - - def set_user_information(self, user_information_view): - self.user_information_view = user_information_view - self.group_box_recipient.layout().addWidget(user_information_view) def recipient_mode_changed(self, radio): """ diff --git a/src/sakia/gui/main_window/controller.py b/src/sakia/gui/main_window/controller.py index 27ff212cb33129bc6aba0679c907bdbb417422f9..89e9dd06480e4f7a32421a236a8f5f6d5c02d915 100644 --- a/src/sakia/gui/main_window/controller.py +++ b/src/sakia/gui/main_window/controller.py @@ -1,17 +1,17 @@ import logging -from PyQt5.QtWidgets import QMessageBox, QApplication from PyQt5.QtCore import QEvent, pyqtSlot, QObject from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QMessageBox, QApplication -from ..password_asker import PasswordAskerDialog -from ...__init__ import __version__ -from ..widgets import toast -from .view import MainWindowView +from sakia.gui.sub.password_input import PasswordInputController from .model import MainWindowModel from .status_bar.controller import StatusBarController from .toolbar.controller import ToolbarController +from .view import MainWindowView from ..navigation.controller import NavigationController +from ..widgets import toast +from ...__init__ import __version__ class MainWindowController(QObject): @@ -19,7 +19,7 @@ class MainWindowController(QObject): classdocs """ - def __init__(self, view, model, password_asker, status_bar, toolbar, navigation): + def __init__(self, view, model, status_bar, toolbar, navigation): """ Init :param MainWindowView view: the ui of the mainwindow component @@ -36,7 +36,6 @@ class MainWindowController(QObject): self.view = view self.model = model self.initialized = False - self.password_asker = password_asker self.status_bar = status_bar self.toolbar = toolbar self.navigation = navigation @@ -48,7 +47,7 @@ class MainWindowController(QObject): QApplication.setWindowIcon(QIcon(":/icons/sakia_logo")) @classmethod - def create(cls, app, password_asker, status_bar, toolbar, navigation): + def create(cls, app, status_bar, toolbar, navigation): """ Instanciate a navigation component :param sakia.gui.status_bar.controller.StatusBarController status_bar: the controller of the status bar component @@ -60,7 +59,7 @@ class MainWindowController(QObject): """ view = MainWindowView() model = MainWindowModel(None, app) - main_window = cls(view, model, password_asker, status_bar, toolbar, navigation) + main_window = cls(view, model, status_bar, toolbar, navigation) model.setParent(main_window) return main_window @@ -71,11 +70,9 @@ class MainWindowController(QObject): :param sakia.app.Application app: :return: """ - password_asker = PasswordAskerDialog(None) navigation = NavigationController.create(None, app) toolbar = ToolbarController.create(app, navigation) - main_window = cls.create(app, password_asker=password_asker, - status_bar=StatusBarController.create(app), + main_window = cls.create(app, status_bar=StatusBarController.create(app), navigation=navigation, toolbar=toolbar ) diff --git a/src/sakia/gui/main_window/toolbar/controller.py b/src/sakia/gui/main_window/toolbar/controller.py index b0c856bc25f068251f34fc2f4154ec1043ee861e..d93e2fa81db50b0e8c89a48c0fd3c58dc0f8d4ea 100644 --- a/src/sakia/gui/main_window/toolbar/controller.py +++ b/src/sakia/gui/main_window/toolbar/controller.py @@ -1,15 +1,15 @@ -from PyQt5.QtCore import Qt, QObject -from PyQt5.QtWidgets import QDialog, QMessageBox +from PyQt5.QtCore import QObject +from PyQt5.QtWidgets import QDialog from sakia.decorators import asyncify -from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController from sakia.gui.dialogs.certification.controller import CertificationController +from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController from sakia.gui.dialogs.revocation.controller import RevocationController from sakia.gui.dialogs.transfer.controller import TransferController -from sakia.gui.widgets import toast -from sakia.gui.widgets.dialogs import QAsyncMessageBox, QAsyncFileDialog, dialog_async_exec -from sakia.gui.password_asker import PasswordAskerDialog from sakia.gui.preferences import PreferencesDialog +from sakia.gui.sub.password_input import PasswordInputController +from sakia.gui.widgets import toast +from sakia.gui.widgets.dialogs import QAsyncMessageBox from .model import ToolbarModel from .view import ToolbarView @@ -60,7 +60,7 @@ class ToolbarController(QObject): connection = await self.view.ask_for_connection(self.model.connections_with_uids()) if not connection: return - password = await PasswordAskerDialog(connection).async_exec() + password = await PasswordInputController.open_dialog(self, connection) if not password: return result = await self.model.send_join(connection, password) @@ -68,13 +68,13 @@ class ToolbarController(QObject): if self.model.notifications(): toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) else: - await QAsyncMessageBox.information(self, self.tr("Membership"), + await QAsyncMessageBox.information(self.view, self.tr("Membership"), self.tr("Success sending Membership demand")) else: if self.model.notifications(): toast.display(self.tr("Membership"), result[1]) else: - await QAsyncMessageBox.critical(self, self.tr("Membership"), + await QAsyncMessageBox.critical(self.view, self.tr("Membership"), result[1]) def open_certification_dialog(self): diff --git a/src/sakia/gui/navigation/controller.py b/src/sakia/gui/navigation/controller.py index da7c7de6cee67de46e0a064050b97ade1453c75c..4195612bb13eaa82a313067e20cd842423a85048 100644 --- a/src/sakia/gui/navigation/controller.py +++ b/src/sakia/gui/navigation/controller.py @@ -1,20 +1,21 @@ -from .model import NavigationModel -from .view import NavigationView -from sakia.models.generic_tree import GenericTreeModel -from .txhistory.controller import TxHistoryController -from .homescreen.controller import HomeScreenController -from .network.controller import NetworkController -from .identities.controller import IdentitiesController -from .informations.controller import InformationsController -from .graphs.wot.controller import WotController -from sakia.data.entities import Connection from PyQt5.QtCore import pyqtSignal, QObject, Qt -from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QDialog, QFileDialog from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QFileDialog + +from sakia.data.entities import Connection from sakia.decorators import asyncify -from sakia.gui.password_asker import PasswordAskerDialog -from sakia.gui.widgets.dialogs import QAsyncMessageBox +from sakia.gui.sub.password_input import PasswordInputController from sakia.gui.widgets import toast +from sakia.gui.widgets.dialogs import QAsyncMessageBox +from sakia.models.generic_tree import GenericTreeModel +from .graphs.wot.controller import WotController +from .homescreen.controller import HomeScreenController +from .identities.controller import IdentitiesController +from .informations.controller import InformationsController +from .model import NavigationModel +from .network.controller import NetworkController +from .txhistory.controller import TxHistoryController +from .view import NavigationView class NavigationController(QObject): @@ -140,7 +141,7 @@ class NavigationController(QObject): @asyncify async def publish_uid(self, connection): - password = await PasswordAskerDialog(connection).async_exec() + password = await PasswordInputController.open_dialog(self, connection) if not password: return result = await self.account.send_selfcert(password, self.community) @@ -165,7 +166,7 @@ Sending a leaving demand cannot be canceled. The process to join back the community later will have to be done again.""") .format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) if reply == QMessageBox.Ok: - password = PasswordAskerDialog(self.model.navigation_model.navigation.current_connection()).async_exec() + password = await PasswordInputController.open_dialog(self.model.navigation_model.navigation.current_connection()).async_exec() if not password: return result = await self.model.send_leave(password) @@ -191,8 +192,9 @@ neither your identity from the network."""), QMessageBox.Ok | QMessageBox.Cancel await self.model.remove_connection(connection) self.init_navigation() - def action_save_revokation(self, connection): - password = PasswordAskerDialog(connection).exec() + @asyncify + async def action_save_revokation(self, connection): + password = await PasswordInputController.open_dialog(connection) if not password: return diff --git a/src/sakia/gui/password_asker.py b/src/sakia/gui/password_asker.py deleted file mode 100644 index 865bf2baf7efe7b6741b76c709650491dc54ce34..0000000000000000000000000000000000000000 --- a/src/sakia/gui/password_asker.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -Created on 24 dec. 2014 - -@author: inso -""" - -import logging -import re -import asyncio -from duniterpy.key import SigningKey -from PyQt5.QtCore import QEvent -from PyQt5.QtWidgets import QDialog, QMessageBox - -from .password_asker_uic import Ui_PasswordAskerDialog - - -class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): - - """ - A dialog to get password. - """ - - def __init__(self, connection): - """ - Constructor - - :param sakia.data.entities.Connection connection: a given connection - """ - super().__init__() - self.setupUi(self) - self.password = "" - self.connection = connection - self.remember = False - - def async_exec(self): - future = asyncio.Future() - if not self.connection.password: - def future_show(): - pwd = self.password - if self.remember: - self.connection.password = self.password - self.finished.disconnect(future_show) - future.set_result(pwd) - self.open() - self.finished.connect(future_show) - else: - self.setResult(QDialog.Accepted) - future.set_result(self.connection.password) - return future - - def exec(self): - if not self.connection.password: - super().exec_() - pwd = self.password - if self.remember: - self.connection.password = self.password - return pwd - else: - self.setResult(QDialog.Accepted) - return self.connection.password - - def accept(self): - password = self.edit_password.text() - - if detect_non_printable(password): - QMessageBox.warning(self, self.tr("Bad password"), - self.tr("Non printable characters in password"), - QMessageBox.Ok) - return False - - if SigningKey(self.connection.salt, password, self.connection.scrypt_params).pubkey != self.connection.pubkey: - QMessageBox.warning(self, self.tr("Failed to get private key"), - self.tr("Wrong password typed. Cannot open the private key"), - QMessageBox.Ok) - return False - - self.remember = self.check_remember.isChecked() - self.password = password - self.edit_password.setText("") - logging.debug("Password is valid") - super().accept() - - def reject(self): - self.edit_password.setText("") - logging.debug("Cancelled") - self.setResult(QDialog.Accepted) - self.password = "" - super().reject() - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - return super(PasswordAskerDialog, self).changeEvent(event) - - -def detect_non_printable(data): - control_chars = ''.join(map(chr, list(range(0, 32)) + list(range(127, 160)))) - control_char_re = re.compile('[%s]' % re.escape(control_chars)) - if control_char_re.search(data): - return True diff --git a/src/sakia/gui/password_asker.ui b/src/sakia/gui/password_asker.ui deleted file mode 100644 index 93a357622ef5246c1840eb8af14946cdbbb7fad5..0000000000000000000000000000000000000000 --- a/src/sakia/gui/password_asker.ui +++ /dev/null @@ -1,85 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>PasswordAskerDialog</class> - <widget class="QDialog" name="PasswordAskerDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>274</width> - <height>99</height> - </rect> - </property> - <property name="windowTitle"> - <string>Password</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QLineEdit" name="edit_password"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - <property name="placeholderText"> - <string>Please enter your account password</string> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QCheckBox" name="check_remember"> - <property name="text"> - <string>Remember my password during this session</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - </layout> - </widget> - <resources/> - <connections> - <connection> - <sender>buttonBox</sender> - <signal>accepted()</signal> - <receiver>PasswordAskerDialog</receiver> - <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>136</x> - <y>78</y> - </hint> - <hint type="destinationlabel"> - <x>136</x> - <y>49</y> - </hint> - </hints> - </connection> - <connection> - <sender>buttonBox</sender> - <signal>rejected()</signal> - <receiver>PasswordAskerDialog</receiver> - <slot>reject()</slot> - <hints> - <hint type="sourcelabel"> - <x>136</x> - <y>78</y> - </hint> - <hint type="destinationlabel"> - <x>136</x> - <y>49</y> - </hint> - </hints> - </connection> - </connections> -</ui> diff --git a/src/sakia/gui/sub/password_input/__init__.py b/src/sakia/gui/sub/password_input/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d42b6910b749d4dcba401c24843f2e2500c4ff70 --- /dev/null +++ b/src/sakia/gui/sub/password_input/__init__.py @@ -0,0 +1 @@ +from .controller import PasswordInputController \ No newline at end of file diff --git a/src/sakia/gui/sub/password_input/controller.py b/src/sakia/gui/sub/password_input/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..2847621ad4fab485487c821c892805af603a61b9 --- /dev/null +++ b/src/sakia/gui/sub/password_input/controller.py @@ -0,0 +1,84 @@ +import asyncio +from duniterpy.key import SigningKey +from .view import PasswordInputView +from sakia.helpers import detect_non_printable +from sakia.gui.widgets.dialogs import dialog_async_exec +from PyQt5.QtCore import QObject, pyqtSignal, QMetaObject +from PyQt5.QtWidgets import QDialog, QVBoxLayout + + +class PasswordInputController(QObject): + + """ + A dialog to get password. + """ + password_changed = pyqtSignal(bool) + + def __init__(self, view, connection): + """ + + :param PasswordInputView view: + :param sakia.data.entities.Connection connection: a given connection + """ + super().__init__() + self.view = view + self._password = "" + self.connection = connection + self.remember = False + self.set_connection(connection) + + def set_info_visible(self, visible): + self.view.label_info.setVisible(visible) + + @classmethod + def create(cls, parent, connection): + view = PasswordInputView(parent.view if parent else None) + password_input = cls(view, connection) + view.edit_password.textChanged.connect(password_input.handle_text_change) + return password_input + + @classmethod + async def open_dialog(cls, parent, connection): + dialog = QDialog(parent.view) + dialog.setLayout(QVBoxLayout(dialog)) + password_input = cls.create(parent, connection) + dialog.setWindowTitle(password_input.tr("Please enter your password")) + dialog.layout().addWidget(password_input.view) + password_input.view.button_box.accepted.connect(dialog.accept) + password_input.view.button_box.rejected.connect(dialog.reject) + password_input.view.setParent(dialog) + password_input.view.button_box.show() + result = await dialog_async_exec(dialog) + if result == QDialog.Accepted: + return password_input.get_password() + else: + return "" + + def valid(self): + return self._password is not "" + + def handle_text_change(self, password): + self._password = "" + if detect_non_printable(password): + self.view.error(self.tr("Non printable characters in password")) + self.password_changed.emit(False) + return + + if SigningKey(self.connection.salt, password, self.connection.scrypt_params).pubkey != self.connection.pubkey: + self.view.error(self.tr("Wrong password typed. Cannot open the private key")) + self.password_changed.emit(False) + return + + self.view.valid() + self._password = password + self.password_changed.emit(True) + + def get_password(self): + if self.view.check_remember.isChecked(): + self.connection.password = self._password + return self._password + + def set_connection(self, connection): + if connection: + self.connection = connection + self.view.edit_password.setText(connection.password) diff --git a/src/sakia/gui/sub/password_input/password_input.ui b/src/sakia/gui/sub/password_input/password_input.ui new file mode 100644 index 0000000000000000000000000000000000000000..6b248a68c7cfb87b141047b68bca4ed9d2674035 --- /dev/null +++ b/src/sakia/gui/sub/password_input/password_input.ui @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PasswordInputWidget</class> + <widget class="QWidget" name="PasswordInputWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>98</height> + </rect> + </property> + <property name="windowTitle"> + <string>Please enter your password</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLineEdit" name="edit_password"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Please enter your account password</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="check_remember"> + <property name="text"> + <string>Remember my password during this session</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_info"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/sakia/gui/sub/password_input/view.py b/src/sakia/gui/sub/password_input/view.py new file mode 100644 index 0000000000000000000000000000000000000000..075fec6f9dfdd295bd4257f609281f3f5e1b4e23 --- /dev/null +++ b/src/sakia/gui/sub/password_input/view.py @@ -0,0 +1,39 @@ +from PyQt5.QtWidgets import QWidget, QDialogButtonBox +from PyQt5.QtCore import QEvent, Qt +from duniterpy.key import SigningKey +from .password_input_uic import Ui_PasswordInputWidget +import re + + +class PasswordInputView(QWidget, Ui_PasswordInputWidget): + """ + The model of Navigation component + """ + def __init__(self, parent): + # construct from qtDesigner + super().__init__(parent) + self.setupUi(self) + self.button_box = QDialogButtonBox(self) + self.button_box.setOrientation(Qt.Horizontal) + self.button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) + self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) + self.layout().addWidget(self.button_box) + self.button_box.hide() + + def error(self, text): + self.label_info.setText(text) + self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) + + def valid(self): + self.label_info.setText(self.tr("Password is valid")) + self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + return super(PasswordInputView, self).changeEvent(event) diff --git a/src/sakia/gui/sub/search_user/controller.py b/src/sakia/gui/sub/search_user/controller.py index 787c2aed85d7ad806e1f08e5e8376b2842e8f9ab..4777d654c51418f300072c850cba2863bf6c482a 100644 --- a/src/sakia/gui/sub/search_user/controller.py +++ b/src/sakia/gui/sub/search_user/controller.py @@ -28,7 +28,7 @@ class SearchUserController(QObject): @classmethod def create(cls, parent, app, currency): - view = SearchUserView(parent.view) + view = SearchUserView(parent.view if parent else None) model = SearchUserModel(parent, app, currency) search_user = cls(parent, view, model) model.setParent(search_user) diff --git a/src/sakia/gui/sub/search_user/search_user.ui b/src/sakia/gui/sub/search_user/search_user.ui index ac85b8f6315e27911a64ed37dd722ee9bae882e8..c93e6fc13bcb5cd66b415fccfb1a441b29f2bd13 100644 --- a/src/sakia/gui/sub/search_user/search_user.ui +++ b/src/sakia/gui/sub/search_user/search_user.ui @@ -36,7 +36,7 @@ <string/> </property> <property name="icon"> - <iconset resource="../icons/icons.qrc"> + <iconset resource="../../../../../res/icons/icons.qrc"> <normaloff>:/icons/home_icon</normaloff>:/icons/home_icon</iconset> </property> </widget> @@ -44,7 +44,7 @@ </layout> </widget> <resources> - <include location="../icons/icons.qrc"/> + <include location="../../../../../res/icons/icons.qrc"/> </resources> <connections/> </ui> diff --git a/src/sakia/helpers.py b/src/sakia/helpers.py index 05a9a018ae861d275198225348bf0c210800a06d..44f131bfc9b91a3cccf2d40743ba3a88f0f3cb86 100644 --- a/src/sakia/helpers.py +++ b/src/sakia/helpers.py @@ -1,7 +1,15 @@ +import re def timestamp_to_dhms(ts): days, remainder = divmod(ts, 3600 * 24) hours, remainder = divmod(remainder, 3600) minutes, seconds = divmod(remainder, 60) - return days, hours, minutes, seconds \ No newline at end of file + return days, hours, minutes, seconds + + +def detect_non_printable(data): + control_chars = ''.join(map(chr, list(range(0, 32)) + list(range(127, 160)))) + control_char_re = re.compile('[%s]' % re.escape(control_chars)) + if control_char_re.search(data): + return True