# -*- coding: iso8859-1 -*-
#
# Copyright (C) 2003-2005 Edgewall Software
# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.com/license.html.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://projects.edgewall.com/trac/.
#
# Author: Jonas Borgström <jonas@edgewall.com>

from __future__ import generators
import re
import time

from trac.core import *
from trac.web.api import IAuthenticator, IRequestHandler
from trac.web.chrome import INavigationContributor
from trac.util import escape, hex_entropy, TRUE


class LoginModule(Component):
    """Implements user authentication based on HTTP authentication provided by
    the web-server, combined with cookies for communicating the login
    information across the whole site.

    This plugin traps any /login path request and responds with a 401 basic 
    authentication header meaning that the client's browser has to provide 
    user credentials. The web-server should be set up in a way that these
    credentials get validated (e.g. against a NT domain) before setting the
    remote_user server variable.
    The login name is then stored in the database and associated with a unique
    key that gets passed back to the user agent using the 'trac_auth' cookie.
    This cookie is used to identify the user in subsequent requests to
    non-protected resources.
    Please note: trac_auth cookies are not supported on IIS5, see 
    http://support.microsoft.com/?scid=kb%3Ben-us%3B176113&x=15&y=11. In this
    case IIS5 must provide a valid remote_user with every request.
    """

    implements(IAuthenticator, INavigationContributor, IRequestHandler)

    # IAuthenticator methods

    def authenticate(self, req):
        authname = None

        if req.remote_user:
            authname = req.remote_user
        elif req.incookie.has_key('trac_auth'):
            authname = self._get_name_for_cookie(req, req.incookie['trac_auth'])

        if not authname:
            return None

        ignore_case = self.env.config.get('trac', 'ignore_auth_case')
        ignore_case = ignore_case.strip().lower() in TRUE
        if ignore_case:
            authname = authname.lower()
        return authname

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'login'

    def get_navigation_items(self, req):
        if req.authname and req.authname != 'anonymous':
            yield 'metanav', 'login', 'logged in as %s' % escape(req.authname)
            yield 'metanav', 'logout', '<a href="%s">Logout</a>' \
                  % escape(self.env.href.logout())
        else:
            yield 'metanav', 'login', '<a href="%s">Login</a>' \
                  % escape(self.env.href.login())

    # IRequestHandler methods

    def match_request(self, req):
        return re.match('/(login|logout)/?', req.path_info)

    def process_request(self, req):
        if req.path_info.startswith('/login'):
            if not req.remote_user or req.remote_user == 'anonymous':
                req.send_response(401)
                req.send_header('WWW-Authenticate', 'Basic')
                req.end_headers()
                return
            else:
                self._do_login(req)
        elif req.path_info.startswith('/logout'):
            self._do_logout(req)
        self._redirect_back(req)

    # Internal methods

    def _do_login(self, req):
        """Log the remote user in.

        This function expects to be called when the remote user name is
        available. The user name is inserted into the `auth_cookie` table and a
        cookie identifying the user on subsequent requests is sent back to the
        client.

        If the Authenticator was created with `ignore_case` set to true, then 
        the authentication name passed from the web server in req.remote_user
        will be converted to lower case before being used. This is to avoid
        problems on installations authenticating against Windows which is not
        case sensitive regarding user names and domain names
        """
        assert req.remote_user, 'Authentication information not available.'

        remote_user = req.remote_user
        ignore_case = self.env.config.get('trac', 'ignore_auth_case')
        ignore_case = ignore_case.strip().lower() in TRUE
        if ignore_case:
            remote_user = remote_user.lower()

        assert req.authname in ('anonymous', remote_user), \
               'Already logged in as %s.' % req.authname

        cookie = hex_entropy()
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) "
                       "VALUES (%s, %s, %s, %s)", (cookie, remote_user,
                       req.remote_addr, int(time.time())))
        db.commit()

        req.authname = remote_user
        req.outcookie['trac_auth'] = cookie
        req.outcookie['trac_auth']['path'] = self.env.href()

    def _do_logout(self, req):
        """Log the user out.

        Simply deletes the corresponding record from the auth_cookie table.
        """
        if req.authname == 'anonymous':
            # Not logged in
            return

        # While deleting this cookie we also take the opportunity to delete
        # cookies older than 10 days
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s",
                       (req.authname, int(time.time()) - 86400 * 10))
        db.commit()
        self._expire_cookie(req)

    def _expire_cookie(self, req):
        """Instruct the user agent to drop the auth cookie by setting the
        "expires" property to a date in the past.
        """
        req.outcookie['trac_auth'] = ''
        req.outcookie['trac_auth']['path'] = self.env.href()
        req.outcookie['trac_auth']['expires'] = -10000

    def _get_name_for_cookie(self, req, cookie):
        check_ip = self.env.config.get('trac', 'check_auth_ip')
        check_ip = check_ip.strip().lower() in TRUE

        db = self.env.get_db_cnx()
        cursor = db.cursor()
        if check_ip:
            cursor.execute("SELECT name FROM auth_cookie "
                           "WHERE cookie=%s AND ipnr=%s",
                           (cookie.value, req.remote_addr))
        else:
            cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s",
                           (cookie.value,))
        row = cursor.fetchone()
        if not row:
            # The cookie is invalid (or has been purged from the database), so
            # tell the user agent to drop it as it is invalid
            self._expire_cookie(req)
            return None

        return row[0]

    def _redirect_back(self, req):
        """Redirect the user back to the URL she came from."""
        referer = req.get_header('Referer')

        # Since we cannot create a trac_auth cookie on IIS5, we depend
        # on the remote_user information with every request.
        # But the browser won't send authentication information when we
        # redirect below the trac.cgi/ path.
        if not self.env.abs_href.endswith('/'):
            abs_href = self.env.abs_href + '/'
        else:
            abs_href = self.env.abs_href

        if referer:
            if not referer.startswith(req.base_url):
                # only redirect to referer if the latter is from the same
                # instance
                referer = None
            else:
                if not referer.endswith('/'):
                    referer = referer + '/'
        req.redirect(referer or self.env.abs_href)

