diff --git a/weboob/backends/ing/__init__.py b/weboob/backends/ing/__init__.py new file mode 100644 index 00000000..134b07d0 --- /dev/null +++ b/weboob/backends/ing/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2011 Romain Bignon, Florent Fourcot +# +# 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 .backend import INGBackend + +__all__ = ['INGBackend'] diff --git a/weboob/backends/ing/backend.py b/weboob/backends/ing/backend.py new file mode 100644 index 00000000..f2256ab2 --- /dev/null +++ b/weboob/backends/ing/backend.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2011 Romain Bignon, Florent Fourcot +# +# 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 . + + +# python2.5 compatibility +from __future__ import with_statement + +from weboob.capabilities.bank import ICapBank, AccountNotFound +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import Ing + + +__all__ = ['INGBackend'] + + +class INGBackend(BaseBackend, ICapBank): + NAME = 'ing' + MAINTAINER = 'Florent Fourcot' + EMAIL = 'weboob@flo.fourcot.fr' + VERSION = '0.a' + LICENSE = 'AGPLv3+' + DESCRIPTION = 'ING french bank\' website' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password', regexp='^(\d{6}|)$'), + ValueBackendPassword('birthday', label='Birthday', regexp='^(\d{8}|)$', masked=False) + ) + BROWSER = Ing + + def create_default_browser(self): + return self.create_browser(self.config['login'].get(), + self.config['password'].get(), + birthday=self.config['birthday'].get()) + + def iter_accounts(self): + for account in self.browser.get_accounts_list(): + yield account + + def get_account(self, _id): + if not _id.isdigit(): + raise AccountNotFound() + with self.browser: + account = self.browser.get_account(_id) + if account: + return account + else: + raise AccountNotFound() + + def iter_history(self, account): + with self.browser: + for history in self.browser.get_history(account.id): + yield history + + def iter_operations(self, account): + with self.browser: + for coming in self.browser.get_coming_operations(account.id): + yield coming + diff --git a/weboob/backends/ing/browser.py b/weboob/backends/ing/browser.py new file mode 100644 index 00000000..72cab2ec --- /dev/null +++ b/weboob/backends/ing/browser.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2009-2011 Romain Bignon, Florent Fourcot +# +# 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 weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword +from weboob.backends.ing import pages + + +__all__ = ['Ing'] + + +class Ing(BaseBrowser): + DOMAIN = 'secure.ingdirect.fr' + PROTOCOL = 'https' + ENCODING = None # refer to the HTML encoding + PAGES = {'.*displayTRAccountSummary.*': pages.AccountsList, + '.*displayLogin.jsf': pages.LoginPage, + '.*displayLogin.jsf.*': pages.LoginPage2, + '.*accountDetail.jsf.*': pages.AccountHistory + } + + def __init__(self, *args, **kwargs): + self.birthday = kwargs.pop('birthday', None) + BaseBrowser.__init__(self, *args, **kwargs) + + def home(self): + self.location('https://secure.ingdirect.fr/public/displayLogin.jsf') + + 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 isinstance(self.birthday, basestring) + assert self.password.isdigit() + assert self.birthday.isdigit() + + if not self.is_on_page(pages.LoginPage): + self.location('https://secure.ingdirect.fr/public/displayLogin.jsf') + + self.page.prelogin(self.username, self.birthday) + self.page.login(self.password) + + def get_accounts_list(self): + if not self.is_on_page(pages.AccountsList): + self.location('/general?command=displayTRAccountSummary') + + return self.page.get_list() + + def get_account(self, id): + assert isinstance(id, basestring) + + if not self.is_on_page(pages.AccountsList): + self.location('/general?command=displayTRAccountSummary') + + l = self.page.get_list() + for a in l: + if a.id == id: + return a + + return None + + def get_history(self, id): + # TODO: It works only with the Compte Courant, Livret A use an another page... + self.location('https://secure.ingdirect.fr/protected/pages/cc/accountDetail.jsf') + return self.page.get_operations() + + # TODO + # def get_coming_operations diff --git a/weboob/backends/ing/pages/__init__.py b/weboob/backends/ing/pages/__init__.py new file mode 100644 index 00000000..bcc20aa5 --- /dev/null +++ b/weboob/backends/ing/pages/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2009-2011 Romain Bignon, Florent Fourcot +# +# 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 .accounts_list import AccountsList +from .account_coming import AccountComing +from .account_history import AccountHistory +from .login import LoginPage, LoginPage2, ConfirmPage, MessagePage + +class AccountPrelevement(AccountsList): pass + +__all__ = ['AccountsList', 'AccountComing', 'AccountHistory', 'LoginPage', + 'ConfirmPage', 'MessagePage', 'AccountPrelevement'] diff --git a/weboob/backends/ing/pages/account_history.py b/weboob/backends/ing/pages/account_history.py new file mode 100644 index 00000000..d4135077 --- /dev/null +++ b/weboob/backends/ing/pages/account_history.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2009-2011 Romain Bignon, Florent Fourcot +# +# 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 re +from datetime import date + +from weboob.tools.browser import BasePage +from weboob.capabilities.bank import Operation +from weboob.capabilities.base import NotAvailable + + +__all__ = ['AccountHistory'] + + +class AccountHistory(BasePage): + + def on_loaded(self): + self.operations = [] + table = self.document.findall('//tbody')[0] + i = 1 + for tr in table.xpath('tr'): + id = i + texte = tr.text_content().split('\n') + op = Operation(id) + op.label = texte[2] + op.date = date(*reversed([int(x) for x in texte[0].split('/')])) + op.category = texte[4] + + amount = texte[5].replace('\t','').strip().replace(u'€', '').replace(',', '.').replace(u'\xa0', u'') + op.amount = float(amount) + + self.operations.append(op) + i += 1 + + def get_operations(self): + return self.operations diff --git a/weboob/backends/ing/pages/accounts_list.py b/weboob/backends/ing/pages/accounts_list.py new file mode 100644 index 00000000..cf36c972 --- /dev/null +++ b/weboob/backends/ing/pages/accounts_list.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2009-2011 Romain Bignon, Florent Fourcot +# +# 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 re + +from weboob.capabilities.bank import Account +from weboob.capabilities.base import NotAvailable +from weboob.tools.browser import BasePage + + +__all__ = ['AccountsList'] + + +class AccountsList(BasePage): + def on_loaded(self): + pass + + def get_list(self): + l = [] + for td in self.document.xpath('.//td[@nowrap="nowrap"]'): + account = Account() + link = td.xpath('.//a')[0] + account.id = re.search('\d', link.attrib['href']).group(0) + account.label = link.text + urltofind = './/a[@href="' + link.attrib['href'] + '"]' + linkbis = self.document.xpath(urltofind).pop() + account.balance = float(linkbis.text.replace('.', '').replace(',','.')) + account.coming = NotAvailable + l.append(account) + + return l + diff --git a/weboob/backends/ing/pages/login.py b/weboob/backends/ing/pages/login.py new file mode 100644 index 00000000..399d08bb --- /dev/null +++ b/weboob/backends/ing/pages/login.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2009-2011 Romain Bignon, Florent Fourcot +# +# 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 re +from weboob.tools.mech import ClientForm +import urllib +from logging import error + +from weboob.tools.browser import BasePage, BrowserUnavailable +from weboob.tools.captcha.virtkeyboard import VirtKeyboard,VirtKeyboardError +import tempfile + +__all__ = ['LoginPage', 'LoginPage2', 'ConfirmPage', 'ChangePasswordPage'] + +class INGVirtKeyboard(VirtKeyboard): + symbols={'0':'327208d491507341908cf6920f26b586', + '1':'615ff37b15645da106cebc4605b399de', + '2':'fb04e648c93620f8b187981f9742b57e', + '3':'b786d471a70de83657d57bdedb6a2f38', + '4':'41b5501219e8d8f6d3b0baef3352ce88', + '5':'c72b372fb035160f2ff8dae59cd7e174', + '6':'392fa79e9a1749f5c8c0170f6a8ec68b', + '7':'fb495b5cf7f46201af0b4977899b56d4', + '8':'e8fea1e1aa86f8fca7f771db9a1dca4d', + '9':'82e63914f2e52ec04c11cfc6fecf7e08' + } + color=64 + + + def __init__(self,basepage): + img=basepage.document.find("//img[@id='mrc:j_id86']") + if img is None: + return False + url=img.attrib.get("src") + coords={} + coords["11"] = (5, 5, 33, 33) + coords["21"] = (45, 5, 73, 33) + coords["31"] = (85, 5, 113, 33) + coords["41"] = (125, 5, 153, 33) + coords["51"] = (165, 5, 193, 33) + coords["12"] = (5, 45, 33, 73) + coords["22"] = (45, 45, 73, 73) + coords["32"] = (85, 45, 113, 73) + coords["42"] = (125, 45, 153, 73) + coords["52"] = (165, 45, 193, 73) + + VirtKeyboard.__init__(self, basepage.browser.openurl(url), coords, self.color) + + if basepage.browser.responses_dirname is None: + basepage.browser.responses_dirname = \ + tempfile.mkdtemp(prefix='weboob_session_') + self.check_symbols(self.symbols,basepage.browser.responses_dirname) + + def get_string_code(self,string): + code='' + first = True + for c in string: + if not first: + code+="," + else : + first = False + codesymbol = self.get_symbol_code(self.symbols[c]) + x = (self.coords[codesymbol][0] + self.coords[codesymbol][2]) / 2 # In the middle + y = (self.coords[codesymbol][1] + self.coords[codesymbol][3]) / 2 + code+=str(x) + code+="," + code+=str(y) + return code + + + +class LoginPage(BasePage): + def on_loaded(self): + pass + + def prelogin(self, login, birthday): + # First step : login and birthday + self.browser.select_form('zone1Form') + self.browser.set_all_readonly(False) + self.browser['zone1Form:numClient'] = login + self.browser['zone1Form:dateDay'] = birthday[0:2] + self.browser['zone1Form:dateMonth'] = birthday[2:4] + self.browser['zone1Form:dateYear'] = birthday[4:9] + self.browser['zone1Form:radioSaveClientNumber'] = False + self.browser.submit(nologin=True) + +class LoginPage2(BasePage): + def on_loaded(self): + pass + + def login(self, password): + # 2) And now, the virtual Keyboard + try: + vk=INGVirtKeyboard(self) + except VirtKeyboardError,err: + error("Error: %s"%err) + return False + realpasswd = "" + span = self.document.find('//span[@id="digitpaddisplayLogin"]') + i = 0 + for font in span.getiterator('font'): + if font.attrib.get('class') == "vide": + realpasswd += password[i] + i+=1 + self.browser.logger.debug('We are looking for : ' + realpasswd) + self.browser.select_form('mrc') + self.browser.set_all_readonly(False) + self.browser.logger.debug("Coordonates: "+ vk.get_string_code(realpasswd)) + self.browser.controls.append(ClientForm.TextControl('text', 'mrc:mrg', {'value': ''})) + self.browser.controls.append(ClientForm.TextControl('text', 'AJAXREQUEST', {'value': ''})) + self.browser['AJAXREQUEST']='_viewRoot' + self.browser['mrc:mrldisplayLogin'] = vk.get_string_code(realpasswd) + self.browser['mrc:mrg'] = 'mrc:mrg' + self.browser.submit(nologin=True) + + +class ConfirmPage(BasePage): + def get_error(self): + for td in self.document.xpath('//td[@class="hdvon1"]'): + if td.text: + return td.text.strip() + return None + + def get_relocate_url(self): + script = self.document.xpath('//script')[0] + m = re.match('document.location.replace\("(.*)"\)', script.text[script.text.find('document.location.replace'):]) + if m: + return m.group(1) + +class MessagePage(BasePage): + def on_loaded(self): + pass + +class ChangePasswordPage(BasePage): + def on_loaded(self): + pass + diff --git a/weboob/backends/ing/test.py b/weboob/backends/ing/test.py new file mode 100644 index 00000000..2ab2aa54 --- /dev/null +++ b/weboob/backends/ing/test.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2011 Romain Bignon, Florent Fourcot +# +# 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 weboob.tools.test import BackendTest + +class INGTest(BackendTest): + BACKEND = 'ing' + + def test_ing(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))