Skip to content
browser.py 22 KiB
Newer Older
# -*- coding: utf-8 -*-

Florent's avatar
Florent committed
# Copyright(C) 2009-2014  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 <http://www.gnu.org/licenses/>.
Florian Duguet's avatar
Florian Duguet committed
from __future__ import unicode_literals
Florent's avatar
Florent committed
import hashlib
Romain Bignon's avatar
Romain Bignon committed
import time
import json
from weboob.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser.exceptions import ServerError
Baptiste Delpey's avatar
Baptiste Delpey committed
from weboob.capabilities.bank import Account, AccountNotFound
from weboob.capabilities.base import find_object, NotAvailable
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from .pages import (
    AccountsList, LoginPage, NetissimaPage, TitrePage,
    TitreHistory, TransferPage, BillsPage, StopPage, TitreDetails,
    TitreValuePage, ASVHistory, ASVInvest, DetailFondsPage, IbanPage,
    ActionNeededPage, ReturnPage, ProfilePage, LoanTokenPage, LoanDetailPage,
)
Florent's avatar
Florent committed
__all__ = ['IngBrowser']


    def wrapper(*args, **kwargs):
        browser = args[0]
Maxime Pommier's avatar
Maxime Pommier committed
        if browser.url and browser.url.startswith('https://bourse.ingdirect.fr/'):
ntome's avatar
ntome committed
            for i in range(3):
Maxime Pommier's avatar
Maxime Pommier committed
                    browser.location('https://bourse.ingdirect.fr/priv/redirectIng.php?pageIng=COMPTE')
                except ServerError:
                    pass
                else:
                    break
        elif browser.url and browser.url.startswith('https://ingdirectvie.ing.fr/'):
            browser.lifeback.go()
            browser.where = 'start'

        elif browser.url and browser.url.startswith('https://subscribe.ing.fr/'):
            browser.return_from_loan_site()

        return f(*args, **kwargs)
Florent's avatar
Florent committed
class IngBrowser(LoginBrowser):
    BASEURL = 'https://secure.ing.fr'
    TIMEOUT = 60.0
    DEFERRED_CB = 'deferred'
    IMMEDIATE_CB = 'immediate'
    # avoid relogin every time
    lifeback = URL(r'https://ingdirectvie.ing.fr/b2b2c/entreesite/EntAccExit', ReturnPage)
Florent's avatar
Florent committed
    # Login and error
Florian Duguet's avatar
Florian Duguet committed
    loginpage = URL(r'/public/displayLogin.jsf.*', LoginPage)
    errorpage = URL(r'.*displayCoordonneesCommand.*', StopPage)
    actioneeded = URL(r'/general\?command=displayTRAlertMessage',
                      r'/protected/pages/common/eco1/moveMoneyForbidden.jsf', ActionNeededPage)
Florent's avatar
Florent committed

    # CapBank
Florian Duguet's avatar
Florian Duguet committed
    accountspage = URL(r'/protected/pages/index.jsf',
                       r'/protected/pages/asv/contract/(?P<asvpage>.*).jsf', AccountsList)
    titredetails = URL(r'/general\?command=display.*', TitreDetails)
    ibanpage = URL(r'/protected/pages/common/rib/initialRib.jsf', IbanPage)
    loantokenpage = URL(r'general\?command=goToConsumerLoanCommand&redirectUrl=account-details', LoanTokenPage)
    loandetailpage = URL(r'https://subscribe.ing.fr/consumerloan/consumerloan-v1/consumer/details', LoanDetailPage)
    # CapBank-Market
