Skip to content
login.py 8.69 KiB
Newer Older
# -*- coding: utf-8 -*-

Romain Bignon's avatar
Romain Bignon committed
# Copyright(C) 2010-2011 Nicolas Duhamel
# This file is part of a weboob module.
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
Romain Bignon's avatar
Romain Bignon committed
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Romain Bignon's avatar
Romain Bignon committed
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
ntome's avatar
ntome committed
# flake8: compatible

ntome's avatar
ntome committed
from __future__ import unicode_literals, division
from weboob.exceptions import BrowserUnavailable, BrowserIncorrectPassword, NoAccountsException, ActionNeeded
from weboob.browser.pages import LoggedPage
Maxime Gasselin's avatar
Maxime Gasselin committed
from weboob.browser.filters.html import Link
from weboob.browser.filters.standard import CleanText, Regexp
from weboob.tools.captcha.virtkeyboard import VirtKeyboard
from .base import MyHTMLPage

class UnavailablePage(MyHTMLPage):
    def on_load(self):
Romain Bignon's avatar
Romain Bignon committed
        raise BrowserUnavailable()

class Keyboard(VirtKeyboard):
ntome's avatar
ntome committed
    symbols = {
        '0': ('daa52d75287bea58f505823ef6c8b96c', 'e5d6dc589f00e7ec3ba0e45a1fee1220'),
        '1': ('f5da96c2592803a8cdc5a928a2e4a3b0', '9732b03ce3bdae7a44df9a7b4e092a07'),
        '2': ('9ff78367d5cb89cacae475368a11e3af', '3b4387242c42bd39dbc263eac0718a49'),
        '3': ('908a0a42a424b95d4d885ce91bc3d920', '14fa1e5083fa0a0c0cded72a2139921b'),
        '4': ('3fc069f33b801b3d0cdce6655a65c0ac', '72792dbef888f1176f1974c86a94a084'),
        '5': ('58a2afebf1551d45ccad79fad1600fc3', '1e9ddf1e5a12ebaeaea26cca6f752a87'),
        '6': ('7fedfd9e57007f2985c3a1f44fb38ea1', '4e3a917198e89a2c16b9379f9a33f2a1'),
        '7': ('389b8ef432ae996ac0141a2fcc7b540f', '33b90787a8014667b2acd5493e5641d2'),
        '8': ('bf357ff09cc29ea544991642cd97d453', 'e4b30e90bbc2c26c2893120c8adc9d64'),
        '9': ('b744015eb89c1b950e13a81364112cd6', 'b400c35438960de101233b9c846cd5eb'),
ntome's avatar
ntome committed
    }

    color = (0xff, 0xff, 0xff)

    def __init__(self, page):
ntome's avatar
ntome committed
        img_url = (
            Regexp(CleanText('//style'), r'background:url\((.*?)\)', default=None)(page.doc)
            or Regexp(CleanText('//script'), r'IMG_ALL = "(.*?)"', default=None)(page.doc)
        )

        size = 252
        if not img_url:
            img_url = page.doc.xpath('//img[@id="imageCVS"]')[0].attrib['src']
            size = 146
ntome's avatar
ntome committed

