web/request.py
brancholdstable
changeset 6665 90f2f20367bc
parent 6353 b622602f8e9d
child 6543 66145280a7e6
equal deleted inserted replaced
6018:f4d1d5d9ccbb 6665:90f2f20367bc
    25 import random
    25 import random
    26 import base64
    26 import base64
    27 from datetime import date
    27 from datetime import date
    28 from urlparse import urlsplit
    28 from urlparse import urlsplit
    29 from itertools import count
    29 from itertools import count
       
    30 from warnings import warn
    30 
    31 
    31 from rql.utils import rqlvar_maker
    32 from rql.utils import rqlvar_maker
    32 
    33 
    33 from logilab.common.decorators import cached
    34 from logilab.common.decorators import cached
    34 from logilab.common.deprecation import deprecated
    35 from logilab.common.deprecation import deprecated
    35 from logilab.mtconverter import xml_escape
    36 from logilab.mtconverter import xml_escape
    36 
    37 
    37 from cubicweb.dbapi import DBAPIRequest
    38 from cubicweb.dbapi import DBAPIRequest
    38 from cubicweb.mail import header
    39 from cubicweb.mail import header
    39 from cubicweb.uilib import remove_html_tags
    40 from cubicweb.uilib import remove_html_tags, js
    40 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
    41 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
    41 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
    42 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
    42 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
    43 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
    43                           RequestError, StatusResponse, json)
    44                           RequestError, StatusResponse)
    44 from cubicweb.web.http_headers import Headers
    45 from cubicweb.web.http_headers import Headers
    45 
       
    46 dumps = json.dumps
       
    47 
    46 
    48 _MARKER = object()
    47 _MARKER = object()
    49 
    48 
    50 
    49 
    51 def list_form_param(form, param, pop=False):
    50 def list_form_param(form, param, pop=False):
    81 
    80 
    82     def __init__(self, vreg, https, form=None):
    81     def __init__(self, vreg, https, form=None):
    83         super(CubicWebRequestBase, self).__init__(vreg)
    82         super(CubicWebRequestBase, self).__init__(vreg)
    84         self.authmode = vreg.config['auth-mode']
    83         self.authmode = vreg.config['auth-mode']
    85         self.https = https
    84         self.https = https
       
    85         if https:
       
    86             self.uiprops = vreg.config.https_uiprops
       
    87             self.datadir_url = vreg.config.https_datadir_url
       
    88         else:
       
    89             self.uiprops = vreg.config.uiprops
       
    90             self.datadir_url = vreg.config.datadir_url
    86         # raw html headers that can be added from any view
    91         # raw html headers that can be added from any view
    87         self.html_headers = HTMLHead()
    92         self.html_headers = HTMLHead()
    88         # form parameters
    93         # form parameters
    89         self.setup_params(form)
    94         self.setup_params(form)
    90         # dictionnary that may be used to store request data that has to be
    95         # dictionnary that may be used to store request data that has to be
    92         # controller, application...)
    97         # controller, application...)
    93         self.data = {}
    98         self.data = {}
    94         # search state: 'normal' or 'linksearch' (eg searching for an object
    99         # search state: 'normal' or 'linksearch' (eg searching for an object
    95         # to create a relation with another)
   100         # to create a relation with another)
    96         self.search_state = ('normal',)
   101         self.search_state = ('normal',)
    97         # tabindex generator
       
    98         self.tabindexgen = count(1)
       
    99         self.next_tabindex = self.tabindexgen.next
       
   100         # page id, set by htmlheader template
   102         # page id, set by htmlheader template
   101         self.pageid = None
   103         self.pageid = None
   102         self.datadir_url = self._datadir_url()
       
   103         self._set_pageid()
   104         self._set_pageid()
   104         # prepare output header
   105         # prepare output header
   105         self.headers_out = Headers()
   106         self.headers_out = Headers()
   106 
   107 
   107     def _set_pageid(self):
   108     def _set_pageid(self):
   124 
   125 
   125         * calling req.set_varmaker() to ensure a varmaker is set for later ajax
   126         * calling req.set_varmaker() to ensure a varmaker is set for later ajax
   126           calls sharing our .pageid
   127           calls sharing our .pageid
   127         """
   128         """
   128         return self.set_varmaker()
   129         return self.set_varmaker()
       
   130 
       
   131     def _get_tabindex_func(self):
       
   132         nextfunc = self.get_page_data('nexttabfunc')
       
   133         if nextfunc is None:
       
   134             nextfunc = count(1).next
       
   135             self.set_page_data('nexttabfunc', nextfunc)
       
   136         return nextfunc
   129 
   137 
   130     def set_varmaker(self):
   138     def set_varmaker(self):
   131         varmaker = self.get_page_data('rql_varmaker')
   139         varmaker = self.get_page_data('rql_varmaker')
   132         if varmaker is None:
   140         if varmaker is None:
   133             varmaker = rqlvar_maker()
   141             varmaker = rqlvar_maker()
   137     def set_session(self, session, user=None):
   145     def set_session(self, session, user=None):
   138         """method called by the session handler when the user is authenticated
   146         """method called by the session handler when the user is authenticated
   139         or an anonymous connection is open
   147         or an anonymous connection is open
   140         """
   148         """
   141         super(CubicWebRequestBase, self).set_session(session, user)
   149         super(CubicWebRequestBase, self).set_session(session, user)
       
   150         # tabindex generator
       
   151         self.next_tabindex = self._get_tabindex_func()
   142         # set request language
   152         # set request language
   143         vreg = self.vreg
   153         vreg = self.vreg
   144         if self.user:
   154         if self.user:
   145             try:
   155             try:
   146                 # 1. user specified language
   156                 # 1. user specified language
   337         breadcrumbs = self.session.data.get('breadcrumbs')
   347         breadcrumbs = self.session.data.get('breadcrumbs')
   338         if breadcrumbs:
   348         if breadcrumbs:
   339             return breadcrumbs.pop()
   349             return breadcrumbs.pop()
   340         return self.base_url()
   350         return self.base_url()
   341 
   351 
   342     def user_rql_callback(self, args, msg=None):
   352     def user_rql_callback(self, rqlargs, *args, **kwargs):
   343         """register a user callback to execute some rql query and return an url
   353         """register a user callback to execute some rql query and return an url
   344         to call it ready to be inserted in html
   354         to call it ready to be inserted in html.
       
   355 
       
   356         rqlargs should be a tuple containing argument to give to the execute function.
       
   357 
       
   358         For other allowed arguments, see :meth:`user_callback` method
   345         """
   359         """
   346         def rqlexec(req, rql, args=None, key=None):
   360         def rqlexec(req, rql, args=None, key=None):
   347             req.execute(rql, args, key)
   361             req.execute(rql, args, key)
   348         return self.user_callback(rqlexec, args, msg)
   362         return self.user_callback(rqlexec, rqlargs, *args, **kwargs)
   349 
   363 
   350     def user_callback(self, cb, args, msg=None, nonify=False):
   364     def user_callback(self, cb, cbargs, *args, **kwargs):
   351         """register the given user callback and return an url to call it ready to be
   365         """register the given user callback and return an url to call it ready
   352         inserted in html
   366         to be inserted in html.
       
   367 
       
   368         You can specify the underlying js function to call using a 'jsfunc'
       
   369         named args, to one of :func:`userCallback`,
       
   370         ':func:`userCallbackThenUpdateUI`, ':func:`userCallbackThenReloadPage`
       
   371         (the default). Take care arguments may vary according to the used
       
   372         function.
   353         """
   373         """
   354         self.add_js('cubicweb.ajax.js')
   374         self.add_js('cubicweb.ajax.js')
   355         cbname = self.register_onetime_callback(cb, *args)
   375         jsfunc = kwargs.pop('jsfunc', 'userCallbackThenReloadPage')
   356         msg = dumps(msg or '')
   376         if 'msg' in kwargs:
   357         return "javascript:userCallbackThenReloadPage('%s', %s)" % (
   377             warn('[3.10] msg should be given as positional argument',
   358             cbname, msg)
   378                  DeprecationWarning, stacklevel=2)
       
   379             args = (kwargs.pop('msg'),) + args
       
   380         assert not kwargs, 'dunno what to do with remaining kwargs: %s' % kwargs
       
   381         cbname = self.register_onetime_callback(cb, *cbargs)
       
   382         return "javascript: %s" % getattr(js, jsfunc)(cbname, *args)
   359 
   383 
   360     def register_onetime_callback(self, func, *args):
   384     def register_onetime_callback(self, func, *args):
   361         cbname = 'cb_%s' % (
   385         cbname = 'cb_%s' % (
   362             hashlib.sha1('%s%s%s%s' % (time.time(), func.__name__,
   386             hashlib.sha1('%s%s%s%s' % (time.time(), func.__name__,
   363                                        random.random(),
   387                                        random.random(),
   364                                        self.user.login)).hexdigest())
   388                                        self.user.login)).hexdigest())
   365         def _cb(req):
   389         def _cb(req):
   366             try:
   390             try:
   367                 ret = func(req, *args)
   391                 ret = func(req, *args)
   368             except TypeError:
   392             except TypeError:
   369                 from warnings import warn
       
   370                 warn('[3.2] user callback should now take request as argument')
   393                 warn('[3.2] user callback should now take request as argument')
   371                 ret = func(*args)
   394                 ret = func(*args)
   372             self.unregister_callback(self.pageid, cbname)
   395             self.unregister_callback(self.pageid, cbname)
   373             return ret
   396             return ret
   374         self.set_page_data(cbname, _cb)
   397         self.set_page_data(cbname, _cb)
   506 
   529 
   507     def set_content_type(self, content_type, filename=None, encoding=None):
   530     def set_content_type(self, content_type, filename=None, encoding=None):
   508         """set output content type for this request. An optional filename
   531         """set output content type for this request. An optional filename
   509         may be given
   532         may be given
   510         """
   533         """
   511         if content_type.startswith('text/'):
   534         if content_type.startswith('text/') and ';charset=' not in content_type:
   512             content_type += ';charset=' + (encoding or self.encoding)
   535             content_type += ';charset=' + (encoding or self.encoding)
   513         self.set_header('content-type', content_type)
   536         self.set_header('content-type', content_type)
   514         if filename:
   537         if filename:
   515             if isinstance(filename, unicode):
   538             if isinstance(filename, unicode):
   516                 filename = header(filename).encode()
   539                 filename = header(filename).encode()
   562         for cssfile in cssfiles:
   585         for cssfile in cssfiles:
   563             if localfile:
   586             if localfile:
   564                 cssfile = self.datadir_url + cssfile
   587                 cssfile = self.datadir_url + cssfile
   565             add_css(cssfile, media, *extraargs)
   588             add_css(cssfile, media, *extraargs)
   566 
   589 
       
   590     @deprecated('[3.9] use ajax_replace_url() instead, naming rql and vid arguments')
   567     def build_ajax_replace_url(self, nodeid, rql, vid, replacemode='replace',
   591     def build_ajax_replace_url(self, nodeid, rql, vid, replacemode='replace',
   568                                **extraparams):
   592                                **extraparams):
       
   593         return self.ajax_replace_url(nodeid, replacemode, rql=rql, vid=vid,
       
   594                                      **extraparams)
       
   595 
       
   596     def ajax_replace_url(self, nodeid, replacemode='replace', **extraparams):
   569         """builds an ajax url that will replace nodeid's content
   597         """builds an ajax url that will replace nodeid's content
   570 
   598 
   571         :param nodeid: the dom id of the node to replace
   599         :param nodeid: the dom id of the node to replace
   572         :param rql: rql to execute
       
   573         :param vid: the view to apply on the resultset
       
   574         :param replacemode: defines how the replacement should be done.
   600         :param replacemode: defines how the replacement should be done.
   575 
   601 
   576         Possible values are :
   602           Possible values are :
   577         - 'replace' to replace the node's content with the generated HTML
   603           - 'replace' to replace the node's content with the generated HTML
   578         - 'swap' to replace the node itself with the generated HTML
   604           - 'swap' to replace the node itself with the generated HTML
   579         - 'append' to append the generated HTML to the node's content
   605           - 'append' to append the generated HTML to the node's content
   580         """
   606 
   581         url = self.build_url('view', rql=rql, vid=vid, __notemplate=1,
   607         Arbitrary extra named arguments may be given, they will be included as
   582                              **extraparams)
   608         parameters of the generated url.
   583         return "javascript: loadxhtml('%s', '%s', '%s')" % (
   609         """
   584             nodeid, xml_escape(url), replacemode)
   610         extraparams.setdefault('fname', 'view')
       
   611         url = self.build_url('json', **extraparams)
       
   612         return "javascript: $('#%s').%s; noop()" % (
       
   613             nodeid, js.loadxhtml(url, None, 'get', replacemode))
   585 
   614 
   586     # urls/path management ####################################################
   615     # urls/path management ####################################################
   587 
   616 
   588     def url(self, includeparams=True):
   617     def url(self, includeparams=True):
   589         """return currently accessed url"""
   618         """return currently accessed url"""
   590         return self.base_url() + self.relative_path(includeparams)
   619         return self.base_url() + self.relative_path(includeparams)
   591 
       
   592     def _datadir_url(self):
       
   593         """return url of the instance's data directory"""
       
   594         return self.base_url() + 'data%s/' % self.vreg.config.instance_md5_version()
       
   595 
   620 
   596     def selected(self, url):
   621     def selected(self, url):
   597         """return True if the url is equivalent to currently accessed url"""
   622         """return True if the url is equivalent to currently accessed url"""
   598         reqpath = self.relative_path().lower()
   623         reqpath = self.relative_path().lower()
   599         baselen = len(self.base_url())
   624         baselen = len(self.base_url())
   615         controller = self.relative_path(False).split('/', 1)[0]
   640         controller = self.relative_path(False).split('/', 1)[0]
   616         registered_controllers = self.vreg['controllers'].keys()
   641         registered_controllers = self.vreg['controllers'].keys()
   617         if controller in registered_controllers:
   642         if controller in registered_controllers:
   618             return controller
   643             return controller
   619         return 'view'
   644         return 'view'
   620 
       
   621     def external_resource(self, rid, default=_MARKER):
       
   622         """return a path to an external resource, using its identifier
       
   623 
       
   624         raise KeyError  if the resource is not defined
       
   625         """
       
   626         try:
       
   627             value = self.vreg.config.ext_resources[rid]
       
   628         except KeyError:
       
   629             if default is _MARKER:
       
   630                 raise
       
   631             return default
       
   632         if value is None:
       
   633             return None
       
   634         baseurl = self.datadir_url[:-1] # remove trailing /
       
   635         if isinstance(value, list):
       
   636             return [v.replace('DATADIR', baseurl) for v in value]
       
   637         return value.replace('DATADIR', baseurl)
       
   638     external_resource = cached(external_resource, keyarg=1)
       
   639 
   645 
   640     def validate_cache(self):
   646     def validate_cache(self):
   641         """raise a `DirectResponse` exception if a cached page along the way
   647         """raise a `DirectResponse` exception if a cached page along the way
   642         exists and is still usable.
   648         exists and is still usable.
   643 
   649 
   709                 return user.decode('UTF8'), passwd
   715                 return user.decode('UTF8'), passwd
   710             except Exception, ex:
   716             except Exception, ex:
   711                 self.debug('bad authorization %s (%s: %s)',
   717                 self.debug('bad authorization %s (%s: %s)',
   712                            auth, ex.__class__.__name__, ex)
   718                            auth, ex.__class__.__name__, ex)
   713         return None, None
   719         return None, None
   714 
       
   715     @deprecated("[3.4] use parse_accept_header('Accept-Language')")
       
   716     def header_accept_language(self):
       
   717         """returns an ordered list of preferred languages"""
       
   718         return [value.split('-')[0] for value in
       
   719                 self.parse_accept_header('Accept-Language')]
       
   720 
   720 
   721     def parse_accept_header(self, header):
   721     def parse_accept_header(self, header):
   722         """returns an ordered list of preferred languages"""
   722         """returns an ordered list of preferred languages"""
   723         accepteds = self.get_header(header, '')
   723         accepteds = self.get_header(header, '')
   724         values = []
   724         values = []
   821         if self.xhtml_browser():
   821         if self.xhtml_browser():
   822             return (u'<?xml version="1.0"?>\n' + STRICT_DOCTYPE + # XXX encoding ?
   822             return (u'<?xml version="1.0"?>\n' + STRICT_DOCTYPE + # XXX encoding ?
   823                     u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">')
   823                     u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">')
   824         return u'<div>'
   824         return u'<div>'
   825 
   825 
       
   826     @deprecated('[3.9] use req.uiprops[rid]')
       
   827     def external_resource(self, rid, default=_MARKER):
       
   828         """return a path to an external resource, using its identifier
       
   829 
       
   830         raise `KeyError` if the resource is not defined
       
   831         """
       
   832         try:
       
   833             return self.uiprops[rid]
       
   834         except KeyError:
       
   835             if default is _MARKER:
       
   836                 raise
       
   837             return default
       
   838 
       
   839     @deprecated("[3.4] use parse_accept_header('Accept-Language')")
       
   840     def header_accept_language(self):
       
   841         """returns an ordered list of preferred languages"""
       
   842         return [value.split('-')[0] for value in
       
   843                 self.parse_accept_header('Accept-Language')]
       
   844 
       
   845 
   826 from cubicweb import set_log_methods
   846 from cubicweb import set_log_methods
   827 set_log_methods(CubicWebRequestBase, LOGGER)
   847 set_log_methods(CubicWebRequestBase, LOGGER)