# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""abstract class for http request"""__docformat__="restructuredtext en"importCookieimporthashlibimporttimeimportrandomimportbase64fromdatetimeimportdatefromurlparseimporturlsplitfromitertoolsimportcountfromwarningsimportwarnfromrql.utilsimportrqlvar_makerfromlogilab.common.decoratorsimportcachedfromlogilab.common.deprecationimportdeprecatedfromlogilab.mtconverterimportxml_escapefromcubicweb.dbapiimportDBAPIRequestfromcubicweb.mailimportheaderfromcubicweb.uilibimportremove_html_tags,jsfromcubicweb.utilsimportSizeConstrainedList,HTMLHead,make_uidfromcubicweb.viewimportSTRICT_DOCTYPE,TRANSITIONAL_DOCTYPE_NOEXTfromcubicweb.webimport(INTERNAL_FIELD_VALUE,LOGGER,NothingToEdit,RequestError,StatusResponse)fromcubicweb.web.http_headersimportHeaders_MARKER=object()defbuild_cb_uid(seed):sha=hashlib.sha1('%s%s%s'%(time.time(),seed,random.random()))return'cb_%s'%(sha.hexdigest())deflist_form_param(form,param,pop=False):"""get param from form parameters and return its value as a list, skipping internal markers if any * if the parameter isn't defined, return an empty list * if the parameter is a single (unicode) value, return a list containing that value * if the parameter is already a list or tuple, just skip internal markers if pop is True, the parameter is removed from the form dictionnary """ifpop:try:value=form.pop(param)exceptKeyError:return[]else:value=form.get(param,())ifvalueisNone:value=()elifnotisinstance(value,(list,tuple)):value=[value]return[vforvinvalueifv!=INTERNAL_FIELD_VALUE]classCubicWebRequestBase(DBAPIRequest):"""abstract HTTP request, should be extended according to the HTTP backend"""json_request=False# to be set to True by json controllersdef__init__(self,vreg,https,form=None):super(CubicWebRequestBase,self).__init__(vreg)self.https=httpsifhttps:self.uiprops=vreg.config.https_uipropsself.datadir_url=vreg.config.https_datadir_urlelse:self.uiprops=vreg.config.uipropsself.datadir_url=vreg.config.datadir_url# raw html headers that can be added from any viewself.html_headers=HTMLHead()# form parametersself.setup_params(form)# dictionnary that may be used to store request data that has to be# shared among various components used to publish the request (views,# controller, application...)self.data={}# search state: 'normal' or 'linksearch' (eg searching for an object# to create a relation with another)self.search_state=('normal',)# page id, set by htmlheader templateself.pageid=Noneself._set_pageid()# prepare output headerself.headers_out=Headers()def_set_pageid(self):"""initialize self.pageid if req.form provides a specific pageid, use it, otherwise build a new one. """pid=self.form.get('pageid')ifpidisNone:pid=make_uid(id(self))self.pageid=pidself.html_headers.define_var('pageid',pid,override=False)@propertydefauthmode(self):returnself.vreg.config['auth-mode']@propertydefvarmaker(self):"""the rql varmaker is exposed both as a property and as the set_varmaker function since we've two use cases: * accessing the req.varmaker property to get a new variable name * calling req.set_varmaker() to ensure a varmaker is set for later ajax calls sharing our .pageid """returnself.set_varmaker()def_get_tabindex_func(self):nextfunc=self.get_page_data('nexttabfunc')ifnextfuncisNone:nextfunc=count(1).nextself.set_page_data('nexttabfunc',nextfunc)returnnextfuncdefset_varmaker(self):varmaker=self.get_page_data('rql_varmaker')ifvarmakerisNone:varmaker=rqlvar_maker()self.set_page_data('rql_varmaker',varmaker)returnvarmakerdefset_session(self,session,user=None):"""method called by the session handler when the user is authenticated or an anonymous connection is open """super(CubicWebRequestBase,self).set_session(session,user)# tabindex generatorself.next_tabindex=self._get_tabindex_func()# set request languagevreg=self.vregifself.user:try:# 1. user specified languagelang=vreg.typed_value('ui.language',self.user.properties['ui.language'])self.set_language(lang)returnexceptKeyError:passifvreg.config['language-negociation']:# 2. http negociated languageforlanginself.header_accept_language():iflanginself.translations:self.set_language(lang)return# 3. default languageself.set_default_language(vreg)defset_language(self,lang):gettext,self.pgettext=self.translations[lang]self._=self.__=gettextself.lang=langself.debug('request language: %s',lang)ifself.cnx:self.cnx.set_session_props(lang=lang)# input form parameters management ######################################### common form parameters which should be protected against html values# XXX can't add 'eid' for instance since it may be multivalued# dont put rql as well, if query contains < and > it will be corrupted!no_script_form_params=set(('vid','etype','vtitle','title','__message','__redirectvid','__redirectrql'))defsetup_params(self,params):"""WARNING: we're intentionaly leaving INTERNAL_FIELD_VALUE here subclasses should overrides to """self.form={}ifparamsisNone:returnencoding=self.encodingforparam,valinparams.iteritems():ifisinstance(val,(tuple,list)):val=[unicode(x,encoding)forxinval]iflen(val)==1:val=val[0]elifisinstance(val,str):val=unicode(val,encoding)ifparaminself.no_script_form_paramsandval:val=self.no_script_form_param(param,val)ifparam=='_cwmsgid':self.set_message_id(val)elifparam=='__message':self.set_message(val)else:self.form[param]=valdefno_script_form_param(self,param,value):"""ensure there is no script in a user form param by default return a cleaned string instead of raising a security exception this method should be called on every user input (form at least) fields that are at some point inserted in a generated html page to protect against script kiddies """# safety belt for strange urls like http://...?vtitle=yo&vtitle=yoifisinstance(value,(list,tuple)):self.error('no_script_form_param got a list (%s). Who generated the URL ?',repr(value))value=value[0]returnremove_html_tags(value)deflist_form_param(self,param,form=None,pop=False):"""get param from form parameters and return its value as a list, skipping internal markers if any * if the parameter isn't defined, return an empty list * if the parameter is a single (unicode) value, return a list containing that value * if the parameter is already a list or tuple, just skip internal markers if pop is True, the parameter is removed from the form dictionnary """ifformisNone:form=self.formreturnlist_form_param(form,param,pop)defreset_headers(self):"""used by AutomaticWebTest to clear html headers between tests on the same resultset """self.html_headers=HTMLHead()returnself# web state helpers #######################################################@propertydefmessage(self):try:returnself.session.data.pop(self._msgid,'')exceptAttributeError:try:returnself._msgexceptAttributeError:returnNonedefset_message(self,msg):assertisinstance(msg,unicode)self._msg=msgdefset_message_id(self,msgid):self._msgid=msgid@cacheddefredirect_message_id(self):returnmake_uid()defset_redirect_message(self,msg):assertisinstance(msg,unicode)msgid=self.redirect_message_id()self.session.data[msgid]=msgreturnmsgiddefappend_to_redirect_message(self,msg):msgid=self.redirect_message_id()currentmsg=self.session.data.get(msgid)ifcurrentmsgisnotNone:currentmsg='%s%s'%(currentmsg,msg)else:currentmsg=msgself.session.data[msgid]=currentmsgreturnmsgiddefreset_message(self):ifhasattr(self,'_msg'):delself._msgifhasattr(self,'_msgid'):delself._msgiddefupdate_search_state(self):"""update the current search state"""searchstate=self.form.get('__mode')ifnotsearchstateandself.cnx:searchstate=self.session.data.get('search_state','normal')self.set_search_state(searchstate)defset_search_state(self,searchstate):"""set a new search state"""ifsearchstateisNoneorsearchstate=='normal':self.search_state=(searchstateor'normal',)else:self.search_state=('linksearch',searchstate.split(':'))assertlen(self.search_state[-1])==4ifself.cnx:self.session.data['search_state']=searchstatedefmatch_search_state(self,rset):"""when searching an entity to create a relation, return True if entities in the given rset may be used as relation end """try:searchedtype=self.search_state[1][-1]exceptIndexError:returnFalse# no searching for associationforetypeinrset.column_types(0):ifetype!=searchedtype:returnFalsereturnTruedefupdate_breadcrumbs(self):"""stores the last visisted page in session data"""searchstate=self.session.data.get('search_state')ifsearchstate=='normal':breadcrumbs=self.session.data.get('breadcrumbs')ifbreadcrumbsisNone:breadcrumbs=SizeConstrainedList(10)self.session.data['breadcrumbs']=breadcrumbsbreadcrumbs.append(self.url())else:url=self.url()ifbreadcrumbsandbreadcrumbs[-1]!=url:breadcrumbs.append(url)deflast_visited_page(self):breadcrumbs=self.session.data.get('breadcrumbs')ifbreadcrumbs:returnbreadcrumbs.pop()returnself.base_url()defuser_rql_callback(self,rqlargs,*args,**kwargs):"""register a user callback to execute some rql query and return an url to call it ready to be inserted in html. rqlargs should be a tuple containing argument to give to the execute function. For other allowed arguments, see :meth:`user_callback` method """defrqlexec(req,rql,args=None,key=None):req.execute(rql,args,key)returnself.user_callback(rqlexec,rqlargs,*args,**kwargs)defuser_callback(self,cb,cbargs,*args,**kwargs):"""register the given user callback and return an url to call it ready to be inserted in html. You can specify the underlying js function to call using a 'jsfunc' named args, to one of :func:`userCallback`, ':func:`userCallbackThenUpdateUI`, ':func:`userCallbackThenReloadPage` (the default). Take care arguments may vary according to the used function. """self.add_js('cubicweb.ajax.js')jsfunc=kwargs.pop('jsfunc','userCallbackThenReloadPage')if'msg'inkwargs:warn('[3.10] msg should be given as positional argument',DeprecationWarning,stacklevel=2)args=(kwargs.pop('msg'),)+argsassertnotkwargs,'dunno what to do with remaining kwargs: %s'%kwargscbname=self.register_onetime_callback(cb,*cbargs)return"javascript: %s"%getattr(js,jsfunc)(cbname,*args)defregister_onetime_callback(self,func,*args):cbname=build_cb_uid(func.__name__)def_cb(req):try:ret=func(req,*args)exceptTypeError:warn('[3.2] user callback should now take request as argument')ret=func(*args)self.unregister_callback(self.pageid,cbname)returnretself.set_page_data(cbname,_cb)returncbnamedefunregister_callback(self,pageid,cbname):assertpageidisnotNoneassertcbname.startswith('cb_')self.info('unregistering callback %s for pageid %s',cbname,pageid)self.del_page_data(cbname)defclear_user_callbacks(self):ifself.sessionisnotNone:# XXXforkeyinself.session.data.keys():ifkey.startswith('cb_'):delself.session.data[key]# web edition helpers #####################################################@cached# so it's writed only oncedeffckeditor_config(self):self.add_js('fckeditor/fckeditor.js')self.html_headers.define_var('fcklang',self.lang)self.html_headers.define_var('fckconfigpath',self.data_url('cubicweb.fckcwconfig.js'))defuse_fckeditor(self):returnself.vreg.config.fckeditor_installed()andself.property_value('ui.fckeditor')defedited_eids(self,withtype=False):"""return a list of edited eids"""yielded=False# warning: use .keys since the caller may change `form`form=self.formtry:eids=form['eid']exceptKeyError:raiseNothingToEdit(self._('no selected entities'))ifisinstance(eids,basestring):eids=(eids,)forpeidineids:ifwithtype:typekey='__type:%s'%peidasserttypekeyinform,'no entity type specified'yieldpeid,form[typekey]else:yieldpeidyielded=Trueifnotyielded:raiseNothingToEdit(self._('no selected entities'))# minparams=3 by default: at least eid, __type, and some params to changedefextract_entity_params(self,eid,minparams=3):"""extract form parameters relative to the given eid"""params={}eid=str(eid)form=self.formforparaminform:try:name,peid=param.split(':',1)exceptValueError:ifnotparam.startswith('__')andparam!="eid":self.warning('param %s mis-formatted',param)continueifpeid==eid:value=form[param]ifvalue==INTERNAL_FIELD_VALUE:value=Noneparams[name]=valueparams['eid']=eidiflen(params)<minparams:raiseRequestError(self._('missing parameters for entity %s')%eid)returnparams# XXX this should go to the GenericRelationsField. missing edition cancel protocol.defremove_pending_operations(self):"""shortcut to clear req's pending_{delete,insert} entries This is needed when the edition is completed (whether it's validated or cancelled) """self.session.data.pop('pending_insert',None)self.session.data.pop('pending_delete',None)defcancel_edition(self,errorurl):"""remove pending operations and `errorurl`'s specific stored data """self.session.data.pop(errorurl,None)self.remove_pending_operations()# high level methods for HTTP headers management ########################### must be cached since login/password are popped from the form dictionary# and this method may be called multiple times during authentication@cacheddefget_authorization(self):"""Parse and return the Authorization header"""ifself.authmode=="cookie":try:user=self.form.pop("__login")passwd=self.form.pop("__password",'')returnuser,passwd.encode('UTF8')exceptKeyError:self.debug('no login/password in form params')returnNone,Noneelse:returnself.header_authorization()defget_cookie(self):"""retrieve request cookies, returns an empty cookie if not found"""try:returnCookie.SimpleCookie(self.get_header('Cookie'))exceptKeyError:returnCookie.SimpleCookie()defset_cookie(self,cookie,key,maxage=300,expires=None):"""set / update a cookie key by default, cookie will be available for the next 5 minutes. Give maxage = None to have a "session" cookie expiring when the client close its browser """morsel=cookie[key]ifmaxageisnotNone:morsel['Max-Age']=maxageifexpires:morsel['expires']=expires.strftime('%a, %d %b %Y %H:%M:%S %z')# make sure cookie is set on the correct pathmorsel['path']=self.base_url_path()self.add_header('Set-Cookie',morsel.OutputString())defremove_cookie(self,cookie,key):"""remove a cookie by expiring it"""self.set_cookie(cookie,key,maxage=0,expires=date(1970,1,1))defset_content_type(self,content_type,filename=None,encoding=None):"""set output content type for this request. An optional filename may be given """ifcontent_type.startswith('text/')and';charset='notincontent_type:content_type+=';charset='+(encodingorself.encoding)self.set_header('content-type',content_type)iffilename:ifisinstance(filename,unicode):filename=header(filename).encode()self.set_header('content-disposition','inline; filename=%s'%filename)# high level methods for HTML headers management ##########################defadd_onload(self,jscode):self.html_headers.add_onload(jscode)defadd_js(self,jsfiles,localfile=True):"""specify a list of JS files to include in the HTML headers :param jsfiles: a JS filename or a list of JS filenames :param localfile: if True, the default data dir prefix is added to the JS filename """ifisinstance(jsfiles,basestring):jsfiles=(jsfiles,)forjsfileinjsfiles:iflocalfile:jsfile=self.data_url(jsfile)self.html_headers.add_js(jsfile)defadd_css(self,cssfiles,media=u'all',localfile=True,ieonly=False,iespec=u'[if lt IE 8]'):"""specify a CSS file to include in the HTML headers :param cssfiles: a CSS filename or a list of CSS filenames :param media: the CSS's media if necessary :param localfile: if True, the default data dir prefix is added to the CSS filename :param ieonly: True if this css is specific to IE :param iespec: conditional expression that will be used around the css inclusion. cf: http://msdn.microsoft.com/en-us/library/ms537512(VS.85).aspx """ifisinstance(cssfiles,basestring):cssfiles=(cssfiles,)ifieonly:ifself.ie_browser():extraargs=[iespec]add_css=self.html_headers.add_ie_csselse:return# no need to do anything on non IE browserselse:extraargs=[]add_css=self.html_headers.add_cssforcssfileincssfiles:iflocalfile:cssfile=self.data_url(cssfile)add_css(cssfile,media,*extraargs)@deprecated('[3.9] use ajax_replace_url() instead, naming rql and vid arguments')defbuild_ajax_replace_url(self,nodeid,rql,vid,replacemode='replace',**extraparams):returnself.ajax_replace_url(nodeid,replacemode,rql=rql,vid=vid,**extraparams)defajax_replace_url(self,nodeid,replacemode='replace',**extraparams):"""builds an ajax url that will replace nodeid's content :param nodeid: the dom id of the node to replace :param replacemode: defines how the replacement should be done. Possible values are : - 'replace' to replace the node's content with the generated HTML - 'swap' to replace the node itself with the generated HTML - 'append' to append the generated HTML to the node's content Arbitrary extra named arguments may be given, they will be included as parameters of the generated url. """# define a function in headers and use it in the link to avoid url# unescaping pb: browsers give the js expression to the interpreter# after having url unescaping the content. This may make appear some# quote or other special characters that will break the js expression.extraparams.setdefault('fname','view')url=self.build_url('json',**extraparams)cbname=build_cb_uid(url[:50])jscode='function %s() { $("#%s").%s; }'%(cbname,nodeid,js.loadxhtml(url,None,'get',replacemode))self.html_headers.add_post_inline_script(jscode)return"javascript: %s()"%cbname# urls/path management ####################################################defurl(self,includeparams=True):"""return currently accessed url"""returnself.base_url()+self.relative_path(includeparams)defselected(self,url):"""return True if the url is equivalent to currently accessed url"""reqpath=self.relative_path().lower()baselen=len(self.base_url())return(reqpath==url[baselen:].lower())defbase_url_prepend_host(self,hostname):protocol,roothost=urlsplit(self.base_url())[:2]ifroothost.startswith('www.'):roothost=roothost[4:]return'%s://%s.%s'%(protocol,hostname,roothost)defbase_url_path(self):"""returns the absolute path of the base url"""returnurlsplit(self.base_url())[2]defdata_url(self,relpath):"""returns the absolute path for a data resouce"""returnself.datadir_url+relpath@cacheddeffrom_controller(self):"""return the id (string) of the controller issuing the request"""controller=self.relative_path(False).split('/',1)[0]registered_controllers=self.vreg['controllers'].keys()ifcontrollerinregistered_controllers:returncontrollerreturn'view'defvalidate_cache(self):"""raise a `DirectResponse` exception if a cached page along the way exists and is still usable. calls the client-dependant implementation of `_validate_cache` """self._validate_cache()ifself.http_method()=='HEAD':raiseStatusResponse(200,'')# abstract methods to override according to the web front-end #############defhttp_method(self):"""returns 'POST', 'GET', 'HEAD', etc."""raiseNotImplementedError()def_validate_cache(self):"""raise a `DirectResponse` exception if a cached page along the way exists and is still usable """raiseNotImplementedError()defrelative_path(self,includeparams=True):"""return the normalized path of the request (ie at least relative to the instance's root, but some other normalization may be needed so that the returned path may be used to compare to generated urls :param includeparams: boolean indicating if GET form parameters should be kept in the path """raiseNotImplementedError()defget_header(self,header,default=None):"""return the value associated with the given input HTTP header, raise KeyError if the header is not set """raiseNotImplementedError()defset_header(self,header,value,raw=True):"""set an output HTTP header"""ifraw:# adding encoded header is important, else page content# will be reconverted back to unicode and apart unefficiency, this# may cause decoding problem (e.g. when downloading a file)self.headers_out.setRawHeaders(header,[str(value)])else:self.headers_out.setHeader(header,value)defadd_header(self,header,value):"""add an output HTTP header"""# adding encoded header is important, else page content# will be reconverted back to unicode and apart unefficiency, this# may cause decoding problem (e.g. when downloading a file)self.headers_out.addRawHeader(header,str(value))defremove_header(self,header):"""remove an output HTTP header"""self.headers_out.removeHeader(header)defheader_authorization(self):"""returns a couple (auth-type, auth-value)"""auth=self.get_header("Authorization",None)ifauth:scheme,rest=auth.split(' ',1)scheme=scheme.lower()try:assertscheme=="basic"user,passwd=base64.decodestring(rest).split(":",1)# XXX HTTP header encoding: use email.Header?returnuser.decode('UTF8'),passwdexceptException,ex:self.debug('bad authorization %s (%s: %s)',auth,ex.__class__.__name__,ex)returnNone,Nonedefparse_accept_header(self,header):"""returns an ordered list of accepted values"""try:value_parser,value_sort_key=ACCEPT_HEADER_PARSER[header.lower()]exceptKeyError:value_parser=value_sort_key=Noneaccepteds=self.get_header(header,'')values=_parse_accept_header(accepteds,value_parser,value_sort_key)return(raw_valuefor(raw_value,parsed_value,score)invalues)defheader_if_modified_since(self):"""If the HTTP header If-modified-since is set, return the equivalent mx date time value (GMT), else return None """raiseNotImplementedError()defdemote_to_html(self):"""helper method to dynamically set request content type to text/html The global doctype and xmldec must also be changed otherwise the browser will display '<[' at the beginning of the page """self.set_content_type('text/html')self.main_stream.doctype=TRANSITIONAL_DOCTYPE_NOEXTself.main_stream.xmldecl=u''# page data management ####################################################defget_page_data(self,key,default=None):"""return value associated to `key` in current page data"""page_data=self.session.data.get(self.pageid)ifpage_dataisNone:returndefaultreturnpage_data.get(key,default)defset_page_data(self,key,value):"""set value associated to `key` in current page data"""self.html_headers.add_unload_pagedata()page_data=self.session.data.setdefault(self.pageid,{})page_data[key]=valueself.session.data[self.pageid]=page_datadefdel_page_data(self,key=None):"""remove value associated to `key` in current page data if `key` is None, all page data will be cleared """ifkeyisNone:self.session.data.pop(self.pageid,None)else:try:delself.session.data[self.pageid][key]exceptKeyError:pass# user-agent detection ####################################################@cacheddefuseragent(self):returnself.get_header('User-Agent',None)defie_browser(self):useragent=self.useragent()returnuseragentand'MSIE'inuseragentdefxhtml_browser(self):"""return True if the browser is considered as xhtml compatible. If the instance is configured to always return text/html and not application/xhtml+xml, this method will always return False, even though this is semantically different """ifself.vreg.config['force-html-content-type']:returnFalseuseragent=self.useragent()# * MSIE/Konqueror does not support xml content-type# * Opera supports xhtml and handles namespaces properly but it breaks# jQuery.attr()ifuseragentand('MSIE'inuseragentor'KHTML'inuseragentor'Opera'inuseragent):returnFalsereturnTruedefhtml_content_type(self):ifself.xhtml_browser():return'application/xhtml+xml'return'text/html'defdocument_surrounding_div(self):ifself.xhtml_browser():return(u'<?xml version="1.0"?>\n'+STRICT_DOCTYPE+# XXX encoding ?u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">')returnu'<div>'@deprecated('[3.9] use req.uiprops[rid]')defexternal_resource(self,rid,default=_MARKER):"""return a path to an external resource, using its identifier raise `KeyError` if the resource is not defined """try:returnself.uiprops[rid]exceptKeyError:ifdefaultis_MARKER:raisereturndefault@deprecated("[3.4] use parse_accept_header('Accept-Language')")defheader_accept_language(self):"""returns an ordered list of preferred languages"""return[value.split('-')[0]forvalueinself.parse_accept_header('Accept-Language')]## HTTP-accept parsers / utilies ##############################################def_mimetype_sort_key(accept_info):"""accepted mimetypes must be sorted by : 1/ highest score first 2/ most specific mimetype first, e.g. : - 'text/html level=1' is more specific 'text/html' - 'text/html' is more specific than 'text/*' - 'text/*' itself more specific than '*/*' """raw_value,(media_type,media_subtype,media_type_params),score=accept_info# FIXME: handle '+' in media_subtype ? (should xhtml+xml have a# higher precedence than xml ?)ifmedia_subtype=='*':score-=0.0001ifmedia_type=='*':score-=0.0001return1./score,media_type,media_subtype,1./(1+len(media_type_params))def_charset_sort_key(accept_info):"""accepted mimetypes must be sorted by : 1/ highest score first 2/ most specific charset first, e.g. : - 'utf-8' is more specific than '*' """raw_value,value,score=accept_infoifvalue=='*':score-=0.0001return1./score,valuedef_parse_accept_header(raw_header,value_parser=None,value_sort_key=None):"""returns an ordered list accepted types returned value is a list of 2-tuple (value, score), ordered by score. Exact type of `value` will depend on what `value_parser` will reutrn. if `value_parser` is None, then the raw value, as found in the http header, is used. """ifvalue_sort_keyisNone:value_sort_key=lambdainfos:1./infos[-1]values=[]forinfoinraw_header.split(','):score=1.0other_params={}try:value,infodef=info.split(';',1)exceptValueError:value=infoelse:forinfoininfodef.split(';'):try:infokey,infoval=info.split('=')ifinfokey=='q':# XXX 'level'score=float(infoval)continueexceptValueError:continueother_params[infokey]=infovalparsed_value=value_parser(value,other_params)ifvalue_parserelsevaluevalues.append((value.strip(),parsed_value,score))values.sort(key=value_sort_key)returnvaluesdef_mimetype_parser(value,other_params):"""return a 3-tuple (type, subtype, type_params) corresponding to the mimetype definition e.g. : for 'text/*', `mimetypeinfo` will be ('text', '*', {}), for 'text/html;level=1', `mimetypeinfo` will be ('text', '*', {'level': '1'}) """try:media_type,media_subtype=value.strip().split('/')exceptValueError:# safety belt : '/' should always be presentmedia_type=value.strip()media_subtype='*'return(media_type,media_subtype,other_params)ACCEPT_HEADER_PARSER={'accept':(_mimetype_parser,_mimetype_sort_key),'accept-charset':(None,_charset_sort_key),}fromcubicwebimportset_log_methodsset_log_methods(CubicWebRequestBase,LOGGER)