ntome's avatar
ntome committed
        x, y, width, height = (0, 0, size // 4, size // 4)
        for i, _ in enumerate(page.doc.xpath('//div[@id="imageclavier"]//button')):
            code = '%02d' % i
ntome's avatar
ntome committed
            coords[code] = (x + 4, y + 4, x + width - 8, y + height - 8)
            if (x + width + 1) >= size:
                y += height + 1
                x += width + 1
        data = page.browser.open(img_url).content
        VirtKeyboard.__init__(self, BytesIO(data), coords, self.color)

        self.check_symbols(self.symbols, page.browser.responses_dirname)

ntome's avatar
ntome committed
    def get_symbol_code(self, md5sum):
        code = VirtKeyboard.get_symbol_code(self, md5sum)
        return '%02d' % int(code.split('_')[-1])

ntome's avatar
ntome committed
    def get_string_code(self, string):
        code = ''
        for c in string:
            code += self.get_symbol_code(self.symbols[c])
        return code

    def get_symbol_coords(self, coords):
        x1, y1, x2, y2 = coords
ntome's avatar
ntome committed
        return VirtKeyboard.get_symbol_coords(self, (x1 + 3, y1 + 3, x2 - 3, y2 - 3))
class LoginPage(MyHTMLPage):
    def login(self, login, pwd):
        vk = Keyboard(self)
        form = self.get_form(name='formAccesCompte')
        form['password'] = vk.get_string_code(pwd)
        form['username'] = login
        form.submit()
Romain Bignon's avatar
Romain Bignon committed

class repositionnerCheminCourant(LoggedPage, MyHTMLPage):
    def on_load(self):
        super(repositionnerCheminCourant, self).on_load()
        response = self.browser.open("https://voscomptesenligne.labanquepostale.fr/voscomptes/canalXHTML/securite/authentification/initialiser-identif.ea")
        if isinstance(response.page, Initident):
            response.page.on_load()
        if "vous ne disposez pas" in response.text:
            raise BrowserIncorrectPassword("No online banking service for these ids")
        if 'Nous vous invitons à renouveler votre opération ultérieurement' in response.text:
            raise BrowserUnavailable()
class PersonalLoanRoutagePage(LoggedPage, MyHTMLPage):
    def form_submit(self):
        form = self.get_form()
        form.submit()


class Initident(LoggedPage, MyHTMLPage):
    def on_load(self):
        self.browser.open("https://voscomptesenligne.labanquepostale.fr/voscomptes/canalXHTML/securite/authentification/verifierMotDePasse-identif.ea")
ntome's avatar
ntome committed
        if self.doc.xpath("""//span[contains(text(), "L'identifiant utilisé est celui d'une Entreprise ou d'une Association")]"""):
            raise BrowserIncorrectPassword("L'identifiant utilisé est celui d'une Entreprise ou d'une Association")
        no_accounts = CleanText('//div[@class="textFCK"]')(self.doc)
Baptiste Delpey's avatar
Baptiste Delpey committed
        if no_accounts:
            raise NoAccountsException(no_accounts)
        MyHTMLPage.on_load(self)
class CheckPassword(LoggedPage, MyHTMLPage):
    def on_load(self):
        MyHTMLPage.on_load(self)
        self.browser.location("https://voscomptesenligne.labanquepostale.fr/voscomptes/canalXHTML/comptesCommun/synthese_assurancesEtComptes/init-synthese.ea")
class BadLoginPage(MyHTMLPage):
class AccountDesactivate(LoggedPage, MyHTMLPage):


class TwoFAPage(MyHTMLPage):
    def on_load(self):
        # For pro browser this page can provoke a disconnection
        # We have to do login again without 2fa
        deconnexion = self.doc.xpath('//iframe[contains(@id, "deconnexion")] | //p[@class="txt" and contains(text(), "Session expir")]')
        if deconnexion:
            self.browser.login_without_2fa()

    def get_auth_method(self):
        status_message = CleanText('//div[@class="textFCK"]')(self.doc)
        if re.search(
                'Une authentification forte via Certicode Plus vous'
                + '|vous rendre sur l’application mobile La Banque Postale',
                status_message
        ):
            return 'cer+'
        elif re.search(
                'authentification forte via Certicode vous'
                + '|code de sécurité que vous recevrez par SMS',
                status_message
        ):
            return 'cer'
        elif re.search(
                'avez pas de solution d’authentification forte'
                + "|avez pas encore activé votre service gratuit d'authentification forte",
                status_message
        ):
Maxime Gasselin's avatar
Maxime Gasselin committed
            return 'no2fa'
ntome's avatar
ntome committed
        elif (
            'Nous rencontrons un problème pour valider votre opération. Veuillez reessayer plus tard'
            in status_message
        ):
            raise BrowserUnavailable(status_message)
ntome's avatar
ntome committed
        elif (
            'votre Espace Client Internet requiert une authentification forte tous les 90 jours'
            in status_message
        ):
            # Only first sentence explains 'why', the rest is 'how'
            short_message = CleanText('(//div[@class="textFCK"])[1]//p[1]')(self.doc)
            url = self.get_skip_url()
            if not url:
                raise ActionNeeded(
                    "Une authentification forte est requise sur votre espace client : %s" % short_message
                )
            else:
                # raise an error to avoid silencing other no2fa cases
                raise AssertionError("No 2FA case to skip, or new 2FA case to trigger")
        raise AssertionError('Unhandled login message: "%s"' % status_message)
Maxime Gasselin's avatar
Maxime Gasselin committed
    def get_skip_url(self):
        return Link('//div[@class="certicode_footer"]/a')(self.doc)


class Validated2FAPage(MyHTMLPage):
    pass


class SmsPage(MyHTMLPage):
    def check_if_is_blocked(self):
        error_message = CleanText('//div[@class="textFCK"]')(self.doc)
        if "l'accès à votre Espace client est bloqué" in error_message:
            raise ActionNeeded(error_message)

    def get_sms_form(self):
        return self.get_form()

    def is_sms_wrong(self):
ntome's avatar
ntome committed
        return (
            'Le code de sécurité que vous avez saisi est erroné'
            in CleanText('//div[@id="DSP2_Certicode_AF_ErreurCode1"]//div[@class="textFCK"]')(self.doc)
        )


class DecoupledPage(MyHTMLPage):
    def get_decoupled_message(self):
        return CleanText('//div[@class="textFCK"]/p[contains(text(), "Validez votre authentification")]')(self.doc)