# Copyright (C) 2005 JanRain, Inc.
# Copyright (C) 2009, 2010 Canonical Ltd
#
# This program 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.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os

from apache_openid import logging
from apache_openid.handlers.openid.authenticator import Authenticator
from apache_openid.handlers.openid.actions import (LoginAction, LogoutAction,
    ReturnAction)
from apache_openid.utils import OK, HTTP_FORBIDDEN, SERVER_RETURN
from openid.consumer import consumer
from openid.store import filestore, memstore


class OpenIDLoginHandler(object):
    session_timeout = 60 * 60 * 24 * 7 # Time out sessions after a week

    actions = {
        '+login': LoginAction,
        '+logout': LogoutAction,
        '+return': ReturnAction,
        }

    @property
    def authenticator(self):
        try:
            return self._auth
        except AttributeError:
            self._auth = self._get_authenticator()
            return self._auth

    @property
    def consumer(self):
        try:
            return self._consumer
        except AttributeError:
            store_type = self.options.get('store-type', 'memory')
            store = StoreFactory(self.options).get_store(store_type)
            self._consumer = consumer.Consumer(self.session, store)
            return self._consumer

    @property
    def known_actions(self):
        return self.actions.keys()

    # Cache for authorized identities
    _authorized_cache = {}
    _max_authorized_cache_size = 100

    def __init__(self, request, response, options, session, action_path):
        self.request = request
        self.response = response
        self.options = options
        self.session = session
        self.action_path = action_path

    def protect(self):
        """Decide how to handle the current request for a protected resource."""
        if self.request.uri.endswith('favicon.ico'):
            # Ignore special case of "favicon.ico" which browsers request
            # automatically and will incorrectly set the session target.
            logging.debug("Allowed request for favicon.ico")
            return OK
        elif self.request.action is not None:
            # To enable us to overlay the handler on top of a normal directory,
            # we only set the python handler if it handles the requested action.
            logging.debug("Creating handler for %s", self.request.action)
            self.request.add_handler('PythonHandler', self.handle_request)
            return OK
        elif self.is_authenticated():
            # If the user is authenticated, we make sure the protected content
            # isn't cached and set apache's REMOTE_USER value.
            logging.debug("Allowing user %s", self.request.cookied_user)
            self.request.user = self.request.cookied_user
            self.request.headers_out['Cache-Control'] = 'no-cache'
            return OK
        elif self.request.cookied_user is not None:
            # If the user is logged in and we get here then the user is not
            # entitled to access the protected resource.  We log them out and
            # present a "Permission denied" message.
            logging.debug("Access denied for user %s" % self.request.cookied_user)
            self.response.login_redirect('denied', self.request.uri)
        elif self.request.method == 'GET':
            # If a protected resource is requested using GET then we redirect
            # the user to login, storing the requested URL.
            logging.debug("Access denied: %s. Forcing login", self.request.uri)
            self.response.login_redirect(None, self.request.uri)
        else:
            # It's not always safe to redirect non-GET requests so we just
            # present a "Permission denied" error.
            logging.debug("Access denied for non-GET request.")
            raise SERVER_RETURN, HTTP_FORBIDDEN

    def handle_request(self, request):
        """Dispatch to the appropriate access control action."""
        for action in self.known_actions:
            if self.request.action == action:
                action = self.actions[action](self.request, self.response,
                    self.session, self.consumer, self.options)
                return action.do()
        logging.error("Unknown OpenID access control action: %r "
            "(action_path=%r)", self.request.action, self.action_path)
        self.response.login_redirect()

    def is_authenticated(self):
        return self.authenticator.authenticate(self.request.cookied_user)

    def _get_authenticator(self):
        return Authenticator(self.request, self.options)


class UnknownStoreException(Exception):
    pass


class StoreFactory(object):

    def __init__(self, options):
        self.options = options

    def get_store(self, store_type):
        """Get the store for this request. If you want to use a different kind
        of store, change this function to support creating it.
        """
        if store_type == 'file':
            store_dir = os.path.normpath(self.options['store-directory'])
            return filestore.FileOpenIDStore(store_dir)
        elif store_type == 'memory':
            return memstore.MemoryStore()
        else:
            # XXX: implement other store types (SQL...)
            raise UnknownStoreException()
