From 675ce763ac7b07ea651db889eac6485746fd800e Mon Sep 17 00:00:00 2001 From: Florent Viard Date: Thu, 8 Apr 2021 15:59:42 +0200 Subject: [PATCH] [creditmutuel] Reworked addrecipient and transfer otp handling to be more reliable and to support sms otp for transfer --- modules/creditmutuel/browser.py | 146 +++++++++++++++++++++----------- modules/creditmutuel/module.py | 2 +- modules/creditmutuel/pages.py | 20 ++++- 3 files changed, 115 insertions(+), 53 deletions(-) diff --git a/modules/creditmutuel/browser.py b/modules/creditmutuel/browser.py index b73d25b3ad..d355ff2049 100644 --- a/modules/creditmutuel/browser.py +++ b/modules/creditmutuel/browser.py @@ -41,7 +41,7 @@ from woob.capabilities.bank import ( Account, AddRecipientStep, Recipient, AccountOwnership, AddRecipientTimeout, TransferStep, TransferBankError, - AddRecipientBankError, + AddRecipientBankError, TransferTimeout, ) from woob.tools.capabilities.bank.investments import create_french_liquidity from woob.capabilities import NotAvailable @@ -226,7 +226,7 @@ def __init__(self, config, *args, **kwargs): 'currentSubBank', 'logged', 'is_new_website', 'need_clear_storage', 'recipient_form', 'twofa_auth_state', 'polling_data', 'otp_data', - 'key_form', + 'key_form', 'transfer_code_form', ) self.twofa_auth_state = {} self.polling_data = {} @@ -234,6 +234,7 @@ def __init__(self, config, *args, **kwargs): self.keep_session = None self.recipient_form = None self.key_form = None + self.transfer_code_form = None self.AUTHENTICATION_METHODS = { 'resume': self.handle_polling, @@ -264,6 +265,7 @@ def load_state(self, state): or state.get('recipient_form') or state.get('otp_data') or state.get('key_form') + or state.get('transfer_code_form') ): # can't start on an url in the middle of a validation process # or server will cancel it and launch another one @@ -957,6 +959,8 @@ def iter_recipients(self, origin_account): def continue_transfer(self, transfer, **params): if 'Clé' in params: + if not self.key_form: + raise TransferTimeout(message="La validation du transfert par carte de clés personnelles a expiré") url = self.key_form.pop('url') self.format_personal_key_card_form(params['Clé']) self.location(url, data=self.key_form) @@ -972,9 +976,29 @@ def continue_transfer(self, transfer, **params): if self.login.is_here(): # User took too much time to input the personal key. - raise TransferBankError(message='La validation du transfert par carte de clés personnelles a expiré') + raise TransferBankError(message="La validation du transfert par carte de clés personnelles a expiré") + + transfer_id = self.page.get_transfer_webid() + if transfer_id and (empty(transfer.id) or transfer.id != transfer_id): + transfer.id = self.page.get_transfer_webid() + + elif 'code' in params: + code_form = self.transfer_code_form + if not code_form: + raise TransferTimeout(message="Le code de confirmation envoyé par SMS n'est plus utilisable") + # Specific field of the confirmation page + code_form['Bool:data_input_confirmationDoublon'] = 'true' + self.send_sms(code_form, params['code']) + self.transfer_code_form = None + + # OTP is expired after 15', we end up on login page + if self.login.is_here(): + raise TransferBankError(message="Le code de confirmation envoyé par SMS n'est plus utilisable") + + transfer_id = self.page.get_transfer_webid() + if transfer_id and (empty(transfer.id) or transfer.id != transfer_id): + transfer.id = self.page.get_transfer_webid() - transfer.id = self.page.get_transfer_webid() elif 'resume' in params: self.poll_decoupled(self.polling_data['polling_id']) @@ -982,11 +1006,41 @@ def continue_transfer(self, transfer, **params): self.polling_data['final_url'], data=self.polling_data['final_url_params'], ) - # Dont set `self.polling_data = None` yet because we need to know in - # execute_transfer if we just did an app validation. + self.polling_data = None + + transfer = self.check_and_initiate_transfer_otp(transfer) + + return transfer - # At this point the app validation has already been sent (after validating the - # personal key card code). + def check_and_initiate_transfer_otp(self, transfer, account=None, recipient=None): + if self.page.needs_personal_key_card_validation(): + self.location(self.page.get_card_key_validation_link()) + error = self.page.get_personal_keys_error() + if error: + raise TransferBankError(message=error) + + self.key_form = self.page.get_personal_key_card_code_form() + raise TransferStep( + transfer, + Value('Clé', label=self.page.get_question()) + ) + + if account and transfer: + transfer = self.page.handle_response_create_transfer( + account, recipient, transfer.amount, transfer.label, transfer.exec_date + ) + else: + transfer = self.page.handle_response_reuse_transfer(transfer) + + if self.page.needs_otp_validation(): + self.transfer_code_form = self.page.get_transfer_code_form() + raise TransferStep( + transfer, + Value('code', label='Veuillez saisir le code reçu par sms pour confirmer votre opération') + ) + + # The app validation, if needed, could have already been started + # (for example, after validating the personal key card code). msg = self.page.get_validation_msg() if msg: self.polling_data = self.page.get_polling_data(form_xpath='//form[contains(@action, "virements")]') @@ -1015,35 +1069,17 @@ def init_transfer(self, transfer, account, recipient): self.page.prepare_transfer(account, recipient, transfer.amount, transfer.label, transfer.exec_date) - if self.page.needs_personal_key_card_validation(): - self.location(self.page.get_card_key_validation_link()) - error = self.page.get_personal_keys_error() - if error: - raise TransferBankError(message=error) - - self.key_form = self.page.get_personal_key_card_code_form() - raise TransferStep(transfer, Value('Clé', label=self.page.get_question())) - elif self.page.needs_otp_validation(): - raise AuthMethodNotImplemented("La validation des transferts avec un code sms n'est pas encore disponible.") - - msg = self.page.get_validation_msg() - if msg: - self.polling_data = self.page.get_polling_data(form_xpath='//form[contains(@action, "virements")]') - assert self.polling_data, "Can't proceed without polling data" - raise AppValidation( - resource=transfer, - message=msg, - ) - - return self.page.handle_response(account, recipient, transfer.amount, transfer.label, transfer.exec_date) + new_transfer = self.check_and_initiate_transfer_otp(transfer, account, recipient) + return new_transfer @need_login def execute_transfer(self, transfer, **params): - if self.polling_data: - # If we just did a transfer to a new recipient the transfer has already - # been confirmed with the app validation. - self.polling_data = None - else: + # If we just did a transfer to a new recipient the transfer has already + # been confirmed because of the app validation or the sms otp + # Otherwise, do the confirmation when still needed + if self.page.doc.xpath( + '//form[@id="P:F"]//input[@type="submit" and contains(@value, "Confirmer")]' + ): form = self.page.get_form(id='P:F', submit='//input[@type="submit" and contains(@value, "Confirmer")]') # For the moment, don't ask the user if he confirms the duplicate. form['Bool:data_input_confirmationDoublon'] = 'true' @@ -1106,6 +1142,8 @@ def format_personal_key_card_form(self, key): def continue_new_recipient(self, recipient, **params): if 'Clé' in params: + if not self.key_form: + raise AddRecipientTimeout(message="La validation par carte de clés personnelles a expiré") url = self.key_form.pop('url') self.format_personal_key_card_form(params['Clé']) self.location(url, data=self.key_form) @@ -1121,37 +1159,43 @@ def continue_new_recipient(self, recipient, **params): if self.login.is_here(): # User took too much time to input the personal key. - raise AddRecipientTimeout() + raise AddRecipientBankError(message="La validation par carte de clés personnelles a expiré") self.page.add_recipient(recipient) if self.page.bic_needed(): self.page.ask_bic(self.get_recipient_object(recipient)) self.page.ask_auth_validation(self.get_recipient_object(recipient)) - def send_sms(self, sms): - url = self.recipient_form.pop('url') - self.recipient_form['otp_password'] = sms - self.recipient_form['_FID_DoConfirm.x'] = '1' - self.recipient_form['_FID_DoConfirm.y'] = '1' - self.recipient_form['global_backup_hidden_key'] = '' - self.location(url, data=self.recipient_form) + def send_sms(self, form, sms): + url = form.pop('url') + form['otp_password'] = sms + form['_FID_DoConfirm.x'] = '1' + form['_FID_DoConfirm.y'] = '1' + form['global_backup_hidden_key'] = '' + self.location(url, data=form) - def send_decoupled(self): - url = self.recipient_form.pop('url') - transactionId = self.recipient_form.pop('transactionId') + def send_decoupled(self, form): + url = form.pop('url') + transactionId = form.pop('transactionId') self.poll_decoupled(transactionId) - self.recipient_form['_FID_DoConfirm.x'] = '1' - self.recipient_form['_FID_DoConfirm.y'] = '1' - self.recipient_form['global_backup_hidden_key'] = '' - self.location(url, data=self.recipient_form) + form['_FID_DoConfirm.x'] = '1' + form['_FID_DoConfirm.y'] = '1' + form['global_backup_hidden_key'] = '' + self.location(url, data=form) def end_new_recipient_with_auth_validation(self, recipient, **params): if 'code' in params: - self.send_sms(params['code']) + if not self.recipient_form: + raise AddRecipientTimeout(message="Le code de confirmation envoyé par SMS n'est plus utilisable") + self.send_sms(self.recipient_form, params['code']) + elif 'resume' in params: - self.send_decoupled() + if not self.recipient_form: + raise AddRecipientTimeout(message="Le demande de confirmation a expiré") + self.send_decoupled(self.recipient_form) + self.recipient_form = None self.page = None return self.get_recipient_object(recipient) diff --git a/modules/creditmutuel/module.py b/modules/creditmutuel/module.py index 61bf8ceefd..b422183ef7 100644 --- a/modules/creditmutuel/module.py +++ b/modules/creditmutuel/module.py @@ -117,7 +117,7 @@ def new_recipient(self, recipient, **params): return self.browser.new_recipient(recipient, **params) def init_transfer(self, transfer, **params): - if {'Clé', 'resume'} & set(params.keys()): + if {'Clé', 'resume', 'code'} & set(params.keys()): return self.browser.continue_transfer(transfer, **params) # There is a check on the website, transfer can't be done with too long reason. diff --git a/modules/creditmutuel/pages.py b/modules/creditmutuel/pages.py index b05037f2a6..558bd55ed5 100644 --- a/modules/creditmutuel/pages.py +++ b/modules/creditmutuel/pages.py @@ -2073,6 +2073,12 @@ def needs_personal_key_card_validation(self): def needs_otp_validation(self): return bool(self.doc.xpath('//input[@name="otp_password"]')) + def get_transfer_code_form(self): + form = self.get_form(id='P:F') + code_form = dict(form.items()) + code_form['url'] = form.url + return code_form + def can_transfer_pro(self, origin_account): for li in self.doc.xpath('//ul[@id="idDetailsListCptDebiterVertical:ul"]//ul/li'): if CleanText(li.xpath('.//span[@class="_c1 doux _c1"]'), replace=[(' ', '')])(self) in origin_account: @@ -2201,7 +2207,19 @@ def get_transfer_webid(self): parsed = urlparse(self.url) return parse_qs(parsed.query)['_saguid'][0] - def handle_response(self, account, recipient, amount, reason, exec_date): + def handle_response_reuse_transfer(self, transfer): + self.check_errors() + + exec_date, r_amount, currency = self.check_data_consistency( + transfer.account_id, transfer.recipient_id, transfer.amount, transfer.label) + + transfer.exec_date = exec_date + transfer.amount = r_amount + transfer.currency = currency + + return transfer + + def handle_response_create_transfer(self, account, recipient, amount, reason, exec_date): self.check_errors() self.check_success() -- GitLab