'''
$Id: GenericUserFolder.py,v 1.34 2000/05/30 00:58:28 zen Exp $
'''

import Globals
from Globals import HTMLFile, MessageDialog
from string import split,join,lower,count
from OFS.Document import Document
from base64 import decodestring, encodestring
from urllib import quote, unquote
from AccessControl.User import _remote_user_mode,nobody,super,\
    addr_match,host_match
from AccessControl.PermissionRole import _what_not_even_god_should_do
from OFS.Folder import Folder
from time import time
from sys import exc_info
import types
import sha
import whrandom
import Globals
import threading
from zLOG import LOG,ERROR,WARNING

from User import User # Our user object

# _hashmap contains our authentication tokens
# _hashmap[_p_oid][hash] == (username,lastseentime)
_hashmap = {} 		

def _debug(msg): pass
#def _debug(msg): LOG('GUF',WARNING,msg)

# Memory leak fix stolen from LoginManager
#
from ZPublisher.BaseRequest import BaseRequest
oldclose = BaseRequest.close.im_func
def close(self,oldclose=oldclose):
    try: del self.response.unauthorized
    except: pass
    oldclose(self)
BaseRequest.close = close

class GenericUserFolder(Folder):
    '''A Generic User authentication userlist'''

    meta_type='Generic User Folder'
    id       ='acl_users'
    title    ='Generic User Folder'
    icon     ='misc_/GenericUserFolder/Folder_icon'

    _super = super
    _nobody = nobody

    #isAUserFolder=1   This needed?
    isAGenericUserFolder=1

    __ac_permissions__=(
	('Manage users', 
	    ['getUserNames','getUser','getUsers','manage_listUsers',
	     'manage_userInfo','cachestats','flushcache',
	     'manage_cache'],['Manager',]),
	('Can Login and Logout',
	    ['login','logout','loginSuccess'],['Anonymous','Manager']))

    manage_options=(
	{'label':'Contents',	'action':'manage_main'},
	{'label':'User List',	'action':'manage_listUsers'},
	{'label':'Cache',	'action':'manage_cache'},
	{'label':'User Info',	'action':'manage_userInfo'},
	{'label':'Properties',	'action':'manage_properties'},
	{'label':'Security',	'action':'manage_access'},
	{'label':'Undo',	'action':'manage_UndoForm'}
    )

    manage_properties=HTMLFile('manage_properties', globals())
    manage_listUsers=HTMLFile('manage_listUsers',globals())
    manage_userInfo=HTMLFile('manage_userInfo',globals())
    manage_cache=HTMLFile('manage_cache',globals())
    
    def __init__(self, cookie_mode=0, REQUEST=None):
        self.cookie_mode=cookie_mode
	self.cookie_expires=900
	self.userlist_expires=900 # not currently used
	self.user_expires=900  # not currently used

	self._init_caches()

    def manage_afterAdd(self,item,container):
        if item is self:
            if hasattr(self, 'aq_base'): self=self.aq_base
            container.__allow_groups__=self
	
    def manage_beforeDelete(self,item,container):
	if item is self:
	    try: del container.__allow_groups__
	    except: pass

    def _setId(self, id):
        if id != self.id:
            raise Globals.MessageDialog(
                title='Invalid Id',
                message='Cannot change the id of a UserFolder',
                action ='./manage_main',)

    def __setstate__(self,state):
	"Initialize on reload from ZODB"
	Folder.__setstate__(self,state)
	self._init_caches()

    def _init_caches(self):
	gufid = self._p_oid

	if not _hashmap.has_key(gufid):
	    _hashmap[gufid] = {}

    def cachestats(self):
	'Return meaningless statistics'
	gufid = self._p_oid

	if not _hashmap.has_key(gufid):
	    _hashmap[gufid] = {}

	return (\
	    {'text':	'Items in Authorization Token cache',
	     'value':	len(_hashmap[gufid] or [])},)
		 
    def flushcache(self,REQUEST):
	'Flush the caches'

	gufid = self._p_oid
	if REQUEST and REQUEST.has_key('flushcookies') and REQUEST.flushcookies:
	    _hashmap[gufid] = {}
	    LOG('GUF',0,"Authentication tokens flushed by %s" \
		% REQUEST.AUTHENTICATED_USER.getUserName())
            return MessageDialog(
                   title  ='GenericUserFolder Caches Flushed',
                   message='''GenericUserFolder authentication tokens 
			    have been flushed''',
                   action = './manage_cache')
	
        if REQUEST:
            return MessageDialog(
                   title  ='GenericUserFolder Caches WERE NOT Flushed',
                   message='''GenericUserFolder authentication tokens have 
			<strong>NOT</strong> been flushed, as the safty-valve 
			checkbox was not selected''',
                   action = './manage_cache')

    def domainSpecValidate(self, spec):
        for ob in spec:
            sz=len(ob)
            if not ((addr_match(ob) == sz) or (host_match(ob) == sz)):
                return 0
        return 1

    def user_names(self):
	return self.getUserNames()

    def getUserNames(self,sortem=0):
        '''Returns a list of user names or [] if no users exist'''

	if not hasattr(self,'userList'):
	    LOG('GUF',ERROR,'%s has no userList method' \
		% self.absolute_url())
	    return []

	try:
	    data = self.userList(self,self.REQUEST)

	    if type(data) == types.StringType:
		data = split(data)

	    data = list(data)

	    if len(data) > 0 and type(data[0]) != types.StringType:
		if hasattr(data[0],'username'):
		    data = map(lambda x: x.username,data)
		elif hasattr(data[0],'uname'):
		    data = map(lambda x: x.uname,data)
		
		if type(data[0]) != types.StringType:
		    raise 'Unsupported Type',\
			'userList method returned an unsupported type'

	    if sortem:
		data.sort()
	    return data

	except:
	    try:
		w = self.userList.absolute_url()
	    except:
		w = 'userList'
	    LOG('GUF',ERROR, 'An exception was raised by %s (%s)' \
		% (w,str(exc_info())),'',exc_info())
	    return []


    def getUsers(self):
        """Return a list of user objects or [] if no users exist"""

	data = self.getUserNames()

	userlist   = []

	for name in data:
	    user = User(name,self)
	    user = user.__of__(self)
	    userlist.append(user)

	return userlist


    def getUser(self, name):
        """Return the named user object or None if no such user exists"""

	# Special case for the superuser. This stops the chance of locking
	# out a subtree by having a user with the same usename as the
	# superuser.
	super = self._super	
	if name==super.getUserName():
	    return super

	# Shortcut method for sites with large numbers of users
	#
	if hasattr(self,'userExists'):
	    try:
		if self.userExists(self,self.REQUEST,username=name) == 1:
		    _debug('userExists returned true for %s' % name)
		    user = User(name,self)
		    user = user.__of__(self)
		    return user
		else:
		    _debug('userExists returned false for %s' % name)
		    return None
	    except:
		try:
		    w = self.userExists.absolute_url()
		except:
		    w = 'userExists'
		LOG('GUF',ERROR,'%s raised an exception (%s)' \
		    % (w,str(exc_info())),'',exc_info())

	data = self.getUserNames()
	if name in data:
	    user = User(name,self)
	    user = user.__of__(self)
	    return user

	return None

    def validate(self,request,auth=None,roles=None):
	# No docstring for a reason

	_debug('validate url=%s,roles=%s,auth=%s' \
	    % (request['PATH_INFO'],roles,type(auth)))

        if roles is _what_not_even_god_should_do:
            request.response.notFoundError()

	parents = request.get('PARENTS',[])
	if not parents:
	    parent = self.aq_parent
	else:
	    parent = parents[0]

        if self.cookie_mode:
            u = self._cookie_validate(request,auth,roles,parent)
        else:
	    u = self._basic_validate(request,auth,roles,parent)

	if u is None:
	    # If Anonymous has required permissions, no need to login.
	    # Don't check unless we are the top acl_users folder, as a higher
	    # acl_users folder might still validate us and give higher privs.
	    anon = self._nobody
	    if self._isTop() and anon.allowed(parent, roles):
		anon = anon.__of__(self)
		return anon
	    _debug("Returning None from validate (parent=%s,roles=%s)" \
		% (parent.absolute_url(),roles))
	    return None
	else:
	    _debug("Returning %s from validate (parent=%s,roles=%s)" \
		% (u.getUserName(),parent.absolute_url(),roles))
	    return u

    def _basic_validate(self,request,auth,roles,parent):
	'''
	Similar to BasicUserFolder.validate, except don't try
	"If no authorization, only a user with a domain spec and no passwd 
	 or nobody can match" section, as this is real bad with large user
	bases
	'''

        # Only do basic authentication
        if not auth or lower(auth[:6])!='basic ':
            return None

        name,password=tuple(split(decodestring(split(auth)[-1]), ':', 1))

        # Try to get GUF user, or possible superuser
        user=self.getUser(name)
        if user is not None and user.allowed(parent,roles):
	    if user.authenticate(password,request):
		return user
	    else:
		LOG('GUF',0,"Failed authentication for %s" % name)

        return None

	
    def _cookie_validate(self,request,auth,roles,parent):
	'''
	First attempt basic auth (for non-cookie protocols),
	and then try authenticating via form inputs from our login
	screen or a authentication token passed via cookie
	'''

	# If we have been authenticated using basic authentication
	# using a different acl_users folder, we can try authenticating
	# in the guf with the same username & password. 
	# This handles non-cookie connections like FTP
	if auth and lower(auth[:6]) == 'basic ':
	    user = self._basic_validate(request,auth,roles,parent)
	    if user:
		return user

	cname = '_gufauth' 
	nameparm = cname + '_name'
	passparm = cname + '_password'

	username = None
	password = None
	hash = None
	response = request['RESPONSE']

	ib = 'This space intentionally blank'

	if request.form.has_key(nameparm):
	    #
	    # Pull username & cleartext from form input.
	    # Ensure cleartext isn't left in the REQUEST for
	    # malicious DTML
	    #
	    # Hmm... I can delete from request.form, but not request...
	    #
	    _debug('Got username from form')
	    username = request[nameparm]
	    request.form[nameparm] = ib
	    request[nameparm] = ib

	if request.form.has_key(passparm):
	    _debug('Got password from form')
	    password = request[passparm]
	    request.form[passparm] = ib
	    request[passparm] = ib
	
	e = request.environ
	if _remote_user_mode and e.has_key('REMOTE_USER'):
	    username = e['REMOTE_USER']
	    _debug('Username overridden by REMOTE_USER to ' % username)
    
	if request.cookies.has_key(cname):
	    # Pull username & hash from cookie	
	    hash = unquote(request.cookies[cname])  
	    _debug('Pulled auth token from cookie')

	#
	# Username/Password login - someone has just filled in the
	# login form.
	#
	if username is not None or password is not None:

	    # If user is relogging in again, nuke their
	    # old cookie right now
	    if hash is not None:
		try: del _hashmap[self._p_oid][hash]
		except KeyError: pass

	    #
	    # Try validating with username & password
	    #
	    _debug('Attempting username/password validation')
	    user = self.getUser(username)
	    if user is not None and user.allowed(parent,roles) \
		    and user.authenticate(password,request):
		self._stashhash(username,password,request)
		_debug("Authenticated %s with password" % username)
		return user

	#
	# Try authenticating with token
	#
	elif hash is not None:
	    gufid = self._p_oid
	    try:
		hashmap = _hashmap[gufid]
	    except KeyError:
		hashmap = {}
		_hashmap[gufid] = hashmap
		    
	    # If we are using cookie authentication, check our hashmap
	    # stash. We do it this way so that our hashes are shared
	    # between threads (otherwise, you would have to authenticate
	    # once for each thread that handles one of your requests).
	    # Need storecookie and fetchcookie hooks so we can do
	    # this in a distributed ZEO environment
	    if hashmap.has_key(hash):

		hashitem = hashmap[hash]

		if time() > hashitem[0] + self.cookie_expires:
		    # Our cookie has expired!
		    _debug('Auth token found, but expired')
		    del hashmap[hash]
		    response.expireCookie(cname, path='/')

		else:
		    # Update our lastseen time
		    hashitem[0] = time()
		    username = hashmap[hash][1]

		    user = self.getUser(username)
		    if user is not None:
			_debug('Authenticated %s via token' % username)
			if user.allowed(parent,roles):
			    return user
			else:
			    _debug('But they dont have enough privs')

	# WooHoo - if replace unauthorized method in response object so
	# our login screen is displayed if all higher auth fails.
	# This means that the old code where I manually crawled up the
	# tree is no longer required.
	def guf_unauthorized(guf=self,request=request,username=username):
	    if username is not None:
		LOG('GUF',0,'Failed authorization for user %s (%s)' \
		    % (username,guf.absolute_url()))
	    loginScreen = guf.docLogin(guf,request,
		loginSuccessURL=guf.absolute_url() + '/loginSuccess')
	    raise 'LoginRequired', loginScreen

	_debug('Replacing response.unauthorized with our login screen')
	response=request['RESPONSE']
	response.unauthorized = guf_unauthorized
	
	return None


    def _stashhash(self,username,passwd,request):
	'''stash the hash in our _hashmap cache. Set the cookie.'''

	gufid = self._p_oid

	response = request['RESPONSE']
	hash = encodestring(sha.new('%s%s%f%f%s' \
	    % (username,passwd,time(),whrandom.random(),str(request))).digest())

	try:
	    hashmap = _hashmap[gufid]
	except KeyError:
	    hashmap = {}
	    _hashmap[gufid] = hashmap

	# The Universe will probably end before a sha hash isn't unique,
	# but call me paranoid
	while hashmap.has_key(hash):
	    hash = encodestring(sha.new('%s%s%f%f%s' \
		% (username,passwd,time(),
		   whrandom.random(),str(request))).digest())

		    # List rather than tuple so its mutable
	hashmap[hash] = [time(),username]

	# TODO: Calculate path so that cookie only gets passed to the
	# acl_users folder's parent folder and below?
	response.setCookie('_gufauth',quote(hash),path='/')

	if request.has_key('URL'):
	    _debug('URL is %s' % request['URL'])
	else:
	    _debug('No URL!')

    def __len__(self):
        return len(self.getUserNames())

    def manage_edit(self, cookie_mode, userlist_expires,user_expires,
	    cookie_expires, REQUEST=None):
	"""Change properties"""

        self.cookie_mode=cookie_mode
	self.userlist_expires=userlist_expires 	# Obsolete?
	self.user_expires=user_expires		# Obsolete?
	self.cookie_expires=cookie_expires

        if REQUEST:
            return MessageDialog(
                   title  ='GenericUserFolder Changed',
                   message='GenericUserFolder properties have been updated',
                   action =REQUEST['URL1']+'/manage_properties',
                   target ='manage_main')

    def _isTop(self):
	try: 
	    return self.aq_parent.aq_base.isTopLevelPrincipiaApplicationObject
	except: 
	    return 0

    def _killcookie(self,request):
	'Helper method to login and logoff'
	if not self.cookie_mode: raise 'Method only works in cookie mode'
	cname='_gufauth'
	if request.cookies.has_key(cname):
	    # Pull username & hash from cookie	
	    try:
		hash = unquote(request.cookies[cname])  
		del _hashmap[self._p_oid][hash]
	    except KeyError: pass
	request['RESPONSE'].expireCookie(cname,path='/')

    def login(self, REQUEST):
	"""Login"""
	# Don't log the user out if they accidently click on
	# a link they arn't authorized to view
	#self._killcookie(REQUEST)
        return self.docLogin(self, REQUEST,
	    loginSuccessURL=self.absolute_url() + '/loginSuccess')

    def logout(self, REQUEST):
        """Logout"""
	self._killcookie(REQUEST)
        return self.docLogout(self, REQUEST)

    def loginSuccess(self,REQUEST):
	"""Successful Login"""
	return self.docLoginSuccess(self,REQUEST)

    def _templateFile(self,file,title='',template='dumb'):
	t = HTMLFile('templates/%s/%s' % (template,file),globals())
	self.manage_addDTMLMethod(file,title,t)
    
    def _templateReset(self, template='dumb'):
	self._templateFile('userList','List Users',template)
	self._templateFile('userRoles',"Return a user's roles",template)
	self._templateFile('userDomains','Return domains a user can login from',
	    template)
	self._templateFile('userAuthenticate','Authenticate a user',template)
	self._templateFile('docLogin','Cookie mode login screen',template)
	self._templateFile('docLogout','Cookie mode logout screen',template)
	self._templateFile('docLoginSuccess','Successful Authentication Screen',
	    template)


# Magic bullet?
Globals.default__class_init__(GenericUserFolder)

manage_addGenericUserFolderForm=HTMLFile('addGenericUserFolder', globals())

def manage_addGenericUserFolder(self,cookie_mode=0,
				template='dumb',REQUEST=None):
    'Create a GenericUserFolder object'

    if hasattr(self.aq_base, 'acl_users'):
        return MessageDialog(
               title  ='Item Exists',
               message='This object already contains a User Folder',
               action ='%s/manage_main' % REQUEST['PARENT_URL'])

    ob=GenericUserFolder(cookie_mode, REQUEST)
    try:
	self._setObject('acl_users', ob)
    except:
	return MessageDialog(title  ='Item Exists',
	    message='This object already contains an acl_users',
	    action ='%s/manage_main' % REQUEST['PARENT_URL'])
    self.__allow_groups__=self.acl_users
    ob._templateReset(template)

    if REQUEST: return self.manage_main(self,REQUEST,update_menu=1)

