From 8694bcbcde630c511b07df5a684d9411e1545449 Mon Sep 17 00:00:00 2001 From: Jocelyn Jaubert Date: Sun, 21 Nov 2010 22:04:03 +0100 Subject: [PATCH] =?UTF-8?q?Add=20backend=20for=20french=20bank=20Soci?= =?UTF-8?q?=C3=A9t=C3=A9=20G=C3=A9n=C3=A9rale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- weboob/backends/societegenerale/__init__.py | 21 +++ weboob/backends/societegenerale/backend.py | 60 +++++++++ weboob/backends/societegenerale/browser.py | 90 +++++++++++++ weboob/backends/societegenerale/captcha.py | 123 ++++++++++++++++++ .../societegenerale/pages/__init__.py | 26 ++++ .../societegenerale/pages/accounts_list.py | 61 +++++++++ .../backends/societegenerale/pages/login.py | 80 ++++++++++++ weboob/backends/societegenerale/test.py | 29 +++++ 8 files changed, 490 insertions(+) create mode 100644 weboob/backends/societegenerale/__init__.py create mode 100644 weboob/backends/societegenerale/backend.py create mode 100644 weboob/backends/societegenerale/browser.py create mode 100644 weboob/backends/societegenerale/captcha.py create mode 100644 weboob/backends/societegenerale/pages/__init__.py create mode 100644 weboob/backends/societegenerale/pages/accounts_list.py create mode 100644 weboob/backends/societegenerale/pages/login.py create mode 100644 weboob/backends/societegenerale/test.py diff --git a/weboob/backends/societegenerale/__init__.py b/weboob/backends/societegenerale/__init__.py new file mode 100644 index 0000000000..0a9fda53e4 --- /dev/null +++ b/weboob/backends/societegenerale/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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 SocieteGeneraleBackend + +__all__ = ['SocieteGeneraleBackend'] diff --git a/weboob/backends/societegenerale/backend.py b/weboob/backends/societegenerale/backend.py new file mode 100644 index 0000000000..23804bb41d --- /dev/null +++ b/weboob/backends/societegenerale/backend.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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. + + +# python2.5 compatibility +from __future__ import with_statement + +from weboob.capabilities.bank import ICapBank, AccountNotFound, Account +from weboob.tools.backend import BaseBackend +from weboob.tools.value import ValuesDict, Value + +from .browser import SocieteGenerale + + +__all__ = ['SocieteGeneraleBackend'] + + +class SocieteGeneraleBackend(BaseBackend, ICapBank): + NAME = 'societegenerale' + MAINTAINER = 'Jocelyn Jaubert' + EMAIL = 'jocelyn.jaubert@gmail.com' + VERSION = '0.1' + LICENSE = 'GPLv3' + DESCRIPTION = u'Société Générale french bank\' website' + CONFIG = ValuesDict(Value('login', label='Account ID'), + Value('password', label='Password', masked=True)) + BROWSER = SocieteGenerale + + def create_default_browser(self): + return self.create_browser(self.config['login'], + self.config['password']) + + def iter_accounts(self): + for account in self.browser.get_accounts_list(): + yield account + + def get_account(self, _id): + print _id + if not _id.isdigit(): + raise AccountNotFound() + with self.browser: + account = self.browser.get_account(_id) + if account: + return account + else: + raise AccountNotFound() diff --git a/weboob/backends/societegenerale/browser.py b/weboob/backends/societegenerale/browser.py new file mode 100644 index 0000000000..ae06e1fbfd --- /dev/null +++ b/weboob/backends/societegenerale/browser.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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 datetime import datetime +from logging import warning + +from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword +from weboob.capabilities.bank import TransferError, Transfer +from weboob.backends.societegenerale import pages + + +__all__ = ['SocieteGenerale'] + + +class SocieteGenerale(BaseBrowser): + DOMAIN_LOGIN = 'particuliers.societegenerale.fr' + DOMAIN = 'particuliers.secure.societegenerale.fr' + PROTOCOL = 'https' + ENCODING = None # refer to the HTML encoding + PAGES = { + 'https://particuliers.societegenerale.fr/.*': pages.LoginPage, + '.*restitution/cns_listeprestation.html': pages.AccountsList, +# '.*restitution/cns_detailCav.html.*': pages.AccountHistory, + } + + def __init__(self, *args, **kwargs): + BaseBrowser.__init__(self, *args, **kwargs) + + def home(self): + self.location('https://' + self.DOMAIN_LOGIN + '/index.html') + + def is_logged(self): + return not self.is_on_page(pages.LoginPage) + + def login(self): + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + assert self.password.isdigit() + + if not self.is_on_page(pages.LoginPage): + self.location('https://' + self.DOMAIN_LOGIN + '/index.html') + + self.page.login(self.username, self.password) + + if self.is_on_page(pages.LoginPage): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + if not self.is_on_page(pages.AccountsList): + self.location('/restitution/cns_listeprestation.html') + + return self.page.get_list() + + def get_account(self, id): + assert isinstance(id, basestring) + + if not self.is_on_page(pages.AccountsList): + self.location('/restitution/cns_listeprestation.html') + + l = self.page.get_list() + for a in l: + if a.id == id: + return a + + return None + + def get_history(self, account): + raise NotImplementedError() + + if not self.is_on_page(pages.AccountHistory) or self.page.account.id != account.id: + self.location(account.link_id) + return self.page.get_operations() + + def transfer(self, from_id, to_id, amount, reason=None): + raise NotImplementedError() diff --git a/weboob/backends/societegenerale/captcha.py b/weboob/backends/societegenerale/captcha.py new file mode 100644 index 0000000000..723d2fd3ac --- /dev/null +++ b/weboob/backends/societegenerale/captcha.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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, infos): + self.inim = Image.open(file) + self.infos = infos + self.nbr = int(infos["nblignes"]) + self.nbc = int(infos["nbcolonnes"]) + (self.nx,self.ny) = self.inim.size + self.inmat = self.inim.load() + self.map = {} + + self.tiles = [[Tile(y * self.nbc + x) for y in xrange(4)] for x in xrange(4)] + + 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 = '' + num = 0 + for c in code: + index = self.map[int(c)].id + keycode = self.infos["keyCodes"][num * self.nbr * self.nbc + index] + s += keycode + if num < 5: + s += ',' + num += 1 + return s + + def build_tiles(self): + y = 0 + ty = 0 + while y < self.ny: + x = 0 + tx = 0 + while x < self.nx: + tile = self.tiles[tx][ty] + + for yy in xrange(y, y + 23): + for xx in xrange(x, x + 24): + tile.map.append(self[xx, yy]) + + num = tile.get_num() + if num > -1: + tile.valid = True + self.map[num] = tile + + x += 24 + tx += 1 + + y += 23 + ty += 1 + +class Tile: + hash = {'ff1441b2c5f90703ef04e688e399aca5': 1, + '53d7f3dfd64f54723b231fc398b6be57': 2, + '5bcba7fa2107ba9a606e8d0131c162eb': 3, + '9db6e7ed063e5f9a69ab831e6cc0d721': 4, + '30ebb75bfa5077f41ccfb72e8c9cc15b': 5, + '61e27275e494038e524bc9fbbd0be130': 6, + '0e0816f1b743f320ca561f090df0fbb1': 7, + '11e7d4a6d447e66a5a112c1d9f7fc442': 8, + '2ea3c82768030d91571d360acf7a0f75': 9, + '28a834ebbf0238b46d3fffae1a0b781b': 0, + '04211db029ce488e07010f618a589c71': -1 + } + + 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 get_num(self): + sum = self.checksum() + try: + return self.hash[sum] + except KeyError: + self.display() + raise TileError('Tile not found ' + sum, self) + + def display(self): + print self.checksum() + diff --git a/weboob/backends/societegenerale/pages/__init__.py b/weboob/backends/societegenerale/pages/__init__.py new file mode 100644 index 0000000000..79aef1e62e --- /dev/null +++ b/weboob/backends/societegenerale/pages/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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 .login import LoginPage + +class AccountPrelevement(AccountsList): pass + +__all__ = ['LoginPage', + 'AccountsList', + ] diff --git a/weboob/backends/societegenerale/pages/accounts_list.py b/weboob/backends/societegenerale/pages/accounts_list.py new file mode 100644 index 0000000000..e65b89b857 --- /dev/null +++ b/weboob/backends/societegenerale/pages/accounts_list.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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.capabilities.base import NotAvailable +from weboob.tools.browser import BasePage +from lxml import etree + +class AccountsList(BasePage): + LINKID_REGEXP = re.compile(".*ch4=(\w+).*") + + def on_loaded(self): + pass + + def get_list(self): + l = [] + for tr in self.document.getiterator('tr'): + if tr.attrib.get('class', '') == 'LGNTableRow': + account = Account() + for td in tr.getiterator('td'): + if td.attrib.get('headers', '') == 'TypeCompte': + a = td.find('a') + account.label = a.text + account.link_id = a.get('href', '') + + elif td.attrib.get('headers', '') == 'NumeroCompte': + id = td.text + id = id.replace(u'\xa0','') + account.id = id + + elif td.attrib.get('headers', '') == 'Libelle': + pass + + elif td.attrib.get('headers', '') == 'Solde': + balance = td.text + balance = balance.replace(u'\xa0','').replace(',','.') + if balance != "": + account.balance = float(balance) + else: + account.balance = 0.0 + + l.append(account) + + return l diff --git a/weboob/backends/societegenerale/pages/login.py b/weboob/backends/societegenerale/pages/login.py new file mode 100644 index 0000000000..7f83ebdaeb --- /dev/null +++ b/weboob/backends/societegenerale/pages/login.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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.mech import ClientForm +import urllib +from logging import error + +from weboob.tools.browser import BasePage, BrowserUnavailable +from weboob.backends.societegenerale.captcha import Captcha, TileError +from lxml import etree + + +__all__ = ['LoginPage'] + + +class LoginPage(BasePage): + def on_loaded(self): + for td in self.document.getroot().cssselect('td.LibelleErreur'): + if td.text is None: + continue + msg = td.text.strip() + if 'indisponible' in msg: + raise BrowserUnavailable(msg) + + def login(self, login, password): + DOMAIN_LOGIN = self.browser.DOMAIN_LOGIN + DOMAIN = self.browser.DOMAIN + + url_login = 'https://' + DOMAIN_LOGIN + '/index.html' + + base_url = 'https://' + DOMAIN + url = base_url + '/cvcsgenclavier?mode=jsom&estSession=0' + headers = { + 'Referer': url_login + } + request = self.browser.request_class(url, None, headers) + infos_data = self.browser.readurl(request) + infos_xml = etree.XML(infos_data) + infos = {} + for el in ("cryptogramme", "nblignes", "nbcolonnes"): + infos[el] = infos_xml.find(el).text + + infos["grille"] = "" + for g in infos_xml.findall("grille"): + infos["grille"] += g.text + "," + infos["keyCodes"] = infos["grille"].split(",") + + url = base_url + '/cvcsgenimage?modeClavier=0&cryptogramme=' + infos["cryptogramme"] + img = Captcha(self.browser.openurl(url), infos) + + try: + img.build_tiles() + except TileError, err: + error("Error: %s" % err) + if err.tile: + err.tile.display() + + self.browser.openurl(url_login) + self.browser.select_form('authentification') + self.browser.set_all_readonly(False) + + self.browser['codcli'] = login + self.browser['codsec'] = img.get_codes(password) + self.browser['cryptocvcs'] = infos["cryptogramme"] + self.browser.submit() diff --git a/weboob/backends/societegenerale/test.py b/weboob/backends/societegenerale/test.py new file mode 100644 index 0000000000..2ad761148d --- /dev/null +++ b/weboob/backends/societegenerale/test.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010 Jocelyn Jaubert +# +# 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.test import BackendTest + +class SocieteGeneraleTest(BackendTest): + BACKEND = 'societegenerale' + + def test_societegenerale(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_operations(a)) + list(self.backend.iter_history(a)) -- GitLab