# -*- coding: UTF-8 -*-
#    This file is part of arena.
#
#    arena is free software: you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the
#    Free Software Foundation; either version 3 of the License or, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
#    License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with arena.  If not, see <http://www.gnu.org/licenses/>.

## @file
#  This file defines and implements a user authentication engine, which is
#  a subclass of AnArenaContentProvider that handles user authentication 
#  (logins).
#
#  This engine requires the following query parameters:
#  - <tt>username</tt>: The name of the user to be authenticated
#  - <tt>pwd</tt>: The hashed value of the user password, which will be
#    compared to the hashed value stored in the user record.  In the future,
#    this value will be hashed using a secure token known only to the server
#    and the client.
#  There is an additional, optional query parameter:
#  - <tt>logout</tt>: This query parameter is used to clear user credentials
#    from the current session; as a result, the session becomes an anonymous
#    one again, with the limited privileges associated therewith.  It is worth
#    noting that this query parameter, if provided, negates all others: you 
#    cannot use it with /p username or /p pwd, as it will clear the results of
#    the login process and force the user to begin again at step one (entering
#    a username).
#
#  For security purposes, this engine is presently set up such that logging in
#  with it is a two-step process; this is done on purpose to help prevent 
#  brute-force attacks.  The engine will NOT accept both the user name and
#  password query parameters at the same time.  Additionally, the password-entry
#  view is designed, by default, to require a three-word passphrase rather than 
#  a single password; this helps to prevent dictionary-based attacks from being 
#  successful, as the complexity of a three-word passphrase is enough to prevent 
#  modern computers (as of 2013) from easily cracking the passphrase.
# 
#  At present, this engine also validates the client's User Agent, to ensure
#  that it does not change between the time the user name is entered and the
#  the time that the password is entered.  This is not the greatest guarantor
#  of security, but...
#
#  The views used by this engine are designed with the assumption that the user
#  name is provided by an AJAX request, while the final password step is
#  submitted as a regular browser request.  This can be changed to suit the
#  needs of your site without changing the code; one needs only to change the
#  associated views.
#
#  In the future, AJAX will be used to hash the passphrase using a random secure 
#  token that is known only to the server (and which is calculated by the 
#  client).  If the user enters the token into their browser incorrectly, it 
#  will be hashed incorrectly, and authentication will fail.  This token is 
#  randomly generated each time it is requested, thus preventing passwords from
#  being easily cracked.

from Plugins.Content.PerUser.Arena.ContentProviders import \
    AnArenaParsedContentProvider, AnArenaContentProviderError
    
from Local.Configuration import Site

from Plugins.User.Arena.Users import AnArenaUser

from cgi import MiniFieldStorage
import time

## The name of the query key used to specify the landing point after 
#  authentication
uaLandingQueryKey = "landing"
## The name of the query key used to specify that the user wishes to log out
uaLogOutQueryKey = "logout"
## The name of the query key used to specify the user name
uaUserNameQueryKey = "username"
## The name of the query key used to specify the password
uaPasswordQueryKey = "pwd"

## The name of the view used to display the login prompts
uaLoginViewName = "site/user/login"
## The name of the template used to display the user name prompt
uaUserNamePrompt = "username.tpl.html"
## The name of the template used to display the password prompt
uaPasswordPrompt = "password.tpl.html"

## The session variable used to store the desired landing channel
uaLandingVariable = "/user/authentication/landing"
## The session variable used to track authentication progress
uaProgressIndicator = "/user/authentication/progress"
## The name of the session variable used to track what user is attempting to authenticate
uaUserNameVariable = "/user/authentication/username"
## The name of the session variable used to track the user agent between authentication steps
uaUserAgentVariable = "/user/authentication/user-agent"
## The name of the session variable used to track the time between authentication steps
uaTimestampVariable = "/user/authentication/timestamp"

# A list of temporary variables
uaSessionVariables = uaLandingVariable, uaProgressIndicator, \
    uaUserNameVariable, uaTimestampVariable, uaUserAgentVariable

## The format of the error message used when failing to authenticate a user
aeAuthenticationError = "the user name or password provided is incorrect"
## The format of the error message used when a user account is locked
aeUserAccountDisabled = "\"{}\": this account is disabled"
## The format of the log message used when there is a user agent mismatch
aeUserAgentMismatch = "the user agent strings do not match; got \"{}\" but " + \
    "\"{}\" was expected"
## The format of the log message used when no password is provided
aePasswordMissing = "the password component (\"{}\") is missing from the query"
## The format of the log message used when an incorrect password is provided
aePasswordIncorrect = "the password supplied for the user \"{}\" is incorrect"

## Represents an error that might occur within the authentication module itself.
class AnAuthenticationError(AnArenaContentProviderError):
    pass

