backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 04 Aug 2010 11:13:11 +0200
changeset 6066 953578709324
parent 6055 bb7bd9cafacf (diff)
parent 6064 2a164fabcbfc (current diff)
child 6067 efca814587e2
backport stable
dbapi.py
server/repository.py
web/webconfig.py
--- a/dbapi.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/dbapi.py	Wed Aug 04 11:13:11 2010 +0200
@@ -313,19 +313,17 @@
 
     # low level session data management #######################################
 
-    def get_shared_data(self, key, default=None, pop=False):
-        """return value associated to `key` in shared data"""
-        return self.cnx.get_shared_data(key, default, pop)
-
-    def set_shared_data(self, key, value, querydata=False):
-        """set value associated to `key` in shared data
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
+        """see :meth:`Connection.get_shared_data`"""
+        return self.cnx.get_shared_data(key, default, pop, txdata)
 
-        if `querydata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction, and won't be available through the connexion, only on the
-        repository side.
-        """
-        return self.cnx.set_shared_data(key, value, querydata)
+    def set_shared_data(self, key, value, txdata=False, querydata=None):
+        """see :meth:`Connection.set_shared_data`"""
+        if querydata is not None:
+            txdata = querydata
+            warn('[3.10] querydata argument has been renamed to txdata',
+                 DeprecationWarning, stacklevel=2)
+        return self.cnx.set_shared_data(key, value, txdata)
 
     # server session compat layer #############################################
 
@@ -507,10 +505,12 @@
         return DBAPIRequest(self.vreg, DBAPISession(self))
 
     def check(self):
-        """raise `BadConnectionId` if the connection is no more valid"""
+        """raise `BadConnectionId` if the connection is no more valid, else
+        return its latest activity timestamp.
+        """
         if self._closed is not None:
             raise ProgrammingError('Closed connection')
-        self._repo.check_session(self.sessionid)
+        return self._repo.check_session(self.sessionid)
 
     def set_session_props(self, **props):
         """raise `BadConnectionId` if the connection is no more valid"""
@@ -518,23 +518,29 @@
             raise ProgrammingError('Closed connection')
         self._repo.set_session_props(self.sessionid, props)
 
-    def get_shared_data(self, key, default=None, pop=False):
-        """return value associated to `key` in shared data"""
-        if self._closed is not None:
-            raise ProgrammingError('Closed connection')
-        return self._repo.get_shared_data(self.sessionid, key, default, pop)
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
+        """return value associated to key in the session's data dictionary or
+        session's transaction's data if `txdata` is true.
 
-    def set_shared_data(self, key, value, querydata=False):
-        """set value associated to `key` in shared data
+        If pop is True, value will be removed from the dictionnary.
 
