[server] Port BFSS to py3k
The BFSS API changes in python 3:
* 'defaultdir' MUST be a unicode object
* 'fsencoding' MUST NOT be set
In python 2, fsencoding handles both the encoding of file paths on the
file system (utf-8 by default, but the system may actually be using
something else) and the encoding of file paths that will be stored in
the database.
So in python 3, we wipe the slate clean:
* rely on sys.getfilesystemencoding() to convert unicode objects to
bytes
* always encode paths to utf-8 for storage in the database
Caveat emptor / here be dragons:
* sys.getfilesystemencoding() depends on the current locale, which
therefore MUST be set properly
* when migrating an existing instance from py2 to py3, one MAY need
to reencode file paths stored in the database
# copyright 2003-2014 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"importtimeimportrandomimportbase64fromhashlibimportsha1# pylint: disable=E0611fromcalendarimporttimegmfromdatetimeimportdate,datetimefromwarningsimportwarnfromioimportBytesIOfromsiximportPY2,text_type,string_typesfromsix.movesimporthttp_clientfromsix.moves.urllib.parseimporturlsplit,quoteasurlquotefromsix.moves.http_cookiesimportSimpleCookiefromrql.utilsimportrqlvar_makerfromlogilab.common.decoratorsimportcachedfromlogilab.common.deprecationimportdeprecatedfromlogilab.mtconverterimportxml_escapefromcubicwebimportAuthenticationErrorfromcubicweb.reqimportRequestSessionBasefromcubicweb.uilibimportremove_html_tags,jsfromcubicweb.utilsimportSizeConstrainedList,HTMLHead,make_uidfromcubicweb.viewimportTRANSITIONAL_DOCTYPE_NOEXTfromcubicweb.webimport(INTERNAL_FIELD_VALUE,LOGGER,NothingToEdit,RequestError,StatusResponse)fromcubicweb.web.httpcacheimportget_validatorsfromcubicweb.web.http_headersimportHeaders,Cookie,parseDateTime_MARKER=object()defbuild_cb_uid(seed):sha=sha1(('%s%s%s'%(time.time(),seed,random.random())).encode('ascii'))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 dictionary """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]classCounter(object):"""A picklable counter object, usable for e.g. page tab index count"""__slots__=('value',)def__init__(self,initialvalue=0):self.value=initialvaluedef__call__(self):value=self.valueself.value+=1returnvaluedef__getstate__(self):return{'value':self.value}def__setstate__(self,state):self.value=state['value']class_CubicWebRequestBase(RequestSessionBase):"""abstract HTTP request, should be extended according to the HTTP backend Immutable attributes that describe the received query and generic configuration """ajax_request=False# to be set to True by ajax controllersdef__init__(self,vreg,https=False,form=None,headers=None):""" :vreg: Vregistry, :https: boolean, s this a https request :form: Forms value :headers: dict, request header """super(_CubicWebRequestBase,self).__init__(vreg)#: (Boolean) Is this an https request.self.https=https#: User interface property (vary with https) (see :ref:`uiprops`)self.uiprops=None#: url for serving datadir (vary with https) (see :ref:`resources`)self.datadir_url=Noneifhttpsandvreg.config.https_uipropsisnotNone:self.uiprops=vreg.config.https_uipropselse:self.uiprops=vreg.config.uipropsifhttpsandvreg.config.https_datadir_urlisnotNone:self.datadir_url=vreg.config.https_datadir_urlelse:self.datadir_url=vreg.config.datadir_url#: enable UStringIO's write tracingself.tracehtml=Falseifvreg.config.debugmode:self.tracehtml=bool(form.pop('_cwtracehtml',False))#: raw html headers that can be added from any viewself.html_headers=HTMLHead(self,tracewrites=self.tracehtml)#: received headersself._headers_in=Headers()ifheadersisnotNone:fork,vinheaders.items():self._headers_in.addRawHeader(k,v)#: form parametersself.setup_params(form)#: received bodyself.content=BytesIO()# prepare output header#: Header used for the final responseself.headers_out=Headers()#: HTTP status use by the final responseself.status_out=200# set up language based on request headers or site default (we don't# have a user yet, and might not get one)self.set_user_language(None)#: dictionary 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={}self._search_state=None#: page id, set by htmlheader templateself.pageid=Noneself._set_pageid()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.html_headers.define_var('pageid',pid,override=False)self.pageid=piddef_get_json_request(self):warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',DeprecationWarning,stacklevel=2)returnself.ajax_requestdef_set_json_request(self,value):warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',DeprecationWarning,stacklevel=2)self.ajax_request=valuejson_request=property(_get_json_request,_set_json_request)def_base_url(self,secure=None):"""return the root url of the instance secure = False -> base-url secure = None -> https-url if req.https secure = True -> https if it exist """ifsecureisNone:secure=self.httpsbase_url=Noneifsecure:base_url=self.vreg.config.get('https-url')ifbase_urlisNone:base_url=super(_CubicWebRequestBase,self)._base_url()returnbase_url@propertydefauthmode(self):"""Authentification mode of the instance (see :ref:`WebServerConfig`)"""returnself.vreg.config['auth-mode']# Various variable generator.@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()defnext_tabindex(self):nextfunc=self.get_page_data('nexttabfunc')ifnextfuncisNone:nextfunc=Counter(1)self.set_page_data('nexttabfunc',nextfunc)returnnextfunc()defset_varmaker(self):varmaker=self.get_page_data('rql_varmaker')ifvarmakerisNone:varmaker=rqlvar_maker()self.set_page_data('rql_varmaker',varmaker)returnvarmaker# 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','__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.items():ifisinstance(val,(tuple,list)):ifPY2:val=[unicode(x,encoding)forxinval]iflen(val)==1:val=val[0]elifPY2andisinstance(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)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 dictionary """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(self)returnself# web state helpers #######################################################@propertydefmessage(self):try:returnself.session.data.pop(self._msgid,u'')exceptAttributeError:try:returnself._msgexceptAttributeError:returnNonedefset_message(self,msg):assertisinstance(msg,unicode)self.reset_message()self._msg=msgdefset_message_id(self,msgid):self._msgid=msgid@cacheddefredirect_message_id(self):returnmake_uid()defset_redirect_message(self,msg):# TODO - this should probably be merged with append_to_redirect_messageassertisinstance(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=u'%s%s'%(currentmsg,msg)else:currentmsg=msgself.session.data[msgid]=currentmsgreturnmsgiddefreset_message(self):ifhasattr(self,'_msg'):delself._msgifhasattr(self,'_msgid'):self.session.data.pop(self._msgid,u'')delself._msgiddef_load_search_state(self,searchstate):ifsearchstateisNoneorsearchstate=='normal':self._search_state=('normal',)else:self._search_state=('linksearch',searchstate.split(':'))assertlen(self._search_state[-1])==4,'invalid searchstate'@propertydefsearch_state(self):"""search state: 'normal' or 'linksearch' (i.e. searching for an object to create a relation with another)"""ifself._search_stateisNone:searchstate=self.session.data.get('search_state','normal')self._load_search_state(searchstate)returnself._search_state@search_state.setterdefsearch_state(self,searchstate):self._search_state=searchstatedefupdate_search_state(self):"""update the current search state if needed"""searchstate=self.form.get('__mode')ifsearchstate:self.set_search_state(searchstate)defset_search_state(self,searchstate):"""set a new search state"""self.session.data['search_state']=searchstateself._load_search_state(searchstate)defmatch_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.search_state[0]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()# web edition helpers #####################################################@cached# so it's writed only oncedeffckeditor_config(self):fckeditor_url=self.build_url('fckeditor/fckeditor.js')self.add_js(fckeditor_url,localfile=False)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,string_types):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('__')andparamnotin('eid','_cw_fields'):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"""# XXX use http_headers implementationtry:returnSimpleCookie(self.get_header('Cookie'))exceptKeyError:returnSimpleCookie()defset_cookie(self,name,value,maxage=300,expires=None,secure=False,httponly=False):"""set / update a cookie 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 """ifisinstance(name,SimpleCookie):warn('[3.13] set_cookie now takes name and value as two first ''argument, not anymore cookie object and name',DeprecationWarning,stacklevel=2)secure=name[value]['secure']name,value=value,name[value].valueifmaxage:# don't check is None, 0 may be specifiedassertexpiresisNone,'both max age and expires cant be specified'expires=maxage+time.time()elifexpires:# we don't want to handle times before the EPOCH (cause bug on# windows). Also use > and not >= else expires == 0 and Cookie think# that means no expire...assertexpires>date(1970,1,1)expires=timegm(expires.timetuple())else:expires=None# make sure cookie is set on the correct pathcookie=Cookie(str(name),str(value),self.base_url_path(),expires=expires,secure=secure,httponly=httponly)self.headers_out.addHeader('Set-cookie',cookie)defremove_cookie(self,name,bwcompat=None):"""remove a cookie by expiring it"""ifbwcompatisnotNone:warn('[3.13] remove_cookie now take only a name as argument',DeprecationWarning,stacklevel=2)name=bwcompatself.set_cookie(name,'',maxage=0,expires=date(2000,1,1))defset_content_type(self,content_type,filename=None,encoding=None,disposition='inline'):"""set output content type for this request. An optional filename may be given. The disposition argument may be `attachement` or `inline` as specified for the Content-disposition HTTP header. The disposition parameter have no effect if no filename are specified. """ifcontent_type.startswith('text/')and';charset='notincontent_type:content_type+=';charset='+(encodingorself.encoding)self.set_header('content-type',content_type)iffilename:header=[disposition]unicode_filename=Nonetry:ascii_filename=filename.encode('ascii')exceptUnicodeEncodeError:# fallback filename for very old browserunicode_filename=filenameascii_filename=filename.encode('ascii','ignore')# escape " and \# see http://greenbytes.de/tech/tc2231/#attwithfilenameandextparamescapedascii_filename=ascii_filename.replace('\x5c',r'\\').replace('"',r'\"')header.append('filename="%s"'%ascii_filename)ifunicode_filenameisnotNone:# encoded filename according RFC5987urlquoted_filename=urlquote(unicode_filename.encode('utf-8'),'')header.append("filename*=utf-8''"+urlquoted_filename)self.set_header('content-disposition',';'.join(header))# 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,string_types):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,string_types):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)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')# remove pageid from the generated URL as it's forced as a parameter# to the loadxhtml call below.extraparams.pop('pageid',None)url=self.build_url('ajax',**extraparams)cbname=build_cb_uid(url[:50])# think to propagate pageid. XXX see https://www.cubicweb.org/ticket/1753121jscode=u'function %s() { $("#%s").%s; }'%(cbname,nodeid,js.loadxhtml(url,{'pageid':self.pageid},'get',replacemode))self.html_headers.add_post_inline_script(jscode)return"javascript: %s()"%cbname# urls/path management ####################################################defbuild_url(self,*args,**kwargs):"""return an absolute URL using params dictionary key/values as URL parameters. Values are automatically URL quoted, and the publishing method to use may be specified or will be guessed. """if'__message'inkwargs:msg=kwargs.pop('__message')kwargs['_cwmsgid']=self.set_redirect_message(msg)ifnotargs:method='view'if(self.from_controller()=='view'andnot'_restpath'inkwargs):method=self.relative_path(includeparams=False)or'view'args=(method,)returnsuper(_CubicWebRequestBase,self).build_url(*args,**kwargs)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]ifcontrollerinself.vreg['controllers']:returncontrollerreturn'view'defis_client_cache_valid(self):"""check if a client cached page exists (as specified in request headers) and is still usable. Return False if the page has to be calculated, else True. Some response cache headers may be set by this method. """modified=Trueifself.get_header('Cache-Control')notin('max-age=0','no-cache'):# Here, we search for any invalid 'not modified' condition# see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3validators=get_validators(self._headers_in)ifvalidators:# if we have nomodified=any(func(val,self.headers_out)forfunc,valinvalidators)# Forge expected responseifmodified:if'Expires'notinself.headers_out:# Expires header seems to be required by IE7 -- Are you sure ?self.add_header('Expires','Sat, 01 Jan 2000 00:00:00 GMT')# /!\ no raise, the function returns and we keep processing the requestelse:# overwrite headers_out to forge a brand new not-modified responseself.headers_out=self._forge_cached_headers()ifself.http_method()in('HEAD','GET'):self.status_out=http_client.NOT_MODIFIEDelse:self.status_out=http_client.PRECONDITION_FAILED# XXX replace by True once validate_cache bw compat method is droppedreturnself.status_out# XXX replace by False once validate_cache bw compat method is droppedreturnNone@deprecated('[3.18] use .is_client_cache_valid() method instead')defvalidate_cache(self):"""raise a `StatusResponse` exception if a cached page along the way exists and is still usable. """status_code=self.is_client_cache_valid()ifstatus_codeisnotNone:raiseStatusResponse(status_code)# abstract methods to override according to the web front-end #############defhttp_method(self):"""returns 'POST', 'GET', 'HEAD', etc."""raiseNotImplementedError()def_forge_cached_headers(self):# overwrite headers_out to forge a brand new not-modified responseheaders=Headers()forheaderin(# Required from sec 10.3.5:'date','etag','content-location','expires','cache-control','vary',# Others:'server','proxy-authenticate','www-authenticate','warning'):value=self._headers_in.getRawHeaders(header)ifvalueisnotNone:headers.setRawHeaders(header,value)returnheadersdefrelative_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()# http headers ############################################################### incoming headersdefget_header(self,header,default=None,raw=True):"""return the value associated with the given input header, raise KeyError if the header is not set """ifraw:returnself._headers_in.getRawHeaders(header,[default])[0]returnself._headers_in.getHeader(header,default)defheader_accept_language(self):"""returns an ordered list of preferred languages"""acceptedlangs=self.get_header('Accept-Language',raw=False)or{}forlang,_insorted(acceptedlangs.items(),key=lambdax:x[1],reverse=True):lang=lang.split('-')[0]yieldlangdefheader_if_modified_since(self):"""If the HTTP header If-modified-since is set, return the equivalent date time value (GMT), else return None """mtime=self.get_header('If-modified-since',raw=False)ifmtime:returndatetime.utcfromtimestamp(mtime)returnNone### outcoming headersdefset_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'),passwdexceptExceptionasex: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)@deprecated('[3.17] demote_to_html is deprecated as we always serve html')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 """pass# xml doctype #############################################################defset_doctype(self,doctype,reset_xmldecl=None):"""helper method to dynamically change page doctype :param doctype: the new doctype, e.g. '<!DOCTYPE html>' """ifreset_xmldeclisnotNone:warn('[3.17] reset_xmldecl is deprecated as we only serve html',DeprecationWarning,stacklevel=2)self.main_stream.set_doctype(doctype)# 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'inuseragent@deprecated('[3.17] xhtml_browser is deprecated (xhtml is no longer served)')defxhtml_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 """returnFalsedefhtml_content_type(self):return'text/html'defset_user_language(self,user):vreg=self.vregifuserisnotNone:try:# 1. user-specified languagelang=vreg.typed_value('ui.language',user.properties['ui.language'])self.set_language(lang)returnexceptKeyError:passifvreg.config.get('language-negociation',False):# 2. http accept-languageself.headers_out.addHeader('Vary','Accept-Language')forlanginself.header_accept_language():iflanginself.translations:self.set_language(lang)return# 3. site's default languageself.set_default_language(vreg)def_cnx_func(name):defproxy(req,*args,**kwargs):returngetattr(req.cnx,name)(*args,**kwargs)returnproxyclass_NeedAuthAccessMock(object):def__getattribute__(self,attr):raiseAuthenticationError()def__bool__(self):returnFalse__nonzero__=__bool__class_MockAnonymousSession(object):sessionid='thisisnotarealsession'@propertydefdata(self):return{}@propertydefanonymous_session(self):returnTrueclassConnectionCubicWebRequestBase(_CubicWebRequestBase):cnx=Nonesession=Nonedef__init__(self,vreg,https=False,form=None,headers={}):""""""self.vreg=vregtry:# no vreg or config which doesn't handle translationsself.translations=vreg.config.translationsexceptAttributeError:self.translations={}super(ConnectionCubicWebRequestBase,self).__init__(vreg,https=https,form=form,headers=headers)self.session=_MockAnonymousSession()self.cnx=self.user=_NeedAuthAccessMock()@propertydeftransaction_data(self):returnself.cnx.transaction_datadefset_cnx(self,cnx):if'ecache'incnx.transaction_data:delcnx.transaction_data['ecache']self.cnx=cnxself.session=cnx.sessionself._set_user(cnx.user)self.set_user_language(cnx.user)defexecute(self,*args,**kwargs):rset=self.cnx.execute(*args,**kwargs)rset.req=selfreturnrsetdefset_default_language(self,vreg):try:lang=vreg.property_value('ui.language')exceptException:# property may not be registeredlang='en'try:self.set_language(lang)exceptKeyError:# this occurs usually during test executionself._=self.__=text_typeself.pgettext=lambdax,y:text_type(y)entity_metas=_cnx_func('entity_metas')source_defs=_cnx_func('source_defs')get_shared_data=_cnx_func('get_shared_data')set_shared_data=_cnx_func('set_shared_data')describe=_cnx_func('describe')# deprecated XXX# security #################################################################security_enabled=_cnx_func('security_enabled')# server-side service call #################################################defcall_service(self,regid,**kwargs):returnself.cnx.call_service(regid,**kwargs)# entities cache management ###############################################entity_cache=_cnx_func('entity_cache')set_entity_cache=_cnx_func('set_entity_cache')cached_entities=_cnx_func('cached_entities')drop_entity_cache=_cnx_func('drop_entity_cache')CubicWebRequestBase=ConnectionCubicWebRequestBase## 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 :param value_parser: a function to parse a raw accept chunk. If None is provided, the function defaults to identity. If a function is provided, it must accept 2 parameters ``value`` and ``other_params``. ``value`` is the value found before the first ';', `other_params` is a dictionary built from all other chunks after this first ';' :param value_sort_key: a key function to sort values found in the accept header. This function will be passed a 3-tuple (raw_value, parsed_value, score). If None is provided, the default sort_key is 1./score :return: a list of 3-tuple (raw_value, parsed_value, score), ordered by score. ``parsed_value`` will be the return value of ``value_parser(raw_value)`` """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('/',1)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)