Florian Duguet's avatar
Florian Duguet committed
    netissima = URL(r'/data/asv/fiches-fonds/fonds-netissima.html', NetissimaPage)
    starttitre = URL(r'/general\?command=goToAccount&zone=COMPTE', TitrePage)
    titrepage = URL(r'https://bourse.ingdirect.fr/priv/portefeuille-TR.php', TitrePage)
    titrehistory = URL(r'https://bourse.ingdirect.fr/priv/compte.php\?ong=3', TitreHistory)
    titrerealtime = URL(r'https://bourse.ingdirect.fr/streaming/compteTempsReelCK.php', TitrePage)
    titrevalue = URL(r'https://bourse.ingdirect.fr/priv/fiche-valeur.php\?val=(?P<val>.*)&pl=(?P<pl>.*)&popup=1', TitreValuePage)
    asv_history = URL(r'https://ingdirectvie.ing.fr/b2b2c/epargne/CoeLisMvt',
                      r'https://ingdirectvie.ing.fr/b2b2c/epargne/CoeDetMvt', ASVHistory)
    asv_invest = URL(r'https://ingdirectvie.ing.fr/b2b2c/epargne/CoeDetCon', ASVInvest)
    detailfonds = URL(r'https://ingdirectvie.ing.fr/b2b2c/fonds/PerDesFac\?codeFonds=(.*)', DetailFondsPage)
    # CapDocument
Florian Duguet's avatar
Florian Duguet committed
    billpage = URL(r'/protected/pages/common/estatement/eStatement.jsf', BillsPage)
    # CapProfile
Florian Duguet's avatar
Florian Duguet committed
    profile = URL(r'/protected/pages/common/profil/(?P<page>\w+).jsf', ProfilePage)
Florian Duguet's avatar
Florian Duguet committed
    transfer = URL(r'/protected/pages/common/virement/index.jsf', TransferPage)
Baptiste Delpey's avatar
Baptiste Delpey committed

Florent's avatar
Florent committed
    __states__ = ['where']

    def __init__(self, *args, **kwargs):
        self.birthday = kwargs.pop('birthday')
        self.where = None
Florent's avatar
Florent committed
        LoginBrowser.__init__(self, *args, **kwargs)
        self.cache = {}
        self.cache["investments_data"] = {}
        self.only_deferred_cards = {}
Célande Adrien's avatar
Célande Adrien committed
        # will contain a list of the spaces
        # (the parameters needed to check and change them)
        # if not, it is an empty list
        self.multispace = None
        self.current_space = None

        # ing website is stateful, so we need to store the current subscription when download document to be sure
        # we download file for the right subscription
        self.current_subscription = None

Florent's avatar
Florent committed
    def do_login(self):
        assert self.password.isdigit()
        assert self.birthday.isdigit()

        self.do_logout()
        self.loginpage.go()

        self.page.prelogin(self.username, self.birthday)
Florent's avatar
Florent committed
        self.page.login(self.password)
            raise BrowserIncorrectPassword()
        if self.errorpage.is_here():
            raise BrowserIncorrectPassword('Please login on website to fill the form and retry')
Baptiste Delpey's avatar
Baptiste Delpey committed
        self.page.check_for_action_needed()
Célande Adrien's avatar
Célande Adrien committed
    def set_multispace(self):
Florian Duguet's avatar
Florian Duguet committed
        self.where = 'start'
Célande Adrien's avatar
Célande Adrien committed
        self.page.load_space_page()

        self.multispace = self.page.get_multispace()

        # setting the current_space depending on the current state of the page
        for space in self.multispace:
            if space['is_active']:
                self.current_space = space
                break

Célande Adrien's avatar
Célande Adrien committed
    def change_space(self, space):
        if self.multispace and not self.is_same_space(space, self.current_space):
            self.accountspage.go()
Florian Duguet's avatar
Florian Duguet committed
            self.where = 'start'
Célande Adrien's avatar
Célande Adrien committed
            self.page.load_space_page()

            self.page.change_space(space)
            self.current_space = space

    def is_same_space(self, a, b):
        return (
            a['name'] == b['name']
            and a['id'] == b['id']
            and a['form'] == b['form']
        )

    @start_with_main_site
    def get_market_balance(self, account):
Florian Duguet's avatar
Florian Duguet committed
        if self.where != 'start':
            self.accountspage.go()
Florian Duguet's avatar
Florian Duguet committed
            self.where = 'start'
