# -*- coding: utf-8 -*-
# Copyright(C) 2012 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
%s
" % x)) elif isinstance(x, bytes): x = x.decode('utf-8') return CleanText(xpath)(html.fromstring("%s
" % x)) else: return CleanText(xpath)(html.fromstring(CleanText('.')(x))) class CDNVirtKeyboard(GridVirtKeyboard): symbols = {'0': '3de2346a63b658c977fce4da925ded28', '1': 'c571018d2dc267cdf72fafeeb9693037', '2': '72d7bad4beb833d85047f6912ed42b1d', '3': 'fbfce4677a8b2f31f3724143531079e3', '4': '54c723c5b0b5848a0475b4784100b9e0', '5': 'd00164307cacd4ca21b930db09403baa', '6': '101adc6f5d03df0f512c3ec2bef88de9', '7': '3b48f598209718397eb1118d81cf07ba', '8': '881f0acdaba2c44b6a5e64331f4f53d3', '9': 'a47d9a0a2ebbc65a0e625f20cb07822b', } margin = 1 color = (0xff,0xf7,0xff) nrow = 4 ncol = 4 def __init__(self, browser, crypto, grid): f = BytesIO(browser.open('/sec/vk/gen_ui?modeClavier=0&cryptogramme=%s' % crypto).content) super(CDNVirtKeyboard, self).__init__(range(16), self.ncol, self.nrow, f, self.color) self.check_symbols(self.symbols, browser.responses_dirname) self.codes = grid def check_color(self, pixel): for p in pixel: if p > 0xd0: return False return True def get_string_code(self, string): res = [] ndata = self.nrow * self.ncol for nbchar, c in enumerate(string): index = self.get_symbol_code(self.symbols[c]) res.append(self.codes[(nbchar * ndata) + index]) return ','.join(res) class RedirectPage(HTMLPage): def on_load(self): for script in self.doc.xpath('//script'): self.browser.location(re.search(r'href="([^"]+)"', script.text).group(1)) class LoginPage(HTMLPage): VIRTUALKEYBOARD = CDNVirtKeyboard def login(self, username, password): login_selector = self.doc.xpath('//input[@id="codsec"]') if login_selector: if not password.isdigit() or not len(password) == 6: raise BrowserIncorrectPassword('The credentials have changed on website %s. Please update them.' % self.browser.BASEURL) self.vk_login(username, password) else: self.classic_login(username,password) def vk_login(self, username, password): res = self.browser.open('/sec/vk/gen_crypto?estSession=0').text crypto = re.search(r"'crypto': '([^']+)'", res).group(1) grid = re.search(r"'grid': \[([^\]]+)]", res).group(1).split(',') vk = self.VIRTUALKEYBOARD(self.browser, crypto, grid) data = {'user_id': username, 'codsec': vk.get_string_code(password), 'cryptocvcs': crypto, 'vk_op': 'auth', } self.browser.location('/swm/redirectCDN.html', data=data) def classic_login(self, username, password): m = re.match('www.([^\.]+).fr', self.browser.BASEURL) if not m: bank_name = 'credit-du-nord' self.logger.error('Unable to find bank name for %s' % self.browser.BASEURL) else: bank_name = m.group(1) data = {'bank': bank_name, 'pagecible': 'vos-comptes', 'password': password.encode(self.browser.ENCODING), 'pwAuth': 'Authentification+mot+de+passe', 'username': username.encode(self.browser.ENCODING), } self.browser.location('/saga/authentification', data=data) def get_error(self): return CleanText('//b[has-class("x-attentionErreurLigneHaut")]', default="")(self.doc) class CDNBasePage(HTMLPage): def get_from_js(self, pattern, end_pattern, is_list=False): """ find a pattern in any javascript text """ for script in self.doc.xpath('//script'): txt = script.text if txt is None: continue start = txt.find(pattern) if start < 0: continue values = [] while start >= 0: start += len(pattern) end = txt.find(end_pattern, start) values.append(txt[start:end]) if not is_list: break start = txt.find(pattern, end) return ','.join(values) def get_execution(self): return self.get_from_js("name: 'execution', value: '", "'") def iban_go(self): return '%s%s' % ('/vos-comptes/IPT/cdnProxyResource', self.get_from_js('C_PROXY.StaticResourceClientTranslation( "', '"')) class AccountsPage(LoggedPage, CDNBasePage): COL_HISTORY = 2 COL_FIRE_EVENT = 3 COL_ID = 4 COL_LABEL = 5 COL_BALANCE = -1 TYPES = { u'CARTE': Account.TYPE_CARD, u'COMPTE COURANT': Account.TYPE_CHECKING, u'CPT COURANT': Account.TYPE_CHECKING, u'PEA': Account.TYPE_PEA, u'P.E.A': Account.TYPE_PEA, u'COMPTE ÉPARGNE': Account.TYPE_SAVINGS, u'COMPTE EPARGNE': Account.TYPE_SAVINGS, u'COMPTE SUR LIVRET': Account.TYPE_SAVINGS, u'LDDS': Account.TYPE_SAVINGS, u'LIVRET': Account.TYPE_SAVINGS, u"PLAN D'EPARGNE": Account.TYPE_SAVINGS, u'PLAN ÉPARGNE': Account.TYPE_SAVINGS, u'ASS.VIE': Account.TYPE_LIFE_INSURANCE, u'ÉTOILE AVANCE': Account.TYPE_LOAN, u'PRÊT': Account.TYPE_LOAN, u'CREDIT': Account.TYPE_LOAN, u'FACILINVEST': Account.TYPE_LOAN, u'TITRES': Account.TYPE_MARKET, u'COMPTE A TERME': Account.TYPE_DEPOSIT, } def get_password_expired(self): error = CleanText('//div[@class="x-attentionErreur"]/b')(self.doc) if "vous devez modifier votre code confidentiel à la première connexion" in error: return error def get_account_type(self, label): for pattern, actype in sorted(self.TYPES.items()): if label.startswith(pattern) or label.endswith(pattern): return actype return Account.TYPE_UNKNOWN def get_history_link(self): return CleanText().filter(self.get_from_js(",url: Ext.util.Format.htmlDecode('", "'")).replace('&', '&') def get_av_link(self): return self.doc.xpath('//a[contains(text(), "Consultation")]')[0].attrib['href'] def make__args_dict(self, line): return {'_eventId': 'clicDetailCompte', '_ipc_eventValue': '', '_ipc_fireEvent': '', 'deviseAffichee': 'DEVISE', 'execution': self.get_execution(), 'idCompteClique': line[self.COL_ID], } def get_list(self): accounts = [] previous_account = None noaccounts = self.get_from_js('_js_noMvts =', ';') if noaccounts is not None: assert 'avez aucun compte' in noaccounts return [] txt = self.get_from_js('_data = new Array(', ');', is_list=True) if txt is None: raise BrowserUnavailable('Unable to find accounts list in scripts') data = json.loads('[%s]' % txt.replace("'", '"')) for line in data: a = Account() a.id = line[self.COL_ID].replace(' ', '') if re.match(r'Classement=(.*?):::Banque=(.*?):::Agence=(.*?):::SScompte=(.*?):::Serie=(.*)', a.id): a.id = str(CleanDecimal().filter(a.id)) a._acc_nb = a.id.split('_')[0] if len(a.id.split('_')) > 1 else None a.label = MyStrip(line[self.COL_LABEL], xpath='.//div[@class="libelleCompteTDB"]') # This account can be multiple life insurance accounts if a.label == 'ASSURANCE VIE-BON CAPI-SCPI-DIVERS *': continue a.balance = Decimal(FrenchTransaction.clean_amount(line[self.COL_BALANCE])) a.currency = a.get_currency(line[self.COL_BALANCE]) a.type = self.get_account_type(a.label) # The parent account must be created right before if a.type == Account.TYPE_CARD: # duplicate if find_object(accounts, id=a.id): self.logger.warning('Ignoring duplicate card %r', a.id) continue a.parent = previous_account if line[self.COL_HISTORY] == 'true': a._inv = False a._link = self.get_history_link() a._args = self.make__args_dict(line) else: a._inv = True a._args = {'_ipc_eventValue': line[self.COL_ID], '_ipc_fireEvent': line[self.COL_FIRE_EVENT], } a._link = self.doc.xpath('//form[@name="changePageForm"]')[0].attrib['action'] if a.type is Account.TYPE_CARD: a.coming = a.balance a.balance = Decimal('0.0') accounts.append(a) previous_account = a return accounts def iban_page(self): form = self.get_form(name="changePageForm") form['_ipc_fireEvent'] = 'V1_rib' form['_ipc_eventValue'] = 'bouchon=bouchon' form.submit() @method class get_profile(ItemElement): klass = Profile obj_name = CleanText('//p[@class="nom"]') def get_strid(self): return re.search(r'(\d{4,})', Attr('//form[@name="changePageForm"]', 'action')(self.doc)).group(0) class AVPage(LoggedPage, CDNBasePage): COL_LABEL = 0 COL_BALANCE = 3 ARGS = ['IndiceClassement', 'IndiceCompte', 'Banque', 'Agence', 'Classement', 'Serie', 'SScompte', 'Categorie', 'IndiceSupport', 'NumPolice', 'LinkHypertext'] def get_params(self, text): url = self.get_from_js('document.detail.action="', '";') args = {} l = [] for sub in re.findall("'([^']*)'", text): l.append(sub) for i, key in enumerate(self.ARGS): args[key] = l[self.ARGS.index(key)] return url, args def get_av_accounts(self): for table in self.doc.xpath('//table[@class="datas"]'): head_cols = table.xpath('./tr[@class="entete"]/td') for tr in table.xpath('./tr[not(@class)]'): cols = tr.findall('td') if len(cols) != 4: continue a = Account() a.label = CleanText('.')(cols[self.COL_LABEL]) a.type = Account.TYPE_LIFE_INSURANCE a.balance = MyDecimal('.')(cols[self.COL_BALANCE]) a.currency = a.get_currency(CleanText('.')(head_cols[self.COL_BALANCE])) a._link, a._args = self.get_params(cols[self.COL_LABEL].find('span/a').attrib['href']) a.id = a._args['IndiceSupport'] + a._args['NumPolice'] a._acc_nb = None a._inv = True yield a class ProAccountsPage(AccountsPage): COL_ID = 0 COL_BALANCE = 1 ARGS = ['Banque', 'Agence', 'Classement', 'Serie', 'SSCompte', 'Devise', 'CodeDeviseCCB', 'LibelleCompte', 'IntituleCompte', 'Indiceclassement', 'IndiceCompte', 'NomClassement'] def on_load(self): if self.doc.xpath('//h1[contains(text(), "Erreur")]'): raise BrowserUnavailable(CleanText('//h1[contains(text(), "Erreur")]//span')(self.doc)) msg = CleanText('//div[@class="x-attentionErreur"]/b')(self.doc) if 'vous devez modifier votre code confidentiel' in msg: raise BrowserPasswordExpired(msg) def params_from_js(self, text): l = [] for sub in re.findall("'([^']*)'", text): l.append(sub) if len(l) <= 1: #For account that have no history return None, None url = '/vos-comptes/IPT/appmanager/transac/' + self.browser.account_type + '?_nfpb=true&_windowLabel=portletInstance_18&_pageLabel=page_synthese_v1' + '&_cdnCltUrl=' + "/transacClippe/" + quote(l.pop(0)) args = {} for input in self.doc.xpath('//form[@name="detail"]/input'): args[input.attrib['name']] = input.attrib.get('value', '') for i, key in enumerate(self.ARGS): args[key] = unicode(l[self.ARGS.index(key)]).encode(self.browser.ENCODING) args['PageDemandee'] = 1 args['PagePrecedente'] = 1 return url, args def get_list(self): no_accounts_message = self.doc.xpath(u'//span/b[contains(text(),"Votre abonnement est clôturé. Veuillez contacter votre conseiller.")]/text()') if no_accounts_message: raise ActionNeeded(no_accounts_message[0]) previous_checking_account = None # Several deposit accounts ('Compte à terme') have the same id and the same label # So a number is added to distinguish them previous_deposit_account = None deposit_count = 1 for tr in self.doc.xpath('//table[has-class("datas")]//tr'): if tr.attrib.get('class', '') == 'entete': continue cols = tr.findall('td') a = Account() a.label = unicode(cols[self.COL_ID].xpath('.//span[@class="left-underline"] | .//span[@class="left"]/a')[0].text.strip()) a.type = self.get_account_type(a.label) balance = CleanText('.')(cols[self.COL_BALANCE]) if balance == '': continue a.balance = CleanDecimal(replace_dots=True).filter(balance) a.currency = a.get_currency(balance) if cols[self.COL_ID].find('a'): a._link, a._args = self.params_from_js(cols[self.COL_ID].find('a').attrib['href']) # There may be a href with 'javascript:NoDetail();' # The _link and _args should be None else: a._link, a._args = None, None a._acc_nb = cols[self.COL_ID].xpath('.//span[@class="right-underline"] | .//span[@class="right"]')[0].text.replace(' ', '').strip() if hasattr(a, '_args') and a._args: a.id = '%s%s%s' % (a._acc_nb, a._args['IndiceCompte'], a._args['Indiceclassement']) else: a.id = a._acc_nb # This account can be multiple life insurance accounts if (any(a.label.startswith(lab) for lab in ['ASS.VIE-BONS CAPI-SCPI-DIVERS', 'BONS CAPI-SCPI-DIVERS']) or (u'Aucun d\\351tail correspondant pour ce compte' in tr.xpath('.//a/@href')[0]) and 'COMPTE A TERME' not in tr.xpath('.//span[contains(@class, "left")]/text()')[0]): continue if a.type is Account.TYPE_CARD: a.coming = a.balance a.balance = Decimal('0.0') # Take the predecessiong checking account as parent if previous_checking_account: a.parent = previous_checking_account else: self.logger.warning('The card account %s has no parent account' % a.id) a._inv = False if a.type == Account.TYPE_CHECKING: previous_checking_account = a if previous_deposit_account and previous_deposit_account.id == a.id: a.id = a.id + '_%s' % deposit_count deposit_count += 1 previous_deposit_account = a if a.type == Account.TYPE_DEPOSIT: previous_deposit_account = a yield a def iban_page(self): self.browser.location(self.doc.xpath('.//a[contains(text(), "Impression IBAN")]')[0].attrib['href']) def has_iban(self): return not bool(CleanText('//*[contains(., "pas de compte vous permettant l\'impression de RIB")]')(self.doc)) @method class get_profile(ItemElement): klass = Profile obj_name = CleanText('//p[@class="nom"]') class IbanPage(LoggedPage, HTMLPage): def get_iban(self): try: return unicode(self.doc.xpath('.//td[@width="315"]/font')[0].text.replace(' ', '').strip()) except AttributeError: return NotAvailable class Transaction(FrenchTransaction): PATTERNS = [(re.compile(r'^(?P