-        if `querydata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction, and won't be available through the connexion, only on the
-        repository side.
+        If key isn't defined in the dictionnary, value specified by the
+        `default` argument will be returned.
         """
         if self._closed is not None:
             raise ProgrammingError('Closed connection')
-        return self._repo.set_shared_data(self.sessionid, key, value, querydata)
+        return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
+
+    def set_shared_data(self, key, value, txdata=False):
+        """set value associated to `key` in shared data
+
+        if `txdata` is true, the value will be added to the repository session's
+        transaction's data which are cleared on commit/rollback of the current
+        transaction.
+        """
+        if self._closed is not None:
+            raise ProgrammingError('Closed connection')
+        return self._repo.set_shared_data(self.sessionid, key, value, txdata)
 
     def get_schema(self):
         """Return the schema currently used by the repository.
--- a/entities/__init__.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/entities/__init__.py	Wed Aug 04 11:13:11 2010 +0200
@@ -35,6 +35,11 @@
     __regid__ = 'Any'
     __implements__ = ()
 
+    @classmethod
+    def cw_create_url(cls, req, **kwargs):
+        """ return the url of the entity creation form for this entity type"""
+        return req.build_url('add/%s' % cls.__regid__, **kwargs)
+
     # meta data api ###########################################################
 
     def dc_title(self):
--- a/etwist/twconfig.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/etwist/twconfig.py	Wed Aug 04 11:13:11 2010 +0200
@@ -76,12 +76,6 @@
 the repository rather than the user running the command',
           'group': 'main', 'level': WebConfiguration.mode == 'system'
           }),
-        ('session-time',
-         {'type' : 'time',
-          'default': '30min',
-          'help': 'session expiration time, default to 30 minutes',
-          'group': 'main', 'level': 1,
-          }),
         ('pyro-server',
          {'type' : 'yn',
           # pyro is only a recommends by default, so don't activate it here
--- a/goa/appobjects/components.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/goa/appobjects/components.py	Wed Aug 04 11:13:11 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""overrides some base views for cubicweb on google appengine
+"""overrides some base views for cubicweb on google appengine"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from logilab.mtconverter import xml_escape
@@ -88,7 +87,8 @@
         view = self.vreg.select('views', 'list', req, req.etype_rset(etype))
         url = view.url()
         etypelink = u'&#160;<a href="%s">%s</a>' % (xml_escape(url), label)
-        yield (label, etypelink, self.add_entity_link(eschema, req))
+        if eschema.has_perm(req, 'add'):
+            yield (label, etypelink, self.add_entity_link(etype))
 
 ManageView.entity_types = entity_types_no_count
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.10.0_common.py	Wed Aug 04 11:13:11 2010 +0200
@@ -0,0 +1,1 @@
+option_group_changed('cleanup-session-time', 'web', 'main')
--- a/server/repository.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/server/repository.py	Wed Aug 04 11:13:11 2010 +0200
@@ -267,7 +267,10 @@
             # call instance level initialisation hooks
             self.hm.call_hooks('server_startup', repo=self)
             # register a task to cleanup expired session
-            self.looping_task(self.config['session-time']/3., self.clean_sessions)
+            self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24
+            assert self.cleanup_session_time > 0
+            cleanup_session_interval = min(60*60, self.cleanup_session_time / 3)
+            self.looping_task(cleanup_session_interval, self.clean_sessions)
         assert isinstance(self._looping_tasks, list), 'already started'
         for i, (interval, func, args) in enumerate(self._looping_tasks):
             self._looping_tasks[i] = task = utils.LoopTask(interval, func, args)
@@ -622,24 +625,32 @@
             session.reset_pool()
 
     def check_session(self, sessionid):
-        """raise `BadConnectionId` if the connection is no more valid"""
-        self._get_session(sessionid, setpool=False)
+        """raise `BadConnectionId` if the connection is no more valid, else
+        return its latest activity timestamp.
+        """
+        return self._get_session(sessionid, setpool=False).timestamp
+
+    def get_shared_data(self, sessionid, key, default=None, pop=False, txdata=False):
+        """return value associated to key in the session's data dictionary or
+        session's transaction's data if `txdata` is true.
 
-    def get_shared_data(self, sessionid, key, default=None, pop=False):
-        """return the session's data dictionary"""
+        If pop is True, value will be removed from the dictionnary.
+
+        If key isn't defined in the dictionnary, value specified by the
+        `default` argument will be returned.
+        """
         session = self._get_session(sessionid, setpool=False)
-        return session.get_shared_data(key, default, pop)
+        return session.get_shared_data(key, default, pop, txdata)
 
-    def set_shared_data(self, sessionid, key, value, querydata=False):
+    def set_shared_data(self, sessionid, key, value, txdata=False):
         """set value associated to `key` in shared data
 
-        if `querydata` is true, the value will be added to the repository
-        session's query data which are cleared on commit/rollback of the current
-        transaction, and won't be available through the connexion, only on the
-        repository side.
+        if `txdata` is true, the value will be added to the repository session's
+        transaction's data which are cleared on commit/rollback of the current
+        transaction.
         """
         session = self._get_session(sessionid, setpool=False)
-        session.set_shared_data(key, value, querydata)
+        session.set_shared_data(key, value, txdata)
 
     def commit(self, sessionid, txid=None):
         """commit transaction for the session with the given id"""
@@ -771,7 +782,7 @@
         """close sessions not used since an amount of time specified in the
         configuration
         """
-        mintime = time() - self.config['session-time']
+        mintime = time() - self.cleanup_session_time
         self.debug('cleaning session unused since %s',
                    strftime('%T', localtime(mintime)))
         nbclosed = 0
--- a/server/serverconfig.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/server/serverconfig.py	Wed Aug 04 11:13:11 2010 +0200
@@ -120,10 +120,16 @@
 the repository rather than the user running the command',
           'group': 'main', 'level': (CubicWebConfiguration.mode == 'installed') and 0 or 1,
           }),
-        ('session-time',
+        ('cleanup-session-time',
          {'type' : 'time',
-          'default': '30min',
-          'help': 'session expiration time, default to 30 minutes',
+          'default': '24h',
+          'help': 'duration of inactivity after which a session '
+          'will be closed, to limit memory consumption (avoid sessions that '
+          'never expire and cause memory leak when http-session-time is 0, or '
+          'because of bad client that never closes their connection). '
+          'So notice that even if http-session-time is 0 and the user don\'t '
+          'close his browser, he will have to reauthenticate after this time '
+          'of inactivity. Default to 24h.',
           'group': 'main', 'level': 3,
           }),
         ('connections-pool-size',
--- a/server/session.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/server/session.py	Wed Aug 04 11:13:11 2010 +0200
@@ -618,16 +618,20 @@
 
     # shared data handling ###################################################
 
-    def get_shared_data(self, key, default=None, pop=False):
+    def get_shared_data(self, key, default=None, pop=False, txdata=False):
         """return value associated to `key` in session data"""
-        if pop:
-            return self.data.pop(key, default)
+        if txdata:
+            data = self.transaction_data
         else:
-            return self.data.get(key, default)
+            data = self.data
+        if pop:
+            return data.pop(key, default)
+        else:
+            return data.get(key, default)
 
-    def set_shared_data(self, key, value, querydata=False):
+    def set_shared_data(self, key, value, txdata=False):
         """set value associated to `key` in session data"""
-        if querydata:
+        if txdata:
             self.transaction_data[key] = value
         else:
             self.data[key] = value
--- a/server/test/unittest_repository.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/server/test/unittest_repository.py	Wed Aug 04 11:13:11 2010 +0200
@@ -161,7 +161,7 @@
     def test_check_session(self):
         repo = self.repo
         cnxid = repo.connect(self.admlogin, password=self.admpassword)
-        self.assertEquals(repo.check_session(cnxid), None)
+        self.assertIsInstance(repo.check_session(cnxid), float)
         repo.close(cnxid)
         self.assertRaises(BadConnectionId, repo.check_session, cnxid)
 
--- a/view.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/view.py	Wed Aug 04 11:13:11 2010 +0200
@@ -319,21 +319,11 @@
             clabel = vtitle
         return u'%s (%s)' % (clabel, self._cw.property_value('ui.site-title'))
 
-    def output_url_builder( self, name, url, args ):
-        self.w(u'<script language="JavaScript"><!--\n' \
-               u'function %s( %s ) {\n' % (name, ','.join(args) ) )
-        url_parts = url.split("%s")
-        self.w(u' url="%s"' % url_parts[0] )
-        for arg, part in zip(args, url_parts[1:]):
-            self.w(u'+str(%s)' % arg )
-            if part:
-                self.w(u'+"%s"' % part)
-        self.w('\n document.window.href=url;\n')
-        self.w('}\n-->\n</script>\n')
-
+    @deprecated('[3.10] use vreg["etypes"].etype_class(etype).cw_create_url(req)')
     def create_url(self, etype, **kwargs):
         """ return the url of the entity creation form for a given entity type"""
-        return self._cw.build_url('add/%s' % etype, **kwargs)
+        return self._cw.vreg["etypes"].etype_class(etype).cw_create_url(
+            self._cw, **kwargs)
 
     def field(self, label, value, row=True, show_label=True, w=None, tr=True,
               table=False):
--- a/web/application.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/application.py	Wed Aug 04 11:13:11 2010 +0200
@@ -31,7 +31,7 @@
 from cubicweb import set_log_methods, cwvreg
 from cubicweb import (
     ValidationError, Unauthorized, AuthenticationError, NoSelectableObject,
-    RepositoryError, CW_EVENT_MANAGER)
+    RepositoryError, BadConnectionId, CW_EVENT_MANAGER)
 from cubicweb.dbapi import DBAPISession
 from cubicweb.web import LOGGER, component
 from cubicweb.web import (
@@ -48,48 +48,43 @@
 
     def __init__(self, vreg):
         self.session_time = vreg.config['http-session-time'] or None
-        if self.session_time is not None:
-            assert self.session_time > 0
-            self.cleanup_session_time = self.session_time
-        else:
-            self.cleanup_session_time = vreg.config['cleanup-session-time'] or 1440 * 60
-            assert self.cleanup_session_time > 0
-        self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
-        assert self.cleanup_anon_session_time > 0
         self.authmanager = vreg['components'].select('authmanager', vreg=vreg)
+        interval = (self.session_time or 0) / 2.
         if vreg.config.anonymous_user() is not None:
-            self.clean_sessions_interval = max(
-                5 * 60, min(self.cleanup_session_time / 2.,
-                            self.cleanup_anon_session_time / 2.))
-        else:
-            self.clean_sessions_interval = max(
-                5 * 60,
-                self.cleanup_session_time / 2.)
+            self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60
+            assert self.cleanup_anon_session_time > 0
+            if self.session_time is not None:
+                self.cleanup_anon_session_time = min(self.session_time,
+                                                     self.cleanup_anon_session_time)
+            interval = self.cleanup_anon_session_time / 2.
+        # we don't want to check session more than once every 5 minutes
+        self.clean_sessions_interval = max(5 * 60, interval)
 
     def clean_sessions(self):
         """cleanup sessions which has not been unused since a given amount of
         time. Return the number of sessions which have been closed.
         """
         self.debug('cleaning http sessions')
+        session_time = self.session_time
         closed, total = 0, 0
         for session in self.current_sessions():
-            no_use_time = (time() - session.last_usage_time)
             total += 1
-            if session.anonymous_session:
-                if no_use_time >= self.cleanup_anon_session_time:
+            try:
+                last_usage_time = session.cnx.check()
+            except BadConnectionId:
+                self.close_session(session)
+                closed += 1
+            else:
+                no_use_time = (time() - last_usage_time)
+                if session.anonymous_session:
+                    if no_use_time >= self.cleanup_anon_session_time:
+                        self.close_session(session)
+                        closed += 1
+                elif session_time is not None and no_use_time >= session_time:
                     self.close_session(session)
                     closed += 1
-            elif no_use_time >= self.cleanup_session_time:
-                self.close_session(session)
-                closed += 1
         return closed, total - closed
 
-    def has_expired(self, session):
-        """return True if the web session associated to the session is expired
-        """
-        return not (self.session_time is None or
-                    time() < session.last_usage_time + self.session_time)
-
     def current_sessions(self):
         """return currently open sessions"""
         raise NotImplementedError()
@@ -213,8 +208,6 @@
                 except AuthenticationError:
                     req.remove_cookie(cookie, self.SESSION_VAR)
                     raise
-        # remember last usage time for web session tracking
-        session.last_usage_time = time()
 
     def get_session(self, req, sessionid):
         return self.session_manager.get_session(req, sessionid)
@@ -224,8 +217,6 @@
         cookie = req.get_cookie()
         cookie[self.SESSION_VAR] = session.sessionid
         req.set_cookie(cookie, self.SESSION_VAR, maxage=None)
-        # remember last usage time for web session tracking
-        session.last_usage_time = time()
         if not session.anonymous_session:
             self._postlogin(req)
         return session
--- a/web/test/unittest_session.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/test/unittest_session.py	Wed Aug 04 11:13:11 2010 +0200
@@ -7,10 +7,11 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web import InvalidSession
 
 class SessionTC(CubicWebTC):
 
-    def test_auto_reconnection(self):
+    def test_session_expiration(self):
         sm = self.app.session_handler.session_manager
         # make is if the web session has been opened by the session manager
         sm._sessions[self.cnx.sessionid] = self.websession
@@ -23,11 +24,8 @@
             # fake an incoming http query with sessionid in session cookie
             # don't use self.request() which try to call req.set_session
             req = self.requestcls(self.vreg)
-            websession = sm.get_session(req, sessionid)
-            self.assertEquals(len(sm._sessions), 1)
-            self.assertIs(websession, self.websession)
-            self.assertEquals(websession.sessionid, sessionid)
-            self.assertNotEquals(websession.sessionid, websession.cnx.sessionid)
+            self.assertRaises(InvalidSession, sm.get_session, req, sessionid)
+            self.assertEquals(len(sm._sessions), 0)
         finally:
             # avoid error in tearDown by telling this connection is closed...
             self.cnx._closed = True
--- a/web/views/authentication.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/authentication.py	Wed Aug 04 11:13:11 2010 +0200
@@ -74,7 +74,7 @@
         self.repo = vreg.config.repository(vreg)
         self.log_queries = vreg.config['query-log-file']
         self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg),
-                                    key=lambda x: x.order)
+                                         key=lambda x: x.order)
         # 2-uple login / password, login is None when no anonymous access
         # configured
         self.anoninfo = vreg.config.anonymous_user()
@@ -98,25 +98,11 @@
         if login and session.login != login:
             raise InvalidSession('login mismatch')
         try:
-            lock = session.reconnection_lock
-        except AttributeError:
-            lock = session.reconnection_lock = Lock()
-        # need to be locked two avoid duplicated reconnections on concurrent
-        # requests
-        with lock:
-            cnx = session.cnx
-            try:
-                # calling cnx.user() check connection validity, raise
-                # BadConnectionId on failure
-                user = cnx.user(req)
-            except BadConnectionId:
-                # check if a connection should be automatically restablished
-                if (login is None or login == session.login):
-                    cnx = self._authenticate(session.login, session.authinfo)
-                    user = cnx.user(req)
-                    session.cnx = cnx
-                else:
-                    raise InvalidSession('bad connection id')
+            # calling cnx.user() check connection validity, raise
+            # BadConnectionId on failure
+            user = session.cnx.user(req)
+        except BadConnectionId:
+            raise InvalidSession('bad connection id')
         return user
 
     def authenticate(self, req):
--- a/web/views/bookmark.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/bookmark.py	Wed Aug 04 11:13:11 2010 +0200
@@ -115,7 +115,8 @@
             path = req.relative_path()
             # XXX if vtitle specified in params, extract it and use it as default value
             # for bookmark's title
-            url = self.create_url(self.etype, __linkto=linkto, path=path)
+            url = req.vreg['etypes'].etype_class('Bookmark').cw_create_url(
+                req, __linkto=linkto, path=path)
             boxmenu.append(self.mk_action(req._('bookmark this page'), url,
                                           category='manage', id='bookmark'))
             if rset:
--- a/web/views/debug.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/debug.py	Wed Aug 04 11:13:11 2010 +0200
@@ -119,10 +119,15 @@
             if sessions:
                 w(u'<ul>')
                 for session in sessions:
+                    try:
+                        last_usage_time = session.cnx.check()
+                    except BadConnectionId:
+                        w(u'<li>%s (INVALID)</li>' % session.sessionid)
+                        continue
                     w(u'<li>%s (%s: %s)<br/>' % (
                         session.sessionid,
                         _('last usage'),
-                        strftime(dtformat, localtime(session.last_usage_time))))
+                        strftime(dtformat, localtime(last_usage_time))))
                     dict_to_html(w, session.data)
                     w(u'</li>')
                 w(u'</ul>')
--- a/web/views/editcontroller.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/editcontroller.py	Wed Aug 04 11:13:11 2010 +0200
@@ -115,7 +115,7 @@
         form = req.form
         # so we're able to know the main entity from the repository side
         if '__maineid' in form:
-            req.set_shared_data('__maineid', form['__maineid'], querydata=True)
+            req.set_shared_data('__maineid', form['__maineid'], txdata=True)
         # no specific action, generic edition
         self._to_create = req.data['eidmap'] = {}
         self._pending_fields = req.data['pendingfields'] = set()
--- a/web/views/editforms.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/editforms.py	Wed Aug 04 11:13:11 2010 +0200
@@ -169,7 +169,9 @@
 
     def url(self):
         """return the url associated with this view"""
-        return self.create_url(self._cw.form.get('etype'))
+        req = self._cw
+        return req.vreg["etypes"].etype_class(req.form['etype']).cw_create_url(
+            req)
 
     def submited_message(self):
         """return the message that will be displayed on successful edition"""
--- a/web/views/facets.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/facets.py	Wed Aug 04 11:13:11 2010 +0200
@@ -97,9 +97,9 @@
             return
         if vid is None:
             vid = req.form.get('vid')
-        if self.bk_linkbox_template:
-            self.display_bookmark_link(rset)
         w = self.w
+        if self.bk_linkbox_template and req.vreg.schema['Bookmark'].has_perm(req, 'add'):
+            w(self.bookmark_link(rset))
         w(u'<form method="post" id="%sForm" cubicweb:facetargs="%s" action="">'  % (
             divid, xml_escape(json_dumps([divid, vid, paginate, self.facetargs()]))))
         w(u'<fieldset>')
@@ -113,22 +113,22 @@
             wdg.render(w=self.w)
         w(u'</fieldset>\n</form>\n')
 
-    def display_bookmark_link(self, rset):
-        eschema = self._cw.vreg.schema.eschema('Bookmark')
-        if eschema.has_perm(self._cw, 'add'):
-            bk_path = 'rql=%s' % self._cw.url_quote(rset.printable_rql())
-            if self._cw.form.get('vid'):
-                bk_path += '&vid=%s' % self._cw.url_quote(self._cw.form['vid'])
-            bk_path = 'view?' + bk_path
-            bk_title = self._cw._('my custom search')
-            linkto = 'bookmarked_by:%s:subject' % self._cw.user.eid
-            bk_add_url = self._cw.build_url('add/Bookmark', path=bk_path, title=bk_title, __linkto=linkto)
-            bk_base_url = self._cw.build_url('add/Bookmark', title=bk_title, __linkto=linkto)
-            bk_link = u'<a cubicweb:target="%s" id="facetBkLink" href="%s">%s</a>' % (
-                    xml_escape(bk_base_url),
-                    xml_escape(bk_add_url),
-                    self._cw._('bookmark this search'))
-            self.w(self.bk_linkbox_template % bk_link)
+    def bookmark_link(self, rset):
+        req = self._cw
+        bk_path = u'rql=%s' % req.url_quote(rset.printable_rql())
+        if req.form.get('vid'):
+            bk_path += u'&vid=%s' % req.url_quote(req.form['vid'])
+        bk_path = u'view?' + bk_path
+        bk_title = req._('my custom search')
+        linkto = u'bookmarked_by:%s:subject' % req.user.eid
+        bkcls = req.vreg['etypes'].etype_class('Bookmark')
+        bk_add_url = bkcls.cw_create_url(req, path=bk_path, title=bk_title,
+                                         __linkto=linkto)
+        bk_base_url = bkcls.cw_create_url(req, title=bk_title, __linkto=linkto)
+        bk_link = u'<a cubicweb:target="%s" id="facetBkLink" href="%s">%s</a>' % (
+                xml_escape(bk_base_url), xml_escape(bk_add_url),
+                req._('bookmark this search'))
+        return self.bk_linkbox_template % bk_link
 
     def get_facets(self, rset, rqlst, mainvar):
         return self._cw.vreg['facets'].poss_visible_objects(
--- a/web/views/sessions.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/sessions.py	Wed Aug 04 11:13:11 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """web session component: by dfault the session is actually the db connection
 object :/
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb.web import InvalidSession
@@ -51,9 +51,6 @@
         if not sessionid in self._sessions:
             raise InvalidSession()
         session = self._sessions[sessionid]
-        if self.has_expired(session):
-            self.close_session(session)
-            raise InvalidSession()
         try:
             user = self.authmanager.validate_session(req, session)
         except InvalidSession:
--- a/web/views/startup.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/views/startup.py	Wed Aug 04 11:13:11 2010 +0200
@@ -159,15 +159,14 @@
             url = self._cw.build_url(etype)
             etypelink = u'&#160;<a href="%s">%s</a> (%d)' % (
                 xml_escape(url), label, nb)
-            yield (label, etypelink, self.add_entity_link(eschema, req))
+            if eschema.has_perm(req, 'add'):
+                yield (label, etypelink, self.add_entity_link(etype))
 
-    def add_entity_link(self, eschema, req):
-        """creates a [+] link for adding an entity if user has permission to do so"""
-        if not eschema.has_perm(req, 'add'):
-            return u''
+    def add_entity_link(self, etype):
+        """creates a [+] link for adding an entity"""
+        url = self._cw.vreg["etypes"].etype_class(etype).cw_create_url(self._cw)
         return u'[<a href="%s" title="%s">+</a>]' % (
-            xml_escape(self.create_url(eschema.type)),
-            self._cw.__('add a %s' % eschema))
+            xml_escape(url), self._cw.__('add a %s' % etype))
 
 
 class IndexView(ManageView):
--- a/web/webconfig.py	Wed Aug 04 10:55:32 2010 +0200
+++ b/web/webconfig.py	Wed Aug 04 11:13:11 2010 +0200
@@ -135,17 +135,6 @@
           "Should be 0 or greater than repository\'s session-time.",
           'group': 'web', 'level': 2,
           }),
-        ('cleanup-session-time',
-         {'type' : 'time',
-          'default': '24h',
-          'help': 'duration of inactivity after which a connection '
-          'will be closed, to limit memory consumption (avoid sessions that '
-          'never expire and cause memory leak when http-session-time is 0). '
-          'So even if http-session-time is 0 and the user don\'t close his '
-          'browser, he will have to reauthenticate after this time of '
-          'inactivity. Default to 24h.',
-          'group': 'web', 'level': 3,
-          }),
         ('cleanup-anonymous-session-time',
          {'type' : 'time',
           'default': '5min',