Célande Adrien's avatar
Célande Adrien committed
        self.change_space(account._space)

        data = self.get_investments_data(account)
        for i in range(5):
            if i > 0:
Florian Duguet's avatar
Florian Duguet committed
                self.logger.debug("Can't get market balance, retrying in %s seconds...", (2**i))
                time.sleep(2**i)
            if self.accountspage.go(data=data).has_link():
                break

Florian Duguet's avatar
Florian Duguet committed
        self.where = 'titre'
        self.titrepage.go()
        self.titrerealtime.go()
        account.balance = self.page.get_balance() or account.balance
        self.cache["investments_data"][account.id] = self.page.doc or None
Florent's avatar
Florent committed
    @need_login
Célande Adrien's avatar
Célande Adrien committed
        if account.type in [Account.TYPE_CHECKING, Account.TYPE_SAVINGS]:
            self.go_account_page(account)
            account.iban = self.ibanpage.go().get_iban()
Célande Adrien's avatar
Célande Adrien committed
        if account.type in (Account.TYPE_MARKET, Account.TYPE_PEA):
            self.get_market_balance(account)
    def get_accounts_on_space(self, space, fill_account=True):
Célande Adrien's avatar
Célande Adrien committed
        accounts_list = []
Célande Adrien's avatar
Célande Adrien committed
        self.change_space(space)
Célande Adrien's avatar
Célande Adrien committed
        for acc in self.page.get_list():
            acc._space = space
            if fill_account:
                try:
                    self.fill_account(acc)
                except ServerError:
                    pass
Célande Adrien's avatar
Célande Adrien committed
            assert not find_object(accounts_list, id=acc.id), 'There is a duplicate account.'
            accounts_list.append(acc)
            yield acc

        for loan in self.iter_detailed_loans():
            loan._space = space
            assert not find_object(accounts_list, id=loan.id), 'There is a duplicate loan.'
            accounts_list.append(loan)
            yield loan
    @need_login
    @start_with_main_site
    def get_accounts_list(self, space=None, fill_account=True):
Célande Adrien's avatar
Célande Adrien committed
        self.accountspage.go()
Florian Duguet's avatar
Florian Duguet committed
        self.where = 'start'
Célande Adrien's avatar
Célande Adrien committed
        self.set_multispace()
Célande Adrien's avatar
Célande Adrien committed
        if space:
            for acc in self.get_accounts_on_space(space, fill_account=fill_account):
Célande Adrien's avatar
Célande Adrien committed
                yield acc
Célande Adrien's avatar
Célande Adrien committed
        elif self.multispace:
            for space in self.multispace:
                for acc in self.get_accounts_on_space(space, fill_account=fill_account):
Célande Adrien's avatar
Célande Adrien committed
                    yield acc
        else:
            for acc in self.page.get_list():
                acc._space = None
                if fill_account:
                    try:
                        self.fill_account(acc)
                    except ServerError:
                        pass
                yield acc

            for loan in self.iter_detailed_loans():
Célande Adrien's avatar
Célande Adrien committed
                loan._space = None
    @need_login
    @start_with_main_site
    def iter_detailed_loans(self):
        self.accountspage.go()
Florian Duguet's avatar
Florian Duguet committed
        self.where = 'start'

        for loan in self.page.get_detailed_loans():
            data = {'AJAXREQUEST': '_viewRoot',
                    'index': 'index',
                    'autoScroll': '',
                    'javax.faces.ViewState': loan._jid,
                    'accountNumber': loan._id,
                    'index:goToConsumerLoanUI': 'index:goToConsumerLoanUI'}

            self.accountspage.go(data=data)
            self.loantokenpage.go(data=data)
            try:
                self.loandetailpage.go()

            except ServerError as exception:
                json_error = json.loads(exception.response.text)
                if json_error['error']['code'] == "INTERNAL_ERROR":
                    raise BrowserUnavailable(json_error['error']['message'])
                raise
            else:
                self.page.getdetails(loan)
            yield loan
            self.return_from_loan_site()

    def return_from_loan_site(self):
        data = {'context': '{"originatingApplication":"SECUREUI"}',
                    'targetSystem': 'INTERNET'}
        self.location('https://subscribe.ing.fr/consumerloan/consumerloan-v1/sso/exit', data=data)
        self.location('https://secure.ing.fr/', data={'token': self.response.text})
