1 #!/usr/bin/env python 2 3 """ 4 Session helper functions. 5 6 Copyright (C) 2004, 2005 Paul Boddie <paul@boddie.org.uk> 7 8 This library is free software; you can redistribute it and/or 9 modify it under the terms of the GNU Lesser General Public 10 License as published by the Free Software Foundation; either 11 version 2.1 of the License, or (at your option) any later version. 12 13 This library is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 Lesser General Public License for more details. 17 18 You should have received a copy of the GNU Lesser General Public 19 License along with this library; if not, write to the Free Software 20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 """ 22 23 import shelve 24 import os 25 import time 26 import random 27 import sys 28 29 class SessionStore: 30 31 "A class representing a session store." 32 33 def __init__(self, trans, session_directory, session_cookie_name="SID", concurrent=1, delay=1): 34 35 """ 36 Initialise the session store, specifying the transaction 'trans' within 37 which all session access will occur, a base 'session_directory', the 38 optional 'session_cookie_name' where the session identifier is held for 39 each user, and specifying using the optional 'concurrent' parameter 40 whether concurrent access within the framework might occur (1) or 41 whether the framework queues accesses at some other level (0). The 42 optional 'delay' argument specifies the time in seconds between each 43 poll of the session file when that file is found to be locked for 44 editing. 45 """ 46 47 self.trans = trans 48 self.session_directory = session_directory 49 self.session_cookie_name = session_cookie_name 50 self.concurrent = concurrent 51 self.delay = delay 52 53 # Internal state. 54 55 self.store = None 56 self.store_filename, self.edit_filename = None, None 57 self.to_expire = None 58 59 # Attempt to create a session directory if it does not exist. 60 61 if not os.path.exists(self.session_directory): 62 os.mkdir(self.session_directory) 63 64 def close(self): 65 66 "Close the store, tidying up files and filenames." 67 68 if self.store is not None: 69 self.store.close() 70 self.store = None 71 if self.edit_filename is not None: 72 try: 73 os.rename(self.edit_filename, self.store_filename) 74 except OSError: 75 pass 76 self.edit_filename, self.store_filename = None, None 77 78 # Handle expiry appropriately. 79 80 if self.to_expire is not None: 81 self._expire_session(self.to_expire) 82 self.trans.delete_cookie(self.session_cookie_name) 83 84 def expire_session(self): 85 86 """ 87 Expire the session in the given transaction. 88 """ 89 90 # Perform expiry. 91 92 cookie = self.trans.get_cookie(self.session_cookie_name) 93 if cookie: 94 self.to_expire = cookie.value 95 96 def _expire_session(self, session_id): 97 98 """ 99 Expire the session with the given 'session_id'. Note that in concurrent 100 session stores, this operation will block if another execution context 101 is editing the session. 102 """ 103 104 filename = os.path.join(self.session_directory, session_id) 105 if self.concurrent: 106 while 1: 107 try: 108 os.unlink(filename) 109 except OSError: 110 time.sleep(self.delay) 111 else: 112 break 113 else: 114 try: 115 os.unlink(filename) 116 except OSError: 117 pass 118 119 def get_session(self, create): 120 121 """ 122 Get the session for the given transaction, creating a new session if 123 'create' is set to 1 (rather than 0). Where new sessions are created, an 124 appropriate session identifier cookie will be created. 125 Returns a session object or None if no session exists and none is then 126 created. 127 """ 128 129 if self.store is not None: 130 return self.store 131 132 # No existing store - get the token and the actual session. 133 134 cookie = self.trans.get_cookie(self.session_cookie_name) 135 if cookie: 136 return self._get_session(cookie.value, create) 137 elif create: 138 session_id = self._get_session_identifier() 139 self.trans.set_cookie_value(self.session_cookie_name, session_id) 140 return self._get_session(session_id, create) 141 else: 142 return None 143 144 def _get_session(self, session_id, create): 145 146 """ 147 Get a session with the given 'session_id' and whether new sessions 148 should be created ('create' set to 1). 149 Returns a dictionary-like object representing the session. 150 """ 151 152 filename = os.path.join(self.session_directory, session_id) 153 154 # Enforce locking. 155 156 if self.concurrent: 157 158 # Where the session is present (possibly being edited)... 159 160 if os.path.exists(filename) or os.path.exists(filename + ".edit"): 161 while 1: 162 try: 163 os.rename(filename, filename + ".edit") 164 except OSError: 165 time.sleep(self.delay) 166 else: 167 break 168 169 # Where no session is present and none should be created, return. 170 171 elif not create: 172 return None 173 174 self.store_filename = filename 175 filename = filename + ".edit" 176 self.edit_filename = filename 177 178 # For non-concurrent situations, return if no session exists and none 179 # should be created. 180 181 elif not os.path.exists(filename) and not create: 182 return None 183 184 self.store = shelve.open(filename) 185 return Wrapper(self.store) 186 187 def _get_session_identifier(self): 188 189 "Return a session identifier as a string." 190 191 g = random.Random() 192 return str(g.randint(0, sys.maxint - 1)) 193 194 class Wrapper: 195 196 "A wrapper around shelf objects." 197 198 def __init__(self, store): 199 self.store = store 200 201 def __getattr__(self, name): 202 if hasattr(self.store, name): 203 return getattr(self.store, name) 204 else: 205 raise AttributeError, name 206 207 def __getitem__(self, name): 208 # Convert to UTF-8 to avoid bsddb limitations. 209 return self.store[name.encode("utf-8")] 210 211 def __delitem__(self, name): 212 # Convert to UTF-8 to avoid bsddb limitations. 213 del self.store[name.encode("utf-8")] 214 215 def __setitem__(self, name, value): 216 # Convert to UTF-8 to avoid bsddb limitations. 217 self.store[name.encode("utf-8")] = value 218 219 def keys(self): 220 l = [] 221 for key in self.store.keys(): 222 # Convert from UTF-8 to avoid bsddb limitations. 223 l.append(unicode(key, "utf-8")) 224 return l 225 226 def items(self): 227 l = [] 228 for key in self.keys(): 229 l.append((key, self[key])) 230 return l 231 232 # vim: tabstop=4 expandtab shiftwidth=4