# -*- coding: utf-8 -*-
#
# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
#
# "THE BEER-WARE LICENSE" (Revision 42):
# <trac@matt-good.net> wrote this file.  As long as you retain this notice you
# can do whatever you want with this stuff. If we meet some day, and you think
# this stuff is worth it, you can buy me a beer in return.   Matthew Good
#
# Author: Matthew Good <trac@matt-good.net>

from __future__ import generators

import random
import string

from trac import perm, util
from trac.core import *
from trac.config import IntOption
from trac.notification import NotificationSystem, NotifyEmail
from trac.web import auth
from trac.web.api import IAuthenticator
from trac.web.main import IRequestHandler
from trac.web.chrome import INavigationContributor, ITemplateProvider
from trac.util import Markup

from api import AccountManager

def _create_user(req, env, check_permissions=True):
    mgr = AccountManager(env)

    user = req.args.get('user')
    if not user:
        raise TracError(u'Le nom d\'utilisateur ne peut être vide.')

    if mgr.has_user(user):
        raise TracError(u'Un autre coompte avec ce nom existe déjà.')

    if check_permissions:
        # disallow registration of accounts which have existing permissions
        permission_system = perm.PermissionSystem(env)
        if permission_system.get_user_permissions(user) != \
           permission_system.get_user_permissions('authenticated'):
            raise TracError(u'Un autre coompte avec ce nom existe déjà.')

    password = req.args.get('password')
    if not password:
        raise TracError(u'Le mot de passe ne peut pas être vide.')

    if password != req.args.get('password_confirm'):
        raise TracError(u'Le mot de passe doit correspondre.')

    mgr.set_password(user, password)

    db = env.get_db_cnx()
    cursor = db.cursor()
    cursor.execute("SELECT count(*) FROM session "
                   "WHERE sid=%s AND authenticated=1",
                   (user,))
    exists, = cursor.fetchone()
    if not exists:
        cursor.execute("INSERT INTO session "
                       "(sid, authenticated, last_visit) "
                       "VALUES (%s, 1, 0)",
                       (user,))

    for key in ('name', 'email'):
        value = req.args.get(key)
        if not value:
            continue
        cursor.execute("UPDATE session_attribute SET value=%s "
                       "WHERE name=%s AND sid=%s AND authenticated=1",
                       (value, key, user))
        if not cursor.rowcount:
            cursor.execute("INSERT INTO session_attribute "
                           "(sid,authenticated,name,value) "
                           "VALUES (%s,1,%s,%s)",
                           (user, key, value))
    db.commit()


class PasswordResetNotification(NotifyEmail):
    template_name = 'reset_password_email.cs'
    _username = None

    def get_recipients(self, resid):
        return ([resid],[])

    def get_smtp_address(self, addr):
        """Overrides `get_smtp_address` in order to prevent CCing users
        other than the user whose password is being reset.
        """
        if addr == self._username:
            return NotifyEmail.get_smtp_address(self, addr)
        else:
            return None

    def notify(self, username, password):
        # save the username for use in `get_smtp_address`
        self._username = username
        self.hdf['account.username'] = username
        self.hdf['account.password'] = password
        self.hdf['login.link'] = self.env.abs_href.login()

        projname = self.config.get('project', 'name')
        subject = u'[%s] Ré-initialisation du mot de passe pour l\'utilisateur: %s' % (projname, username)

        NotifyEmail.notify(self, username, subject)


