Commit 22355a1c authored by Vincent A's avatar Vincent A

backport master modules fixes

parent 372520a8
Pipeline #3394 failed with stages
in 13 minutes and 59 seconds
......@@ -144,7 +144,9 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
def check_app_validation(self):
# client has 60 seconds to unlock this page
timeout = time.time() + 60.00
# the resend link will appear from 60 seconds is why there are 2 additional seconds, it's to have a margin
timeout = time.time() + 62.00
second_try = True
while time.time() < timeout:
link = self.page.get_link_app_validation()
self.location(link)
......@@ -152,6 +154,13 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
time.sleep(2)
else:
return
if time.time() >= timeout and second_try:
# second try because 60 seconds is short, the second try is longger
second_try = False
timeout = time.time() + 70.00
self.page.resend_link()
else:
raise AppValidationExpired()
......@@ -169,6 +178,8 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
if self.config['resume'].get():
self.check_app_validation()
# we are logged
return
if self.security.is_here():
self.handle_security()
......@@ -192,8 +203,11 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
raise WrongCaptchaResponse(msg)
else:
assert False, msg
else:
return
if self.approval_page.is_here():
# if we have captcha and app validation
msg_validation = self.page.get_msg_app_validation()
raise AppValidation(msg_validation)
# Change language so everything is handled the same way
self.to_english(self.LANGUAGE)
......@@ -210,6 +224,7 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
self.page.login(self.username, self.password)
if self.approval_page.is_here():
# if we don't have captcha and we have app validation
msg_validation = self.page.get_msg_app_validation()
raise AppValidation(msg_validation)
......
......@@ -102,7 +102,11 @@ class ApprovalPage(HTMLPage, LoggedPage):
return msg(self.doc)
def get_link_app_validation(self):
return Link('//a[contains(text(), "Click here to refresh the page")]')(self.doc)
return Link('//a[@id="resend-approval-link"]')(self.doc)
def resend_link(self):
form = self.get_form(id='resend-approval-form')
form.submit()
class LanguagePage(HTMLPage):
......
......@@ -33,16 +33,16 @@ class MyVirtKeyboard(VirtKeyboard):
color = (255, 255, 255)
symbols = {
'0': '7c19886349f1b8f41d9876bbb4182786',
'1': '7825fb0dade1227999abd21ab44529a6',
'2': '94790a9747373a540995f132c0d46686',
'3': '237154eb1838b2d995e789c4b97b1454',
'4': 'a6fd31cb646e5fd0c9c6c4bfb5467ede',
'5': '5c7823607874fbc7cd6cdd058f9c05c7',
'6': '5eb962c5f38be89e17b2c2acc4d61a94',
'7': '8c926a882094ce769579786b50bb7a69',
'8': '1d9c6b845dc4f85dc56426bbf23faa80',
'9': 'f817f2a21497fc32438b07fd15beedbc',
'0': '962575659eb1bb72b15f856c0358c644',
'1': '36ccbc0fff397cef567b5be362127484',
'2': 'b823b3078cbfa1707ecbf8b9a92dea44',
'3': 'f4790e47f878eba58ef93cfd6956726b',
'4': '577620e004518fb057cc704842d59245',
'5': '01e11c7a7092f2f7ec119b78be605923',
'6': '0b7b051871b6bf4e2c91282cfcae09bc',
'7': '920313cbddda9934447d8f1daa71e76b',
'8': 'bdb35b451e6de3fb7221f50669fe52fb',
'9': '1ee5fdfd7877ec9e0a957e14db5d29e6',
}
coords = {
......
......@@ -202,7 +202,7 @@ class BanquePopulaire(LoginBrowser):
advisor = URL(r'https://[^/]+/cyber/internet/StartTask.do\?taskInfoOID=accueil.*',
r'https://[^/]+/cyber/internet/StartTask.do\?taskInfoOID=contacter.*', AdvisorPage)
basic_token_page = URL(r'/SRVATE/context/mde/1.1.5', BasicTokenPage)
basic_token_page = URL(r'https://(?P<website>.[\w\.]+)/SRVATE/context/mde/1.1.5', BasicTokenPage)
subscriber_page = URL(r'https://[^/]+/api-bp/wapi/2.0/abonnes/current/mes-documents-electroniques', SubscriberPage)
subscription_page = URL(r'https://[^/]+/api-bp/wapi/2.0/abonnes/current/contrats', SubscriptionsPage)
documents_page = URL(r'/api-bp/wapi/2.0/abonnes/current/documents/recherche-avancee', DocumentsPage)
......@@ -937,7 +937,10 @@ class BanquePopulaire(LoginBrowser):
@need_login
def iter_subscriptions(self):
self.location('/SRVATE/context/mde/1.1.5')
# specify the website url in order to avoid 404 errors.
# 404 errors occur when the baseurl is a website we have
# been redirected to, like natixis or linebourse
self.basic_token_page.go(website=self.website)
headers = {'Authorization': 'Basic %s' % self.page.get_basic_token()}
response = self.location('/as-bp/as/2.0/tokens', method='POST', headers=headers)
self.documents_headers = {'Authorization': 'Bearer %s' % response.json()['access_token']}
......
......@@ -24,7 +24,7 @@ from weboob.browser.profiles import Wget
from .compat.weboob_browser_url import URL
from .compat.weboob_browser_browsers import need_login
from .pages import AdvisorPage, LoginPage
from .pages import AdvisorPage, LoginPage, DecoupledStatePage, CancelDecoupled
__all__ = ['BECMBrowser']
......@@ -38,6 +38,8 @@ class BECMBrowser(AbstractBrowser):
login = URL('/fr/authentification.html', LoginPage)
advisor = URL('/fr/banques/Details.aspx\?banque=.*', AdvisorPage)
decoupled_state = URL(r'/(?P<subbank>.*)fr/otp/SOSD_OTP_GetTransactionState.htm', DecoupledStatePage)
cancel_decoupled = URL(r'/(?P<subbank>.*)fr/otp/SOSD_OTP_CancelTransaction.htm', CancelDecoupled)
@need_login
def get_advisor(self):
......
......@@ -40,3 +40,13 @@ class AdvisorPage(LoggedPage, HTMLPage):
obj_address = Format('%s %s %s', CleanText('//table//*[@itemprop="streetAddress"]'),
CleanText('//table//*[@itemprop="postalCode"]'),
CleanText('//table//*[@itemprop="addressLocality"]'))
class DecoupledStatePage(AbstractPage):
PARENT = 'creditmutuel'
PARENT_URL = 'decoupled_state'
class CancelDecoupled(AbstractPage):
PARENT = 'creditmutuel'
PARENT_URL = 'cancel_decoupled'
......@@ -22,6 +22,7 @@
import datetime
from dateutil.relativedelta import relativedelta
from weboob.exceptions import BrowserIncorrectPassword, ActionNeeded
from weboob.browser.browsers import LoginBrowser, URL, need_login
from .compat.weboob_capabilities_bank import Account, AccountNotFound
......
......@@ -28,6 +28,7 @@ from io import BytesIO
import re
from PIL import Image
from weboob.exceptions import ActionNeeded
from .compat.weboob_browser_pages import LoggedPage, HTMLPage, pagination, AbstractPage, JsonPage
from weboob.browser.elements import method, ListElement, ItemElement, TableElement
......
......@@ -22,11 +22,11 @@
from __future__ import unicode_literals
import re
from datetime import datetime
from io import BytesIO
import dateutil.parser
from .compat.weboob_browser_pages import LoggedPage, HTMLPage, JsonPage
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import TableCell, Attr
......
......@@ -56,9 +56,9 @@ from .pages import (
AddRecipPage, ActivateRecipPage, ProfilePage, ListDetailCardPage, ListErrorPage,
UselessPage, TransferAssertionError, LoanDetailsPage, TransfersPage, OTPPage,
)
from .document_pages import DocumentsPage, DocumentsResearchPage, TitulairePage, RIBPage
__all__ = ['BNPPartPro', 'HelloBank']
......
......@@ -27,6 +27,7 @@ from io import BytesIO
from random import randint
from decimal import Decimal
from datetime import datetime, timedelta
import lxml.html as html
from requests.exceptions import ConnectionError
......
This diff is collapsed.
......@@ -131,3 +131,15 @@ class BoursoramaModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapPr
def iter_emitters(self):
return self.browser.iter_emitters()
def fill_account(self, account, fields):
if (
'opening_date' in fields
and account.type == Account.TYPE_LIFE_INSURANCE
and '/compte/derive' not in account.url
):
account.opening_date = self.browser.get_opening_date(account.url)
OBJECTS = {
Account: fill_account,
}
This diff is collapsed.
......@@ -21,6 +21,7 @@
from decimal import Decimal
from datetime import timedelta
from .compat.weboob_capabilities_bank import CapBankTransferAddRecipient, Account, AccountNotFound, RecipientNotFound
from .compat.weboob_capabilities_wealth import CapBankWealth
from weboob.capabilities.contact import CapContact
......
......@@ -367,7 +367,16 @@ class BredBrowser(LoginBrowser, StatesMixin):
self.profile.go()
profile = self.page.get_profile()
self.emails.go()
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
......@@ -382,9 +391,17 @@ class BredBrowser(LoginBrowser, StatesMixin):
def iter_transfer_recipients(self, account):
self.get_and_update_bred_token()
self.emitters_list.go(json={
'typeVirement': 'C',
})
try:
self.emitters_list.go(json={
'typeVirement': 'C',
})
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.":
# Means the account cannot emit transfers
return
raise
if not self.page.can_account_emit_transfer(account.id):
return
......
......@@ -58,8 +58,11 @@ class BredModule(Module, CapBankWealth, CapProfile, CapBankTransferAddRecipient)
'dispobank': DispoBankBrowser,
}
def get_website(self):
return self.config['website'].get()
def create_default_browser(self):
self.BROWSER = self.BROWSERS[self.config['website'].get()]
self.BROWSER = self.BROWSERS[self.get_website()]
return self.create_browser(
self.config['accnum'].get().replace(' ', '').zfill(11),
......@@ -90,7 +93,7 @@ class BredModule(Module, CapBankWealth, CapProfile, CapBankTransferAddRecipient)
return self.browser.get_profile()
def fill_account(self, account, fields):
if self.config['website'].get() != 'bred':
if self.get_website() != 'bred':
return
self.browser.fill_account(account, fields)
......@@ -100,7 +103,7 @@ class BredModule(Module, CapBankWealth, CapProfile, CapBankTransferAddRecipient)
}
def iter_transfer_recipients(self, account):
if self.config['website'].get() != 'bred':
if self.get_website() != 'bred':
raise NotImplementedError()
if not isinstance(account, Account):
......@@ -109,7 +112,7 @@ class BredModule(Module, CapBankWealth, CapProfile, CapBankTransferAddRecipient)
return self.browser.iter_transfer_recipients(account)
def new_recipient(self, recipient, **params):
if self.config['website'].get() != 'bred':
if self.get_website() != 'bred':
raise NotImplementedError()
recipient.label = recipient.label[:32].strip()
......@@ -122,7 +125,7 @@ class BredModule(Module, CapBankWealth, CapProfile, CapBankTransferAddRecipient)
return self.browser.new_recipient(recipient, **params)
def init_transfer(self, transfer, **params):
if self.config['website'].get() != 'bred':
if self.get_website() != 'bred':
raise NotImplementedError()
transfer.label = transfer.label[:140].strip()
......@@ -144,6 +147,6 @@ class BredModule(Module, CapBankWealth, CapProfile, CapBankTransferAddRecipient)
return self.browser.init_transfer(transfer, account, recipient, **params)
def execute_transfer(self, transfer, **params):
if self.config['website'].get() != 'bred':
if self.get_website() != 'bred':
raise NotImplementedError()
return self.browser.execute_transfer(transfer, **params)
......@@ -74,7 +74,6 @@ from .pages import (
AppValidationPage,
)
from .transfer_pages import CheckingPage, TransferListPage
from .linebourse_browser import LinebourseAPIBrowser
......@@ -772,7 +771,12 @@ class CaisseEpargne(LoginBrowser, StatesMixin):
'bpcesta': json.dumps(bpcesta, separators=(',', ':')),
}
if self.nuser:
params['login_hint'] += ' %s' % self.nuser
if len(self.username) == 10:
# We must fill with the missing 0 expected by the caissedepargne server
# Some clues are given in js file
params['login_hint'] += self.nuser.zfill(6)
else:
params['login_hint'] += ' %s' % self.nuser
self.authorize.go(params=params)
self.page.send_form()
......
......@@ -25,11 +25,12 @@ from __future__ import unicode_literals
import re
from base64 import b64decode
from collections import OrderedDict
from PIL import Image, ImageFilter
from io import BytesIO
from decimal import Decimal
from datetime import datetime
from lxml import html
from PIL import Image, ImageFilter
from .compat.weboob_browser_pages import (
LoggedPage, HTMLPage, JsonPage, pagination,
......@@ -485,10 +486,12 @@ class IndexPage(LoggedPage, BasePage):
return bool(CleanText('//span[contains(text(), "Authentification non rejouable")]')(self.doc))
def check_no_loans(self):
return (
not bool(CleanText('//table[@class="menu"]//div[contains(., "Crédits")]')(self.doc))
and not bool(CleanText('//table[@class="header-navigation_main"]//a[contains(., "Crédits")]')(self.doc))
)
return not any((
CleanText('//table[@class="menu"]//div[contains(., "Crédits")]')(self.doc),
CleanText(
'//table[@class="header-navigation_main"]//a[contains(@href, "CRESYNT0")]'
)(self.doc),
))
def check_measure_accounts(self):
return not CleanText(
......
......@@ -40,5 +40,5 @@ class CICBrowser(AbstractBrowser):
LoginPage
)
decoupled_state = URL(r'/fr/otp/SOSD_OTP_GetTransactionState.htm', DecoupledStatePage)
cancel_decoupled = URL(r'/fr/otp/SOSD_OTP_CancelTransaction.htm', CancelDecoupled)
decoupled_state = URL(r'/(?P<subbank>.*)fr/otp/SOSD_OTP_GetTransactionState.htm', DecoupledStatePage)
cancel_decoupled = URL(r'/(?P<subbank>.*)fr/otp/SOSD_OTP_CancelTransaction.htm', CancelDecoupled)
......@@ -103,16 +103,21 @@ class CmesBrowser(LoginBrowser):
return self.page.iter_accounts()
def go_investment(self, form, inv_param):
form[inv_param] = ''
form.submit()
@need_login
def iter_investment(self, account):
if 'compte courant bloqué' in account.label.lower():
# CCB accounts have Pockets but no Investments
return
self.accounts.stay_or_go(subsite=self.subsite, client_space=self.client_space)
form = self.page.get_investment_form()
for inv in self.page.iter_investments(account=account):
if inv._url:
# Go to the investment details to get employee savings attributes
self.location(inv._url)
# Go to the investment details to get employee savings attributes
self.go_investment(form, inv._form_param)
if self.investments.is_here():
asset_management_url = self.page.get_asset_management_url()
# Fetch SRRI, asset category & recommended period
......@@ -126,7 +131,7 @@ class CmesBrowser(LoginBrowser):
self.page.fill_investment(obj=inv)
# We need to return to the investment page
self.location(inv._url)
self.go_investment(form, inv._form_param)
else:
performances = {}
# Get 1-year performance
......@@ -151,13 +156,13 @@ class CmesBrowser(LoginBrowser):
performances[3] = self.page.get_performance()
inv.performance_history = performances
# Fetch investment quantity on the 'Mes Avoirs' tab
# Fetch investment quantity on the 'Mes Avoirs'/'Mon épargne' tab
self.page.go_investment_details()
inv.quantity = self.page.get_quantity()
self.page.go_back()
else:
self.logger.info('No available details for investment %s.', inv.label)
self.accounts.stay_or_go(subsite=self.subsite, client_space=self.client_space)
yield inv
@need_login
......
......@@ -116,18 +116,22 @@ class AccountsPage(LoggedPage, HTMLPage):
row.xpath('//div[contains(@id, "dv::s::%s")]' % id_diff[0].rsplit(':', 1)[0])[0] if id_diff else None,
)
def get_investment_form(self):
form = self.get_form(id='I0:P5:F')
# Each investment uses the same form with a different submit input.
# We remove all relevant inputs and will add the one we want manually as we submit the form.
keys_to_remove = [key for key in form if key.startswith('_FID_')]
for key in keys_to_remove:
form.pop(key)
return form
def iter_investments(self, account):
for row, elem_repartition, elem_pocket, elem_diff in self.iter_invest_rows(account=account):
inv = Investment()
inv._account = account
inv._el_pocket = elem_pocket
inv.label = CleanText('.//td[1]')(row)
_url = Link('.//td[1]/a', default=None)(row)
if _url:
inv._url = self.absurl(_url)
else:
# If _url is None, self.absurl returns the BASEURL, so we need to set the value manually.
inv._url = None
inv._form_param = CleanText('.//td[1]/input/@name')(row)
inv.valuation = MyDecimal('.//td[2]')(row)
# On all Cmes children the row shows percentages and the popup shows absolute values in currency.
......@@ -214,7 +218,7 @@ class InvestmentPage(LoggedPage, HTMLPage):
return Eval(lambda x: x/100, CleanDecimal.French('//p[contains(@class, "plusvalue--value")]'))(self.doc)
def go_investment_details(self):
investment_details_url = Link('//a[text()="Mes avoirs"]')(self.doc)
investment_details_url = Link('//a[text()="Mes avoirs" or text()="Mon épargne"]')(self.doc)
self.browser.location(investment_details_url)
......
......@@ -22,13 +22,13 @@
from __future__ import unicode_literals
import re
import requests
import json
import datetime as dt
import datetime
from hashlib import md5
from collections import OrderedDict
import requests
from weboob.exceptions import BrowserUnavailable
from .compat.weboob_browser_pages import HTMLPage, JsonPage, RawPage, LoggedPage, pagination
from weboob.browser.elements import DictElement, ItemElement, TableElement, SkipItem, method
......@@ -366,14 +366,14 @@ class AccountsPage(LoggedPage, JsonPage):
# Key not always available, when revolving credit not yet consummed
timestamp = Dict('dateFin', default=None)(self)
if timestamp:
return dt.date.fromtimestamp(timestamp / 1000)
return datetime.date.fromtimestamp(timestamp / 1000)
return NotAvailable
def obj_next_payment_date(self):
# Key not always available, when revolving credit not yet consummed
timestamp = Dict('dateProchaineEcheance', default=None)(self)
if timestamp:
return dt.date.fromtimestamp(timestamp / 1000)
return datetime.date.fromtimestamp(timestamp / 1000)
return NotAvailable
def obj_balance(self):
......@@ -448,7 +448,7 @@ class HistoryPage(LoggedPage, JsonPage):
class FromTimestamp(Filter):
def filter(self, timestamp):
try:
return dt.date.fromtimestamp(int(timestamp[:-3]))
return datetime.date.fromtimestamp(int(timestamp[:-3]))
except TypeError:
return self.default_or_raise(ParseError('Element %r not found' % self.selector))
......
......@@ -22,9 +22,10 @@
from __future__ import unicode_literals
import datetime
from dateutil.relativedelta import relativedelta
import re
from dateutil.relativedelta import relativedelta
from weboob.tools.capabilities.bank.transactions import sorted_transactions
from weboob.capabilities.base import find_object
from .compat.weboob_capabilities_bank import Account
......@@ -38,7 +39,6 @@ from .pages import (
LoginPage, PasswordCreationPage, AccountsPage, HistoryPage, SubscriptionPage, InvestmentPage,
InvestmentAccountPage, UselessPage, SSODomiPage, AuthCheckUser, ErrorPage,
)
from ..par.pages import ProfilePage
......
......@@ -58,9 +58,9 @@ from .transfer_pages import (
VerifyNewRecipientPage, ValidateNewRecipientPage, CheckSmsPage,
EndNewRecipientPage,
)
from .netfinca_browser import NetfincaBrowser
__all__ = ['CreditAgricoleBrowser']
......
......@@ -26,6 +26,7 @@ import re
import json
import dateutil
from .compat.weboob_browser_pages import HTMLPage, JsonPage, LoggedPage
from weboob.exceptions import ActionNeeded
from weboob.capabilities import NotAvailable
......@@ -247,6 +248,8 @@ ACCOUNT_TYPES = {
'OPTA': Account.TYPE_LIFE_INSURANCE, # Optalissime
'RENV VITAL': Account.TYPE_LIFE_INSURANCE, # Rente viagère Vitalité
'ANAE': Account.TYPE_LIFE_INSURANCE,
'PAT STH': Account.TYPE_LIFE_INSURANCE, # Patrimoine ST Honoré
'PRSH2': Account.TYPE_LIFE_INSURANCE, # Prestige ST Honoré 2
'ATOUT LIB': Account.TYPE_REVOLVING_CREDIT,
'PACC': Account.TYPE_CONSUMER_CREDIT, # 'PAC' = 'Prêt à consommer'
'PACP': Account.TYPE_CONSUMER_CREDIT,
......
......@@ -87,8 +87,8 @@ class CreditMutuelBrowser(TwoFactorBrowser):
twofa_unabled_page = URL(r'/(?P<subbank>.*)fr/banque/validation.aspx', TwoFAUnabledPage)
mobile_confirmation = URL(r'/(?P<subbank>.*)fr/banque/validation.aspx', MobileConfirmationPage)
safetrans_page = URL(r'/(?P<subbank>.*)fr/banque/validation.aspx', SafeTransPage)
decoupled_state = URL(r'/fr/banque/async/otp/SOSD_OTP_GetTransactionState.htm', DecoupledStatePage)
cancel_decoupled = URL(r'/fr/banque/async/otp/SOSD_OTP_CancelTransaction.htm', CancelDecoupled)
decoupled_state = URL(r'/(?P<subbank>.*)fr/banque/async/otp/SOSD_OTP_GetTransactionState.htm', DecoupledStatePage)
cancel_decoupled = URL(r'/(?P<subbank>.*)fr/banque/async/otp/SOSD_OTP_CancelTransaction.htm', CancelDecoupled)
otp_validation_page = URL(r'/(?P<subbank>.*)fr/banque/validation.aspx', OtpValidationPage)
otp_blocked_error_page = URL(r'/(?P<subbank>.*)fr/banque/validation.aspx', OtpBlockedErrorPage)
fiscality = URL(r'/(?P<subbank>.*)fr/banque/residencefiscale.aspx', FiscalityConfirmationPage)
......@@ -299,10 +299,10 @@ class CreditMutuelBrowser(TwoFactorBrowser):
"""
# 15' on website, we don't wait that much, but leave sufficient time for the user
timeout = time.time() + 600.00 # 15' on webview, need not to wait that much
data = {'transactionId': transactionId}
while time.time() < timeout:
data = {'transactionId': transactionId}
self.decoupled_state.go(data=data)
self.decoupled_state.go(data=data, subbank=self.currentSubBank)
decoupled_state = self.page.get_decoupled_state()
if decoupled_state == 'VALIDATED':
......@@ -312,10 +312,10 @@ class CreditMutuelBrowser(TwoFactorBrowser):
raise AppValidationCancelled()
assert decoupled_state == 'PENDING', 'Unhandled polling state: "%s"' % decoupled_state
time.sleep(5) # every second on wbesite, need to slow that down
time.sleep(5) # every second on website, need to slow that down
# manually cancel polling before website max duration for it
self.cancel_decoupled.go(data=data)
self.cancel_decoupled.go(data=data, subbank=self.currentSubBank)
raise AppValidationExpired()
def handle_polling(self):
......@@ -368,6 +368,8 @@ class CreditMutuelBrowser(TwoFactorBrowser):
self.location(location, allow_redirects=False)
def check_auth_methods(self):
self.getCurrentSubBank()
if self.mobile_confirmation.is_here():
self.page.check_bypass()
if self.mobile_confirmation.is_here():
......
......@@ -231,10 +231,11 @@ class CancelDecoupled(HTMLPage):
# and might be empty of text while used in a redirection
class OtpValidationPage(PartialHTMLPage):
def is_here(self):
return 'envoyé par SMS' in CleanText('//div[contains(@id, "OTPDeliveryChannelText")]')(self.doc)
return 'code de confirmation vient de vous être envoyé par' in CleanText('//div[contains(@id, "OTPDeliveryChannelText")]')(self.doc)
def get_message(self):
# Ex: 'Un code de confirmation vient de vous être envoyé par SMS au 06 XX XX X1 23, le jeudi 26 décembre 2019 à 18:12:56.'
# can be 'par SMS', 'par appel téléphonique', or 'par email'
return Regexp(CleanText('//div[contains(@id, "OTPDeliveryChannelText")]'), r'(.+\d{2}), le')(self.doc)
def get_error_message(self):
......
......@@ -22,16 +22,19 @@
from __future__ import unicode_literals
import re
from collections import OrderedDict
from datetime import timedelta, date
from lxml.etree import XMLSyntaxError
from collections import OrderedDict
from weboob.tools.date import LinearDateGuesser
from .compat.weboob_capabilities_bank import Account, AccountNotFound, AccountOwnership
from weboob.tools.capabilities.bank.transactions import sorted_transactions, keep_only_card_transactions
from weboob.tools.compat import parse_qsl, urlparse
from weboob.exceptions import ActionNeeded, BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser.browsers import LoginBrowser, URL, need_login
from .compat.weboob_tools_value import Value
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, BrowserQuestion
from weboob.browser.browsers import URL, need_login
from .compat.weboob_browser_browsers import TwoFactorBrowser
from weboob.browser.exceptions import HTTPNotFound
from weboob.capabilities.base import find_object
......@@ -53,8 +56,9 @@ from .pages.landing_pages import JSMiddleFramePage, JSMiddleAuthPage, Investment
__all__ = ['HSBC']
class HSBC(LoginBrowser):
class HSBC(TwoFactorBrowser):
BASEURL = 'https://client.hsbc.fr'
HAS_CREDENTIALS_ONLY = True
app_gone = False
......@@ -140,8 +144,9 @@ class HSBC(LoginBrowser):
# catch-all
other_page = URL(r'/cgi-bin/emcgi', OtherPage)
def __init__(self, username, password, secret, *args, **kwargs):
super(HSBC, self).__init__(username, password, *args, **kwargs)
def __init__(self, config, username, password, secret, *args, **kwargs):
self.config = config
super(HSBC, self).__init__(config, username, password, *args, **kwargs)
self.accounts_dict = OrderedDict()
self.unique_accounts_dict = dict()
self.secret = secret
......@@ -149,11 +154,22 @@ class HSBC(LoginBrowser):
self.owners_url_list = []
self.web_space = None
self.home_url = None
self.AUTHENTICATION_METHODS = {
'otp': self.handle_otp,
}
def load_state(self, state):
# when the otp is being handled, we want to keep the same session
if self.config['otp'].get():
return super(HSBC, self).load_state(state)
return
def do_login(self):
def handle_otp(self):
otp = self.config['otp'].get()
self.page.login_with_secure_key(self.secret, otp)
self.end_login()
def init_login(self):
self.session.cookies.clear()
self.app_gone = False
......@@ -166,14 +182,26 @@ class HSBC(LoginBrowser):
self.connection2.go()
self.page.login(self.username)
# The handling of 2FA is unusual. When authenticating, the user has the choice to use an OTP or his password
# when the sca is required, the link to log on the website without otp is not available. That's how we know
# this is the only available authentication method.
no_secure_key_link = self.page.get_no_secure_key_link()
if not no_secure_key_link and self.page.is_secure_key():
raise ActionNeeded("Vous devez réaliser l'authentification forte sur le portail internet avec Secure Key")