## A user authentication engine, which authenticates users.
class AUserAuthenticator(AnArenaParsedContentProvider):     
    ## Constructor
    def __init__(Self, **params):
        AnArenaParsedContentProvider.__init__(Self, **params)
        
    ## Clear session variables used by the authentication process
    def removeSessionVariables(Self):
        for varname in uaSessionVariables:
            if varname in Self._Session:
                del Self._Session[varname]
    
    ## Authenticate the user
    def authenticate(Self):
        DesiredUser = None
        
        # Check the user agent -- not the greatest guarantor of security, but
        # at least we can make sure that the user agent does not suddenly
        # change between authentication steps.
        if Self._Request.HTTP.userAgent == Self._Session[uaUserAgentVariable]:        
            if uaPasswordQueryKey in Self._Request:
                # Retrieve the user record
                DesiredUser = AnArenaUser(Self,
                    Log = Self._Log,
                    DataSource = Self._DataSource,
                    ContentProvider = Self
                )
                                    
                DesiredUser.fetch(Self._Session[uaUserNameVariable])
                
                # If the user is disabled, we can go no further
                if DesiredUser["enabled"] != "y":
                    Self.removeSessionVariables()
                    Self.error(AnAuthenticationError(aeUserAccountDisabled,
                        DesiredUser.name()))
            
                # Check the passwords
                if Self._Request[uaPasswordQueryKey].value == DesiredUser["userPassword"]:
                    # Set the user name of the user
                    Self._Session.userName(DesiredUser.name())
                    Self._User = DesiredUser
                    
                else:
                    Self.log(AnAuthenticationError(aePasswordIncorrect,
                        DesiredUser.name()))
                        
                    DesiredUser["failedLogins"] = \
                        int(DesiredUser["failedLogins"]) + 1
                    
                    # Lock the user, if indicated
                    if (DesiredUser.name() != Site.Users.administrator) and \
                        DesiredUser["failedLogins"] >= Site.Users.lockThreshold:
                            DesiredUser["enabled"] = "n"
                            
            else:
                Self.log(AnAuthenticationError(aePasswordMissing,
                    uaPasswordQueryKey))
        
        else:
            Self.log(AnAuthenticationError(aeUserAgentMismatch, 
                Self._Request.HTTP.userAgent, 
                Self._Session[uaUserAgentVariable]))
        
        # If authentication failed...
        if Self._Session.isAnonymous(): 
            Self.removeSessionVariables()
            
            # Ensure the session is closed
            Self._Session.close()
            
            Self.error(AnAuthenticationError(aeAuthenticationError))

    ## Log out the current user
    def logout(Self):
        if Self._Session.isAnonymous() == False:
            # Remove temporary session variables
            Self.removeSessionVariables()
            # Set the new user name
            Self._Session.userName(Site.Users.anonymous)
            # Ensure that the user is reloaded
            Self._User = None
        
    ## Record the time of the login attempt
    def timeLogin(Self):
        loginDelay = 0
        
        # If too much time has elapsed between attempts, then the user must
        # start over
        if uaTimestampVariable in Self._Session:
            loginDelay = time.time() - \
                float(Self._Session[uaTimestampVariable])
                
        if loginDelay > Site.Users.loginDelayMaximum:
            # Log out the user
            Self.logout()
                    
        Self._Session[uaTimestampVariable] = time.time()  
        
    ## Progress through the authentication process
    def progress(Self):
        Self._Request["view"] = uaLoginViewName
        
        if uaUserNameQueryKey in Self._Request.query:
            # Store the user name provided
            Self._Session[uaUserNameVariable] = \
                Self._Request.query[uaUserNameQueryKey].value
            # Store the user agent
            Self._Session[uaUserAgentVariable] = \
                Self._Request.HTTP.userAgent
                
            # Store our progress through the authentication process
            Self._Session[uaProgressIndicator] = uaUserNameQueryKey 
            
            Self._Request["template"] = uaPasswordPrompt
            
        #else:
            #Self._Request.query["template"] = MiniFieldStorage("template",
            #    uaUserNamePrompt)
        
    # Properties ---------------------------------------------------------------
    ## Fetch content
    def content(Self):
        # Begin the arena session
        if Self._Session is None:
            Self.beginSession()
        
        landing = None
        
        # When clean URLs are used, the "title" parameter can be used to
        # specify the desired landing channel.  This allows login links to
        # appear as: yourdomain/arenapath/$site/user/login/channel
            
        # Store the desired landing page, if provided
        if Site.Posts.uriTitleQueryKey in Self._Request:
            landing = Self._Request[Site.Posts.uriTitleQueryKey].value
            del Self._Request.query[Site.Posts.uriTitleQueryKey]
                
        if uaLandingQueryKey in Self._Request:
            landing = Self._Request[uaTitleQueryKey].value        
        
        # Logout the user, if specified
        if uaLogOutQueryKey in Self._Request:
            Self.logout()         
            
            if landing is not None:
                Self._Channel.fetch(landing)
            
            return AnArenaParsedContentProvider.content(Self)
            
        if landing is not None:
            Self._Session[uaLandingVariable] = landing
        
        # Check the login delay
        Self.timeLogin()

        # Authenticate the user
        if Self._Session.isAnonymous():
            if uaProgressIndicator in Self._Session:
                Self.authenticate()
        
            else:
                Self.progress()
        
        # If the user has successfully authenticated...        
        if Self._Session.isAnonymous() == False:
            # Display the desired channel
            if uaLandingVariable in Self._Session:
                Self._Channel.fetch(Self._Session[uaLandingVariable])
                            
            # Clear temporary session variables
            Self.removeSessionVariables()
                            
            return AnArenaParsedContentProvider.content(Self)
            
        # Display the channel
        return AnArenaParsedContentProvider.content(Self)
