diff --git a/weboob/backends/bnporc/__init__.py b/weboob/backends/bnporc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e67008af9456e696700599222ba23f720d3e1f --- /dev/null +++ b/weboob/backends/bnporc/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +from .backend import BNPorcBackend diff --git a/weboob/backends/bnporc/backend.py b/weboob/backends/bnporc/backend.py new file mode 100644 index 0000000000000000000000000000000000000000..162fec10fae5c311f0e4748dab5f41d1784339ca --- /dev/null +++ b/weboob/backends/bnporc/backend.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +from weboob.backend import Backend +from weboob.capabilities.bank import ICapBank, AccountNotFound + +from .browser import BNPorc + +class BNPorcBackend(Backend, ICapBank): + NAME = 'bnporc' + MAINTAINER = 'Romain Bignon' + EMAIL = 'romain@peerfuse.org' + VERSION = '1.0' + LICENSE = 'GPLv3' + + CONFIG = {'login': Backend.ConfigField(description='Account ID'), + 'password': Backend.ConfigField(description='Password of account', is_masked=True) + } + browser = None + + def need_browser(func): + def inner(self, *args, **kwargs): + if not self.browser: + self.browser = BNPorc(self.config['login'], self.config['password']) + + return func(self, *args, **kwargs) + return inner + + @need_browser + def iter_accounts(self): + for account in self.browser.get_accounts_list(): + yield account + + @need_browser + def get_account(self, _id): + try: + _id = long(_id) + except ValueError: + raise AccountNotFound() + else: + account = self.browser.get_account(_id) + if account: + return account + else: + raise AccountNotFound() + + @need_browser + def iter_operations(self, account): + for coming in self.browser.get_coming_operations(account): + yield coming diff --git a/weboob/backends/bnporc/browser.py b/weboob/backends/bnporc/browser.py new file mode 100644 index 0000000000000000000000000000000000000000..c289b30f92039d81be7e885d19e4107a2555c82e --- /dev/null +++ b/weboob/backends/bnporc/browser.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2009-2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +from cStringIO import StringIO + +from weboob.tools.browser import Browser, BrowserIncorrectPassword +from weboob.tools.parser import StandardParser +from weboob.backends.bnporc import pages + +# Parser +class BNParser(StandardParser): + def parse(self, data, encoding): + s = data.read() + s = s.replace('', '') + data = StringIO(s) + return StandardParser.parse(self, data) + +# Browser +class BNPorc(Browser): + DOMAIN = 'www.secure.bnpparibas.net' + PROTOCOL = 'https' + ENCODING = None # refer to the HTML encoding + PAGES = {'.*identifiant=DOSSIER_Releves_D_Operation.*': pages.AccountsList, + '.*identifiant=DSP_HISTOCPT.*': pages.AccountHistory, + '.*NS_AVEEC.*': pages.AccountComing, + '.*NS_AVEDP.*': pages.AccountPrelevement, + '.*Action=DSP_VGLOBALE.*': pages.LoginPage, + '.*type=homeconnex.*': pages.LoginPage, + '.*layout=HomeConnexion': pages.ConfirmPage, + } + + is_logging = False + + def __init__(self, *args, **kwargs): + kwargs['parser'] = BNParser + Browser.__init__(self, *args, **kwargs) + + def home(self): + self.location('https://www.secure.bnpparibas.net/banque/portail/particulier/HomeConnexion?type=homeconnex') + + def is_logged(self): + return not self.is_on_page(pages.LoginPage) or self.is_logging + + def login(self): + assert isinstance(self.username, (str,unicode)) + assert isinstance(self.password, (str,unicode)) + assert self.password.isdigit() + + if not self.is_on_page(pages.LoginPage): + self.location('https://www.secure.bnpparibas.net/banque/portail/particulier/HomeConnexion?type=homeconnex') + + self.is_logging = True + self.page.login(self.username, self.password) + self.location('/NSFR?Action=DSP_VGLOBALE') + + if self.is_on_page(pages.LoginPage): + raise BrowserIncorrectPassword() + self.is_logging = False + + def get_accounts_list(self): + if not self.is_on_page(pages.AccountsList): + self.location('/NSFR?Action=DSP_VGLOBALE') + return self.page.get_list() + + def get_account(self, id): + assert isinstance(id, (int, long)) + + if not self.is_on_page(pages.AccountsList): + self.location('/NSFR?Action=DSP_VGLOBALE') + + l = self.page.get_list() + for a in l: + if a.id == id: + return a + + return None + + def get_coming_operations(self, account): + if not self.is_on_page(pages.AccountComing) or self.page.account.id != account.id: + self.location('/NS_AVEEC?ch4=%s' % account.link_id) + return self.page.get_operations() diff --git a/weboob/backends/bnporc/captcha.py b/weboob/backends/bnporc/captcha.py new file mode 100644 index 0000000000000000000000000000000000000000..6a7511b42f99e91c634b8ad1aa00b3845a089038 --- /dev/null +++ b/weboob/backends/bnporc/captcha.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2009-2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +import hashlib +import sys +import Image + +class TileError(Exception): + def __init__(self, msg, tile = None): + Exception.__init__(self, msg) + self.tile = tile + +class Captcha: + def __init__(self, file): + self.inim = Image.open(file) + self.nx,self.ny = self.inim.size + self.inmat = self.inim.load() + self.map = {} + + self.tiles = [[Tile(x+5*y+1) for y in xrange(5)] for x in xrange(5)] + + def __getitem__(self, (x, y)): + return self.inmat[x % self.nx, y % self.ny] + + def all_coords(self): + for y in xrange(self.ny): + for x in xrange(self.nx): + yield x, y + + def get_codes(self, code): + s = '' + for c in code: + s += '%02d' % self.map[int(c)].id + return s + + def build_tiles(self): + y = 5 + ty = 0 + while y < self.ny: + x = 6 + tx = 0 + while x < self.nx: + if self[x,y] == 8: + tile = self.tiles[tx][ty] + tile.valid = True + yy = y + while not self[x,yy] in (3,7): + l = [] + tile.map.append(l) + xx = x + while not self[xx,yy] in (3,7): + l.append(self[xx,yy]) + xx += 1 + + yy += 1 + + self.map[tile.getNum()] = tile + + x += 26 + tx += 1 + + y += 25 + ty += 1 + +class Tile: + hash = {'b2d25ae11efaaaec6dd6a4c00f0dfc29': 1, + '600873fa288e75ca6cca092ae95bf129': 2, + 'da24ac28930feee169adcbc9bad4acaf': 3, + '76294dec2a3c6a7b8d9fcc7a116d1d4f': 4, + 'd9531059e3834b6b8a97e29417a47dec': 5, + '8ba0c0cfe5e64d6b4afb1aa6f3612c1a': 6, + '19e0120231e7a9cf4544f96d8c388c8a': 7, + '83d8ad340156cb7f5c1e64454b66c773': 8, + '5ee8648d77eeb3e0979f6e59b2fbe66a': 9, + '3f3fb79bf61ebad096e05287119169df': 0 + } + + def __init__(self, _id): + self.id = _id + self.valid = False + self.map = [] + + def __repr__(self): + return "" % (self.id, self.valid) + + def checksum(self): + s = '' + for pxls in self.map: + for pxl in pxls: + s += '%02d' % pxl + return hashlib.md5(s).hexdigest() + + def getNum(self): + sum = self.checksum() + try: + return self.hash[sum] + except KeyError: + raise TileError('Tile not found', self) + + def display(self): + for pxls in self.map: + for pxl in pxls: + sys.stdout.write('%02d' % pxl) + sys.stdout.write('\n') + diff --git a/weboob/backends/bnporc/pages/__init__.py b/weboob/backends/bnporc/pages/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..65e65ee3b4649f970688eab09aca4cc5525370f9 --- /dev/null +++ b/weboob/backends/bnporc/pages/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2009-2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +from .accounts_list import AccountsList +from .account_coming import AccountComing +from .login import LoginPage, ConfirmPage + +class AccountHistory(AccountsList): pass +class AccountPrelevement(AccountsList): pass diff --git a/weboob/backends/bnporc/pages/account_coming.py b/weboob/backends/bnporc/pages/account_coming.py new file mode 100644 index 0000000000000000000000000000000000000000..63aec3c8e9a1bfc44711fed50542c210b371357a --- /dev/null +++ b/weboob/backends/bnporc/pages/account_coming.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2009-2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +from weboob.tools.browser import BasePage +from weboob.capabilities.bank import Operation + +class AccountComing(BasePage): + + def loaded(self): + self.operations = [] + + for tr in self.document.getiterator('tr'): + if tr.attrib.get('class', '') == 'hdoc1' or tr.attrib.get('class', '') == 'hdotc1': + operation = Operation() + tds = tr.findall('td') + if len(tds) != 3: + continue + date = tds[0].getchildren()[0].attrib.get('name', '') + label = u'' + label += tds[1].text + for child in tds[1].getchildren(): + if child.text: label += child.text + if child.tail: label += child.tail + label += tds[1].tail + label = label.strip() + amount = tds[2].text.replace('.','').replace(',','.') + + operation.setDate(date) + operation.setLabel(label) + operation.setAmount(float(amount)) + self.operations.append(operation) + + def get_operations(self): + return self.operations diff --git a/weboob/backends/bnporc/pages/accounts_list.py b/weboob/backends/bnporc/pages/accounts_list.py new file mode 100644 index 0000000000000000000000000000000000000000..f8fb6af81eee7c0a1bd9af2a568725a990bddae8 --- /dev/null +++ b/weboob/backends/bnporc/pages/accounts_list.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2009-2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +import re + +from weboob.capabilities.bank import Account +from weboob.tools.browser import BasePage +from weboob.tools.parser import tostring + +class AccountsList(BasePage): + LINKID_REGEXP = re.compile(".*ch4=(\w+).*") + + def loaded(self): + pass + + def get_list(self): + l = [] + for tr in self.document.getiterator('tr'): + if tr.attrib.get('class', '') == 'comptes': + account = Account() + for td in tr.getiterator('td'): + if td.attrib.get('headers', '').startswith('Numero_'): + id = td.text + account.setID(long(''.join(id.split(' ')))) + elif td.attrib.get('headers', '').startswith('Libelle_'): + a = td.findall('a') + label = unicode(a[0].text) + account.setLabel(label) + m = self.LINKID_REGEXP.match(a[0].attrib.get('href', '')) + if m: + account.setLinkID(m.group(1)) + elif td.attrib.get('headers', '').startswith('Solde'): + a = td.findall('a') + balance = a[0].text + balance = balance.replace('.','').replace(',','.') + account.setBalance(float(balance)) + elif td.attrib.get('headers', '').startswith('Avenir'): + a = td.findall('a') + coming = a[0].text + coming = coming.replace('.','').replace(',','.') + account.setComing(float(coming)) + + l.append(account) + return l diff --git a/weboob/backends/bnporc/pages/login.py b/weboob/backends/bnporc/pages/login.py new file mode 100644 index 0000000000000000000000000000000000000000..5e14eaa57b255341ec33710dfb44d23d7c4ae5a8 --- /dev/null +++ b/weboob/backends/bnporc/pages/login.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +""" +Copyright(C) 2009-2010 Romain Bignon + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" + +import ClientForm +import sys + +from weboob.tools.browser import BasePage +from weboob.backends.bnporc.captcha import Captcha, TileError + +class LoginPage(BasePage): + def loaded(self): + pass + + def login(self, login, password): + img = Captcha(self.browser.openurl('/NSImgGrille')) + self.browser.back() + + try: + img.build_tiles() + except TileError, err: + print >>sys.stderr, "Error: %s" % err + if err.tile: + err.tile.display() + + self.browser.select_form('logincanalnet') + # HACK because of fucking malformed HTML, the field isn't recognized by mechanize. + self.browser.controls.append(ClientForm.TextControl('text', 'ch1', {'value': ''})) + self.browser.set_all_readonly(False) + + self.browser['ch1'] = login + self.browser['ch5'] = img.get_codes(password) + self.browser.submit() + +class ConfirmPage(BasePage): + pass