diff --git a/modules/bp/browser.py b/modules/bp/browser.py index 6184dbf3f7bfd15e1c368744d7bec1ddbbb2afac..9ad1d1a06893264fd8ff7e5f3c824b06585b9c9b 100644 --- a/modules/bp/browser.py +++ b/modules/bp/browser.py @@ -40,7 +40,8 @@ from weboob.tools.decorators import retry from weboob.capabilities.bank import ( Account, Recipient, AddRecipientStep, TransferStep, - TransferInvalidEmitter, RecipientInvalidOTP, + TransferInvalidEmitter, RecipientInvalidOTP, TransferBankError, + AddRecipientBankError, TransferInvalidOTP, ) from weboob.tools.value import Value, ValueBool @@ -51,7 +52,8 @@ ValidateCountry, ConfirmPage, RcptSummary, SubscriptionPage, DownloadPage, ProSubscriptionPage, RevolvingAttributesPage, - TwoFAPage, Validated2FAPage, SmsPage, DecoupledPage, HonorTransferPage, RecipientSubmitDevicePage, RcptErrorPage, + TwoFAPage, Validated2FAPage, SmsPage, DecoupledPage, HonorTransferPage, + RecipientSubmitDevicePage, OtpErrorPage, ) from .pages.accounthistory import ( LifeInsuranceInvest, LifeInsuranceHistory, LifeInsuranceHistoryInv, RetirementHistory, @@ -260,6 +262,18 @@ class BPBrowser(LoginBrowser, StatesMixin): HonorTransferPage ) + # transfer_summary needs to be before transfer_confirm because both + # have one url in common with different is_here conditions. + # We need to check the is_here of transfer_summary first to be on the correct page. + transfer_summary = URL( + r'/voscomptes/canalXHTML/virement/virementSafran_national/confirmerVirementNational-virementNational.ea', + r'/voscomptes/canalXHTML/virement/virementSafran_pea/confirmerInformations-virementPea.ea', + r'/voscomptes/canalXHTML/virement/virementSafran_sepa/confirmer-creerVirementSepa.ea', + r'/voscomptes/canalXHTML/virement/virementSafran_national/confirmer-creerVirementNational.ea', + r'/voscomptes/canalXHTML/virement/virementSafran_sepa/confirmerInformations-virementSepa.ea', + r'/voscomptes/canalXHTML/virement/virementSafran_sepa/finalisation-creerVirementSepa.ea', + TransferSummary + ) transfer_confirm = URL( r'/voscomptes/canalXHTML/virement/virementSafran_pea/validerVirementPea-virementPea.ea', r'/voscomptes/canalXHTML/virement/virementSafran_sepa/valider-creerVirementSepa.ea', @@ -273,14 +287,6 @@ class BPBrowser(LoginBrowser, StatesMixin): r'/voscomptes/canalXHTML/virement/virementSafran_sepa/confirmer-creerVirementSepa.ea', TransferConfirm ) - transfer_summary = URL( - r'/voscomptes/canalXHTML/virement/virementSafran_national/confirmerVirementNational-virementNational.ea', - r'/voscomptes/canalXHTML/virement/virementSafran_pea/confirmerInformations-virementPea.ea', - r'/voscomptes/canalXHTML/virement/virementSafran_sepa/confirmer-creerVirementSepa.ea', - r'/voscomptes/canalXHTML/virement/virementSafran_national/confirmer-creerVirementNational.ea', - r'/voscomptes/canalXHTML/virement/virementSafran_sepa/confirmerInformations-virementSepa.ea', - TransferSummary - ) create_recipient = URL( r'/voscomptes/canalXHTML/virement/mpiGestionBeneficiairesVirementsCreationBeneficiaire/init-creationBeneficiaire.ea', @@ -303,9 +309,9 @@ class BPBrowser(LoginBrowser, StatesMixin): r'/voscomptes/canalXHTML/virement/mpiGestionBeneficiairesVirementsCreationBeneficiaire/validerRecapBeneficiaire-creationBeneficiaire.ea', ConfirmPage ) - rcpt_error = URL( + otp_benef_transfer_error = URL( r'/voscomptes/canalXHTML/securisation/otp/validation-securisationOTP.ea', - RcptErrorPage, + OtpErrorPage, ) rcpt_summary = URL( r'/voscomptes/canalXHTML/virement/mpiGestionBeneficiairesVirementsCreationBeneficiaire/finalisation-creationBeneficiaire.ea', @@ -378,7 +384,7 @@ class BPBrowser(LoginBrowser, StatesMixin): accounts = None - __states__ = ('need_reload_state', 'sms_form', 'recipient_form') + __states__ = ('need_reload_state', 'sms_form') def __init__(self, config, *args, **kwargs): self.weboob = kwargs.pop('weboob') @@ -398,7 +404,6 @@ def __init__(self, config, *args, **kwargs): proxy=self.PROXIES ) - self.recipient_form = None self.sms_form = None self.need_reload_state = None @@ -805,7 +810,6 @@ def init_transfer(self, account, recipient, amount, transfer): return self.page.handle_response(transfer) - @need_login def validate_transfer_eligibility(self, transfer, **params): # Using ValueBool to be sure to handle any kind of response. # If it is not the accepted strings/bools, it crashes. @@ -821,14 +825,33 @@ def validate_transfer_eligibility(self, transfer, **params): return self.page.handle_response(transfer) raise TransferInvalidEmitter("Impossible d'effectuer un virement sans attestation sur l'honneur que vous êtes bien le titulaire, représentant légal ou mandataire du compte à vue ou CCP.") - @need_login - def execute_transfer(self, transfer, code=None): - assert self.transfer_confirm.is_here(), 'Case not handled.' - self.page.confirm() - # Should only happen if double auth. + def validate_transfer_code(self, transfer, code): + if not self.post_code(code): + raise TransferBankError('La validation du code SMS a expirée.') + + if self.otp_benef_transfer_error.is_here(): + error = self.page.get_error() + if error: + if 'Votre code sécurité est incorrect' in error: + raise TransferInvalidOTP(message=error) + raise AssertionError('Unhandled error message : "%s"' % error) + + return transfer + + def execute_transfer(self, transfer): + # If we just validated a code we land on transfer_summary. + # If we just initiated the transfer we land on transfer_confirm. if self.transfer_confirm.is_here(): - self.page.choose_device() - self.page.double_auth(transfer) + # This will send a sms if a certicode validation is needed + self.page.confirm() + if self.transfer_confirm.is_here() and self.page.is_certicode_needed(): + self.need_reload_state = True + self.sms_form = self.page.get_sms_form() + raise TransferStep( + transfer, + Value('code', label='Veuillez saisir le code de validation reçu par SMS'), + ) + return self.page.handle_response(transfer) def build_recipient(self, recipient): @@ -843,12 +866,18 @@ def build_recipient(self, recipient): return r def post_code(self, code): - data = {} - for k, v in self.recipient_form.items(): - if k != 'url': - data[k] = v - data['codeOTPSaisi'] = code - self.location(self.recipient_form['url'], data=data) + url = self.sms_form.pop('url') + self.sms_form['codeOTPSaisi'] = code + self.location(url, data=self.sms_form, allow_redirects=False) + + if self.response.status_code == 302: + location = self.response.headers.get('location') + if 'loginform' in location: + # The form timed out + return False + self.location(location) + + return True def end_new_recipient_with_polling(self, recipient): polling_url = self.absurl( @@ -903,10 +932,11 @@ def new_recipient(self, recipient, is_bp_account=False, **params): if 'code' in params: # Case of SMS OTP - self.post_code(params['code']) - self.recipient_form = None + if not self.post_code(params['code']): + raise AddRecipientBankError('La validation du code SMS a expirée.') + self.sms_form = None - if self.rcpt_error.is_here(): + if self.otp_benef_transfer_error.is_here(): error = self.page.get_error() if error: if 'Votre code sécurité est incorrect' in error: diff --git a/modules/bp/module.py b/modules/bp/module.py index c8c0b202a59b3b6c22bf902f923039e890bdcc00..18543db5aa3e126e411084578baf71df5abc8526 100644 --- a/modules/bp/module.py +++ b/modules/bp/module.py @@ -105,6 +105,8 @@ def init_transfer(self, transfer, **params): if 'transfer_honor_savings' in params: return self.browser.validate_transfer_eligibility(transfer, **params) + elif 'code' in params: + return self.browser.validate_transfer_code(transfer, params['code']) self.logger.info('Going to do a new transfer') account = strict_find_object(self.iter_accounts(), iban=transfer.account_iban) diff --git a/modules/bp/pages/__init__.py b/modules/bp/pages/__init__.py index 5a2b7230ad4522276f4a50bdafb2cd1758ff80fd..a2ad2d3bc30a4adbd62f1a38a7f82d95111c0fd4 100644 --- a/modules/bp/pages/__init__.py +++ b/modules/bp/pages/__init__.py @@ -30,7 +30,7 @@ TransferSummary, CreateRecipient, ValidateRecipient, ValidateCountry, ConfirmPage, RcptSummary, HonorTransferPage, RecipientSubmitDevicePage, - RcptErrorPage, + OtpErrorPage, ) from .subscription import SubscriptionPage, DownloadPage, ProSubscriptionPage @@ -40,5 +40,5 @@ 'AccountDesactivate', 'TransferChooseAccounts', 'CompleteTransfer', 'TransferConfirm', 'TransferSummary', 'UnavailablePage', 'CardsList', 'AccountRIB', 'Advisor', 'CreateRecipient', 'ValidateRecipient', 'ValidateCountry', 'ConfirmPage', 'RcptSummary', 'SubscriptionPage', 'DownloadPage', 'ProSubscriptionPage', 'RevolvingAttributesPage', 'Validated2FAPage', 'TwoFAPage', - 'SmsPage', 'DecoupledPage', 'HonorTransferPage', 'RecipientSubmitDevicePage', 'RcptErrorPage', + 'SmsPage', 'DecoupledPage', 'HonorTransferPage', 'RecipientSubmitDevicePage', 'OtpErrorPage', ] diff --git a/modules/bp/pages/transfer.py b/modules/bp/pages/transfer.py index 393212ce39e2a9235fdfe2b7434db9031869749a..f380edce4fbb8d47d280b3ef3c597fecb0591979 100644 --- a/modules/bp/pages/transfer.py +++ b/modules/bp/pages/transfer.py @@ -25,26 +25,25 @@ from datetime import datetime from weboob.capabilities.bank import ( - TransferBankError, Transfer, TransferStep, Recipient, - AccountNotFound, AddRecipientBankError, Emitter, + TransferBankError, Transfer, Recipient, AccountNotFound, + AddRecipientBankError, Emitter, ) from weboob.capabilities.base import find_object, empty, NotAvailable -from weboob.browser.pages import LoggedPage +from weboob.browser.pages import LoggedPage, PartialHTMLPage from weboob.browser.filters.standard import CleanText, Env, Regexp, Date, CleanDecimal, Currency from weboob.browser.filters.html import Attr, Link from weboob.browser.elements import ListElement, ItemElement, method, SkipItem from weboob.tools.capabilities.bank.transactions import FrenchTransaction from weboob.tools.capabilities.bank.iban import is_iban_valid -from weboob.tools.value import Value from weboob.tools.compat import urljoin -from weboob.exceptions import BrowserUnavailable, AuthMethodNotImplemented +from weboob.exceptions import BrowserUnavailable from .base import MyHTMLPage class CheckTransferError(MyHTMLPage): def on_load(self): - MyHTMLPage.on_load(self) + super(CheckTransferError, self).on_load() error = CleanText(""" //span[@class="app_erreur"] | //p[@class="warning"] @@ -219,23 +218,19 @@ def is_here(self): not CleanText('//p[contains(text(), "Vous pouvez le consulter dans le menu")]')(self.doc) or self.doc.xpath('//input[@title="Confirmer la demande de virement"]') # appears when there is no need for otp/polling or self.doc.xpath("//span[contains(text(), 'cliquant sur le bouton \"CONFIRMER\"')]") # appears on the page when there is a 'Confirmer' button or not + or CleanText('//label[contains(text(), "saisir votre code de validation reçu par SMS)]')(self.doc) # appears when there is an otp ) - def choose_device(self): - # When there is no "Confirmer" button, - # it means that the device pop up appeared (it is called by js) - if ( - not self.doc.xpath('//input[@value="Confirmer"]') - or self.doc.xpath('//input[@name="codeOTPSaisi"]') - ): - # transfer validation form with sms cannot be tested yet - raise AuthMethodNotImplemented() - raise AssertionError('Should not be on confirmation page after posting the form.') - - def double_auth(self, transfer): - code_needed = CleanText('//label[@for="code_securite"]')(self.doc) - if code_needed: - raise TransferStep(transfer, Value('code', label=code_needed)) + def is_certicode_needed(self): + return CleanText('//div[contains(text(), "veuillez saisir votre code de validation reçu par SMS")]')(self.doc) + + def get_sms_form(self): + form = self.get_form(name='SaisieOTP') + # Confirmation url is relative to the current page. We need to + # build it now or the relative path will fail when reloading state + # because we do not reload the url in it. + form['url'] = self.absurl(form.url) + return form def confirm(self): form = self.get_form(id='formID') @@ -254,8 +249,8 @@ def handle_response(self, transfer): '//form//h3[contains(text(), "créditer")]//following::span[1]', replace=[(' ', '')] )(self.doc) - assert transfer.account_id in account_txt or ''.join(transfer.account_label.split()) == account_txt, 'Something went wrong' - assert transfer.recipient_id in recipient_txt or ''.join(transfer.recipient_label.split()) == recipient_txt, 'Something went wrong' + assert transfer.account_id in account_txt, 'Something went wrong' + assert transfer.recipient_id in recipient_txt, 'Something went wrong' exec_date = Date( CleanText('//h3[contains(text(), "virement")]//following::span[@class="date"]'), @@ -294,10 +289,6 @@ def handle_response(self, transfer): # not always available if transfer_id and not transfer.id: transfer.id = transfer_id - else: - # TODO handle transfer with sms code. - if 'veuillez saisir votre code de validation' in CleanText('//div[@class="bloc Tmargin"]')(self.doc): - raise NotImplementedError() # WARNING: At this point, the transfer was made. # The following code is made to retrieve the transfer execution date, @@ -409,14 +400,16 @@ def get_device_choice_url(self): def set_browser_form(self): form = self.get_form(name='SaisieOTP') - self.browser.recipient_form = dict((k, v) for k, v in form.items() if v) + self.browser.sms_form = dict((k, v) for k, v in form.items() if v) # Confirmation url is relative to the current page. We need to # build it now or the relative path will fail when reloading state # because we do not reload the url in it. - self.browser.recipient_form['url'] = urljoin(self.url, form.url) + self.browser.sms_form['url'] = urljoin(self.url, form.url) -class RcptErrorPage(LoggedPage, MyHTMLPage): +class OtpErrorPage(LoggedPage, PartialHTMLPage): + # Need PartialHTMLPage because sometimes we land on this page with + # a status_code 302, so the page is empty and the build_doc crash. def get_error(self): return CleanText('//form//span[@class="warning"]')(self.doc)