""" Classes that represent interface to data in MySQL tables. 

	It is supposed for all this classes to be referenced as _v_ atributes
	of mysql user folder, so each thread has it's own copy (so there is no 
	need for locks).
"""

__version__ = "$Revision: 1.26 $"
# $Source: /home/vladap/src/mysqlUserFolder/RCS/vdb.py,v $
#
# $Author: vladap $
# $Date: 2004/06/16 21:13:37 $

import time, random, array, string

from db import dbConnection
import c_classes, cfg, cache, util, passwords

# --------------------------------------------------------------------- */
# UserDb class

class UserDb:
	""" Class that represents (local) interface to User database.
	"""
	
	def __init__ (Self, db, realm):
		Self.db = db
		Self.realm = realm
		
		if cfg.CACHE_USERS:
			Self.user_cache = cache.v_cache (cfg.CACHE_USERS_LIFE)
			Self.username_cache = cache.v_cache (cfg.CACHE_USERS_LIFE)
	
	def list_usernames (Self, role = None):
		if role is None:
			return Self.db.list_usernames (Self.realm)
		else:
			return Self.db.list_usernames_with_role (Self.realm, role)

	def list_rolenames (Self): 
		return Self.db.list_rolenames (Self.realm)
	
	def refresh_user (Self, user):
		if cfg.DEBUG:
			util.log_debug ("refresh_user (): '%s'" % user.getUserName ())
		 
		ret = Self.db.get_user_data (user)
		
		if (not ret): return ret
		
		if cfg.CACHE_USERS:				
				# This will clean the local thread cache
				# It will not clean caches of other threads
				
			uid = user.getDBId ()
			username = user.getUserName ()
		
			if Self.user_cache.get (uid):
				Self.user_cache.delete (uid)
				
			if Self.username_cache.get (username):
				Self.username_cache.delete (username)
				
		return ret
		
	def get_user_count (Self):
		return Self.db.count_users (Self.realm)
		
	def get_role_count (Self):
		return Self.db.count_roles (Self.realm)
	
	def get_user (Self, username):
		if cfg.DEBUG:
			util.log_debug ("get_user (): Begin (%s)" % username)
	
		if cfg.CACHE_USERS:
			uid = Self.username_cache.get (username)
			if not (uid is None):
				user = Self.user_cache.get (uid)
			else:
				user = None
				
			if not (user is None):
				if cfg.DEBUG:
					util.log_debug ("get_user (): getting user from a cache.")
				
				return user._copy ()
		
		user = c_classes.mysqlUser (username, Self.realm)
		
		if not Self.db.get_user_data (user):
			user = None
		else:
			if cfg.CACHE_USERS:
				uid = user.getDBId ()
				Self.username_cache.add (username, uid)
				Self.user_cache.add (uid, user._copy ())
		
		if cfg.DEBUG:
			util.log_debug ("get_user (): End")
		
		return user
		
	def get_user_by_id (Self, user_id):
		if cfg.DEBUG:
			util.log_debug ("get_user_by_id (): Begin (%s)" % user_id)
	
		if cfg.CACHE_USERS:
			user = Self.user_cache.get (user_id)
			if not (user is None):
				if cfg.DEBUG: 
					util.log_debug ("get_user_by_id (): "
												"getting user from a cache.")
				return user._copy ()
	
		user = c_classes.mysqlUser ("<unknown>", Self.realm)
		user._setDBId (user_id)
		
		if not Self.db.get_user_data (user, 1):
			user = None
		else:
			if cfg.CACHE_USERS:
				Self.user_cache.add (user_id, user._copy ())
				
		if cfg.DEBUG:
			util.log_debug ("get_user_by_id (): End")
			
		return user
		
	def change_user_password (Self, user, new_pass, ptype = None):
		""" Changes user password. If password type is not specified,
			current type will be used.
		"""
		
		if ptype is None:
			ptype = user.getPasswordType ()

		if ptype == cfg.VDB_INVALID_PASSWORD_TYPE:
			return 0

		if ptype != 0:
			new_pass = passwords.encode (new_pass, ptype)
		
		ret = Self.db.change_user_password (user, new_pass, ptype)
		return ret

	def change_user_roles (Self, user, roles):
		roles = filter (lambda x: x not in cfg.ZOPE_SYSTEM_ROLES, roles)
		
		if cfg.AUTO_CREATE_ROLES:
			db_roles = Self.list_rolenames ()

			for r in roles:
				if not r in db_roles:
					Self.create_role (r)
					db_roles.append (r)
		
		ret = Self.db.change_user_roles (user, roles)
		Self.refresh_user (user)
		return ret
		
	def change_user_other (Self, user, realname, email):
		ret = Self.db.change_user_other (user, realname, email)
		Self.refresh_user (user)
		return ret

	def change_user_domains (Self, user, domains):
		ret = Self.db.change_user_domains (user, domains)
		Self.refresh_user (user)
		return ret
		
	def create_user (Self, username, roles):
		ok = Self.db.create_user (username, Self.realm, roles)
		
		if ok: user = Self.get_user (username)
		else: user = None
			
		return user
		
	def del_user (Self, username):
		ok = Self.db.del_user (username, Self.realm, 'user')
		return ok

	def create_role (Self, role):
		ok = Self.db.create_role (role, Self.realm)
		return ok

	def del_role (Self, role):
		ok = Self.db.del_role (role, Self.realm)
		return ok
		
