The new woob repository is here: https://gitlab.com/woob/woob. This gitlab will be removed soon.

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 @@ def handle_captcha(self, captcha):
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 @@ def check_app_validation(self):
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 @@ def do_login(self):
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 @@ def do_login(self):
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 @@ def do_login(self):
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 @@ def get_msg_app_validation(self):
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 @@ def get_advisor(self):
@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 .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 update_advisor(ItemElement):
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 @@
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 @@
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 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 @@ def get_rate(self, currency_from, currency_to):
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 @@ def get_profile(self):
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 @@ def fill_account(self, account, fields):
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 @@ def get_profile(self):
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 @@ def fill_account(self, account, fields):
}
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 @@ def iter_transfer_recipients(self, account):
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 @@ def new_recipient(self, recipient, **params):
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 @@ def init_transfer(self, transfer, **params):
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 @@
AppValidationPage,
)
from .transfer_pages import CheckingPage, TransferListPage
from .linebourse_browser import LinebourseAPIBrowser
......@@ -772,7 +771,12 @@ def do_new_login(self, data):
'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 @@
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 @@ def need_auth(self):
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 @@ def iter_accounts(self):
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 @@ def iter_investment(self, account):
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 @@ def iter_investment(self, account):
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 @@ def iter_invest_rows(self, account):
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 @@ def get_performance(self):
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 @@ def obj_maturity_date(self):
# 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 item(ItemElement):
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 @@
LoginPage, PasswordCreationPage, AccountsPage, HistoryPage, SubscriptionPage, InvestmentPage,
InvestmentAccountPage, UselessPage, SSODomiPage, AuthCheckUser, ErrorPage,
)
from ..par.pages import ProfilePage
......
......@@ -58,9 +58,9 @@
VerifyNewRecipientPage, ValidateNewRecipientPage, CheckSmsPage,
EndNewRecipientPage,
)
from .netfinca_browser import NetfincaBrowser
__all__ = ['CreditAgricoleBrowser']
......
......@@ -26,6 +26,7 @@
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 @@ class ContractsPage(LoggedPage, HTMLPage):
'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 @@ def poll_decoupled(self, transactionId):
"""
# 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 @@ def poll_decoupled(self, transactionId):
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 @@ def check_redirections(self):
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 @@
__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 @@ def __init__(self, username, password, secret, *args, **kwargs):
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 @@ def do_login(self):
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")