"""abstract class for http request:organization: Logilab:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"importCookieimportshaimporttimeimportrandomimportbase64fromurlparseimporturlsplitfromitertoolsimportcountfromrql.utilsimportrqlvar_makerfromlogilab.common.decoratorsimportcached# XXX move _MARKER here once AppObject.external_resource has been removedfromcubicweb.dbapiimportDBAPIRequestfromcubicweb.common.appobjectimport_MARKERfromcubicweb.common.mailimportheaderfromcubicweb.common.uilibimportremove_html_tagsfromcubicweb.common.utilsimportSizeConstrainedList,HTMLHeadfromcubicweb.webimport(INTERNAL_FIELD_VALUE,LOGGER,NothingToEdit,RequestError,StatusResponse)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"""def__init__(self,vreg,https,form=None):super(CubicWebRequestBase,self).__init__(vreg)self.message=Noneself.authmode=vreg.config['auth-mode']self.https=https# 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',)# tabindex generatorself.tabindexgen=count()self.next_tabindex=self.tabindexgen.next# page id, set by htmlheader templateself.pageid=Noneself.varmaker=rqlvar_maker()self.datadir_url=self._datadir_url()defset_connection(self,cnx,user=None):"""method called by the session handler when the user is authenticated or an anonymous connection is open """super(CubicWebRequestBase,self).set_connection(cnx,user)# get request language:vreg=self.vregifself.user:try:# 1. user specified languagelang=vreg.typed_value('ui.language',self.user.properties['ui.language'])self.set_language(lang)returnexceptKeyError,ex: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):self._=self.__=self.translations[lang]self.lang=langself.debug('request language: %s',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 """ifparamsisNone:params={}self.form=paramsencoding=self.encodingfork,vinparams.items():ifisinstance(v,(tuple,list)):v=[unicode(x,encoding)forxinv]iflen(v)==1:v=v[0]ifkinself.no_script_form_params:v=self.no_script_form_param(k,value=v)ifisinstance(v,str):v=unicode(v,encoding)ifk=='__message':self.set_message(v)delself.form[k]else:self.form[k]=vdefno_script_form_param(self,param,default=None,value=None):"""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 """ifvalueisNone:value=self.form.get(param,default)ifnotvalueisdefaultandvalue:# 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)returnvaluedeflist_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 #######################################################defset_message(self,msg):assertisinstance(msg,unicode)self.message=msgdefupdate_search_state(self):"""update the current search state"""searchstate=self.form.get('__mode')ifnotsearchstateandself.cnxisnotNone:searchstate=self.get_session_data('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.cnxisnotNone:self.set_session_data('search_state',searchstate)defupdate_breadcrumbs(self):"""stores the last visisted page in session data"""searchstate=self.get_session_data('search_state')ifsearchstate=='normal':breadcrumbs=self.get_session_data('breadcrumbs',None)ifbreadcrumbsisNone:breadcrumbs=SizeConstrainedList(10)self.set_session_data('breadcrumbs',breadcrumbs)breadcrumbs.append(self.url())deflast_visited_page(self):breadcrumbs=self.get_session_data('breadcrumbs',None)ifbreadcrumbs:returnbreadcrumbs.pop()returnself.base_url()defregister_onetime_callback(self,func,*args):cbname='cb_%s'%(sha.sha('%s%s%s%s'%(time.time(),func.__name__,random.random(),self.user.login)).hexdigest())def_cb(req):try:ret=func(req,*args)exceptTypeError:fromwarningsimportwarnwarn('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.cnxisnotNone:sessdata=self.session_data()callbacks=[keyforkeyinsessdataifkey.startswith('cb_')]forcallbackincallbacks:self.del_session_data(callback)# web edition helpers #####################################################@cached# so it's writed only oncedeffckeditor_config(self):self.html_headers.define_var('fcklang',self.lang)self.html_headers.define_var('fckconfigpath',self.build_url('data/fckcwconfig.js'))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(None,{None: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(None,{None: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:printeid,paramsraiseRequestError(self._('missing parameters for entity %s')%eid)returnparamsdefget_pending_operations(self,entity,relname,role):operations={'insert':[],'delete':[]}foroptypein('insert','delete'):data=self.get_session_data('pending_%s'%optype)or()foreidfrom,rel,eidtoindata:ifrelname==rel:ifrole=='subject'andentity.eid==eidfrom:operations[optype].append(eidto)ifrole=='object'andentity.eid==eidto:operations[optype].append(eidfrom)returnoperationsdefget_pending_inserts(self,eid=None):"""shortcut to access req's pending_insert entry This is where are stored relations being added while editing an entity. This used to be stored in a temporary cookie. """pending=self.get_session_data('pending_insert')or()return['%s:%s:%s'%(subj,rel,obj)forsubj,rel,objinpendingifeidisNoneoreidin(subj,obj)]defget_pending_deletes(self,eid=None):"""shortcut to access req's pending_delete entry This is where are stored relations being removed while editing an entity. This used to be stored in a temporary cookie. """pending=self.get_session_data('pending_delete')or()return['%s:%s:%s'%(subj,rel,obj)forsubj,rel,objinpendingifeidisNoneoreidin(subj,obj)]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.del_session_data('pending_insert')self.del_session_data('pending_delete')defcancel_edition(self,errorurl):"""remove pending operations and `errorurl`'s specific stored data """self.del_session_data(errorurl)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):"""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']=maxage# 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"""morsel=cookie[key]morsel['Max-Age']=0# The only way to set up cookie age for IE is to use an old "expired"# syntax. IE doesn't support Max-Age there is no library support for# managing # ===> Do _NOT_ comment this line :morsel['expires']='Thu, 01-Jan-1970 00:00:00 GMT'self.add_header('Set-Cookie',morsel.OutputString())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/'):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_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.datadir_url+jsfileself.html_headers.add_js(jsfile)defadd_css(self,cssfiles,media=u'all',localfile=True,ieonly=False):"""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 """ifisinstance(cssfiles,basestring):cssfiles=(cssfiles,)ifieonly:ifself.ie_browser():add_css=self.html_headers.add_ie_csselse:return# no need to do anything on non IE browserselse:add_css=self.html_headers.add_cssforcssfileincssfiles:iflocalfile:cssfile=self.datadir_url+cssfileadd_css(cssfile,media)# urls/path management ####################################################defurl(self,includeparams=True):"""return currently accessed url"""returnself.base_url()+self.relative_path(includeparams)def_datadir_url(self):"""return url of the application's data directory"""returnself.base_url()+'data%s/'%self.vreg.config.instance_md5_version()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]@cacheddeffrom_controller(self):"""return the id (string) of the controller issuing the request"""controller=self.relative_path(False).split('/',1)[0]registered_controllers=(ctrl.idforctrlinself.vreg.registry_objects('controllers'))ifcontrollerinregistered_controllers:returncontrollerreturn'view'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:value=self.vreg.config.ext_resources[rid]exceptKeyError:ifdefaultis_MARKER:raisereturndefaultifvalueisNone:returnNonebaseurl=self.datadir_url[:-1]# remove trailing /ifisinstance(value,list):return[v.replace('DATADIR',baseurl)forvinvalue]returnvalue.replace('DATADIR',baseurl)external_resource=cached(external_resource,keyarg=1)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 application'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):"""set an output HTTP header"""raiseNotImplementedError()defadd_header(self,header,value):"""add an output HTTP header"""raiseNotImplementedError()defremove_header(self,header):"""remove an output HTTP header"""raiseNotImplementedError()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,Nonedefheader_accept_language(self):"""returns an ordered list of preferred languages"""acceptedlangs=self.get_header('Accept-Language','')langs=[]forlanginfoinacceptedlangs.split(','):try:lang,score=langinfo.split(';')score=float(score[2:])# remove 'q='exceptValueError:lang=langinfoscore=1.0lang=lang.split('-')[0]langs.append((score,lang))langs.sort(reverse=True)return(langfor(score,lang)inlangs)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()# page data management ####################################################defget_page_data(self,key,default=None):"""return value associated to `key` in curernt page data"""page_data=self.cnx.get_session_data(self.pageid,{})returnpage_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.cnx.get_session_data(self.pageid,{})page_data[key]=valuereturnself.cnx.set_session_data(self.pageid,page_data)defdel_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.cnx.del_session_data(self.pageid)else:page_data=self.cnx.get_session_data(self.pageid,{})page_data.pop(key,None)self.cnx.set_session_data(self.pageid,page_data)# user-agent detection ####################################################@cacheddefuseragent(self):returnself.get_header('User-Agent',None)defie_browser(self):useragent=self.useragent()returnuseragentand'MSIE'inuseragentdefxhtml_browser(self):useragent=self.useragent()# MSIE does not support xml content-type# quick fix: Opera supports xhtml and handles namespaces# properly but it breaks jQuery.attr()ifuseragentand('MSIE'inuseragentor'KHTML'inuseragentor'Opera'inuseragent):returnFalsereturnTruefromcubicwebimportset_log_methodsset_log_methods(CubicWebRequestBase,LOGGER)