# --------------------------------------------------------------------- */
# SessionDb class.

class SessionDb:
	""" Class that represents (local) interface to the session db.
	
		This class will cache sessions only if they belog to a valid user.
		Otherwise, some threads might have outdated session's user id in
		a cache.
	"""
	
	def __init__ (Self, db):
		Self.db = db
		if cfg.CACHE_SESSIONS:
			Self.session_cache = cache.v_cache (cfg.CACHE_SESSIONS_LIFE)
		
	def create_session (Self, user_id):
		s = c_classes.mysqlSession ()
		s._setUserId (user_id)
		
		Self.db.create_session (s)
		
		if user_id != cfg.VDB_INVALID_ID:
			if cfg.CACHE_SESSIONS:
				Self.session_cache.add (s.id, s._copy ())
		
		return s
		
	def get_session_by_id (Self, id):
		if cfg.CACHE_SESSIONS:
			o = Self.session_cache.get (id)
			if not (o is None):
				return o._copy ()
		
		s = c_classes.mysqlSession ()
		s._setId (id)	
		
		Self.db.get_session (s)
		
		if s.getId () == cfg.VDB_INVALID_ID:
			return None
			
		if (cfg.CACHE_SESSIONS) and (s.getUserId () != cfg.VDB_INVALID_ID):
			Self.session_cache.add (id, s._copy ())
		
		return s
		
	def assign_user_id (Self, session, user_id):
		assert (session.user_id == cfg.VDB_INVALID_ID)
	
		Self.db.session_set_user_id (session, user_id)
		session.user_id = user_id
		

# --------------------------------------------------------------------- */
# TokenDb class

class HTTPCookieInfo:
	pass