class AccountModule(Component):
    """Allows users to change their password, reset their password if they've
    forgotten it, or delete their account.  The settings for the AccountManager
    module must be set in trac.ini in order to use this.
    """

    implements(INavigationContributor, IRequestHandler, ITemplateProvider)

    _password_chars = string.ascii_letters + string.digits
    password_length = IntOption('account-manager', 'generated_password_length', 8,
                                u'Longueur des mots de passe aléatoirement générés,'
                                u' créés à l\'occasion de la ré-initialisation du '
                                u'mot de passe d\'un compte.')

    def __init__(self):
        self._write_check(log=True)

    def _write_check(self, log=False):
        writable = AccountManager(self.env).supports('set_password')
        if not writable and log:
            self.log.warn('AccountModule is disabled because the password '
                          'store does not support writing.')
        return writable

    #INavigationContributor methods
    def get_active_navigation_item(self, req):
        if req.path_info == '/account':
            return 'account'
        elif req.path_info == '/reset_password':
            return 'reset_password'

    def get_navigation_items(self, req):
        if not self._write_check():
            return
        if req.authname != 'anonymous':
            yield 'metanav', 'account', Markup(u'<a href="%s">Mon Compte</a>',
                                               (req.href.account()))
        elif self.reset_password_enabled and not LoginModule(self.env).enabled:
            yield 'metanav', 'reset_password', Markup(u'<a href="%s">Mot de passe oublié ?</a>',
                                                      (req.href.reset_password()))

    # IRequestHandler methods
    def match_request(self, req):
        return (req.path_info in ('/account', '/reset_password')
                and self._write_check(log=True))

    def process_request(self, req):
        if req.path_info == '/account':
            self._do_account(req)
            return 'account.cs', None
        elif req.path_info == '/reset_password':
            self._do_reset_password(req)
            return 'reset_password.cs', None

    def reset_password_enabled(self):
        return (self.env.is_component_enabled(AccountModule)
                and NotificationSystem(self.env).smtp_enabled
                and self._write_check())
    reset_password_enabled = property(reset_password_enabled)

    def _do_account(self, req):
        if req.authname == 'anonymous':
            req.redirect(self.env.href.wiki())
        action = req.args.get('action')
        delete_enabled = AccountManager(self.env).supports('delete_user')
        req.hdf['delete_enabled'] = delete_enabled
        if req.method == 'POST':
            if action == 'change_password':
                self._do_change_password(req)
            elif action == 'delete':
                self._do_delete(req)

    def _do_reset_password(self, req):
        if req.authname != 'anonymous':
            req.hdf['reset.logged_in'] = True
            req.hdf['account_href'] = req.href.account()
            return
        if req.method == 'POST':
            username = req.args.get('username')
            email = req.args.get('email')
            if not username:
                req.hdf['reset.error'] = u'Nom d\'utilisateur requis'
                return
            if not email:
                req.hdf['reset.error'] = u'Courriel requis'
                return

            notifier = PasswordResetNotification(self.env)

            if email != notifier.email_map.get(username):
                req.hdf['reset.error'] = u'Le nom d\'utilisateur et le courriel ' \
                                         u'ne correspondent pas à un compte connu.'
                return

            new_password = self._random_password()
            notifier.notify(username, new_password)
            AccountManager(self.env).set_password(username, new_password)
            req.hdf['reset.sent_to_email'] = email

    def _random_password(self):
        return ''.join([random.choice(self._password_chars)
                        for _ in xrange(self.password_length)])

    def _do_change_password(self, req):
        user = req.authname
        mgr = AccountManager(self.env)
        old_password = req.args.get('old_password')
        if not old_password:
            req.hdf['account.save_error'] = u'L\'ancien mot de passe ne peut pas être vide.'
            return
        if not mgr.check_password(user, old_password):
            req.hdf['account.save_error'] = u'L\'ancien mot de passe n\'est pas correct.'
            return

        password = req.args.get('password')
        if not password:
            req.hdf['account.save_error'] = u'Le mot de passe ne peut pas être vide.'
            return

        if password != req.args.get('password_confirm'):
            req.hdf['account.save_error'] = u'Les mots de passe doivent correspondre.'
            return

        mgr.set_password(user, password)
        req.hdf['account.message'] = u'Mot de passe mis à jour avec succés.'

    def _do_delete(self, req):
        user = req.authname
        mgr = AccountManager(self.env)
        password = req.args.get('password')
        if not password:
            req.hdf['account.delete_error'] = u'Le mot de passe ne peut pas être vide.'
            return
        if not mgr.check_password(user, password):
            req.hdf['account.delete_error'] = u'Le mot de passe n\'est pas correct.'
            return

        mgr.delete_user(user)
        req.redirect(self.env.href.logout())

    # ITemplateProvider

    def get_htdocs_dirs(self):
        """Return the absolute path of a directory containing additional
        static resources (such as images, style sheets, etc).
        """
        return []

    def get_templates_dirs(self):
        """Return the absolute path of the directory containing the provided
        ClearSilver templates.
        """
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]


