Skip to content
Commits on Source (3)
......@@ -26,10 +26,9 @@
from woob.exceptions import (
BrowserIncorrectPassword, BrowserUnavailable, ImageCaptchaQuestion, BrowserQuestion,
WrongCaptchaResponse, NeedInteractiveFor2FA, BrowserPasswordExpired,
AppValidation, AppValidationExpired, AppValidationCancelled,
AppValidation, AppValidationExpired, AppValidationCancelled, AuthMethodNotImplemented,
)
from woob.tools.value import Value
from woob.browser.browsers import ClientError
from .pages import (
LoginPage, SubscriptionsPage, DocumentsPage, DownloadDocumentPage, HomePage,
......@@ -47,10 +46,23 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
L_LOGIN = 'Connexion'
L_SUBSCRIBER = 'Nom : (.*) Modifier E-mail'
UNSUPPORTED_TWOFA_MESSAGE = (
"Cette méthode d'authentification forte n'est pas supporté. "
"Veuillez désactiver la vérification en deux étapes depuis votre compte et réessayer."
)
# The details of the message "L'adresse e-mail est déjà utilisée" are "Il y a un autre compte Amazon
# avec l'email <email> mais avec un mot de passe différent. L'adresse email a déjà été vérifiée par cet
# autre compte et seul un compte peut être actif pour une adresse email. Le mot de passe avec lequel vous
# vous êtes connecté est associé à un compte non vérifié." which tell us there are 2 existing account with
# the same email address and one of accounts is verified and not the other one and an email address can only
# be associated to one account which means we are indeed trying to sign in and not to sign up
WRONGPASS_MESSAGES = [
"Votre mot de passe est incorrect",
"Saisissez une adresse e-mail ou un numéro de téléphone portable valable",
"Impossible de trouver un compte correspondant à cette adresse e-mail"
"Impossible de trouver un compte correspondant à cette adresse e-mail",
"L'adresse e-mail est déjà utilisée",
"Numéro de téléphone incorrect",
]
WRONG_CAPTCHA_RESPONSE = "Saisissez les caractères tels qu'ils apparaissent sur l'image."
......@@ -91,15 +103,21 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
otp_style = None
otp_headers = None
summary_documents_content = {}
def __init__(self, config, *args, **kwargs):
self.config = config
kwargs['username'] = self.config['email'].get()
kwargs['password'] = self.config['password'].get()
super(AmazonBrowser, self).__init__(*args, **kwargs)
self.previous_url = None
def locate_browser(self, state):
if '/ap/cvf/verify' not in state['url']:
if not state['url']:
return
if '/ap/cvf/verify' not in state['url'] and not state['url'].endswith('/ap/signin'):
# don't perform a GET to this url, it's the otp url, which will be reached by otp_form
# get requests to /ap/signin raise a 404 Client Error
self.location(state['url'])
def check_interactive(self):
......@@ -139,7 +157,8 @@ def handle_security(self):
# we don't raise an error because for the seller account 2FA is mandatory
self.logger.warning('2FA is enabled, all connections send an OTP')
if self.page.has_form_verify() or self.page.has_form_auth_mfa() or self.page.has_form_select_device():
has_new_otp_form = self.page.has_form_select_device()
if self.page.has_form_verify() or self.page.has_form_auth_mfa() or has_new_otp_form:
self.check_interactive()
self.page.send_code()
captcha = self.page.get_captcha_url()
......@@ -147,6 +166,10 @@ def handle_security(self):
if captcha and not self.config['captcha_response'].get():
image = self.open(captcha).content
raise ImageCaptchaQuestion(image)
# For some accounts after the login page we are redirected to the new-otp page which asks us to send
# a notification but after sending the code we are redirected to the new-otp page again and forever
if has_new_otp_form and self.page.has_form_select_device():
raise AuthMethodNotImplemented(self.UNSUPPORTED_TWOFA_MESSAGE)
if self.page.has_form_verify() or self.page.has_form_auth_mfa():
form = self.page.get_response_form()
......@@ -164,6 +187,10 @@ def handle_captcha(self, captcha):
raise ImageCaptchaQuestion(image)
def check_app_validation(self):
# When the approval page is reloaded from the storage we are redirected to the URL
# we were trying to access if the user approved the connection
if not self.approval_page.is_here():
return
# 25' on website, we don't wait that much, but leave sufficient time for the user
timeout = time.time() + 600.00
app_validation_link = self.page.get_link_app_validation()
......@@ -247,10 +274,12 @@ def do_login(self):
self.to_english(self.LANGUAGE)
# To see if we're connected. If not, we land on LoginPage
try:
# We need to try previous_url first since sometime we can access the history page without being
# redirected to the login page while previous_url is redirected
if self.previous_url:
self.previous_url.go()
else:
self.history.go()
except ClientError:
pass
if not self.login.is_here():
return
......@@ -279,6 +308,9 @@ def do_login(self):
assert any(wrongpass_message in msg for wrongpass_message in self.WRONGPASS_MESSAGES), msg
raise BrowserIncorrectPassword(msg)
if self.reset_password_page.is_here():
raise BrowserPasswordExpired(self.page.get_message())
def is_login(self):
if self.login.is_here():
self.do_login()
......@@ -302,7 +334,9 @@ def iter_subscription(self):
self.subscriptions.go()
if not self.subscriptions.is_here():
self.previous_url = self.subscriptions
self.is_login()
self.previous_url = None
yield self.page.get_item()
......
......@@ -31,5 +31,10 @@ class AmazonEnBrowser(AmazonBrowser):
L_LOGIN = 'Login'
L_SUBSCRIBER = 'Name: (.*) Edit E'
UNSUPPORTED_TWOFA_MESSAGE = (
"This strong authentication method is not supported. "
"Please disable the Two-Step Verification before retrying."
)
WRONGPASS_MESSAGES = ['Your password is incorrect', 'We cannot find an account with that email address']
WRONG_CAPTCHA_RESPONSE = "Enter the characters as they are given in the challenge."
......@@ -90,13 +90,18 @@ def iter_documents(self, subscription):
subscription = self.get_subscription(subscription)
return self.browser.iter_documents(subscription)
def get_pdf_from_cache_or_download_it(self, document):
summary_document = self.browser.summary_documents_content.pop(document.id, None)
if summary_document:
return summary_document
return self.browser.open(document.url).content
def download_document(self, document):
if not isinstance(document, Document):
document = self.get_document(document)
if document.url is NotAvailable:
return
return self.browser.open(document.url).content
return self.get_pdf_from_cache_or_download_it(document)
def download_document_pdf(self, document):
if not isinstance(document, Document):
......@@ -104,7 +109,8 @@ def download_document_pdf(self, document):
if document.url is NotAvailable:
return
if document.format == 'pdf':
return self.browser.open(document.url).content
return self.get_pdf_from_cache_or_download_it(document)
# We can't pass the html document we saved before as a string since there is a freeze when wkhtmltopdf
# takes a string
url = urljoin(self.browser.BASEURL, document.url)
return html_to_pdf(self.browser, url=url)
......@@ -19,6 +19,7 @@
from __future__ import unicode_literals
from woob.browser.exceptions import ServerError
from woob.browser.pages import HTMLPage, LoggedPage, FormNotFound, PartialHTMLPage, pagination
from woob.browser.elements import ItemElement, ListElement, method
from woob.browser.filters.html import Link, Attr
......@@ -126,7 +127,8 @@ def get_approval_status(self):
class ResetPasswordPage(HTMLPage):
pass
def get_message(self):
return CleanText('//h2')(self.doc)
class LanguagePage(HTMLPage):
......@@ -160,7 +162,11 @@ def get_response_form(self):
return form
def get_error_message(self):
return CleanText('//div[@id="auth-error-message-box"]')(self.doc)
return Coalesce(
CleanText('//div[@id="auth-error-message-box"]'),
CleanText('//div[not(@id="auth-cookie-warning-message")]/div/h4[@class="a-alert-heading"]'),
default=NotAvailable,
)(self.doc)
class PasswordExpired(HTMLPage):
......@@ -176,11 +182,23 @@ class get_item(ItemElement):
obj_id = 'amazon'
def obj_subscriber(self):
profile_data = json.loads(Regexp(
completed_customer_profile_data = Regexp(
RawText('//script[contains(text(), "window.CustomerProfileRootProps")]'),
r'window.CustomerProfileRootProps = ({.+});',
)(self))
return profile_data.get('nameHeaderData', {}).get('name', NotAvailable)
default=NotAvailable,
)(self)
if completed_customer_profile_data:
profile_data = json.loads(completed_customer_profile_data)
return profile_data.get('nameHeaderData', {}).get('name', NotAvailable)
# The user didn't complete his profile, so we have to get the data in a different way
# We have to get the name from "Cette action est nécessaire, cependant vous pouvez saisir
# un nom différent de celui associé à votre compte (<fullname>)" (The message change with
# a different language but the regex stays the same)
return Regexp(
CleanText('//div[@data-name="name"]//div[@class="a-row"]/span'),
r'.*\((.*)\)',
default=NotAvailable,
)(self)
def obj_label(self):
return self.page.browser.username
......@@ -226,6 +244,7 @@ def obj_date(self):
Date(CleanText('.//div[has-class("a-span4") and not(has-class("recipient"))]/div[2]'), parse_func=parse_french_date, dayfirst=True, default=NotAvailable),
Date(CleanText('.//div[has-class("a-span3") and not(has-class("recipient"))]/div[2]'), parse_func=parse_french_date, dayfirst=True, default=NotAvailable),
Date(CleanText('.//div[has-class("a-span2") and not(has-class("recipient"))]/div[2]'), parse_func=parse_french_date, dayfirst=True, default=NotAvailable),
default=NotAvailable,
)(self)
def obj_total_price(self):
......@@ -251,20 +270,57 @@ def obj_url(self):
default=NotAvailable,
)(self)
if not url:
url = Coalesce(
Link('//a[contains(@href, "download")]|//a[contains(@href, "generated_invoices")]', default=NotAvailable),
Link('//a[contains(text(), "Récapitulatif de commande")]', default=NotAvailable),
default=NotAvailable
)(async_page.doc)
download_elements = async_page.doc.xpath('//a[contains(@href, "download")]')
if download_elements and len(download_elements) > 1:
# Sometimes there are multiple invoices for one order and to avoid missing the other invoices
# we are taking the order summary instead
url = Link(
'//a[contains(text(), "Récapitulatif de commande")]',
default=NotAvailable
)(async_page.doc)
else:
url = Coalesce(
Link(
'//a[contains(@href, "download")]|//a[contains(@href, "generated_invoices")]',
default=NotAvailable,
),
Link('//a[contains(text(), "Récapitulatif de commande")]', default=NotAvailable),
default=NotAvailable
)(async_page.doc)
doc_id = Field('id')(self)
# We have to check whether the document is available or not and we have to keep the content to use
# it later in obj_format to determine if this document is a PDF. We can't verify this information
# with a HEAD request since Amazon doesn't allow those requests (405 Method Not Allowed)
if 'summary' in url and not self.page.browser.summary_documents_content.get(doc_id, None):
try:
self.page.browser.summary_documents_content[doc_id] = self.page.browser.open(url).content
except ServerError:
# For some orders (witnessed for 1 order right now) amazon respond with a status code 500
# It's also happening using a real browser
return NotAvailable
return url
def obj_format(self):
if not Field('url')(self):
url = Field('url')(self)
if not url:
return NotAvailable
if 'summary' in Field('url')(self):
return 'html'
if 'summary' in url:
content = self.page.browser.summary_documents_content[Field('id')(self)]
# Summary documents can be a PDF file or an HTML file but there are
# no hints of it before trying to get the document
if content[:4] != b'%PDF':
return 'html'
return 'pdf'
def condition(self):
# Sometimes an order can be empty
return bool(Coalesce(
CleanText('.//div[has-class("a-span4")]'),
CleanText('.//div[has-class("a-span3")]'),
CleanText('.//div[has-class("a-span2")]'),
default=NotAvailable,
)(self))
class DownloadDocumentPage(LoggedPage, PartialHTMLPage):
pass
......@@ -26,7 +26,7 @@
from woob.exceptions import (
BrowserIncorrectPassword, ActionNeeded, BrowserUnavailable,
AuthMethodNotImplemented, BrowserQuestion,
AuthMethodNotImplemented, BrowserQuestion, ScrapingBlocked,
)
from woob.browser.browsers import TwoFactorBrowser, need_login
from woob.browser.exceptions import HTTPNotFound, ServerError, ClientError
......@@ -177,8 +177,11 @@ def send_login_request(self, data):
# - headers not in the right order
# - headers with value that doesn't match the one from website
# - headers missing
# - IP blacklisted
# What's next ?
if "CBIS_Challenge_Or_Deny" in message:
# IP blacklisted
raise ScrapingBlocked()
raise AssertionError('Error code "LGON011" (msg:"%s")' % message)
elif error_code == 'LGON013':
self.raise_otp()
......@@ -191,10 +194,6 @@ def prepare_request(self, req):
prep.headers = OrderedDict(sorted(prep.headers.items(), key=lambda i: i[0].lower()))
return prep
def locate_browser(self, url):
# For some reason, it looks like we cannot reconnect from storage.
pass
def clear_init_cookies(self):
# Keep the device-id to prevent an SCA
for cookie in self.session.cookies:
......
......@@ -40,7 +40,7 @@
from .pages.login import (
KeyboardPage, LoginPage, ChangepasswordPage, PredisconnectedPage, DeniedPage,
AccountSpaceLogin, ErrorPage, AuthorizePage,
AccountSpaceLogin, ErrorPage, AuthorizePage, InfiniteLoopPage, LoginEndPage,
)
from .pages.bank import (
AccountsPage as BankAccountsPage, CBTransactionsPage, TransactionsPage,
......@@ -77,6 +77,12 @@ class AXABrowser(LoginBrowser):
r'https://espaceclient.axa.fr/content/ecc-public/errors/500.html',
ErrorPage
)
login_end = URL(r'https://espaceclient.axa.fr/$', LoginEndPage)
infinite_redirect = URL(
r'http[s]?://www.axabanque.fr/webapp/axabanque/jsp(/erreur)?/[\d\.:]+/webapp/axabanque/jsp/erreur/erreurBanque',
# ex: 'http://www.axabanque.fr/webapp/axabanque/jsp/172.25.100.12:80/webapp/axabanque/jsp/erreur/erreurBanque.faces'
InfiniteLoopPage
)
def do_login(self):
# Due to the website change, login changed too.
......@@ -111,7 +117,18 @@ def do_login(self):
raise ActionNeeded('Vous devez réaliser la double authentification sur le portail internet')
# home page to finish login
self.location('https://espaceclient.axa.fr/')
self.location('https://espaceclient.axa.fr/', allow_redirects=False)
for _ in range(13):
# When trying to reach home we are normally redirected a few times.
# But very rarely we are redirected 13 times before entering
# an infinite redirection loop between 'infinite_redirect'
# url to another. Need to try later.
location = self.response.headers.get('location')
if not location:
break
self.location(location)
if self.infinite_redirect.is_here():
raise BrowserUnavailable()
class AXABanque(AXABrowser, StatesMixin):
......
......@@ -117,6 +117,13 @@ def on_load(self):
raise ActionNeeded()
class LoginEndPage(RawPage):
# just a pass-through page at the end of login
# need is_here to avoid confusion with .pages.wealth.WealthAccountsPage
def is_here(self):
return self.response.status_code == 302
class AccountSpaceLogin(JsonPage):
def get_error_link(self):
return self.doc.get('informationUrl')
......@@ -136,6 +143,10 @@ def on_load(self):
raise BrowserUnavailable(error)
class InfiniteLoopPage(HTMLPage):
pass
class AuthorizePage(HTMLPage):
def on_load(self):
form = self.get_form()
......
......@@ -127,15 +127,15 @@ class BanquePopulaire(LoginBrowser):
InfoTokensPage
)
authentication_step = URL(
r'https://www.icgauth.banquepopulaire.fr/dacsrest/api/v1u0/transaction/(?P<validation_id>[^/]+)/step',
r'https://www.icgauth.(?P<subbank>.*).fr/dacsrest/api/v1u0/transaction/(?P<validation_id>[^/]+)/step',
AuthenticationStepPage
)
authentication_method_page = URL(
r'https://www.icgauth.banquepopulaire.fr/dacsrest/api/v1u0/transaction/(?P<validation_id>)',
r'https://www.icgauth.(?P<subbank>.*).fr/dacsrest/api/v1u0/transaction/(?P<validation_id>)',
AuthenticationMethodPage,
)
vk_image = URL(
r'https://www.icgauth.banquepopulaire.fr/dacs-rest-media/api/v1u0/medias/mappings/[a-z0-9-]+/images',
r'https://www.icgauth.(?P<subbank>.*).fr/dacs-rest-media/api/v1u0/medias/mappings/[a-z0-9-]+/images',
VkImagePage,
)
index_page = URL(r'https://[^/]+/cyber/internet/Login.do', IndexPage)
......@@ -284,6 +284,8 @@ class BanquePopulaire(LoginBrowser):
subscription_page = URL(r'https://[^/]+/api-bp/wapi/2.0/abonnes/current2/contrats', SubscriptionsPage)
documents_page = URL(r'/api-bp/wapi/2.0/abonnes/current/documents/recherche-avancee', DocumentsPage)
current_subbank = None
def __init__(self, website, *args, **kwargs):
self.website = website
self.BASEURL = 'https://%s' % website
......@@ -460,6 +462,7 @@ def do_new_login(self):
self.page.send_form()
self.page.check_errors(feature='login')
self.get_current_subbank()
validation_id = self.page.get_validation_id()
validation_unit_id = self.page.validation_unit_id
......@@ -486,6 +489,7 @@ def do_new_login(self):
'Accept': 'application/json, text/plain, */*',
}
self.authentication_step.go(
subbank=self.current_subbank,
validation_id=validation_id,
json={
'validate': {
......@@ -521,7 +525,9 @@ def do_new_login(self):
url_params = parse_qs(urlparse(self.url).query)
validation_id = url_params['transactionID'][0]
self.authentication_method_page.go(validation_id=validation_id)
self.authentication_method_page.go(
subbank=self.current_subbank, validation_id=validation_id
)
# Need to do the redirect a second time to finish login
self.do_redirect(headers)
......@@ -1078,6 +1084,13 @@ def iter_documents(self, subscription):
def download_document(self, document):
return self.open(document.url, headers=self.documents_headers).content
def get_current_subbank(self):
match = re.search(r'icgauth.(?P<domaine>[\.a-z]*).fr', self.url)
if match:
self.current_subbank = match['domaine']
else:
self.current_subbank = 'banquepopulaire'
class iter_retry(object):
# when the callback is retried, it will create a new iterator, but we may already yielded
......
......@@ -1076,7 +1076,7 @@ def get_investment_page_params(self):
if url and params:
return url, params
return None
return None, None
class TransactionsPage(LoggedPage, MyHTMLPage):
......
......@@ -89,7 +89,12 @@ class BNPParibasBrowser(LoginBrowser, StatesMixin):
r'https://mabanque.bnpparibas/rsc/contrib/document/properties/identification-fr-part-V1.json', ListErrorPage
)
useless_page = URL(r'/fr/connexion/comptes-et-contrats', UselessPage)
useless_page = URL(
r'/fr/connexion/comptes-et-contrats',
r'/fr/secure/comptes-et-contrats',
UselessPage
)
otp = URL(
r'/fr/espace-prive/authentification-forte-anr',
r'https://.*/fr/secure/authentification-forte', # We can be redirected on other baseurl
......@@ -99,6 +104,7 @@ class BNPParibasBrowser(LoginBrowser, StatesMixin):
con_threshold = URL(
r'https://.*/100-connexion',
r'/fr/connexion/mot-de-passe-expire',
r'/fr/secure/mot-de-passe-expire',
r'/fr/espace-pro/changer-son-mot-de-passe',
r'/fr/espace-prive/mot-de-passe-expire',
r'/fr/client/mdp-expire',
......@@ -137,7 +143,7 @@ class BNPParibasBrowser(LoginBrowser, StatesMixin):
activate_recip_sms = URL(r'/virement-wspl/rest/activerBeneficiaire', ActivateRecipPage)
activate_recip_digital_key = URL(r'/virement-wspl/rest/verifierAuthentForte', ActivateRecipPage)
request_recip_activation = URL(r'/virement-wspl/rest/demanderCodeActivation', AddRecipPage)
validate_transfer = URL(r'/virement-wspl/rest/validationVirement', ValidateTransferPage)
validate_transfer = URL(r'/virement-wspl/rest/validationVirementIP', ValidateTransferPage)
register_transfer = URL(r'/virement-wspl/rest/enregistrerVirement', RegisterTransferPage)
advisor = URL(r'/conseiller-wspl/rest/monConseiller', AdvisorPage)
......@@ -192,14 +198,9 @@ def do_login(self):
# Get dynamically error messages
rep = self.errors_list.open()
error_message = rep.json().get(message).replace('<br>', ' ')
if message in ('authenticationFailure.ClientNotFoundException201', 'authenticationFailure.SecretErrorException201'):
raise BrowserIncorrectPassword(error_message)
if message in ('authenticationFailure.CurrentS1DelayException3', 'authenticationFailure.CurrentS2DelayException4'):
raise BrowserUserBanned(error_message)
raise AssertionError('Unhandled error at login: %s: %s' % (message, error_message))
exception = self.get_exception_from_message(message, error_message)
raise exception
code = QueryValue(None, 'code').filter(self.url)
......@@ -230,9 +231,55 @@ def do_login(self):
raise ActionNeeded(
"Veuillez réaliser l'authentification forte depuis votre navigateur."
)
# For some errors, bnporc doesn't return a 403 but redirect to the login page with an error message
# Instead of following the redirection, we parse the errorCode and raise exception with accurate error message
error_code = QueryValue(None, 'errorCode', default=None).filter(
self.response.headers.get('location')
)
if error_code:
self.list_error_page.go()
error_message = self.page.get_error_message(error_code)
raise BrowserUnavailable(error_message)
else:
raise AssertionError('Multiple redirects, check if we are not in an infinite loop')
def get_exception_from_message(self, message, error_message):
map_exception_to_messages = {
BrowserIncorrectPassword: {
'authenticationFailure.ClientNotFoundException201',
'authenticationFailure.SecretErrorException201',
'authenticationFailure.CompletedS1ErrorSecretException18',
'authenticationFailure.CompletedS2ErrorSecretException19',
'authenticationFailure.FailedLoginException',
'authenticationFailure.ZosConnectGetIKPIException',
'authenticationFailure.CasInvalidCredentialSecurityAttributeException',
},
BrowserUserBanned: {
'authenticationFailure.CurrentS1DelayException3',
'authenticationFailure.CurrentS2DelayException4',
'authenticationFailure.LockedAccountException202',
},
BrowserUnavailable: {
'authenticationFailure.TechnicalException900',
'authenticationFailure.TechnicalException901',
'authenticationFailure.TechnicalException902',
'authenticationFailure.TechnicalException903',
'authenticationFailure.TechnicalException904',
'authenticationFailure.TechnicalException905',
},
BrowserPasswordExpired: {
'authenticationFailure.ExpiredTmpPwdException50',
},
}
for exception, messages in map_exception_to_messages.items():
if message in messages:
return exception(error_message)
else:
return AssertionError('Unhandled error at login: %s: %s' % (message, error_message))
def load_state(self, state):
# reload state only for new recipient feature
if state.get('rcpt_transfer_id'):
......@@ -349,7 +396,6 @@ def iter_accounts(self):
return iter(self.accounts_list)
@need_login
def get_account(self, _id):
return find_object(self.iter_accounts(), id=_id, error=AccountNotFound)
......@@ -685,6 +731,21 @@ def prepare_transfer(self, account, recipient, amount, reason, exec_date):
data['compteCrediteur'] = recipient.id
return data
@need_login
def prepare_transfer_execution(self, transfer):
assert hasattr(transfer, '_type_operation'), 'Transfer obj attribute _type_operation is missing'
assert hasattr(transfer, '_repartition_frais'), 'Transfer obj attribute _repartition_frais is missing'
data = {
'emailBeneficiaire': '',
'mode': '2',
'notification': True,
'referenceVirement': transfer.id,
'typeOperation': transfer._type_operation,
'typeRepartitionFrais': transfer._repartition_frais,
}
return data
@need_login
def init_transfer(self, account, recipient, amount, reason, exec_date):
if recipient._web_state == 'En attente':
......@@ -695,7 +756,8 @@ def init_transfer(self, account, recipient, amount, reason, exec_date):
@need_login
def execute_transfer(self, transfer):
self.register_transfer.go(json={'referenceVirement': transfer.id})
data = self.prepare_transfer_execution(transfer)
self.register_transfer.go(json=data)
return self.page.handle_response(transfer)
@need_login
......@@ -794,6 +856,8 @@ def iter_documents(self, subscription):
'numCompte': subscription._number,
}
self.document_research.go(json=data)
if self.page.has_error():
return
iter_documents_functions = [self.page.iter_documents_pro, self.page.iter_documents]
for iter_documents in iter_documents_functions:
......@@ -813,3 +877,51 @@ def iter_documents(self, subscription):
class HelloBank(BNPParibasBrowser):
BASEURL = 'https://www.hellobank.fr/'
DIST_ID = 'HelloBank'
errors_list = URL(
r'/rsc/contrib/identification/src/zonespubliables/hellobank/fr/identification-fr-hellobank-CAS.json'
)
def _fetch_rib_document(self, subscription):
self.rib_page.go(
params={
'contractId': subscription.id,
'i18nSiteType': 'part', # site type value doesn't seem to matter as long as it's present
'i18nLang': 'fr',
'i18nVersion': 'V1',
},
)
if self.rib_page.is_here() and self.page.is_rib_available():
d = Document()
d.id = subscription.id + '_RIB'
d.url = self.page.url
d.type = DocumentTypes.RIB
d.format = 'pdf'
d.label = 'RIB'
return d
@need_login
def iter_documents(self, subscription):
rib = self._fetch_rib_document(subscription)
if rib:
yield rib
docs = []
id_docs = []
self.titulaire.go()
self.document.go()
iter_documents_functions = [self.page.iter_documents_pro, self.page.iter_documents]
for iter_documents in iter_documents_functions:
for doc in iter_documents(
sub_id=subscription.id, sub_number=subscription._number, baseurl=self.BASEURL
):
if doc.id not in id_docs:
docs.append(doc)
id_docs.append(doc.id)
# documents are sorted by type then date, sort them directly by date
docs = sorted(docs, key=lambda doc: doc.date, reverse=True)
for doc in docs:
yield doc
......@@ -30,6 +30,8 @@
from woob.capabilities.bill import Document, Bill, DocumentTypes
from woob.tools.compat import urlencode
from .pages import ErrorPage
patterns = {
r'Relevé': DocumentTypes.STATEMENT,
r'Livret(s) A': DocumentTypes.STATEMENT,
......@@ -122,7 +124,7 @@ def obj_type(self):
return get_document_type(Dict('libelleSousFamille')(self))
class DocumentsPage(LoggedPage, JsonPage):
class DocumentsPage(LoggedPage, ErrorPage):
@method
class iter_documents(DictElement):
# * refer to the account, it can be 'Comptes chèques', 'Comptes d'épargne', etc...
......
......@@ -59,6 +59,11 @@
from woob.tools.html import html2text
class ErrorPage(JsonPage):
def has_error(self):
return Dict('message')(self.doc) == "Erreur technique"
class UnavailablePage(HTMLPage):
pass
......@@ -464,6 +469,8 @@ def handle_response(self, account, recipient, amount, reason):
transfer.id = transfer_data['reference']
# This is true if a transfer with the same metadata has already been done recently
transfer._doublon = transfer_data['doublon']
transfer._type_operation = transfer_data['typeOperation']
transfer._repartition_frais = transfer_data['repartitionFrais']
transfer.account_balance = account.balance
return transfer
......@@ -913,10 +920,7 @@ def obj_currency(self):
return Currency(Dict('orderCurrency'), default=NotAvailable)(self)
class AdvisorPage(BNPPage):
def has_error(self):
return (self.doc.get('message') == 'Erreur technique')
class AdvisorPage(BNPPage, ErrorPage):
@method
class get_advisor(ListElement):
class item(ItemElement):
......
......@@ -21,11 +21,11 @@
from __future__ import unicode_literals
from datetime import date, datetime
import re
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
import requests
from dateutil.relativedelta import relativedelta
from woob.browser.retry import login_method, retry_on_logout, RetryLoginBrowser
from woob.browser.browsers import need_login, TwoFactorBrowser
......@@ -34,7 +34,7 @@
BrowserIncorrectPassword, BrowserHTTPNotFound, NoAccountsException,
BrowserUnavailable, ActionNeeded,
)
from woob.browser.exceptions import LoggedOut, ClientError
from woob.browser.exceptions import LoggedOut, ClientError, ServerError
from woob.capabilities.bank import (
Account, AccountNotFound, TransferError, TransferInvalidAmount,
TransferInvalidEmitter, TransferInvalidLabel, TransferInvalidRecipient,
......@@ -56,7 +56,8 @@
TransferAccounts, TransferRecipients, TransferCharacteristics, TransferConfirm, TransferSent,
AddRecipientPage, StatusPage, CardHistoryPage, CardCalendarPage, CurrencyListPage, CurrencyConvertPage,
AccountsErrorPage, NoAccountPage, TransferMainPage, PasswordPage, NewTransferWizard,
NewTransferEstimateFees, NewTransferConfirm, NewTransferSent, CardSumDetailPage, MinorPage,
NewTransferEstimateFees, NewTransferUnexpectedStep, NewTransferConfirm, NewTransferSent, CardSumDetailPage,
MinorPage, AddRecipientOtpSendPage, AddRecipientOtpCheckPage,
)
from .transfer_pages import TransferListPage, TransferInfoPage
from .document_pages import (
......@@ -169,6 +170,11 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
r'/compte/(?P<acc_type>[^/]+)/(?P<webid>\w+)/virements/programme/nouveau/(?P<id>\w+)/[89]$',
NewTransferConfirm
)
new_transfer_unexpected_step = URL(
r'/compte/(?P<acc_type>[^/]+)/(?P<webid>\w+)/virements/immediat/nouveau/(?P<id>\w+)/7$',
r'/compte/(?P<acc_type>[^/]+)/(?P<webid>\w+)/virements/programme/nouveau/(?P<id>\w+)/8$',
NewTransferUnexpectedStep
)
new_transfer_sent = URL(
r'/compte/(?P<acc_type>[^/]+)/(?P<webid>\w+)/virements/immediat/nouveau/(?P<id>\w+)/9$',
r'/compte/(?P<acc_type>[^/]+)/(?P<webid>\w+)/virements/programme/nouveau/(?P<id>\w+)/10$',
......@@ -178,6 +184,14 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
r'/compte/(?P<type>[^/]+)/(?P<webid>\w+)/virements/comptes-externes/nouveau/(?P<id>\w+)/\d',
AddRecipientPage
)
rcpt_send_otp_page = URL(
r'https://api.boursorama.com/services/api/v\d+\.\d+/_user_/_\w+_/session/otp/start(?P<otp_type>\w+)/\d+',
AddRecipientOtpSendPage,
)
rcpt_check_otp_page = URL(
r'https://api.boursorama.com/services/api/v\d+\.\d+/_user_/_\w+_/session/otp/check(?P<otp_type>\w+)/\d+',
AddRecipientOtpCheckPage,
)
asv = URL('/compte/assurance-vie/.*', AsvPage)
saving_history = URL(
......@@ -725,6 +739,12 @@ def init_transfer(self, transfer, **kwargs):
# Reset otp state when a new transfer is created
self.transfer_form = None
# Boursorama doesn't allow labels longer than 50 characters. To avoid
# crash for such a useless problem, we truncate it.
if len(transfer.label) > 50:
self.logger.info('Truncating transfer label from "%s" to "%s"', transfer.label, transfer.label[:50])
transfer.label = transfer.label[:50]
# Transfer_date_type is set and used only for the new transfer wizard flow
# the support for the old transfer wizard is left untouched as much as possible
# until it can be removed.
......@@ -785,10 +805,10 @@ def init_transfer(self, transfer, **kwargs):
fees = self.page.get_transfer_fee()
self.page.submit()
assert self.new_transfer_confirm.is_here()
transfer_error = self.page.get_errors()
if transfer_error:
raise TransferBankError(message=transfer_error)
assert self.new_transfer_confirm.is_here()
ret = self.page.get_transfer()
## Last checks to ensure that the confirmation matches what was expected
......@@ -834,10 +854,11 @@ def otp_validation_continue_transfer(self, transfer, **kwargs):
if not self.transfer_form:
# The session expired
raise TransferTimeout()
# Continue a previously initiated transfer after an otp step
# once the otp is validated, we should be redirected to the
# transfer sent page
self.send_otp_form(self.transfer_form, otp_code)
self.send_otp_data(self.transfer_form, otp_code, TransferBankError)
self.transfer_form = None
return True
......@@ -864,7 +885,7 @@ def execute_transfer(self, transfer, **kwargs):
# We are not sure if the transfer was successful or not, so raise an error
raise AssertionError('Confirmation message not found inside transfer sent page')
# the last page contains no info, return the last transfer object from init_transfer
# The last page contains no info, return the last transfer object from init_transfer
return transfer
@need_login
......@@ -918,9 +939,9 @@ def init_new_recipient(self, recipient):
assert self.page.is_confirm_send_sms(), 'Cannot reach the page asking to send a sms.'
self.page.confirm_send_sms()
otp_form, otp_field_value = self.check_and_initiate_otp(account.url)
if otp_form:
self.recipient_form = otp_form
otp_forms, otp_field_value = self.check_and_initiate_otp(account.url)
if otp_forms:
self.recipient_form = otp_forms
raise AddRecipientStep(recipient, otp_field_value)
# in the unprobable case that no otp was needed, go on
......@@ -939,7 +960,7 @@ def new_recipient(self, recipient, **kwargs):
# there is no confirmation to check the recipient
# validating the sms code directly adds the recipient
account_url = self.send_otp_form(self.recipient_form, otp_code)
account_url = self.send_otp_data(self.recipient_form, otp_code, AddRecipientBankError)
self.recipient_form = None
# Check if another otp step might be needed (ex.: email after sms)
......@@ -949,11 +970,29 @@ def new_recipient(self, recipient, **kwargs):
return self.check_and_update_recipient(recipient, account_url)
def send_otp_form(self, otp_form, value):
url = otp_form.pop('url')
account_url = otp_form.pop('account_url')
otp_form['strong_authentication_confirm[code]'] = value
self.location(url, data=otp_form)
def send_otp_data(self, otp_data, otp_code, exception):
# Validate the OTP
confirm_data = otp_data['confirm_data']
confirm_data['token'] = otp_code
url = confirm_data['url']
try:
self.location(url, data=confirm_data)
except ServerError as e:
# If the code is invalid, we have an error 503
if e.response.status_code == 503:
raise exception(message=e.response.json()['error']['message'])
raise
del otp_data['confirm_data']
account_url = otp_data.pop('account_url')
# Continue the navigation by sending the form data
# we saved.
html_page_form = otp_data.pop('html_page_form')
url = html_page_form.pop('url')
self.location(url, data=html_page_form)
return account_url
......@@ -980,13 +1019,15 @@ def check_and_initiate_otp(self, account_url):
else:
return None, None
otp_data = {'account_url': account_url}
otp_data['html_page_form'] = self.page.get_confirm_otp_form()
otp_data['confirm_data'] = self.page.get_confirm_otp_data()
self.page.send_otp()
assert self.page.is_confirm_otp(), 'The %s was not sent.' % otp_name
otp_form = self.page.get_confirm_otp_form()
otp_form['account_url'] = account_url
return otp_form, otp_field_value
return otp_data, otp_field_value
def check_and_update_recipient(self, recipient, account_url, account=None):
assert self.page.is_created(), 'The recipient was not added.'
......
......@@ -21,11 +21,10 @@
from __future__ import unicode_literals
import datetime
from decimal import Decimal
import re
from datetime import date
import hashlib
import datetime
from decimal import Decimal
from functools import wraps
from woob.browser.pages import (
......@@ -56,6 +55,7 @@
from woob.tools.capabilities.bank.transactions import FrenchTransaction
from woob.tools.compat import urljoin, urlencode, urlparse, range
from woob.tools.date import parse_french_date
from woob.tools.json import json
from woob.tools.value import Value
from woob.exceptions import (
BrowserQuestion, BrowserIncorrectPassword, BrowserHTTPNotFound, BrowserUnavailable,
......@@ -673,7 +673,7 @@ def obj__is_coming(self):
return (
Env('coming', default=False)(self)
or len(self.xpath('.//span[@title="Mouvement à débit différé"]'))
or self.obj_date() > date.today()
or self.obj_date() > datetime.date.today()
)
def obj_date(self):
......@@ -1474,8 +1474,8 @@ def submit_info(self, label, transfer_date_type, exec_date=None):
class NewTransferEstimateFees(LoggedPage, HTMLPage):
# STEP 7 for "immediate" if page "estimation des frais" before
# STEP 8 for "programme" if page "estimation des frais" before
# STEP 7 for "immediate"
# STEP 8 for "programme"
is_here = '//h3[text()="Estimation des frais liés à l\'instrument"]'
XPATH_AMOUNT = '//form[@name="EstimatedFees"]//tr[has-class("definition-list__row")][th[contains(text(),"Frais prélevés")]]/td[1]'
......@@ -1495,6 +1495,21 @@ def submit(self):
form.submit()
class NewTransferUnexpectedStep(LoggedPage, HTMLPage):
# STEP 7 for "immediate" if not "estimation des frais" and a form error
# STEP 8 for "programme" if not "estimation des frais" and a form error
def is_here(self):
# If we are not on the "estimation des frais" page
return not bool(
self.doc.xpath(
'//h3[text()="Estimation des frais liés à l\'instrument" or text()="Confirmer votre virement"]'
)
)
def get_errors(self):
return CleanText('//form//div[@class="form-errors"]//li')(self.doc)
class TransferOtpPage(LoggedPage, HTMLPage):
def _is_form(self, **kwargs):
try:
......@@ -1505,27 +1520,80 @@ def _is_form(self, **kwargs):
def is_send_sms(self):
return (
self._is_form(name='strong_authentication_prepare')
and Attr('//input[@id="strong_authentication_prepare_type"]', 'value')(self.doc) == 'brs-otp-sms'
self._is_form(xpath='//form[@data-strong-authentication-form]')
and 'sms' in Attr(
'//form[@data-strong-authentication-form]/div[@data-strong-authentication-payload]',
'data-strong-authentication-payload'
)(self.doc)
)
def is_send_email(self):
return (
self._is_form(name='strong_authentication_prepare')
and Attr('//input[@id="strong_authentication_prepare_type"]', 'value')(self.doc) == 'brs-otp-email'
self._is_form(xpath='//form[@data-strong-authentication-form]')
and 'email' in Attr(
'//form[@data-strong-authentication-form]/div[@data-strong-authentication-payload]',
'data-strong-authentication-payload'
)(self.doc)
)
def get_user_hash(self):
return Regexp(
CleanText('//script[contains(text(), "USER_HASH")]'),
r'"USER_HASH":"(\w+)"',
)(self.doc)
def get_api_host_url(self):
return Format(
'https://%s',
Regexp(
CleanText('//script[contains(text(), "window.BRS_CONFIG =")]'),
r'"API_HOST":"([\w\.]+)"',
)
)(self.doc)
def _get_otp_data(self, action):
otp_data = {}
otp_json_data = json.loads(
Attr(
'//form[@data-strong-authentication-form]/div[@data-strong-authentication-payload]',
'data-strong-authentication-payload'
)(self.doc)
)
otp_data['url'] = otp_json_data['challenges'][0]['parameters']['actions'][action]['url']
otp_data['url'] = otp_data['url'].replace('{userHash}', self.get_user_hash())
otp_data['url'] = '%s%s' % (self.get_api_host_url(), otp_data['url'])
otp_params = (
otp_json_data['challenges'][0]['parameters']['presentationScreen']
['actions']['start']['api']['params']
)
del otp_params['resourceId']
for k, v in otp_params.items():
otp_data[k] = v
return otp_data
def send_otp(self):
form = self.get_form(name='strong_authentication_prepare')
form.submit()
otp_data = self._get_otp_data('start')
def is_confirm_otp(self):
return self._is_form(name='strong_authentication_confirm')
url = otp_data.pop('url')
self.browser.location(url, data=otp_data)
def get_confirm_otp_data(self):
# The "confirm otp data" is the data required to validate
# the OTP code the user will give us.
return self._get_otp_data('check')
def get_confirm_otp_form(self):
form = self.get_form(name='strong_authentication_confirm')
otp_form = {k: v for k, v in form.items()}
otp_form['url'] = form.url
# The "confirm otp form" is the html form used to go to
# the next step (page) after the otp data has been validated.
otp_form = self.get_form(xpath='//form[@data-strong-authentication-form]')
otp_form = {k: v for k, v in dict(otp_form).items()}
otp_form['url'] = self.url
return otp_form
......@@ -1543,7 +1611,7 @@ def get_errors(self):
class get_transfer(ItemElement):
klass = Transfer
XPATH_TMPL = '//form[@name="Confirm"]//tr[has-class("definition-list__row")][th[contains(text(),"%s")]]/td[1]'
XPATH_TMPL = '//form[@name="Confirm"]//tr[has-class("definition-list__row")][th[contains(text(),"%s")]]/td[1]/span[1]'
mapping_date_type = {
'Ponctuel': TransferDateType.FIRST_OPEN_DAY,
......@@ -1709,6 +1777,15 @@ def is_created(self):
return CleanText('//p[contains(text(), "Le bénéficiaire a bien été ajouté.")]')(self.doc) != ""
class AddRecipientOtpSendPage(LoggedPage, JsonPage):
def is_confirm_otp(self):
return Dict('success')(self.doc)
class AddRecipientOtpCheckPage(LoggedPage, JsonPage):
pass
class PEPPage(LoggedPage, HTMLPage):
pass
......
......@@ -153,7 +153,7 @@ class BPBrowser(LoginBrowser, StatesMixin):
)
revolving_start = URL(r'/voscomptes/canalXHTML/sso/lbpf/souscriptionCristalFormAutoPost.jsp', AccountList)
par_accounts_revolving = URL(
r'https://espaceclientcreditconso.labanquepostale.fr/sav/loginlbpcrypt.do',
r'https://espaceclientcreditconso-sav.labanquepostale.fr/sav/loginlbpcrypt.do',
RevolvingAttributesPage
)
......
......@@ -49,8 +49,7 @@
SendSmsPage, CheckOtpPage, TrustedDevicesPage, UniversePage,
TokenPage, MoveUniversePage, SwitchPage,
LoansPage, AccountsPage, IbanPage, LifeInsurancesPage,
SearchPage, ProfilePage, EmailsPage, ErrorPage,
ErrorCodePage, LinebourseLoginPage,
SearchPage, ProfilePage, ErrorPage, ErrorCodePage, LinebourseLoginPage,
)
from .transfer_pages import (
RecipientListPage, EmittersListPage, ListAuthentPage,
......@@ -84,7 +83,6 @@ class BredBrowser(TwoFactorBrowser):
life_insurances = URL(r'/transactionnel/services/applications/avoirsPrepar/getAvoirs', LifeInsurancesPage)
search = URL(r'/transactionnel/services/applications/operations/getSearch/', SearchPage)
profile = URL(r'/transactionnel/services/rest/User/user', ProfilePage)
emails = URL(r'/transactionnel/services/applications/gestionEmail/getAdressesMails', EmailsPage)
error_code = URL(r'/.*\?errorCode=.*', ErrorCodePage)
accounts_twofa = URL(r'/transactionnel/v2/services/rest/Account/accounts', AccountsTwoFAPage)
......@@ -515,21 +513,7 @@ def get_profile(self):
self.get_universes()
self.profile.go()
profile = self.page.get_profile()
try:
self.emails.go()
except ClientError as e:
if e.response.status_code == 403:
msg = e.response.json().get('erreur', {}).get('libelle', '')
if msg == "Cette fonctionnalité n'est pas disponible avec votre compte.":
# We cannot get the mails, return the profile now.
return profile
raise
self.page.set_email(profile=profile)
return profile
return self.page.get_profile()
@need_login
def fill_account(self, account, fields):
......
......@@ -33,6 +33,7 @@
from woob.tools.capabilities.bank.investments import is_isin_valid
from woob.capabilities.profile import Person
from woob.browser.filters.standard import CleanText, CleanDecimal, Env, Eval
from woob.browser.filters.html import Link
from woob.browser.filters.json import Dict
from woob.browser.elements import DictElement, ItemElement, method
from woob.tools.capabilities.bank.transactions import FrenchTransaction
......@@ -365,13 +366,6 @@ def get_profile(self):
return profile
class EmailsPage(LoggedPage, MyJsonPage):
def set_email(self, profile):
content = self.get_content()
if 'emailPart' in content:
profile.email = content['emailPart']
class ErrorPage(LoggedPage, HTMLPage):
def on_load(self):
if 'gestion-des-erreurs/erreur-pwd' in self.url:
......@@ -398,6 +392,11 @@ def on_load(self):
raise BrowserIncorrectPassword(msg)
# 20104 & 1000: unknown error during login
elif code in ('20104', '1000'):
raise BrowserUnavailable(msg)
# If promotion page is here, skip it and go to the login page
if "Vous n'êtes pas encore abonné" in CleanText("//div[@class='bredco_text_header']")(self.doc):
url = Link("//div[@class='bredco_text_header']//a")(self.doc)
self.browser.location(url)
else:
raise BrowserUnavailable(msg)
assert False, 'Error %s is not handled yet.' % code
......@@ -19,8 +19,7 @@
from woob.capabilities.bank import CapBank
from woob.tools.backend import AbstractModule, BackendConfig
from woob.tools.value import ValueBackendPassword, Value, ValueTransient
from woob.tools.backend import AbstractModule
from .proxy_browser import ProxyBrowser
......@@ -35,16 +34,7 @@ class BtpbanqueModule(AbstractModule, CapBank):
EMAIL = 'elambert@budget-insight.com'
VERSION = '3.0'
LICENSE = 'LGPLv3+'
auth_type = {'weak' : "Code confidentiel (pro)",
'strong': "Sesame (pro)"}
CONFIG = BackendConfig(
Value('auth_type', label='Type de compte', choices=auth_type, default="weak"),
ValueBackendPassword('login', label='Code utilisateur', masked=False),
ValueBackendPassword('password', label='Code confidentiel ou code PIN', regexp='\d+'),
Value('nuser', label="Numéro d'utilisateur (optionnel)", regexp='\d{0,8}', default=''),
ValueTransient('emv_otp', regexp=r'\d{8}'),
ValueTransient('request_information'),
)
PARENT = 'caissedepargne'
BROWSER = ProxyBrowser
......
......@@ -73,7 +73,7 @@
CardsPage, CardsComingPage, CardsOldWebsitePage, TransactionPopupPage,
OldLeviesPage, NewLeviesPage, NewLoginPage, JsFilePage, AuthorizePage,
AuthenticationMethodPage, VkImagePage, AuthenticationStepPage, LoginTokensPage,
AppValidationPage, TokenPage, LoginApi, ConfigPage,
AppValidationPage, TokenPage, LoginApi, ConfigPage, SAMLRequestFailure,
)
from .transfer_pages import CheckingPage, TransferListPage
from .linebourse_browser import LinebourseAPIBrowser
......@@ -166,6 +166,7 @@ class CaisseEpargneLogin(LoginBrowser, StatesMixin):
r'https://www.icgauth.caisse-epargne.fr/dacsrest/api/v1u0/transaction/.*',
AuthenticationMethodPage,
)
saml_failure = URL(r'https://www.icgauth.caisse-epargne.fr/Errors/Errors.html', SAMLRequestFailure)
vk_image = URL(
r'https://(?P<domain>www.icgauth.[^/]+)/dacs-rest-media/api/v1u0/medias/mappings/[a-z0-9-]+/images',
VkImagePage,
......@@ -210,6 +211,7 @@ def __init__(self, nuser, config, *args, **kwargs):
self.browser_switched = False
self.need_emv_authentication = False
self.request_information = config['request_information'].get()
self.auth_type_choice = config['auth_type'].get()
self.connection_type = None
self.cdetab = None
self.continue_url = None
......@@ -269,7 +271,15 @@ def do_api_pre_login(self):
self.login_api.go(json=data, headers=headers)
self.cdetab = self.page.get_cdetab()
self.connection_type = self.page.get_connection_type()
if self.auth_type_choice:
if not self.page.is_auth_type_available(self.auth_type_choice):
raise BrowserIncorrectPassword("L'espace client demandé n'a pas été trouvé")
self.connection_type = self.auth_type_choice
if not self.connection_type:
# no nuser -> part
# else pro/pp/ent (must be only one available)
self.connection_type = self.page.get_connection_type()
def get_cdetab(self):
if not self.cdetab:
......@@ -803,7 +813,10 @@ def do_new_login(self, authentification_data=''):
self.authorize.go(params=params)
self.page.send_form()
if self.response.headers.get('Page_Erreur', '') == 'INDISPO':
if (
self.response.headers.get('Page_Erreur', '') == 'INDISPO'
or (self.saml_failure.is_here() and self.page.is_unavailable())
):
raise BrowserUnavailable()
pre_login_status = self.page.get_wrong_pre_login_status()
......@@ -1289,6 +1302,7 @@ def _get_history(self, info, account_card=None):
if not info['link'].startswith('HISTORIQUE'):
return
if 'measure_id' in info:
self.home_tache.go(tache='CPTSYNT0')
self.go_measure_list(info['measure_id_page_num'])
self.page.go_measure_accounts_list(info['measure_id'])
elif self.home.is_here():
......@@ -1490,6 +1504,11 @@ def get_coming_checking(self, account):
# We need to go to a specific levies page where we can find past and coming levies (such as recurring ones)
trs = []
self.home.go()
if 'measure_id' in account._info:
self.go_measure_list(account._info['measure_id_page_num'])
self.page.go_measure_accounts_list(account._info['measure_id'])
self.page.go_history(account._info)
self.page.go_cards() # need to go to cards page to have access to the nav bar where we can choose LeviesPage from
if not self.page.levies_page_enabled():
return trs
......@@ -1653,12 +1672,15 @@ def get_profile(self):
@need_login
def iter_recipients(self, origin_account):
if origin_account.type in [Account.TYPE_LOAN, Account.TYPE_CARD]:
if origin_account.type in [Account.TYPE_LOAN, Account.TYPE_CARD, Account.TYPE_MARKET]:
return []
if 'measure_id' in origin_account._info:
self.home.go()
self.home_tache.go(tache='MESLIST0')
if 'pro' in self.url:
# If transfer is not yet allowed, the next step will send a sms to the customer to validate it
self.home.go()
self.page.go_pro_transfer_availability()
if not self.page.is_transfer_allowed():
return []
......@@ -1680,13 +1702,19 @@ def iter_recipients(self, origin_account):
self.transfer.is_here() and not self.page.can_transfer(origin_account),
)
if any(go_transfer_errors):
return []
recipients = []
else:
recipients = self.page.iter_recipients(account_id=origin_account.id)
return self.page.iter_recipients(account_id=origin_account.id)
if 'measure_id' in origin_account._info:
# need return to measure home to avoid broken navigation
self.home.go()
self.home_tache.go(tache='MESLIST0')
return recipients
def pre_transfer(self, account):
if self.home.is_here():
if 'measure_id' in account._info:
if hasattr(account, '_info') and 'measure_id' in account._info:
self.go_measure_list(account._info['measure_id_page_num'])
self.page.go_measure_accounts_list(account._info['measure_id'])
else:
......
......@@ -235,11 +235,12 @@ def get_accounts_list(self):
if market_accounts:
linebourse_account_ids = {}
try:
self.go_linebourse()
params = {'_': '{}'.format(int(time.time() * 1000))}
self.linebourse.account_codes.go(params=params)
if self.linebourse.account_codes.is_here():
linebourse_account_ids = self.linebourse.page.get_accounts_list()
if any(account._access_linebourse for account in market_accounts):
self.go_linebourse()
params = {'_': '{}'.format(int(time.time() * 1000))}
self.linebourse.account_codes.go(params=params)
if self.linebourse.account_codes.is_here():
linebourse_account_ids = self.linebourse.page.get_accounts_list()
except AssertionError as e:
if str(e) != 'No linebourse space':
raise e
......