Trac patch for fetching sessions outside of a request
18 March 2008
17:25
as Diff
| 1 | diff --git a/trac/web/session.py b/trac/web/session.py |
|---|---|
| 2 | index c8f724b..cb3b268 100644 |
| 3 | --- a/trac/web/session.py |
| 4 | +++ b/trac/web/session.py |
| 5 | @@ -28,17 +28,104 @@ PURGE_AGE = 3600*24*90 # Purge session after 90 days idle |
| 6 | COOKIE_KEY = 'trac_session' |
| 7 | |
| 8 | |
| 9 | -class Session(dict): |
| 10 | - """Basic session handling and per-session storage.""" |
| 11 | - |
| 12 | - def __init__(self, env, req): |
| 13 | +class DetachedSession(dict): |
| 14 | + def __init__(self, env, sid): |
| 15 | dict.__init__(self) |
| 16 | self.env = env |
| 17 | - self.req = req |
| 18 | self.sid = None |
| 19 | self.last_visit = 0 |
| 20 | self._new = True |
| 21 | self._old = {} |
| 22 | + if sid: |
| 23 | + self.get_session(sid, authenticated=True) |
| 24 | + else: |
| 25 | + self.authenticated = False |
| 26 | + |
| 27 | + def get_session(self, sid, authenticated=False): |
| 28 | + self.env.log.debug('Retrieving session for ID %r', sid) |
| 29 | + |
| 30 | + db = self.env.get_db_cnx() |
| 31 | + cursor = db.cursor() |
| 32 | + |
| 33 | + self.sid = sid |
| 34 | + self.authenticated = authenticated |
| 35 | + |
| 36 | + cursor.execute("SELECT last_visit FROM session " |
| 37 | + "WHERE sid=%s AND authenticated=%s", |
| 38 | + (sid, int(authenticated))) |
| 39 | + row = cursor.fetchone() |
| 40 | + if not row: |
| 41 | + return |
| 42 | + self._new = False |
| 43 | + self.last_visit = int(row[0]) |
| 44 | + |
| 45 | + cursor.execute("SELECT name,value FROM session_attribute " |
| 46 | + "WHERE sid=%s and authenticated=%s", |
| 47 | + (sid, int(authenticated))) |
| 48 | + for name, value in cursor: |
| 49 | + self[name] = value |
| 50 | + self._old.update(self) |
| 51 | + |
| 52 | + def save(self): |
| 53 | + if not self._old and not self.items(): |
| 54 | + # The session doesn't have associated data, so there's no need to |
| 55 | + # persist it |
| 56 | + return |
| 57 | + |
| 58 | + db = self.env.get_db_cnx() |
| 59 | + cursor = db.cursor() |
| 60 | + authenticated = int(self.authenticated) |
| 61 | + |
| 62 | + if self._new: |
| 63 | + self._new = False |
| 64 | + cursor.execute("INSERT INTO session (sid,last_visit,authenticated)" |
| 65 | + " VALUES(%s,%s,%s)", |
| 66 | + (self.sid, self.last_visit, authenticated)) |
| 67 | + if self._old != self: |
| 68 | + attrs = [(self.sid, authenticated, k, v) for k, v in self.items()] |
| 69 | + cursor.execute("DELETE FROM session_attribute WHERE sid=%s", |
| 70 | + (self.sid,)) |
| 71 | + self._old = dict(self.items()) |
| 72 | + if attrs: |
| 73 | + cursor.executemany("INSERT INTO session_attribute " |
| 74 | + "(sid,authenticated,name,value) " |
| 75 | + "VALUES(%s,%s,%s,%s)", attrs) |
| 76 | + elif not authenticated: |
| 77 | + # No need to keep around empty unauthenticated sessions |
| 78 | + cursor.execute("DELETE FROM session " |
| 79 | + "WHERE sid=%s AND authenticated=0", (self.sid,)) |
| 80 | + return |
| 81 | + |
| 82 | + now = int(time.time()) |
| 83 | + # Update the session last visit time if it is over an hour old, |
| 84 | + # so that session doesn't get purged |
| 85 | + if now - self.last_visit > UPDATE_INTERVAL: |
| 86 | + self.last_visit = now |
| 87 | + self.env.log.info("Refreshing session %s" % self.sid) |
| 88 | + cursor.execute('UPDATE session SET last_visit=%s ' |
| 89 | + 'WHERE sid=%s AND authenticated=%s', |
| 90 | + (self.last_visit, self.sid, authenticated)) |
| 91 | + # Purge expired sessions. We do this only when the session was |
| 92 | + # changed as to minimize the purging. |
| 93 | + mintime = now - PURGE_AGE |
| 94 | + self.env.log.debug('Purging old, expired, sessions.') |
| 95 | + cursor.execute("DELETE FROM session_attribute " |
| 96 | + "WHERE authenticated=0 AND sid " |
| 97 | + "IN (SELECT sid FROM session WHERE " |
| 98 | + "authenticated=0 AND last_visit < %s)", |
| 99 | + (mintime,)) |
| 100 | + cursor.execute("DELETE FROM session WHERE " |
| 101 | + "authenticated=0 AND last_visit < %s", |
| 102 | + (mintime,)) |
| 103 | + db.commit() |
| 104 | + |
| 105 | + |
| 106 | +class Session(DetachedSession): |
| 107 | + """Basic session handling and per-session storage.""" |
| 108 | + |
| 109 | + def __init__(self, env, req): |
| 110 | + DetachedSession.__init__(self, env, None) |
| 111 | + self.req = req |
| 112 | if req.authname == 'anonymous': |
| 113 | if not req.incookie.has_key(COOKIE_KEY): |
| 114 | self.sid = hex_entropy(24) |
| 115 | @@ -59,34 +146,15 @@ class Session(dict): |
| 116 | self.req.outcookie[COOKIE_KEY]['expires'] = expires |
| 117 | |
| 118 | def get_session(self, sid, authenticated=False): |
| 119 | - self.env.log.debug('Retrieving session for ID %r', sid) |
| 120 | - |
| 121 | - db = self.env.get_db_cnx() |
| 122 | - cursor = db.cursor() |
| 123 | refresh_cookie = False |
| 124 | |
| 125 | if self.sid and sid != self.sid: |
| 126 | refresh_cookie = True |
| 127 | - self.sid = sid |
| 128 | |
| 129 | - cursor.execute("SELECT last_visit FROM session " |
| 130 | - "WHERE sid=%s AND authenticated=%s", |
| 131 | - (sid, int(authenticated))) |
| 132 | - row = cursor.fetchone() |
| 133 | - if not row: |
| 134 | - return |
| 135 | - self._new = False |
| 136 | - self.last_visit = int(row[0]) |
| 137 | + DetachedSession.get_session(self, sid, authenticated) |
| 138 | if self.last_visit and time.time() - self.last_visit > UPDATE_INTERVAL: |
| 139 | refresh_cookie = True |
| 140 | |
| 141 | - cursor.execute("SELECT name,value FROM session_attribute " |
| 142 | - "WHERE sid=%s and authenticated=%s", |
| 143 | - (sid, int(authenticated))) |
| 144 | - for name, value in cursor: |
| 145 | - self[name] = value |
| 146 | - self._old.update(self) |
| 147 | - |
| 148 | # Refresh the session cookie if this is the first visit since over a day |
| 149 | if not authenticated and refresh_cookie: |
| 150 | self.bake_cookie() |
| 151 | @@ -157,56 +225,3 @@ class Session(dict): |
| 152 | |
| 153 | self.sid = sid |
| 154 | self.bake_cookie(0) # expire the cookie |
| 155 | - |
| 156 | - def save(self): |
| 157 | - if not self._old and not self.items(): |
| 158 | - # The session doesn't have associated data, so there's no need to |
| 159 | - # persist it |
| 160 | - return |
| 161 | - |
| 162 | - db = self.env.get_db_cnx() |
| 163 | - cursor = db.cursor() |
| 164 | - authenticated = int(self.req.authname != 'anonymous') |
| 165 | - |
| 166 | - if self._new: |
| 167 | - self._new = False |
| 168 | - cursor.execute("INSERT INTO session (sid,last_visit,authenticated)" |
| 169 | - " VALUES(%s,%s,%s)", |
| 170 | - (self.sid, self.last_visit, authenticated)) |
| 171 | - if self._old != self: |
| 172 | - attrs = [(self.sid, authenticated, k, v) for k, v in self.items()] |
| 173 | - cursor.execute("DELETE FROM session_attribute WHERE sid=%s", |
| 174 | - (self.sid,)) |
| 175 | - self._old = dict(self.items()) |
| 176 | - if attrs: |
| 177 | - cursor.executemany("INSERT INTO session_attribute " |
| 178 | - "(sid,authenticated,name,value) " |
| 179 | - "VALUES(%s,%s,%s,%s)", attrs) |
| 180 | - elif not authenticated: |
| 181 | - # No need to keep around empty unauthenticated sessions |
| 182 | - cursor.execute("DELETE FROM session " |
| 183 | - "WHERE sid=%s AND authenticated=0", (self.sid,)) |
| 184 | - return |
| 185 | - |
| 186 | - now = int(time.time()) |
| 187 | - # Update the session last visit time if it is over an hour old, |
| 188 | - # so that session doesn't get purged |
| 189 | - if now - self.last_visit > UPDATE_INTERVAL: |
| 190 | - self.last_visit = now |
| 191 | - self.env.log.info("Refreshing session %s" % self.sid) |
| 192 | - cursor.execute('UPDATE session SET last_visit=%s ' |
| 193 | - 'WHERE sid=%s AND authenticated=%s', |
| 194 | - (self.last_visit, self.sid, authenticated)) |
| 195 | - # Purge expired sessions. We do this only when the session was |
| 196 | - # changed as to minimize the purging. |
| 197 | - mintime = now - PURGE_AGE |
| 198 | - self.env.log.debug('Purging old, expired, sessions.') |
| 199 | - cursor.execute("DELETE FROM session_attribute " |
| 200 | - "WHERE authenticated=0 AND sid " |
| 201 | - "IN (SELECT sid FROM session WHERE " |
| 202 | - "authenticated=0 AND last_visit < %s)", |
| 203 | - (mintime,)) |
| 204 | - cursor.execute("DELETE FROM session WHERE " |
| 205 | - "authenticated=0 AND last_visit < %s", |
| 206 | - (mintime,)) |
| 207 | - db.commit() |
| 208 | diff --git a/trac/web/tests/session.py b/trac/web/tests/session.py |
| 209 | index ddb31d1..13b5fcd 100644 |
| 210 | --- a/trac/web/tests/session.py |
| 211 | +++ b/trac/web/tests/session.py |
| 212 | @@ -6,7 +6,7 @@ from trac.core import TracError |
| 213 | from trac.log import logger_factory |
| 214 | from trac.test import EnvironmentStub, Mock |
| 215 | from trac.web.href import Href |
| 216 | -from trac.web.session import Session, PURGE_AGE, UPDATE_INTERVAL |
| 217 | +from trac.web.session import DetachedSession, Session, PURGE_AGE, UPDATE_INTERVAL |
| 218 | |
| 219 | |
| 220 | class SessionTestCase(unittest.TestCase): |
| 221 | @@ -286,6 +286,42 @@ class SessionTestCase(unittest.TestCase): |
| 222 | "authenticated=0") |
| 223 | self.assertAlmostEqual(now, int(cursor.fetchone()[0]), -1) |
| 224 | |
| 225 | + def test_modify_detached_session(self): |
| 226 | + """ |
| 227 | + Verify that a modifying a variable in a session not associated with a |
| 228 | + request updates the database accordingly. |
| 229 | + """ |
| 230 | + cursor = self.db.cursor() |
| 231 | + cursor.execute("INSERT INTO session VALUES ('john', 1, 0)") |
| 232 | + cursor.execute("INSERT INTO session_attribute VALUES " |
| 233 | + "('john', 1, 'foo', 'bar')") |
| 234 | + |
| 235 | + session = DetachedSession(self.env, 'john') |
| 236 | + self.assertEqual('bar', session['foo']) |
| 237 | + session['foo'] = 'baz' |
| 238 | + session.save() |
| 239 | + cursor.execute("SELECT value FROM session_attribute " |
| 240 | + "WHERE sid='john' AND name='foo'") |
| 241 | + self.assertEqual('baz', cursor.fetchone()[0]) |
| 242 | + |
| 243 | + def test_delete_detached_session_var(self): |
| 244 | + """ |
| 245 | + Verify that removing a variable in a session not associated with a |
| 246 | + request deletes the variable from the database. |
| 247 | + """ |
| 248 | + cursor = self.db.cursor() |
| 249 | + cursor.execute("INSERT INTO session VALUES ('john', 1, 0)") |
| 250 | + cursor.execute("INSERT INTO session_attribute VALUES " |
| 251 | + "('john', 1, 'foo', 'bar')") |
| 252 | + |
| 253 | + session = DetachedSession(self.env, 'john') |
| 254 | + self.assertEqual('bar', session['foo']) |
| 255 | + del session['foo'] |
| 256 | + session.save() |
| 257 | + cursor.execute("SELECT COUNT(*) FROM session_attribute " |
| 258 | + "WHERE sid='john' AND name='foo'") |
| 259 | + self.assertEqual(0, cursor.fetchone()[0]) |
| 260 | + |
| 261 | |
| 262 | def suite(): |
| 263 | return unittest.makeSuite(SessionTestCase, 'test') |
Comments
No comments so far.
