web/request.py
changeset 0 b97547f5f1fa
child 495 f8b1edfe9621
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """abstract class for http request
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 import Cookie
       
    10 import sha
       
    11 import time
       
    12 import random
       
    13 import base64
       
    14 from urlparse import urlsplit
       
    15 from itertools import count
       
    16 
       
    17 from rql.utils import rqlvar_maker
       
    18 
       
    19 from logilab.common.decorators import cached
       
    20 
       
    21 # XXX move _MARKER here once AppObject.external_resource has been removed
       
    22 from cubicweb.dbapi import DBAPIRequest
       
    23 from cubicweb.common.appobject import _MARKER 
       
    24 from cubicweb.common.mail import header
       
    25 from cubicweb.common.uilib import remove_html_tags
       
    26 from cubicweb.common.utils import SizeConstrainedList, HTMLHead
       
    27 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit, RequestError,
       
    28                        StatusResponse)
       
    29 
       
    30 
       
    31 def list_form_param(form, param, pop=False):
       
    32     """get param from form parameters and return its value as a list,
       
    33     skipping internal markers if any
       
    34 
       
    35     * if the parameter isn't defined, return an empty list
       
    36     * if the parameter is a single (unicode) value, return a list
       
    37       containing that value
       
    38     * if the parameter is already a list or tuple, just skip internal
       
    39       markers
       
    40 
       
    41     if pop is True, the parameter is removed from the form dictionnary
       
    42     """
       
    43     if pop:
       
    44         try:
       
    45             value = form.pop(param)
       
    46         except KeyError:
       
    47             return []
       
    48     else:
       
    49         value = form.get(param, ())
       
    50     if value is None:
       
    51         value = ()
       
    52     elif not isinstance(value, (list, tuple)):
       
    53         value = [value]
       
    54     return [v for v in value if v != INTERNAL_FIELD_VALUE]
       
    55 
       
    56 
       
    57 
       
    58 class CubicWebRequestBase(DBAPIRequest):
       
    59     """abstract HTTP request, should be extended according to the HTTP backend"""    
       
    60     
       
    61     def __init__(self, vreg, https, form=None):
       
    62         super(CubicWebRequestBase, self).__init__(vreg)
       
    63         self.message = None
       
    64         self.authmode = vreg.config['auth-mode']
       
    65         self.https = https
       
    66         # raw html headers that can be added from any view
       
    67         self.html_headers = HTMLHead()
       
    68         # form parameters
       
    69         self.setup_params(form)
       
    70         # dictionnary that may be used to store request data that has to be
       
    71         # shared among various components used to publish the request (views,
       
    72         # controller, application...)
       
    73         self.data = {}
       
    74         # search state: 'normal' or 'linksearch' (eg searching for an object
       
    75         # to create a relation with another)
       
    76         self.search_state = ('normal',) 
       
    77         # tabindex generator
       
    78         self.tabindexgen = count()
       
    79         self.next_tabindex = self.tabindexgen.next
       
    80         # page id, set by htmlheader template
       
    81         self.pageid = None
       
    82         self.varmaker = rqlvar_maker()
       
    83         self.datadir_url = self._datadir_url()
       
    84 
       
    85     def set_connection(self, cnx, user=None):
       
    86         """method called by the session handler when the user is authenticated
       
    87         or an anonymous connection is open
       
    88         """
       
    89         super(CubicWebRequestBase, self).set_connection(cnx, user)
       
    90         # get request language:
       
    91         vreg = self.vreg
       
    92         if self.user:
       
    93             try:
       
    94                 # 1. user specified language
       
    95                 lang = vreg.typed_value('ui.language',
       
    96                                         self.user.properties['ui.language'])
       
    97                 self.set_language(lang)
       
    98                 return
       
    99             except KeyError, ex:
       
   100                 pass
       
   101         if vreg.config['language-negociation']:
       
   102             # 2. http negociated language
       
   103             for lang in self.header_accept_language():
       
   104                 if lang in self.translations:
       
   105                     self.set_language(lang)
       
   106                     return
       
   107         # 3. default language
       
   108         self.set_default_language(vreg)
       
   109             
       
   110     def set_language(self, lang):
       
   111         self._ = self.__ = self.translations[lang]
       
   112         self.lang = lang
       
   113         self.debug('request language: %s', lang)
       
   114         
       
   115     # input form parameters management ########################################
       
   116     
       
   117     # common form parameters which should be protected against html values
       
   118     # XXX can't add 'eid' for instance since it may be multivalued
       
   119     # dont put rql as well, if query contains < and > it will be corrupted!
       
   120     no_script_form_params = set(('vid', 
       
   121                                  'etype', 
       
   122                                  'vtitle', 'title',
       
   123                                  '__message',
       
   124                                  '__redirectvid', '__redirectrql'))
       
   125         
       
   126     def setup_params(self, params):
       
   127         """WARNING: we're intentionaly leaving INTERNAL_FIELD_VALUE here
       
   128 
       
   129         subclasses should overrides to 
       
   130         """
       
   131         if params is None:
       
   132             params = {}
       
   133         self.form = params
       
   134         encoding = self.encoding
       
   135         for k, v in params.items():
       
   136             if isinstance(v, (tuple, list)):
       
   137                 v = [unicode(x, encoding) for x in v]
       
   138                 if len(v) == 1:
       
   139                     v = v[0]
       
   140             if k in self.no_script_form_params:
       
   141                 v = self.no_script_form_param(k, value=v)
       
   142             if isinstance(v, str):
       
   143                 v = unicode(v, encoding)
       
   144             if k == '__message':
       
   145                 self.set_message(v)
       
   146                 del self.form[k]
       
   147             else:
       
   148                 self.form[k] = v
       
   149     
       
   150     def no_script_form_param(self, param, default=None, value=None):
       
   151         """ensure there is no script in a user form param
       
   152 
       
   153         by default return a cleaned string instead of raising a security
       
   154         exception
       
   155 
       
   156         this method should be called on every user input (form at least) fields
       
   157         that are at some point inserted in a generated html page to protect
       
   158         against script kiddies
       
   159         """
       
   160         if value is None:
       
   161             value = self.form.get(param, default)
       
   162         if not value is default and value:
       
   163             # safety belt for strange urls like http://...?vtitle=yo&vtitle=yo
       
   164             if isinstance(value, (list, tuple)):
       
   165                 self.error('no_script_form_param got a list (%s). Who generated the URL ?',
       
   166                            repr(value))
       
   167                 value = value[0]
       
   168             return remove_html_tags(value)
       
   169         return value
       
   170         
       
   171     def list_form_param(self, param, form=None, pop=False):
       
   172         """get param from form parameters and return its value as a list,
       
   173         skipping internal markers if any
       
   174         
       
   175         * if the parameter isn't defined, return an empty list
       
   176         * if the parameter is a single (unicode) value, return a list
       
   177           containing that value
       
   178         * if the parameter is already a list or tuple, just skip internal
       
   179           markers
       
   180 
       
   181         if pop is True, the parameter is removed from the form dictionnary
       
   182         """
       
   183         if form is None:
       
   184             form = self.form
       
   185         return list_form_param(form, param, pop)            
       
   186     
       
   187 
       
   188     def reset_headers(self):
       
   189         """used by AutomaticWebTest to clear html headers between tests on
       
   190         the same resultset
       
   191         """
       
   192         self.html_headers = HTMLHead()
       
   193         return self
       
   194 
       
   195     # web state helpers #######################################################
       
   196     
       
   197     def set_message(self, msg):
       
   198         assert isinstance(msg, unicode)
       
   199         self.message = msg
       
   200     
       
   201     def update_search_state(self):
       
   202         """update the current search state"""
       
   203         searchstate = self.form.get('__mode')
       
   204         if not searchstate:
       
   205             searchstate = self.get_session_data('search_state', 'normal')
       
   206         self.set_search_state(searchstate)
       
   207 
       
   208     def set_search_state(self, searchstate):
       
   209         """set a new search state"""
       
   210         if searchstate is None or searchstate == 'normal':
       
   211             self.search_state = (searchstate or 'normal',)
       
   212         else:
       
   213             self.search_state = ('linksearch', searchstate.split(':'))
       
   214             assert len(self.search_state[-1]) == 4
       
   215         self.set_session_data('search_state', searchstate)
       
   216 
       
   217     def update_breadcrumbs(self):
       
   218         """stores the last visisted page in session data"""
       
   219         searchstate = self.get_session_data('search_state')
       
   220         if searchstate == 'normal':
       
   221             breadcrumbs = self.get_session_data('breadcrumbs', None)
       
   222             if breadcrumbs is None:
       
   223                 breadcrumbs = SizeConstrainedList(10)
       
   224                 self.set_session_data('breadcrumbs', breadcrumbs)
       
   225             breadcrumbs.append(self.url())
       
   226 
       
   227     def last_visited_page(self):
       
   228         breadcrumbs = self.get_session_data('breadcrumbs', None)
       
   229         if breadcrumbs:
       
   230             return breadcrumbs.pop()
       
   231         return self.base_url()
       
   232 
       
   233     def register_onetime_callback(self, func, *args):
       
   234         cbname = 'cb_%s' % (
       
   235             sha.sha('%s%s%s%s' % (time.time(), func.__name__,
       
   236                                   random.random(), 
       
   237                                   self.user.login)).hexdigest())
       
   238         def _cb(req):
       
   239             try:
       
   240                 ret = func(req, *args)
       
   241             except TypeError:
       
   242                 from warnings import warn
       
   243                 warn('user callback should now take request as argument')
       
   244                 ret = func(*args)            
       
   245             self.unregister_callback(self.pageid, cbname)
       
   246             return ret
       
   247         self.set_page_data(cbname, _cb)
       
   248         return cbname
       
   249     
       
   250     def unregister_callback(self, pageid, cbname):
       
   251         assert pageid is not None
       
   252         assert cbname.startswith('cb_')
       
   253         self.info('unregistering callback %s for pageid %s', cbname, pageid)
       
   254         self.del_page_data(cbname)
       
   255 
       
   256     def clear_user_callbacks(self):
       
   257         if self.cnx is not None:
       
   258             sessdata = self.session_data()
       
   259             callbacks = [key for key in sessdata if key.startswith('cb_')]
       
   260             for callback in callbacks:
       
   261                 self.del_session_data(callback)
       
   262     
       
   263     # web edition helpers #####################################################
       
   264     
       
   265     @cached # so it's writed only once
       
   266     def fckeditor_config(self):
       
   267         self.html_headers.define_var('fcklang', self.lang)
       
   268         self.html_headers.define_var('fckconfigpath',
       
   269                                      self.build_url('data/fckcwconfig.js'))
       
   270 
       
   271     def edited_eids(self, withtype=False):
       
   272         """return a list of edited eids"""
       
   273         yielded = False
       
   274         # warning: use .keys since the caller may change `form`
       
   275         form = self.form
       
   276         try:
       
   277             eids = form['eid']
       
   278         except KeyError:
       
   279             raise NothingToEdit(None, {None: self._('no selected entities')})
       
   280         if isinstance(eids, basestring):
       
   281             eids = (eids,)
       
   282         for peid in eids:
       
   283             if withtype:
       
   284                 typekey = '__type:%s' % peid
       
   285                 assert typekey in form, 'no entity type specified'
       
   286                 yield peid, form[typekey]
       
   287             else:
       
   288                 yield peid
       
   289             yielded = True
       
   290         if not yielded:
       
   291             raise NothingToEdit(None, {None: self._('no selected entities')})
       
   292 
       
   293     # minparams=3 by default: at least eid, __type, and some params to change
       
   294     def extract_entity_params(self, eid, minparams=3):
       
   295         """extract form parameters relative to the given eid"""
       
   296         params = {}
       
   297         eid = str(eid)
       
   298         form = self.form
       
   299         for param in form:
       
   300             try:
       
   301                 name, peid = param.split(':', 1)
       
   302             except ValueError:
       
   303                 if not param.startswith('__') and param != "eid":
       
   304                     self.warning('param %s mis-formatted', param)
       
   305                 continue
       
   306             if peid == eid:
       
   307                 value = form[param]
       
   308                 if value == INTERNAL_FIELD_VALUE:
       
   309                     value = None
       
   310                 params[name] = value
       
   311         params['eid'] = eid
       
   312         if len(params) < minparams:
       
   313             print eid, params
       
   314             raise RequestError(self._('missing parameters for entity %s') % eid)
       
   315         return params
       
   316     
       
   317     def get_pending_operations(self, entity, relname, role):
       
   318         operations = {'insert' : [], 'delete' : []}
       
   319         for optype in ('insert', 'delete'):
       
   320             data = self.get_session_data('pending_%s' % optype) or ()
       
   321             for eidfrom, rel, eidto in data:
       
   322                 if relname == rel:
       
   323                     if role == 'subject' and entity.eid == eidfrom:
       
   324                         operations[optype].append(eidto)
       
   325                     if role == 'object' and entity.eid == eidto:
       
   326                         operations[optype].append(eidfrom)
       
   327         return operations
       
   328     
       
   329     def get_pending_inserts(self, eid=None):
       
   330         """shortcut to access req's pending_insert entry
       
   331 
       
   332         This is where are stored relations being added while editing
       
   333         an entity. This used to be stored in a temporary cookie.
       
   334         """
       
   335         pending = self.get_session_data('pending_insert') or ()
       
   336         return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
       
   337                 if eid is None or eid in (subj, obj)]
       
   338 
       
   339     def get_pending_deletes(self, eid=None):
       
   340         """shortcut to access req's pending_delete entry
       
   341 
       
   342         This is where are stored relations being removed while editing
       
   343         an entity. This used to be stored in a temporary cookie.
       
   344         """
       
   345         pending = self.get_session_data('pending_delete') or ()
       
   346         return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
       
   347                 if eid is None or eid in (subj, obj)]
       
   348 
       
   349     def remove_pending_operations(self):
       
   350         """shortcut to clear req's pending_{delete,insert} entries
       
   351 
       
   352         This is needed when the edition is completed (whether it's validated
       
   353         or cancelled)
       
   354         """
       
   355         self.del_session_data('pending_insert')
       
   356         self.del_session_data('pending_delete')
       
   357 
       
   358     def cancel_edition(self, errorurl):
       
   359         """remove pending operations and `errorurl`'s specific stored data
       
   360         """
       
   361         self.del_session_data(errorurl)
       
   362         self.remove_pending_operations()
       
   363     
       
   364     # high level methods for HTTP headers management ##########################
       
   365 
       
   366     # must be cached since login/password are popped from the form dictionary
       
   367     # and this method may be called multiple times during authentication
       
   368     @cached
       
   369     def get_authorization(self):
       
   370         """Parse and return the Authorization header"""
       
   371         if self.authmode == "cookie":
       
   372             try:
       
   373                 user = self.form.pop("__login")
       
   374                 passwd = self.form.pop("__password", '')
       
   375                 return user, passwd.encode('UTF8')
       
   376             except KeyError:
       
   377                 self.debug('no login/password in form params')
       
   378                 return None, None
       
   379         else:
       
   380             return self.header_authorization()
       
   381     
       
   382     def get_cookie(self):
       
   383         """retrieve request cookies, returns an empty cookie if not found"""
       
   384         try:
       
   385             return Cookie.SimpleCookie(self.get_header('Cookie'))
       
   386         except KeyError:
       
   387             return Cookie.SimpleCookie()
       
   388 
       
   389     def set_cookie(self, cookie, key, maxage=300):
       
   390         """set / update a cookie key
       
   391 
       
   392         by default, cookie will be available for the next 5 minutes.
       
   393         Give maxage = None to have a "session" cookie expiring when the
       
   394         client close its browser
       
   395         """
       
   396         morsel = cookie[key]
       
   397         if maxage is not None:
       
   398             morsel['Max-Age'] = maxage
       
   399         # make sure cookie is set on the correct path
       
   400         morsel['path'] = self.base_url_path()
       
   401         self.add_header('Set-Cookie', morsel.OutputString())
       
   402 
       
   403     def remove_cookie(self, cookie, key):
       
   404         """remove a cookie by expiring it"""
       
   405         morsel = cookie[key]
       
   406         morsel['Max-Age'] = 0
       
   407         # The only way to set up cookie age for IE is to use an old "expired"
       
   408         # syntax. IE doesn't support Max-Age there is no library support for
       
   409         # managing 
       
   410         # ===> Do _NOT_ comment this line :
       
   411         morsel['expires'] = 'Thu, 01-Jan-1970 00:00:00 GMT'
       
   412         self.add_header('Set-Cookie', morsel.OutputString())
       
   413 
       
   414     def set_content_type(self, content_type, filename=None, encoding=None):
       
   415         """set output content type for this request. An optional filename
       
   416         may be given
       
   417         """
       
   418         if content_type.startswith('text/'):
       
   419             content_type += ';charset=' + (encoding or self.encoding)
       
   420         self.set_header('content-type', content_type)
       
   421         if filename:
       
   422             if isinstance(filename, unicode):
       
   423                 filename = header(filename).encode()
       
   424             self.set_header('content-disposition', 'inline; filename=%s'
       
   425                             % filename)
       
   426 
       
   427     # high level methods for HTML headers management ##########################
       
   428 
       
   429     def add_js(self, jsfiles, localfile=True):
       
   430         """specify a list of JS files to include in the HTML headers
       
   431         :param jsfiles: a JS filename or a list of JS filenames
       
   432         :param localfile: if True, the default data dir prefix is added to the
       
   433                           JS filename
       
   434         """
       
   435         if isinstance(jsfiles, basestring):
       
   436             jsfiles = (jsfiles,)
       
   437         for jsfile in jsfiles:
       
   438             if localfile:
       
   439                 jsfile = self.datadir_url + jsfile
       
   440             self.html_headers.add_js(jsfile)
       
   441 
       
   442     def add_css(self, cssfiles, media=u'all', localfile=True, ieonly=False):
       
   443         """specify a CSS file to include in the HTML headers
       
   444         :param cssfiles: a CSS filename or a list of CSS filenames
       
   445         :param media: the CSS's media if necessary
       
   446         :param localfile: if True, the default data dir prefix is added to the
       
   447                           CSS filename
       
   448         """
       
   449         if isinstance(cssfiles, basestring):
       
   450             cssfiles = (cssfiles,)
       
   451         if ieonly:
       
   452             if self.ie_browser():
       
   453                 add_css = self.html_headers.add_ie_css
       
   454             else:
       
   455                 return # no need to do anything on non IE browsers
       
   456         else:
       
   457             add_css = self.html_headers.add_css
       
   458         for cssfile in cssfiles:
       
   459             if localfile:
       
   460                 cssfile = self.datadir_url + cssfile
       
   461             add_css(cssfile, media)
       
   462     
       
   463     # urls/path management ####################################################
       
   464     
       
   465     def url(self, includeparams=True):
       
   466         """return currently accessed url"""
       
   467         return self.base_url() + self.relative_path(includeparams)
       
   468 
       
   469     def _datadir_url(self):
       
   470         """return url of the application's data directory"""
       
   471         return self.base_url() + 'data%s/' % self.vreg.config.instance_md5_version()
       
   472     
       
   473     def selected(self, url):
       
   474         """return True if the url is equivalent to currently accessed url"""
       
   475         reqpath = self.relative_path().lower()
       
   476         baselen = len(self.base_url())
       
   477         return (reqpath == url[baselen:].lower())
       
   478 
       
   479     def base_url_prepend_host(self, hostname):
       
   480         protocol, roothost = urlsplit(self.base_url())[:2]
       
   481         if roothost.startswith('www.'):
       
   482             roothost = roothost[4:]
       
   483         return '%s://%s.%s' % (protocol, hostname, roothost)
       
   484 
       
   485     def base_url_path(self):
       
   486         """returns the absolute path of the base url"""
       
   487         return urlsplit(self.base_url())[2]
       
   488         
       
   489     @cached
       
   490     def from_controller(self):
       
   491         """return the id (string) of the controller issuing the request"""
       
   492         controller = self.relative_path(False).split('/', 1)[0]
       
   493         registered_controllers = (ctrl.id for ctrl in
       
   494                                   self.vreg.registry_objects('controllers'))
       
   495         if controller in registered_controllers:
       
   496             return controller
       
   497         return 'view'
       
   498     
       
   499     def external_resource(self, rid, default=_MARKER):
       
   500         """return a path to an external resource, using its identifier
       
   501 
       
   502         raise KeyError  if the resource is not defined
       
   503         """
       
   504         try:
       
   505             value = self.vreg.config.ext_resources[rid]
       
   506         except KeyError:
       
   507             if default is _MARKER:
       
   508                 raise
       
   509             return default
       
   510         if value is None:
       
   511             return None
       
   512         baseurl = self.datadir_url[:-1] # remove trailing /
       
   513         if isinstance(value, list):
       
   514             return [v.replace('DATADIR', baseurl) for v in value]
       
   515         return value.replace('DATADIR', baseurl)
       
   516     external_resource = cached(external_resource, keyarg=1)
       
   517 
       
   518     def validate_cache(self):
       
   519         """raise a `DirectResponse` exception if a cached page along the way
       
   520         exists and is still usable.
       
   521 
       
   522         calls the client-dependant implementation of `_validate_cache`
       
   523         """
       
   524         self._validate_cache()
       
   525         if self.http_method() == 'HEAD':
       
   526             raise StatusResponse(200, '')
       
   527         
       
   528     # abstract methods to override according to the web front-end #############
       
   529         
       
   530     def http_method(self):
       
   531         """returns 'POST', 'GET', 'HEAD', etc."""
       
   532         raise NotImplementedError()
       
   533 
       
   534     def _validate_cache(self):
       
   535         """raise a `DirectResponse` exception if a cached page along the way
       
   536         exists and is still usable
       
   537         """
       
   538         raise NotImplementedError()
       
   539         
       
   540     def relative_path(self, includeparams=True):
       
   541         """return the normalized path of the request (ie at least relative
       
   542         to the application's root, but some other normalization may be needed
       
   543         so that the returned path may be used to compare to generated urls
       
   544 
       
   545         :param includeparams:
       
   546            boolean indicating if GET form parameters should be kept in the path
       
   547         """
       
   548         raise NotImplementedError()
       
   549 
       
   550     def get_header(self, header, default=None):
       
   551         """return the value associated with the given input HTTP header,
       
   552         raise KeyError if the header is not set
       
   553         """
       
   554         raise NotImplementedError()
       
   555 
       
   556     def set_header(self, header, value):
       
   557         """set an output HTTP header"""
       
   558         raise NotImplementedError()
       
   559 
       
   560     def add_header(self, header, value):
       
   561         """add an output HTTP header"""
       
   562         raise NotImplementedError()
       
   563     
       
   564     def remove_header(self, header):
       
   565         """remove an output HTTP header"""
       
   566         raise NotImplementedError()
       
   567         
       
   568     def header_authorization(self):
       
   569         """returns a couple (auth-type, auth-value)"""
       
   570         auth = self.get_header("Authorization", None)
       
   571         if auth:
       
   572             scheme, rest = auth.split(' ', 1)
       
   573             scheme = scheme.lower()
       
   574             try:
       
   575                 assert scheme == "basic"
       
   576                 user, passwd = base64.decodestring(rest).split(":", 1)
       
   577                 # XXX HTTP header encoding: use email.Header?
       
   578                 return user.decode('UTF8'), passwd
       
   579             except Exception, ex:
       
   580                 self.debug('bad authorization %s (%s: %s)',
       
   581                            auth, ex.__class__.__name__, ex)
       
   582         return None, None
       
   583 
       
   584     def header_accept_language(self):
       
   585         """returns an ordered list of preferred languages"""
       
   586         acceptedlangs = self.get_header('Accept-Language', '')
       
   587         langs = []
       
   588         for langinfo in acceptedlangs.split(','):
       
   589             try:
       
   590                 lang, score = langinfo.split(';')
       
   591                 score = float(score[2:]) # remove 'q='
       
   592             except ValueError:
       
   593                 lang = langinfo
       
   594                 score = 1.0
       
   595             lang = lang.split('-')[0]
       
   596             langs.append( (score, lang) )
       
   597         langs.sort(reverse=True)
       
   598         return (lang for (score, lang) in langs)
       
   599 
       
   600     def header_if_modified_since(self):
       
   601         """If the HTTP header If-modified-since is set, return the equivalent
       
   602         mx date time value (GMT), else return None
       
   603         """
       
   604         raise NotImplementedError()
       
   605     
       
   606     # page data management ####################################################
       
   607 
       
   608     def get_page_data(self, key, default=None):
       
   609         """return value associated to `key` in curernt page data"""
       
   610         page_data = self.cnx.get_session_data(self.pageid, {})
       
   611         return page_data.get(key, default)
       
   612         
       
   613     def set_page_data(self, key, value):
       
   614         """set value associated to `key` in current page data"""
       
   615         self.html_headers.add_unload_pagedata()
       
   616         page_data = self.cnx.get_session_data(self.pageid, {})
       
   617         page_data[key] = value
       
   618         return self.cnx.set_session_data(self.pageid, page_data)
       
   619         
       
   620     def del_page_data(self, key=None):
       
   621         """remove value associated to `key` in current page data
       
   622         if `key` is None, all page data will be cleared
       
   623         """
       
   624         if key is None:
       
   625             self.cnx.del_session_data(self.pageid)
       
   626         else:
       
   627             page_data = self.cnx.get_session_data(self.pageid, {})
       
   628             page_data.pop(key, None)
       
   629             self.cnx.set_session_data(self.pageid, page_data)
       
   630 
       
   631     # user-agent detection ####################################################
       
   632 
       
   633     @cached
       
   634     def useragent(self):
       
   635         return self.get_header('User-Agent', None)
       
   636 
       
   637     def ie_browser(self):
       
   638         useragent = self.useragent()
       
   639         return useragent and 'MSIE' in useragent
       
   640     
       
   641     def xhtml_browser(self):
       
   642         useragent = self.useragent()
       
   643         if useragent and ('MSIE' in useragent or 'KHTML' in useragent):
       
   644             return False
       
   645         return True
       
   646 
       
   647 from cubicweb import set_log_methods
       
   648 set_log_methods(CubicWebRequestBase, LOGGER)