class RegistrationModule(Component):
    """Provides users the ability to register a new account.
    Requires configuration of the AccountManager module in trac.ini.
    """

    implements(INavigationContributor, IRequestHandler, ITemplateProvider)

    def __init__(self):
        self._enable_check(log=True)

    def _enable_check(self, log=False):
        writable = AccountManager(self.env).supports('set_password')
        ignore_case = auth.LoginModule(self.env).ignore_case
        if log:
            if not writable:
                self.log.warn('RegistrationModule is disabled because the '
                              'password store does not support writing.')
            if ignore_case:
                self.log.warn('RegistrationModule is disabled because '
                              'ignore_auth_case is enabled in trac.ini.  '
                              'This setting needs disabled to support '
                              'registration.')
        return writable and not ignore_case

    #INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'register'

    def get_navigation_items(self, req):
        if not self._enable_check():
            return
        if req.authname == 'anonymous':
            yield 'metanav', 'register', Markup(u'<a href="%s">S\'enregistrer</a>',
                                                (self.env.href.register()))

    # IRequestHandler methods

    def match_request(self, req):
        return req.path_info == '/register' and self._enable_check(log=True)

    def process_request(self, req):
        if req.authname != 'anonymous':
            req.redirect(self.env.href.account())
        action = req.args.get('action')
        if req.method == 'POST' and action == 'create':
            try:
                _create_user(req, self.env)
            except TracError, e:
                req.hdf['registration.error'] = e.message
            else:
                req.redirect(self.env.href.login())
        req.hdf['reset_password_enabled'] = \
            (self.env.is_component_enabled(AccountModule)
             and NotificationSystem(self.env).smtp_enabled)

        return 'register.cs', None


    # ITemplateProvider

    def get_htdocs_dirs(self):
        """Return the absolute path of a directory containing additional
        static resources (such as images, style sheets, etc).
        """
        return []

    def get_templates_dirs(self):
        """Return the absolute path of the directory containing the provided
        ClearSilver templates.
        """
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]


def if_enabled(func):
    def wrap(self, *args, **kwds):
        if not self.enabled:
            return None
        return func(self, *args, **kwds)
    return wrap


class LoginModule(auth.LoginModule):

    implements(ITemplateProvider)

    def authenticate(self, req):
        if req.method == 'POST' and req.path_info.startswith('/login'):
            req.environ['REMOTE_USER'] = self._remote_user(req)
        return auth.LoginModule.authenticate(self, req)
    authenticate = if_enabled(authenticate)

    match_request = if_enabled(auth.LoginModule.match_request)

    def process_request(self, req):
        if req.path_info.startswith('/login') and req.authname == 'anonymous':
            req.hdf['referer'] = self._referer(req)
            if AccountModule(self.env).reset_password_enabled:
                req.hdf['trac.href.reset_password'] = req.href.reset_password()
            if req.method == 'POST':
                req.hdf['login.error'] = u'Utilisateur ou mot de passe invalide'
            return 'login.cs', None
        return auth.LoginModule.process_request(self, req)

    def _do_login(self, req):
        if not req.remote_user:
            req.redirect(self.env.abs_href())
        return auth.LoginModule._do_login(self, req)

    def _remote_user(self, req):
        user = req.args.get('user')
        password = req.args.get('password')
        if not user or not password:
            return None
        if AccountManager(self.env).check_password(user, password):
            return user
        return None

    def _redirect_back(self, req):
        """Redirect the user back to the URL she came from."""
        referer = self._referer(req)
        if referer and not referer.startswith(req.base_url):
            # don't redirect to external sites
            referer = None
        req.redirect(referer or self.env.abs_href())

    def _referer(self, req):
        return req.args.get('referer') or req.get_header('Referer')

    def enabled(self):
        # Users should disable the built-in authentication to use this one
        return not self.env.is_component_enabled(auth.LoginModule)
    enabled = property(enabled)

    # ITemplateProvider

    def get_htdocs_dirs(self):
        """Return the absolute path of a directory containing additional
        static resources (such as images, style sheets, etc).
        """
        return []

    def get_templates_dirs(self):
        """Return the absolute path of the directory containing the provided
        ClearSilver templates.
        """
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