Célande Adrien's avatar
Célande Adrien committed
    def get_account(self, _id, space=None):
        return find_object(self.get_accounts_list(fill_account=False, space=space), id=_id, error=AccountNotFound)
Célande Adrien's avatar
Célande Adrien committed

    def go_account_page(self, account):
Florent's avatar
Florent committed
        data = {"AJAX:EVENTS_COUNT": 1,
                "AJAXREQUEST": "_viewRoot",
                "ajaxSingle": "index:setAccount",
                "autoScroll": "",
                "index": "index",
                "index:setAccount": "index:setAccount",
                "javax.faces.ViewState": account._jid,
                "cptnbr": account._id
                }
        self.accountspage.go(data=data)
        card_list = self.page.get_card_list()
        if card_list:
            self.only_deferred_cards[account._id] = all(
                [card['kind'] == self.DEFERRED_CB for card in card_list]
Florian Duguet's avatar
Florian Duguet committed
        self.where = 'history'

    @need_login
    def get_coming(self, account):
Célande Adrien's avatar
Célande Adrien committed
        self.change_space(account._space)

        if account.type != Account.TYPE_CHECKING and\
                account.type != Account.TYPE_SAVINGS:
            raise NotImplementedError()
Célande Adrien's avatar
Célande Adrien committed
        account = self.get_account(account.id, space=account._space)
        self.go_account_page(account)
Florent's avatar
Florent committed
        jid = self.page.get_history_jid()
        if jid is None:
            self.logger.info('There is no history for this account')
            return
        return self.page.get_coming()

    @need_login
Florent's avatar
Florent committed
    def get_history(self, account):
Célande Adrien's avatar
Célande Adrien committed
        self.change_space(account._space)

        if account.type in (Account.TYPE_MARKET, Account.TYPE_PEA, Account.TYPE_LIFE_INSURANCE):
            for result in self.get_history_titre(account):
                yield result
Florent's avatar
Florent committed
            return
Florent's avatar
Florent committed
        elif account.type != Account.TYPE_CHECKING and\
                account.type != Account.TYPE_SAVINGS:
            raise NotImplementedError()
Célande Adrien's avatar
Célande Adrien committed
        account = self.get_account(account.id, space=account._space)
        self.go_account_page(account)
Florent's avatar
Florent committed
        jid = self.page.get_history_jid()
        only_deferred_cb = self.only_deferred_cards.get(account._id)

        if jid is None:
            self.logger.info('There is no history for this account')
            return

Florent's avatar
Florent committed
        if account.type == Account.TYPE_CHECKING:
            history_function = AccountsList.get_transactions_cc
            index = -1  # disable the index. It works without it on CC
Florent's avatar
Florent committed
        else:
            history_function = AccountsList.get_transactions_others
            index = 0
ntome's avatar
ntome committed
        hashlist = set()
            i = index
            for transaction in history_function(self.page, index=index):
                if only_deferred_cb and transaction.type == FrenchTransaction.TYPE_CARD:
                    transaction.type = FrenchTransaction.TYPE_DEFERRED_CARD

Florent's avatar
Florent committed
                transaction.id = hashlib.md5(transaction._hash).hexdigest()
Florent's avatar
Florent committed
                while transaction.id in hashlist:
ntome's avatar
ntome committed
                    transaction.id = hashlib.md5((transaction.id + "1").encode('ascii')).hexdigest()
                hashlist.add(transaction.id)
                i += 1
                yield transaction
            # if there is no more transactions, it is useless to continue
            if self.page.islast() or i == index:
                return
            if index >= 0:
                index = i
Florent's avatar
Florent committed
            data = {"AJAX:EVENTS_COUNT": 1,
                    "AJAXREQUEST": "_viewRoot",
                    "autoScroll": "",
                    "index": "index",
                    "index:%s:moreTransactions" % jid: "index:%s:moreTransactions" % jid,
                    "javax.faces.ViewState": account._jid
                    }
            self.accountspage.go(data=data)
Florent's avatar
Florent committed
    @need_login
Baptiste Delpey's avatar
Baptiste Delpey committed
    def iter_recipients(self, account):
Célande Adrien's avatar
Célande Adrien committed
        self.change_space(account._space)

Baptiste Delpey's avatar
Baptiste Delpey committed
        self.transfer.go()
        if not self.page.able_to_transfer(account):
            return iter([])
Célande Adrien's avatar
Célande Adrien committed

Baptiste Delpey's avatar
Baptiste Delpey committed
        self.page.go_to_recipient_selection(account)
        return self.page.get_recipients(origin=account)
Florent's avatar
Florent committed

Baptiste Delpey's avatar
Baptiste Delpey committed
    @need_login
Baptiste Delpey's avatar
Baptiste Delpey committed
    def init_transfer(self, account, recipient, transfer):
Célande Adrien's avatar
Célande Adrien committed
        self.change_space(account._space)

Baptiste Delpey's avatar
Baptiste Delpey committed
        self.transfer.go()
        self.page.do_transfer(account, recipient, transfer)
        return self.page.recap(account, recipient, transfer)

    @need_login
    @start_with_main_site
    def execute_transfer(self, transfer):
        self.page.confirm(self.password)
        return transfer
Baptiste Delpey's avatar
Baptiste Delpey committed
    def go_on_asv_detail(self, account, link):
        try:
            if self.page.asv_is_other:
                jid = self.page.get_asv_jid()
Florian Duguet's avatar
Florian Duguet committed
                data = {'index': "index", 'javax.faces.ViewState': jid, 'index:j_idcl': 'index:asvInclude:goToAsvPartner'}
Florian Duguet's avatar
Florian Duguet committed
                self.accountspage.go(asvpage='manageASVContract')
            self.page.submit()
Baptiste Delpey's avatar
Baptiste Delpey committed

    def get_investments_data(self, account):
        return {"AJAX:EVENTS_COUNT": 1,
                "AJAXREQUEST": "_viewRoot",
                "ajaxSingle": "index:setAccount",
                "autoScroll": "",
                "index": "index",
                "index:setAccount": "index:setAccount",
                "javax.faces.ViewState": account._jid,
                "cptnbr": account._id
                }
Célande Adrien's avatar
Célande Adrien committed
        account = self.get_account(account.id, space=account._space)

Romain Bignon's avatar
Romain Bignon committed
        # On ASV pages, data maybe not available.
        for i in range(5):
            if i > 0:
                self.logger.debug('Investments list empty, retrying in %s seconds...', (2**i))
                time.sleep(2**i)

                if i > 1:
                    self.do_logout()
                    self.do_login()
Célande Adrien's avatar
Célande Adrien committed
                    account = self.get_account(account.id, space=account._space)
                    data['cptnbr'] = account._id
                    data['javax.faces.ViewState'] = account._jid
Romain Bignon's avatar
Romain Bignon committed
            self.accountspage.go(data=data)

            if not self.page.has_error():
                break

Florian Duguet's avatar
Florian Duguet committed
            self.logger.warning('Unable to get investments list...')
Romain Bignon's avatar
Romain Bignon committed
        if self.page.is_asv:
            return
        self.starttitre.go()
Florian Duguet's avatar
Florian Duguet committed
        self.where = 'titre'
        self.titrepage.go()
Florent's avatar
Florent committed
    def get_investments(self, account):
        if account.type not in (Account.TYPE_MARKET, Account.TYPE_PEA, Account.TYPE_LIFE_INSURANCE):
Florent's avatar
Florent committed
            raise NotImplementedError()
Florent's avatar
Florent committed
        self.go_investments(account)

Florian Duguet's avatar
Florian Duguet committed
        if self.where == 'titre':
            if self.cache['investments_data'].get(account.id) is None:
            for inv in self.page.iter_investments(account):
                yield inv
        elif self.page.asv_has_detail or account._jid:
            self.accountspage.stay_or_go()
            shares = {}
            for asv_investments in self.page.iter_asv_investments():
                shares[asv_investments.label] = asv_investments.portfolio_share
            if self.go_on_asv_detail(account, '/b2b2c/epargne/CoeDetCon') is not False:
Florian Duguet's avatar
Florian Duguet committed
                self.where = 'asv'
                for inv in self.page.iter_investments():
                    inv.portfolio_share = shares[inv.label]
Florent's avatar
Florent committed
    def get_history_titre(self, account):
        self.go_investments(account)
Florian Duguet's avatar
Florian Duguet committed
        if self.where == 'titre':
Romain Bignon's avatar
Romain Bignon committed
            self.titrehistory.go()
        elif self.page.asv_has_detail or account._jid:
            if self.go_on_asv_detail(account, '/b2b2c/epargne/CoeLisMvt') is False:
                return iter([])
Baptiste Delpey's avatar
Baptiste Delpey committed
        transactions = list()
        # In order to reduce the amount of requests just to get ISIN codes, we fill
        # a dictionary with already visited investment pages and store their ISIN codes:
        isin_codes = {}
Baptiste Delpey's avatar
Baptiste Delpey committed
        for tr in self.page.iter_history():
            transactions.append(tr)
        if self.asv_history.is_here():
            for tr in transactions:
                page = tr._detail.result().page if tr._detail else None
                if page and 'numMvt' in page.url:
                    investment_list = list()
                    for inv in page.get_investments():
                        if inv._code_url in isin_codes:
                            inv.code = isin_codes.get(inv._code_url)
                        else:
                            # Fonds en euros (Eurossima) have no _code_url so we must set their code to None
                            if inv._code_url:
                                self.location(inv._code_url)
                                if self.detailfonds.is_here():
                                    inv.code = self.page.get_isin_code()
                                    isin_codes[inv._code_url] = inv.code
                                else:
                                    # In case the page is not available or blocked:
                                    inv.code = NotAvailable
                            else:
                                inv.code = None
                        investment_list.append(inv)
                    tr.investments = investment_list
Baptiste Delpey's avatar
Baptiste Delpey committed
        return iter(transactions)
    ############# CapDocument #############
Baptiste Delpey's avatar
Baptiste Delpey committed
    @need_login
    def get_subscriptions(self):
        self.billpage.go()
        if self.loginpage.is_here():
            self.do_login()
            subscriptions = list(self.billpage.go().iter_subscriptions())
            subscriptions = list(self.page.iter_subscriptions())

        self.cache['subscriptions'] = {}
        for sub in subscriptions:
            self.cache['subscriptions'][sub.id] = sub

        return subscriptions

    def _go_to_subscription(self, subscription):
        # ing website is not stateless, make sure we are on the correct documents page before doing anything else
        if self.current_subscription and self.current_subscription.id == subscription.id:
            return
Florent's avatar
Florent committed
        self.billpage.go()
        data = {
            "AJAXREQUEST": "_viewRoot",
            "accountsel_form": "accountsel_form",
            subscription._formid: subscription._formid,
            "autoScroll": "",
            "javax.faces.ViewState": subscription._javax,
            "transfer_issuer_radio": subscription.id
        }
        self.billpage.go(data=data)
        self.current_subscription = subscription

    @need_login
    def get_documents(self, subscription):
        self._go_to_subscription(subscription)
        return self.page.iter_documents(subid=subscription.id)
    def download_document(self, bill):
        subid = bill.id.split('-')[0]
        # make sure we are on the right page to not download a document from another subscription
        self._go_to_subscription(self.cache['subscriptions'][subid])
        self.page.go_to_year(bill._year)
        return self.page.download_document(bill)
    ############# CapProfile #############
    @start_with_main_site
    @need_login
    def get_profile(self):
        profile = self.profile.go(page='coordonnees').get_profile()
        self.profile.go(page='infosperso').update_profile(profile)
        return profile