[web form] refactor Form.__init__ so that extra kwargs are set earlier in the process. Closes #2443040
(eg if you want to use them in .session_key)
# copyright 2003-2012 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=E0611fromCookieimportSimpleCookiefromcalendarimporttimegmfromdatetimeimportdate,datetimefromurlparseimporturlsplitimporthttplibfromitertoolsimportcountfromwarningsimportwarnfromrql.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.httpcacheimportGMTOFFSET,get_validatorsfromcubicweb.web.http_headersimportHeaders,Cookie,parseDateTime_MARKER=object()defbuild_cb_uid(seed):sha=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 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]classCubicWebRequestBase(DBAPIRequest):"""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={}):""" :vreg: Vregistry, :https: boolean, s this a https request :form: Forms value """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=Noneifhttps: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(self)#: received headersself._headers_in=Headers()fork,vinheaders.iteritems():self._headers_in.addRawHeader(k,v)#: form parametersself.setup_params(form)#: 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={}#: 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 header#: Header used for the final responseself.headers_out=Headers()#: HTTP status use by the final responseself.status_out=200def_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)defbase_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=count(1).nextself.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)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)# 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':warn('[3.13] __message in request parameter is deprecated (may ''only be given to .build_url). Seeing this message usualy ''means your application hold some <form> where you should ''replace use of __message hidden input by form.set_message, ''so new _cwmsgid mechanism is properly used',DeprecationWarning)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 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._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'):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 a URL to call that callback which can be inserted in an HTML view. `rqlargs` should be a tuple containing argument to give to the execute function. The first argument following rqlargs must be the message to be displayed after the callback is called. 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 a URL which can be inserted in an HTML view. When the URL is accessed, the callback function will be called (as 'cb(req, \*cbargs)', and a message will be displayed in the web interface. The third positional argument must be 'msg', containing the message. 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:returnfunc(req,*args)finally:self.unregister_callback(self.pageid,cbname)self.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):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,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('__')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):"""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+GMTOFFSET>date(1970,1,1)expires=timegm((expires+GMTOFFSET).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)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):"""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])# 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)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]registered_controllers=self.vreg['controllers'].keys()ifcontrollerinregistered_controllers:returncontrollerreturn'view'defvalidate_cache(self):"""raise a `StatusResponse` exception if a cached page along the way exists and is still usable. calls the client-dependant implementation of `_validate_cache` """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')ifself.http_method()=='HEAD':raiseStatusResponse(200,'')# /!\ no raise, the function returns and we keep processing the request)else:# overwrite headers_out to forge a brand new not-modified responseself.headers_out=self._forge_cached_headers()ifself.http_method()in('HEAD','GET'):raiseStatusResponse(httplib.NOT_MODIFIED)else:raiseStatusResponse(httplib.PRECONDITION_FAILED)# 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.iteritems(),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:# :/ twisted is returned a localized time stampreturndatetime.fromtimestamp(mtime)+GMTOFFSETreturnNone### 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'),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)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 """ifnotself.vreg.config['force-html-content-type']:ifnothasattr(self,'main_stream'):raiseException("Can't demote to html from an ajax context. You ""should change force-html-content-type to yes ""in the instance configuration file.")self.set_content_type('text/html')self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT)# xml doctype #############################################################defset_doctype(self,doctype,reset_xmldecl=True):"""helper method to dynamically change page doctype :param doctype: the new doctype, e.g. '<!DOCTYPE html>' :param reset_xmldecl: if True, remove the '<?xml version="1.0"?>' declaration from the page """self.main_stream.set_doctype(doctype,reset_xmldecl)# 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## 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)