# -*- coding: utf-8 -*- # Copyright(C) 2010-2012 Julien Veyssier # # This file is part of weboob. # # weboob is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # weboob is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . from __future__ import unicode_literals import re from weboob.capabilities import NotAvailable from weboob.capabilities.bank import Account from weboob.tools.capabilities.bank.transactions import FrenchTransaction from weboob.tools.compat import urlparse, parse_qs, urljoin from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded from weboob.browser.elements import ListElement, ItemElement, method from weboob.browser.pages import HTMLPage, pagination from weboob.browser.filters.standard import ( Filter, Env, CleanText, CleanDecimal, Field, DateGuesser, Regexp ) from weboob.browser.filters.html import Link, AbsoluteLink, TableCell from weboob.browser.filters.javascript import JSVar from .landing_pages import GenericLandingPage class Transaction(FrenchTransaction): PATTERNS = [(re.compile(r'^VIR(EMENT)? (?P.*)'), FrenchTransaction.TYPE_TRANSFER), (re.compile(r'^PRLV (?P.*)'), FrenchTransaction.TYPE_ORDER), (re.compile(r'^CB (?P.*?)\s+(?P
\d+)/(?P[01]\d)\s*(?P.*)'), FrenchTransaction.TYPE_CARD), (re.compile(r'^DAB (?P
\d{2})/(?P\d{2}) ((?P\d{2})H(?P\d{2}) )?(?P.*?)( CB N°.*)?$'), FrenchTransaction.TYPE_WITHDRAWAL), (re.compile(r'^CHEQUE$'), FrenchTransaction.TYPE_CHECK), (re.compile(r'^COTIS\.? (?P.*)'), FrenchTransaction.TYPE_BANK), (re.compile(r'^REMISE (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), (re.compile(r'^FACTURES CB (?P.*)'), FrenchTransaction.TYPE_CARD_SUMMARY), ] class FrameContainer(GenericLandingPage): is_here = '//frameset' # main page, a frameset def on_load(self): txt = CleanText('//p[@class="debit"]', default='')(self.doc) if u"Vos données d'identification (identifiant - code secret) sont incorrectes" in txt: raise BrowserIncorrectPassword() def get_js_url(self): # look for frame url in the top page return urljoin(self.url, JSVar(CleanText('//script'), var='url')(self.doc)) def get_frame(self): try: a = self.doc.xpath(u'//frame["@name=FrameWork"]')[0] except IndexError: return None else: return a.attrib['src'] class UnavailablePage(GenericLandingPage): is_here = '//strong[contains(text(),"Service momentanément indisponible.")]' def on_load(self): raise BrowserUnavailable() class AccountsPage(GenericLandingPage): is_here = '//h1[text()="Synthèse"]' @method class iter_accounts(ListElement): item_xpath = '//tr' flush_at_end = True class item(ItemElement): klass = Account def condition(self): return len(self.el.xpath('./td')) > 2 class Label(Filter): def filter(self, text): return text.lstrip(' 0123456789').title() class Type(Filter): PATTERNS = [ ('Pea', Account.TYPE_PEA), ('invest', Account.TYPE_MARKET), ('ldd', Account.TYPE_SAVINGS), ('livret', Account.TYPE_SAVINGS), ('compte', Account.TYPE_CHECKING), ('account', Account.TYPE_CHECKING), ('pret', Account.TYPE_LOAN), ('vie', Account.TYPE_LIFE_INSURANCE), ('strategie patr.', Account.TYPE_LIFE_INSURANCE), ('essentiel', Account.TYPE_LIFE_INSURANCE), ('elysee', Account.TYPE_LIFE_INSURANCE), ('abondance', Account.TYPE_LIFE_INSURANCE), ('ely. retraite', Account.TYPE_LIFE_INSURANCE), ('lae option assurance', Account.TYPE_LIFE_INSURANCE), ('carte ', Account.TYPE_CARD), ('plan assur. innovat.', Account.TYPE_LIFE_INSURANCE), ] def filter(self, label): label = label.lower() for pattern, type in self.PATTERNS: if pattern in label: return type return Account.TYPE_UNKNOWN obj_label = Label(CleanText('./td[1]/a')) obj_coming = Env('coming') obj_currency = FrenchTransaction.Currency('./td[2]') obj_url = AbsoluteLink('./td[1]/a') obj_type = Type(Field('label')) obj_coming = NotAvailable @property def obj_balance(self): if self.el.xpath('./parent::*/tr/th') and self.el.xpath('./parent::*/tr/th')[0].text in [u'Credits', u'Crédits']: return CleanDecimal(replace_dots=True, sign=lambda x: -1).filter(self.el.xpath('./td[3]')) return CleanDecimal(replace_dots=True).filter(self.el.xpath('./td[3]')) @property def obj_id(self): # Investment account and main account can have the same id # so we had account type in case of Investment to prevent conflict if Field('type')(self) == Account.TYPE_MARKET: return CleanText(replace=[('.', ''), (' ', '')]).filter(self.el.xpath('./td[2]')) + ".INVEST" return CleanText(replace=[('.', ''), (' ', '')]).filter(self.el.xpath('./td[2]')) class RibPage(GenericLandingPage): def is_here(self): return bool(self.doc.xpath('//h1[contains(text(), "RIB/IBAN")]')) def link_rib(self, accounts): for id, acc in accounts.items(): if acc.iban or acc.type is not Account.TYPE_CHECKING: continue digit_id = ''.join(re.findall('\d', id)) if digit_id in CleanText('//div[@class="RIB_content"]')(self.doc): acc.iban = re.search('(FR\d{25})', CleanText('//div[strong[contains(text(), "IBAN")]]', replace=[(' ', '')])(self.doc)).group(1) def get_rib(self, accounts): self.link_rib(accounts) for nb in range(len(self.doc.xpath('//select/option')) - 1): form = self.get_form(name="FORM_RIB") form['index_rib'] = str(nb+1) form.submit() self.browser.page.link_rib(accounts) class Pagination(object): def next_page(self): links = self.page.doc.xpath('//a[@class="fleche"]') if len(links) == 0: return current_page_found = False for link in links: l = link.attrib.get('href') if current_page_found and "#op" not in l: # Adding CB_IdPrestation so browser2 use CBOperationPage return l + "&CB_IdPrestation" elif "#op" in l: current_page_found = True return class CBOperationPage(GenericLandingPage): is_here = '//h1[text()="Historique des opérations"]' def get_params(self, url): parsed = urlparse(url) base_url, params = parsed.path, parse_qs(parsed.query) for a in self.doc.xpath('//form[@name="FORM_LIB_CARTE"]//a[contains(@href, "sessionid")]'): params['sessionid'] = parse_qs(urlparse(Link('.')(a)).query)['sessionid'] yield base_url, params @pagination @method class get_history(Pagination, Transaction.TransactionsElement): head_xpath = '//table//tr/th' item_xpath = '//table//tr' class item(Transaction.TransactionElement): def condition(self): return len(self.el.xpath('./td')) >= 4 obj_rdate = Transaction.Date(TableCell('date')) def obj_date(self): return DateGuesser(Regexp(CleanText(self.page.doc.xpath('//table/tr[2]/td[1]')), r'(\d{2}/\d{2})'), Env("date_guesser"))(self) class CPTOperationPage(GenericLandingPage): is_here = '''//h1[text()="Historique des opérations"] and //h2[text()="Recherche d'opération"]''' def get_history(self): if self.doc.xpath('//form[@name="FORM_SUITE"]'): m = re.search('suite[\s]+=[\s]+([\w]+)', CleanText().filter(self.doc.xpath('//script[contains(text(), "var suite")]'))) if m and m.group(1) == "true": form = self.get_form(name="FORM_SUITE") self.doc = self.browser.location("%s" % form.url, params=dict(form)).page.doc for script in self.doc.xpath('//script'): if script.text is None or script.text.find('\nCL(0') < 0: continue first_history = None for m in re.finditer(r"CL\((\d+),'(.+)','(.+)','(.+)','([\d -\.,]+)',('([\d -\.,]+)',)?'\d+','\d+','[\w\s]+'\);", script.text, flags=re.MULTILINE | re.UNICODE): op = Transaction() raw = re.sub(u'[ ]+', u' ', m.group(4).replace(u'\n', u' ').replace(r"\'", "'")) op.parse(date=m.group(3), raw=raw) op.set_amount(m.group(5)) op._coming = (re.match(r'\d+/\d+/\d+', m.group(2)) is None) if first_history is None: first_history = op.to_dict() elif first_history == op.to_dict(): self.logger.warning("Find already used line {}".format(first_history)) break yield op class AppGonePage(HTMLPage): def on_load(self): self.browser.app_gone = True self.logger.info('Application has gone. Relogging...') self.browser.do_logout() self.browser.do_login() class LoginPage(HTMLPage): @property def logged(self): if self.doc.xpath(u'//p[contains(text(), "You are now being redirected to your Personal Internet Banking.")]'): return True return False def on_load(self): for message in self.doc.xpath('//div[has-class("csPanelErrors")]'): raise BrowserIncorrectPassword(CleanText('.')(message)) def is_here(self): return not self.doc.xpath('//form[@name="launch"]') def login(self, login): form = self.get_form(id='idv_auth_form') form['userid'] = form['__hbfruserid'] = login form.submit() def get_no_secure_key(self): try: a = self.doc.xpath(u'//a[contains(text(), "Without HSBC Secure Key")]')[0] except IndexError: return None else: return a.attrib['href'] def login_w_secure(self, password, secret): form = self.get_form(nr=0) form['memorableAnswer'] = secret inputs = self.doc.xpath(u'//input[starts-with(@id, "keyrcc_password_first")]') split_pass = u'' if len(password) < len(inputs): raise BrowserIncorrectPassword('The password must be at least %d characters' % len(inputs)) elif len(password) > len(inputs): # HSBC only use 6 first and last two from the password password = password[:6] + password[-2:] for i, inpu in enumerate(inputs): # The good field are 1,2,3 and the bad one are 11,12,21,23,24,31 and so one if int(inpu.attrib['id'].split('first')[1]) < 10: split_pass += password[i] form['password'] = split_pass form.submit() def useless_form(self): form = self.get_form(nr=0) form.submit() class OtherPage(HTMLPage): ERROR_CLASSES = [ ('Votre contrat est suspendu', ActionNeeded), ("Vos données d'identification (identifiant - code secret) sont incorrectes", BrowserIncorrectPassword), ('Erreur : Votre contrat est clôturé.', ActionNeeded), ] def on_load(self): for msg, exc in self.ERROR_CLASSES: for tag in self.doc.xpath('//p[@class="debit"]//strong[text()[contains(.,$msg)]]', msg=msg): raise exc(CleanText('.')(tag))