class TokenDb:
	""" This class defines API for token validation.
	
		Tokens are identified by (class_id, token_id). Random value is 
		assigned to a token when it is created. Life and timeout values are
		also recorded for tokens. 
		
		There are three general functions for tokens (create, delete, check) and 
		three functions that save tokens as cookies.
	"""
	
	def __init__ (Self, db, realm, http_cookie_info):
		Self.db = db
		Self.realm = realm
		Self.http_cookie_info = http_cookie_info

		if cfg.CACHE_TOKENS:
			Self.token_caches = {}
			
	def __get_cache_db (Self, class_id):
		d = Self.token_caches.get (class_id, None)
		
		if d is None:
			d = cache.v_cache (cfg.CACHE_TOKENS_LIFE)
			Self.token_caches [class_id] = d
						
		return d
			
	def __mk_cookie_names (Self, class_id):
		if Self.http_cookie_info.use_realm:
			bname = '__ac_%s_%s' % (Self.realm, class_id)
		else:
			bname = '__ac_%s' % (class_id)
		n1 = bname
		n2 = bname + '_value'
		
		return (n1, n2)
		
	def __mk_random_value (Self):
		a = array.array ('b')
	
		for i in range (cfg.TOKEN_RNDSTR_LEN):
			c = random.randrange (ord ('a'), ord ('z') + 1)
			a.append (c)
		
		return a.tostring ()
		
	def __mk_cookie_time_str (Self, ttuple):
		fmt = "%s, %s-%s-%s %s:%s:%s GMT"
		
		dn = cfg.COOKIE_DAY_NAMES [ttuple [6] - 1]
		mn = cfg.COOKIE_MONTH_NAMES [ttuple [1] - 1]
		
		sec = ttuple [5]
		if sec > 59: sec = 59
		
		s = fmt % (dn, ttuple [2], mn, ttuple [0], 
									ttuple [3], ttuple [4], sec)
									
		return s
		
	def __check_token_expired (Self, token_tuple, ctime):
		(db_token_value, db_token_created, db_token_accessed, 
							db_token_timeout, db_token_life) = token_tuple
		
		if db_token_life != -1:
			if ctime > (db_token_created + db_token_life): 
				return 1
		
		if db_token_timeout != -1:
			if ctime > (db_token_accessed + db_token_timeout): 
				return 1
				
		return 0

	def __check_token_value (Self, token_tuple, value):
		(db_token_value, db_token_created, db_token_accessed, 
							db_token_timeout, db_token_life) = token_tuple

		if db_token_value != value:
			return 0

		return 1
		
	def delete_token (Self, class_id, token_id):
		if cfg.CACHE_TOKENS:
			cache_db = Self.__get_cache_db (class_id)
			cache_db.delete (token_id)
	
		Self.db.del_token (class_id, token_id, Self.realm)
		
	def assign_new_token (Self, class_id, token_id, token_timeout, token_life):
		token_value = Self.__mk_random_value ()
		
		created_time = time.time ()
		accessed_time = created_time
		
		Self.db.create_token (class_id, token_id, token_value, 
					token_timeout, token_life, created_time, accessed_time,
																	Self.realm)
					
		return token_value
					
	def check_token (Self, class_id, token_id, token_value):
		ctime = time.time ()
	
		r = None
		found_in_cache = 0
		
		if cfg.CACHE_TOKENS:
			cache_db = Self.__get_cache_db (class_id)
			r = cache_db.get (token_id)
			
			if r:
				token_ok = ((not Self.__check_token_expired (r, ctime)) and
									Self.__check_token_value (r, token_value))
								
				if not token_ok:
					cache_db.delete (token_id)
					r = None					
				else:
					found_in_cache = 1

		if not found_in_cache: 
			r = Self.db.get_token (class_id, token_id, Self.realm)
		
		if r is None: 
			return cfg.VDB_INVALID_ID
					
		(db_token_value, db_token_created, db_token_accessed, 
									db_token_timeout, db_token_life) = r
		
		if not Self.__check_token_value (r, token_value): 
			util.log_debug ("check_token (%s, %s): Invalid token: "
												"value: '%s' != '%s'" \
						% (class_id, token_id, db_token_value, token_value))
			return cfg.VDB_INVALID_ID
		
		if not found_in_cache:
			if Self.__check_token_expired (r, ctime): 
				return cfg.VDB_INVALID_ID
		
			Self.db.update_token_accessed (class_id, token_id, 
													ctime, Self.realm)
			# Access time is not updated for tokens in cache
				
		if cfg.CACHE_TOKENS and (not found_in_cache): 
			cache_db.add (token_id, r)
		
		return token_id
		
	def request_delete_token (Self, RESPONSE, class_id, token_id):
		(n1, n2) = Self.__mk_cookie_names (class_id)
		
		cookie_path = Self.http_cookie_info.path
		cookie_domain = Self.http_cookie_info.domain
		
		if cookie_domain:
			RESPONSE.expireCookie (n1, path = cookie_path, 
													domain = cookie_domain)
			RESPONSE.expireCookie (n2, path = cookie_path,
													domain = cookie_domain)
		else:
			RESPONSE.expireCookie (n1, path = cookie_path)
			RESPONSE.expireCookie (n2, path = cookie_path)	
		
		Self.delete_token (class_id, token_id)
	
	def request_assign_new_token (Self, RESPONSE, class_id, token_id, 
							token_timeout, token_life, request_persistent):
		""" Creates new request (HTTP) token.
		
			This function will create a new token in the MySQL database and
			it will assign it to current request using setCookie () method.

			It request_persistent is false, expire parameter will not be
			passed to setCookie () maing cookies dissapear when browser ic
			closed.
		"""
							
		(n1, n2) = Self.__mk_cookie_names (class_id)
													
		token_value = Self.assign_new_token (class_id, token_id, 
													token_timeout, token_life)
		token_value_str = token_value
		token_id_str = "%i" % token_id

		if request_persistent:		
			if token_life < cfg.COOKIE_MIN_EXPIRE: 
								token_life = cfg.COOKIE_MIN_EXPIRE
		else:
			token_life = -1
	
		if token_life != -1:	
			created_time = time.time ()
		
			exp_time = created_time + token_life
			exp_time_str = Self.__mk_cookie_time_str (time.gmtime (exp_time))
		
		cookie_path = Self.http_cookie_info.path
		cookie_domain = Self.http_cookie_info.domain
		
		arg_list_1 = [n1, token_id_str]
		arg_list_2 = [n2, token_value_str]

		arg_dict = {}
		arg_dict ['path'] = cookie_path

		if cookie_domain:
			arg_dict ['domain'] = cookie_domain

		if request_persistent:
			arg_dict ['expires'] = exp_time_str

		apply (RESPONSE.setCookie, arg_list_1, arg_dict)
		apply (RESPONSE.setCookie, arg_list_2, arg_dict)
		
	def request_check_token (Self, REQUEST, class_id):
		(n1, n2) = Self.__mk_cookie_names (class_id)
		
		found = 0
		if REQUEST.cookies.has_key (n1) and REQUEST.cookies.has_key (n2):
			token_id = REQUEST.cookies [n1]
			token_value = REQUEST.cookies [n2]
			
			found = 1
			
			try:
				token_id = string.atol (token_id, 10)
			except:
				found = 0
		
		if not found:
			util.log_debug ("request_check_token (%s): no token in "
													"REQUEST." %  class_id)
			return cfg.VDB_INVALID_ID
		else:
			util.log_debug ("request_check_token (%s): "
				"Found token: '%s' = '%s'" % (class_id, token_id, token_value))
			
		token_ok = Self.check_token (class_id, token_id, token_value)
		return token_ok
		
# --------------------------------------------------------------------- */		
# MiscData database

class MiscDataDB:
	""" API for accessing misc data.
	
		Each object (identified by class and id) can have arbitrary number
		of miscdata tuples identified by id.
	"""

	def __init__ (Self, db):
		Self.db = db
		
	def save_data (Self, obj_class, obj_id, misc_id, misc_tuple):
		Self.db.save_misc_data (obj_class, obj_id, misc_id, misc_tuple)
		
	def get_data (Self, obj_class, obj_id, misc_id):
		return Self.db.get_misc_data (obj_class, obj_id, misc_id)
		
	def del_data (Self, obj_class, obj_id, misc_id):
		Self.db.del_misc_data (obj_class, obj_id, misc_id)
		

		
			
			
