# -*- coding: utf-8 -*- # Copyright(C) 2013 Romain Bignon # # 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 . import urllib from decimal import Decimal, InvalidOperation import re import lxml.html from weboob.deprecated.browser import Page as _BasePage, BrowserUnavailable, BrokenPageError, BrowserBanned from weboob.capabilities.bank import Account from weboob.tools.capabilities.bank.transactions import FrenchTransaction from weboob.tools.captcha.virtkeyboard import MappedVirtKeyboard class BasePage(_BasePage): def get_view_state(self): return self.document.xpath('//input[@name="javax.faces.ViewState"]')[0].attrib['value'] def is_password_expired(self): return len(self.document.xpath('//div[@id="popup_client_modifier_code_confidentiel"]')) def parse_number(self, number): # For some client they randomly displayed 4,115.00 and 4 115,00. # Browser is waiting for for 4 115,00 so we format the number to match this. if '.' in number and len(number.split('.')[-1]) == 2: return number.replace(',', ' ').replace('.', ',') return number class UnavailablePage(BasePage): def on_loaded(self): raise BrowserUnavailable() class PredisconnectedPage(BasePage): def on_loaded(self): raise BrowserBanned() class VirtKeyboard(MappedVirtKeyboard): margin = 2, 2, 2, 2 symbols={'0':'e2df31c137e6c6cb214f92f7d6cd590a', '1':'6057c05937af4574ff453956fbbd2e0e', '2':'5ea5a38efacd3977f17bbc7af83a1943', '3':'560a86b430d2c77e1bd9688efa1b08f9', '4':'e6b6b156ea34a8ae9304526e091b2960', '5':'914483946ee0e55bcc732fce09a0b7c0', '6':'c2382b8f56a0d902e9b399037a9052b5', '7':'c5294f8154a1407560222ac894539d30', '8':'fa1f25a1d5a674dd7bc0d201413d7cfe', '9':'7658424ff8ab127d27e08b7b9b14d331' } color=(0xFF, 0xFF, 0xFF, 0x0) def check_color(self, pixel): step = 10 return abs(pixel[0] - self.color[0]) < step and abs(pixel[1] - self.color[1]) < step and abs(pixel[2] - self.color[2]) < step def __init__(self, page): key = page.document.getroot().xpath('//input')[0].value page.browser.login_key = key img = page.document.getroot().xpath('//img')[0] img_url = 'https://www.axa.fr/.sendvirtualkeyboard.png?key=' + key img_file = page.browser.openurl(img_url) MappedVirtKeyboard.__init__(self, img_file, page.document, img, self.color) self.check_symbols(self.symbols, page.browser.responses_dirname) def get_symbol_code(self,md5sum): code = MappedVirtKeyboard.get_symbol_code(self,md5sum) return code[-3:-2] def get_string_code(self,string): code = '' for c in string: code += self.get_symbol_code(self.symbols[c]) return code class LoginPage(BasePage): def login(self, login, password): document = lxml.html.fromstring(self.document['html']) self.document = document.getroottree() vk = VirtKeyboard(self) args = {'login': login, 'password': vk.get_string_code(password), 'remeberMe': 'false', 'key': self.browser.login_key, } self.browser.location('https://www.axa.fr/.loginAxa.json', urllib.urlencode(args), no_login=True) class PostLoginPage(BasePage): def redirect(self): if 'tokenBanque' not in self.document: return False url = 'https://www.axabanque.fr/webapp/axabanque/client/sso/connexion?token=%s' % self.document['tokenBanque'] self.browser.location(url) self.browser.location('http://www.axabanque.fr/webapp/axabanque/jsp/panorama.faces') return True class AccountsPage(BasePage): ACCOUNT_TYPES = {'courant-titre': Account.TYPE_CHECKING, } def js2args(self, s): args = {} # For example: # noDoubleClic(this);;return oamSubmitForm('idPanorama','idPanorama:tableaux-comptes-courant-titre:0:tableaux-comptes-courant-titre-cartes:0:_idJsp321',null,[['paramCodeProduit','9'],['paramNumContrat','12234'],['paramNumCompte','12345678901'],['paramNumComptePassage','1234567890123456']]); for sub in re.findall("\['([^']+)','([^']+)'\]", s): args[sub[0]] = sub[1] args['idPanorama:_idcl'] = re.search("'(idPanorama:[^']+)'", s).group(1) args['idPanorama_SUBMIT'] = 1 return args def get_list(self): for table in self.document.getroot().cssselect('div#table-panorama table.table-produit'): tds = table.xpath('./tbody/tr')[0].findall('td') if len(tds) < 3: continue boxes = table.xpath('./tbody//tr') foot = table.xpath('./tfoot//tr') for box in boxes: account = Account() if len(box.xpath('.//a')) != 0 and 'onclick' in box.xpath('.//a')[0].attrib: args = self.js2args(box.xpath('.//a')[0].attrib['onclick']) account.label = u'{0} {1}'.format(unicode(table.xpath('./caption')[0].text.strip()), unicode(box.xpath('.//a')[0].text.strip())) elif len(foot[0].xpath('.//a')) != 0 and 'onclick' in foot[0].xpath('.//a')[0].attrib: args = self.js2args(foot[0].xpath('.//a')[0].attrib['onclick']) account.label = unicode(table.xpath('./caption')[0].text.strip()) else: continue self.logger.debug('Args: %r' % args) if 'paramNumCompte' not in args: try: label = unicode(table.xpath('./caption')[0].text.strip()) except Exception: label = 'Unable to determine' self.logger.warning('Unable to get account ID for %r' % label) continue try: account.id = args['paramNumCompte'] + args['paramNumContrat'] if 'Visa' in account.label: card_id = re.search('(\d+)', box.xpath('./td[2]')[0].text.strip()) if card_id: account.id += card_id.group(1) if 'Valorisation' in account.label or u'Liquidités' in account.label: account.id += args['idPanorama:_idcl'].split('Jsp')[-1] except KeyError: account.id = args['paramNumCompte'] account_type_str = table.attrib['class'].split(' ')[-1][len('tableaux-comptes-'):] account.type = self.ACCOUNT_TYPES.get(account_type_str, Account.TYPE_UNKNOWN) currency_title = table.xpath('./thead//th[@class="montant"]')[0].text.strip() m = re.match('Montant \((\w+)\)', currency_title) if not m: self.logger.warning('Unable to parse currency %r' % currency_title) else: account.currency = account.get_currency(m.group(1)) try: account.balance = Decimal(FrenchTransaction.clean_amount(self.parse_number(u''.join([txt.strip() for txt in box.cssselect("td.montant")[0].itertext()])))) except InvalidOperation: #The account doesn't have a amount pass account._args = args yield account class Transaction(FrenchTransaction): PATTERNS = [(re.compile('^RET(RAIT) DAB (?P
\d{2})/(?P\d{2}) (?P.*)'), FrenchTransaction.TYPE_WITHDRAWAL), (re.compile('^(CARTE|CB ETRANGER) (?P
\d{2})/(?P\d{2}) (?P.*)'), FrenchTransaction.TYPE_CARD), (re.compile('^(?PVIR(EMEN)?T? (SEPA)?(RECU|FAVEUR)?)( /FRM)?(?P.*)'), FrenchTransaction.TYPE_TRANSFER), (re.compile('^PRLV (?P.*)( \d+)?$'), FrenchTransaction.TYPE_ORDER), (re.compile('^(CHQ|CHEQUE) .*$'), FrenchTransaction.TYPE_CHECK), (re.compile('^(AGIOS /|FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK), (re.compile('^(CONVENTION \d+ |F )?COTIS(ATION)? (?P.*)'), FrenchTransaction.TYPE_BANK), (re.compile('^REMISE (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), (re.compile('^(?P.*)( \d+)? QUITTANCE .*'), FrenchTransaction.TYPE_ORDER), (re.compile('^.* LE (?P
\d{2})/(?P\d{2})/(?P\d{2})$'), FrenchTransaction.TYPE_UNKNOWN), ] class TransactionsPage(BasePage): COL_DATE = 0 COL_TEXT = 1 COL_DEBIT = 2 COL_CREDIT = 3 def more_history(self): link = None for a in self.document.xpath('.//a'): if a.text is not None and a.text.strip() == 'Sur les 6 derniers mois': link = a break if link is None: # this is a check account args = {'categorieMouvementSelectionnePagination': 'afficherTout', 'nbLigneParPageSelectionneHautPagination': -1, 'nbLigneParPageSelectionneBasPagination': -1, 'periodeMouvementSelectionneComponent': '', 'categorieMouvementSelectionneComponent': '', 'nbLigneParPageSelectionneComponent': -1, 'idDetail:btnRechercherParNbLigneParPage': '', 'idDetail_SUBMIT': 1, 'paramNumComptePassage': '', 'codeEtablissement': '', 'paramNumCodeSousProduit': '', 'idDetail:_idcl': '', 'idDetail:scroll_banqueHaut': '', 'paramNumContrat': '', 'paramCodeProduit': '', 'paramNumCompte': '', 'codeAgence': '', 'idDetail:_link_hidden_': '', 'paramCodeFamille': '', 'javax.faces.ViewState': self.get_view_state(), } else: # something like a PEA or so value = link.attrib['id'] id = value.split(':')[0] args = {'%s:_idcl' % id: value, '%s:_link_hidden_' % id: '', '%s_SUBMIT' % id: 1, 'javax.faces.ViewState': self.get_view_state(), 'paramNumCompte': '', } form = self.document.xpath('//form')[-1] self.browser.location(form.attrib['action'], urllib.urlencode(args)) def get_history(self): #DAT account can't have transaction if self.document.xpath('//table[@id="table-dat"]'): return #These accounts have investments, no transactions if self.document.xpath('//table[@id="InfosPortefeuille"]'): return tables = self.document.xpath('//table[@id="table-detail-operation"]') if len(tables) == 0: tables = self.document.xpath('//table[@id="table-detail"]') if len(tables) == 0: tables = self.document.getroot().cssselect('table.table-detail') if len(tables) == 0: try: self.parser.select(self.document.getroot(), 'td.no-result', 1) except BrokenPageError: raise BrokenPageError('Unable to find table?') else: return for tr in tables[0].xpath('.//tr'): tds = tr.findall('td') if len(tds) < 4: continue t = Transaction() date = u''.join([txt.strip() for txt in tds[self.COL_DATE].itertext()]) raw = u''.join([txt.strip() for txt in tds[self.COL_TEXT].itertext()]) debit = self.parse_number(u''.join([txt.strip() for txt in tds[self.COL_DEBIT].itertext()])) credit = self.parse_number(u''.join([txt.strip() for txt in tds[self.COL_CREDIT].itertext()])) t.parse(date, re.sub(r'[ ]+', ' ', raw)) t.set_amount(credit, debit) yield t class CBTransactionsPage(TransactionsPage): COL_CB_CREDIT = 2 def get_history(self): tables = self.document.xpath('//table[@id="idDetail:dataCumulAchat"]') transactions =list() if len(tables) == 0: return transactions for tr in tables[0].xpath('.//tr'): tds = tr.findall('td') if len(tds) < 3: continue t = Transaction() date = u''.join([txt.strip() for txt in tds[self.COL_DATE].itertext()]) raw = self.parse_number(u''.join([txt.strip() for txt in tds[self.COL_TEXT].itertext()])) credit = self.parse_number(u''.join([txt.strip() for txt in tds[self.COL_CB_CREDIT].itertext()])) debit = "" t.parse(date, re.sub(r'[ ]+', ' ', raw)) t.set_amount(credit, debit) transactions.append(t) for histo in super(CBTransactionsPage, self).get_history(): transactions.append(histo) transactions.sort(key=lambda transaction: transaction.date, reverse=True) return iter(transactions)