comparison MoinMoin/web/session.py @ 5548:a42e6b2cd528

sessions: implant code from werkzeug 0.6, fix it
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 20 Feb 2010 02:53:25 +0100
parents be7c57d8e2a3
children 0dea6dbebafb
comparison
equal deleted inserted replaced
5547:19cd37af7e99 5548:a42e6b2cd528
9 9
10 @copyright: 2008 MoinMoin:FlorianKrupicka, 10 @copyright: 2008 MoinMoin:FlorianKrupicka,
11 2009 MoinMoin:ThomasWaldmann 11 2009 MoinMoin:ThomasWaldmann
12 @license: GNU GPL, see COPYING for details. 12 @license: GNU GPL, see COPYING for details.
13 """ 13 """
14 import time, os, tempfile 14 import sys, os
15 from os import path
16 import time
17 import tempfile
18 import re
15 try: 19 try:
16 from cPickle import load, dump, HIGHEST_PROTOCOL 20 from cPickle import load, dump, HIGHEST_PROTOCOL
17 except ImportError: 21 except ImportError:
18 from pickle import load, dump, HIGHEST_PROTOCOL 22 from pickle import load, dump, HIGHEST_PROTOCOL
19 23
20 from werkzeug.contrib.sessions import FilesystemSessionStore, Session 24 from werkzeug.contrib.sessions import SessionStore, ModificationTrackingDict
21 25
22 from MoinMoin import config 26 from MoinMoin import config
23 from MoinMoin.util import filesys 27 from MoinMoin.util import filesys
24 28
25 from MoinMoin import log 29 from MoinMoin import log
26 logging = log.getLogger(__name__) 30 logging = log.getLogger(__name__)
27 31
28 class MoinSession(Session): 32
29 """ Compatibility interface to Werkzeug-sessions for old Moin-code. """ 33 # start copy from werkzeug 0.6 - directly import this, if we require >= 0.6:
30 is_new = property(lambda s: s.new) 34
35 class Session(ModificationTrackingDict):
36 """Subclass of a dict that keeps track of direct object changes. Changes
37 in mutable structures are not tracked, for those you have to set
38 `modified` to `True` by hand.
39 """
40 __slots__ = ModificationTrackingDict.__slots__ + ('sid', 'new')
41
42 def __init__(self, data, sid, new=False):
43 ModificationTrackingDict.__init__(self, data)
44 self.sid = sid
45 self.new = new
46
47 @property
48 def should_save(self):
49 """True if the session should be saved.
50
51 .. versionchanged:: 0.6
52 By default the session is now only saved if the session is
53 modified, not if it is new like it was before.
54 """
55 return self.modified
31 56
32 def __repr__(self): 57 def __repr__(self):
33 # TODO: try to get this into werkzeug codebase
34 return '<%s %s %s%s>' % ( 58 return '<%s %s %s%s>' % (
35 self.__class__.__name__, 59 self.__class__.__name__,
36 self.sid, # we want to see sid 60 self.sid,
37 dict.__repr__(self), 61 dict.__repr__(self),
38 self.should_save and '*' or '' 62 self.should_save and '*' or ''
39 ) 63 )
40 64
41 65
42 class FixedFilesystemSessionStore(FilesystemSessionStore): 66 #: used for temporary files by the filesystem session store
43 """ 67 _fs_transaction_suffix = '.__wz_sess'
44 Fix buggy implementation of .get() in werkzeug <= 0.5: 68
45 69
46 If you try to get(somesid) and the file with the contents of sid storage 70 class FilesystemSessionStore(SessionStore):
47 does not exist or is troublesome somehow, it will create a new session 71 """Simple example session store that saves sessions in the filesystem like
48 with a new sid in werkzeug 0.5 original implementation. 72 PHP does.
49 73
50 But we do not want to store a session file for new and empty sessions, 74 .. versionchanged:: 0.6
51 but rather wait for the 2nd request and see whether the user agent sends 75 `renew_missing` was added. Previously this was considered `True`,
52 the cookie back to us. If it doesn't support cookies, we don't want to 76 now the default changed to `False` and it can be explicitly
53 create one new session file per request. If it does support cookies, we 77 deactivated.
54 need to use .get() with the sid although there was no session file stored 78
55 for that sid in the first request. 79 :param path: the path to the folder used for storing the sessions.
56 80 If not provided the default temporary directory is used.
57 TODO: try to get it into werkzeug codebase and remove this class after 81 :param filename_template: a string template used to give the session
58 we REQUIRE a werkzeug release > 0.5 that has it. 82 a filename. ``%s`` is replaced with the
59 """ 83 session id.
84 :param session_class: The session class to use. Defaults to
85 :class:`Session`.
86 :param renew_missing: set to `True` if you want the store to
87 give the user a new sid if the session was
88 not yet saved.
89 """
90
91 def __init__(self, path=None, filename_template='werkzeug_%s.sess',
92 session_class=None, renew_missing=False, mode=0644):
93 SessionStore.__init__(self, session_class)
94 if path is None:
95 path = gettempdir()
96 self.path = path
97 if isinstance(filename_template, unicode):
98 filename_template = filename_template.encode(
99 sys.getfilesystemencoding() or 'utf-8')
100 assert not filename_template.endswith(_fs_transaction_suffix), \
101 'filename templates may not end with %s' % _fs_transaction_suffix
102 self.filename_template = filename_template
103 self.renew_missing = renew_missing
104 self.mode = mode
105
106 def get_session_filename(self, sid):
107 if isinstance(sid, unicode):
108 sid = sid.encode('utf-8')
109 return path.join(self.path, self.filename_template % sid)
110
111 def save(self, session):
112 def _dump(filename):
113 f = file(filename, 'wb')
114 try:
115 dump(dict(session), f, HIGHEST_PROTOCOL)
116 finally:
117 f.close()
118 fn = self.get_session_filename(session.sid)
119 if os.name == 'posix':
120 td, tmp = tempfile.mkstemp(suffix=_fs_transaction_suffix,
121 dir=self.path)
122 _dump(tmp)
123 try:
124 os.rename(tmp, fn)
125 except (IOError, OSError):
126 pass
127 os.chmod(fn, self.mode)
128 else:
129 _dump(fn)
130 try:
131 os.chmod(fn, self.mode)
132 except OSError:
133 # maybe some platforms fail here, have not found
134 # any that do thought.
135 pass
136
137 def delete(self, session):
138 fn = self.get_session_filename(session.sid)
139 try:
140 os.unlink(fn)
141 except OSError:
142 pass
143
60 def get(self, sid): 144 def get(self, sid):
61 if not self.is_valid_key(sid): 145 if not self.is_valid_key(sid):
62 return self.new() 146 return self.new()
63 fn = self.get_session_filename(sid)
64 f = None
65 try: 147 try:
148 f = open(self.get_session_filename(sid), 'rb')
149 except IOError:
150 if self.renew_missing:
151 return self.new()
152 data = {}
153 else:
66 try: 154 try:
67 f = open(fn, 'rb') 155 try:
68 data = load(f) 156 data = load(f)
69 except (IOError, EOFError, KeyError): # XXX check completeness/correctness 157 except Exception:
70 # Note: we do NOT generate a new sid in case of trouble with session *contents* 158 data = {}
71 # IOError: [Errno 2] No such file or directory 159 finally:
72 # IOError: [Errno 13] Permission denied (we will notice permission problems when writing)
73 # EOFError: when trying to load("") - no contents
74 # KeyError: when trying to load("xxx") - crap contents
75 data = {}
76 finally:
77 if f:
78 f.close() 160 f.close()
79 return self.session_class(data, sid, False) 161 return self.session_class(data, sid, False)
80 162
163 def list(self):
164 """Lists all sessions in the store.
165
166 .. versionadded:: 0.6
167 """
168 before, after = self.filename_template.split('%s', 1)
169 filename_re = re.compile(r'%s(.{5,})%s$' % (re.escape(before),
170 re.escape(after)))
171 result = []
172 for filename in os.listdir(self.path):
173 #: this is a session that is still being saved.
174 if filename.endswith(_fs_transaction_suffix):
175 continue
176 match = filename_re.match(filename)
177 if match is not None:
178 result.append(match.group(1))
179 return result
180
181 # end copy of werkzeug 0.6 code
182
183
184 class FixedFilesystemSessionStore(FilesystemSessionStore):
81 """ 185 """
82 Problem: werkzeug 0.5 just directly and non-atomically writes to the session 186 Problem: werkzeug 0.5 just directly and non-atomically writes to the session
83 file when using save(). If another process or thread uses get() after 187 file when using save(). If another process or thread uses get() after
84 save() opened the file for writing, it will get 0 bytes session content, 188 save() opened the file for writing, it will get 0 bytes session content,
85 because open(..., "wb") truncated the file already. 189 because open(..., "wb") truncated the file already.
190
191 werkzeug 0.6 save() is still broken: it reopens the file and does not
192 use the fd given by mkstemp. It still fails in the same way on win32
193 as 0.5 did for both posix and win32 (see above).
86 """ 194 """
87 def save(self, session): 195 def save(self, session):
88 fd, temp_fname = tempfile.mkstemp(suffix='.tmp', dir=self.path) 196 fd, temp_fname = tempfile.mkstemp(suffix=_fs_transaction_suffix, dir=self.path)
89 f = os.fdopen(fd, 'wb') 197 f = os.fdopen(fd, 'wb')
90 try: 198 try:
91 dump(dict(session), f, HIGHEST_PROTOCOL) 199 dump(dict(session), f, HIGHEST_PROTOCOL)
92 finally: 200 finally:
93 f.close() 201 f.close()
94 filesys.chmod(temp_fname, 0666 & config.umask) # relax restrictive mode from mkstemp 202 filesys.chmod(temp_fname, self.mode) # relax restrictive mode from mkstemp
95 fname = self.get_session_filename(session.sid) 203 fname = self.get_session_filename(session.sid)
96 # this is either atomic or happening with real locks set: 204 filesys.rename(temp_fname, fname) # atomic (posix) or quick (win32)
97 filesys.rename(temp_fname, fname) 205
98 206 """
99 """ 207 Problem: werkzeug 0.6 uses inconsistent encoding for template and filename
100 Adds functionality missing in werkzeug 0.5: getting a list of all SIDs, 208 """
101 so that purging sessions can be implemented. 209 def _encode_fs(self, name): # TODO: call this from FilesystemSessionStore.__init__
102 """ 210 if isinstance(name, unicode):
103 def get_all_sids(self): 211 name = name.encode(sys.getfilesystemencoding() or 'utf-8')
104 """ 212 return name
105 return a list of all session ids (sids) 213
106 """ 214 def get_session_filename(self, sid):
107 import re 215 sid = self._encode_fs(sid)
108 regex = re.compile(re.escape(self.filename_template).replace( 216 return path.join(self.path, self.filename_template % sid)
109 r'\%s', r'([0-9a-fA-F]+)')) # sid is hex only, do not match *.tmp 217
110 sids = [] 218
111 for fn in os.listdir(self.path): 219 class MoinSession(Session):
112 m = regex.match(fn) 220 """ Compatibility interface to Werkzeug-sessions for old Moin-code.
113 if m: 221
114 sids.append(m.group(1)) 222 is_new is DEPRECATED and will go away soon.
115 return sids 223 """
224 def _get_is_new(self):
225 logging.warning("Deprecated use of MoinSession.is_new, please use .new")
226 return self.new
227 is_new = property(_get_is_new)
116 228
117 229
118 class SessionService(object): 230 class SessionService(object):
119 """ 231 """
120 A session service returns a session object given a request object and 232 A session service returns a session object given a request object and
220 path = request.cfg.session_dir 332 path = request.cfg.session_dir
221 try: 333 try:
222 filesys.mkdir(path) 334 filesys.mkdir(path)
223 except OSError: 335 except OSError:
224 pass 336 pass
225 return FixedFilesystemSessionStore(path=path, filename_template='%s', session_class=MoinSession) 337 return FixedFilesystemSessionStore(path=path, filename_template='%s',
338 session_class=MoinSession, mode=0666 & config.umask)
226 339
227 def get_session(self, request, sid=None): 340 def get_session(self, request, sid=None):
228 if sid is None: 341 if sid is None:
229 cookie_name = get_cookie_name(request, name=request.cfg.cookie_name, usage=self.cookie_usage) 342 cookie_name = get_cookie_name(request, name=request.cfg.cookie_name, usage=self.cookie_usage)
230 sid = request.cookies.get(cookie_name, None) 343 sid = request.cookies.get(cookie_name, None)
237 logging.debug("get_session returns session %r" % session) 350 logging.debug("get_session returns session %r" % session)
238 return session 351 return session
239 352
240 def get_all_session_ids(self, request): 353 def get_all_session_ids(self, request):
241 store = self._store_get(request) 354 store = self._store_get(request)
242 return store.get_all_sids() 355 return store.list()
243 356
244 def destroy_session(self, request, session): 357 def destroy_session(self, request, session):
245 session.clear() 358 session.clear()
246 store = self._store_get(request) 359 store = self._store_get(request)
247 store.delete(session) 360 store.delete(session)
292 if ((not userobj.valid and not session.new # anon users with a cookie (not first request) 405 if ((not userobj.valid and not session.new # anon users with a cookie (not first request)
293 or 406 or
294 userobj.valid) # logged-in users, even if THIS was the first request (no cookie yet) 407 userobj.valid) # logged-in users, even if THIS was the first request (no cookie yet)
295 # XXX if UA doesn't support cookies, this creates 1 session file per request 408 # XXX if UA doesn't support cookies, this creates 1 session file per request
296 and 409 and
297 session.modified): # only if we really have something to save 410 session.should_save): # only if we really have something to save
298 # add some info about expiry to the sessions, so we can purge them: 411 # add some info about expiry to the sessions, so we can purge them:
299 session['expires'] = cookie_expires 412 session['expires'] = cookie_expires
300 # note: currently, every request of a logged-in user will save 413 # note: currently, every request of a logged-in user will save
301 # the session, even when always requesting same page. 414 # the session, even when always requesting same page.
302 # No big deal, as we store the trail into session and that 415 # No big deal, as we store the trail into session and that