[views/reledit] ajax.js not there by default #1382713
# 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/>."""twisted server for CubicWeb web instances"""__docformat__="restructuredtext en"importsysimportosimportselectimporterrnoimporttracebackimportthreadingfromos.pathimportjoinfromtimeimportmktimefromdatetimeimportdate,timedeltafromurlparseimporturlsplit,urlunsplitfromcgiimportFieldStorage,parse_headerfromcStringIOimportStringIOfromtwisted.internetimportreactor,task,threadsfromtwisted.internet.deferimportmaybeDeferredfromtwisted.webimporthttp,serverfromtwisted.webimportstatic,resourcefromtwisted.web.serverimportNOT_DONE_YETfromlogilab.common.decoratorsimportmonkeypatchfromcubicwebimportAuthenticationError,ConfigurationError,CW_EVENT_MANAGERfromcubicweb.utilsimportjson_dumpsfromcubicweb.webimportRedirect,DirectResponse,StatusResponse,LogOutfromcubicweb.web.applicationimportCubicWebPublisherfromcubicweb.web.http_headersimportgenerateDateTimefromcubicweb.etwist.requestimportCubicWebTwistedRequestAdapterfromcubicweb.etwist.httpimportHTTPResponsedefstart_task(interval,func):lc=task.LoopingCall(func)# wait until interval has expired to actually start the task, else we have# to wait all task to be finished for the server to be actually startedlc.start(interval,now=False)defhost_prefixed_baseurl(baseurl,host):scheme,netloc,url,query,fragment=urlsplit(baseurl)netloc_domain='.'+'.'.join(netloc.split('.')[-2:])ifhost.endswith(netloc_domain):netloc=hostbaseurl=urlunsplit((scheme,netloc,url,query,fragment))returnbaseurlclassForbiddenDirectoryLister(resource.Resource):defrender(self,request):returnHTTPResponse(twisted_request=request,code=http.FORBIDDEN,stream='Access forbidden')classFile(static.File):"""Prevent from listing directories"""defdirectoryListing(self):returnForbiddenDirectoryLister()classLongTimeExpiringFile(File):"""overrides static.File and sets a far future ``Expires`` date on the resouce. versions handling is done by serving static files by different URLs for each version. For instance:: http://localhost:8080/data-2.48.2/cubicweb.css http://localhost:8080/data-2.49.0/cubicweb.css etc. """defrender(self,request):# XXX: Don't provide additional resource information to error responses## the HTTP RFC recommands not going further than 1 year aheadexpires=date.today()+timedelta(days=6*30)request.setHeader('Expires',generateDateTime(mktime(expires.timetuple())))returnFile.render(self,request)classCubicWebRootResource(resource.Resource):def__init__(self,config,vreg=None):self.config=config# instantiate publisher here and not in init_publisher to get some# checks done before daemonization (eg versions consistency)self.appli=CubicWebPublisher(config,vreg=vreg)self.base_url=config['base-url']self.https_url=config['https-url']self.children={}self.static_directories=set(('data%s'%config.instance_md5_version(),'data','static','fckeditor'))globalMAX_POST_LENGTHMAX_POST_LENGTH=config['max-post-length']definit_publisher(self):config=self.config# when we have an in-memory repository, clean unused sessions every XX# seconds and properly shutdown the serverifconfig.repo_method=='inmemory':ifconfig.pyro_enabled():# if pyro is enabled, we have to register to the pyro name# server, create a pyro daemon, and create a task to handle pyro# requestsself.pyro_daemon=self.appli.repo.pyro_register()self.pyro_listen_timeout=0.02self.appli.repo.looping_task(1,self.pyro_loop_event)ifconfig.mode!='test':reactor.addSystemEventTrigger('before','shutdown',self.shutdown_event)self.appli.repo.start_looping_tasks()self.set_url_rewriter()CW_EVENT_MANAGER.bind('after-registry-reload',self.set_url_rewriter)defstart_service(self):start_task(self.appli.session_handler.clean_sessions_interval,self.appli.session_handler.clean_sessions)defset_url_rewriter(self):self.url_rewriter=self.appli.vreg['components'].select_or_none('urlrewriter')defshutdown_event(self):"""callback fired when the server is shutting down to properly clean opened sessions """self.appli.repo.shutdown()defpyro_loop_event(self):"""listen for pyro events"""try:self.pyro_daemon.handleRequests(self.pyro_listen_timeout)exceptselect.error:returndefgetChild(self,path,request):"""Indicate which resource to use to process down the URL's path"""pre_path=request.path.split('/')[1:]ifpre_path[0]=='https':pre_path.pop(0)uiprops=self.config.https_uipropselse:uiprops=self.config.uipropsdirectory=pre_path[0]# Anything in data/, static/, fckeditor/ and the generated versioned# data directory is treated as static filesifdirectoryinself.static_directories:# take care fckeditor may appears as root directory or as a data# subdirectoryifdirectory=='static':returnFile(self.config.static_directory)ifdirectory=='fckeditor':returnFile(uiprops['FCKEDITOR_PATH'])ifdirectory!='data':# versioned directory, use specific file with http cache# headers so their are cached for a very long timecls=LongTimeExpiringFileelse:cls=Fileifpath=='fckeditor':returncls(uiprops['FCKEDITOR_PATH'])ifpath==directory:# recursereturnselfdatadir,path=self.config.locate_resource(path)ifdatadirisNone:returnself# recurseself.debug('static file %s from %s',path,datadir)returncls(join(datadir,path))# Otherwise we use this single resourcereturnselfdefrender(self,request):"""Render a page from the root resource"""# reload modified files in debug modeifself.config.debugmode:self.config.uiprops.reload_if_needed()ifself.https_url:self.config.https_uiprops.reload_if_needed()self.appli.vreg.reload_if_needed()ifself.config['profile']:# default profiler don't trace threadsreturnself.render_request(request)else:deferred=threads.deferToThread(self.render_request,request)returnNOT_DONE_YETdefrender_request(self,request):try:# processing HUGE files (hundred of megabytes) in http.processReceived# blocks other HTTP requests processing# due to the clumsy & slow parsing algorithm of cgi.FieldStorage# so we deferred that part to the cubicweb threadrequest.process_multipart()returnself._render_request(request)except:errorstream=StringIO()traceback.print_exc(file=errorstream)returnHTTPResponse(stream='<pre>%s</pre>'%errorstream.getvalue(),code=500,twisted_request=request)def_render_request(self,request):origpath=request.pathhost=request.host# dual http/https access handling: expect a rewrite rule to prepend# 'https' to the path to detect https accessiforigpath.split('/',2)[1]=='https':origpath=origpath[6:]request.uri=request.uri[6:]https=Truebaseurl=self.https_urlorself.base_urlelse:https=Falsebaseurl=self.base_urlifself.config['use-request-subdomain']:baseurl=host_prefixed_baseurl(baseurl,host)self.warning('used baseurl is %s for this request',baseurl)req=CubicWebTwistedRequestAdapter(request,self.appli.vreg,https,baseurl)ifreq.authmode=='http':# activate realm-based authrealm=self.config['realm']req.set_header('WWW-Authenticate',[('Basic',{'realm':realm})],raw=False)try:self.appli.connect(req)exceptRedirect,ex:returnself.redirect(request=req,location=ex.location)ifhttpsandreq.session.anonymous_session:# don't allow anonymous on https connectionreturnself.request_auth(request=req)ifself.url_rewriterisnotNone:# XXX should occur before authentication?try:path=self.url_rewriter.rewrite(host,origpath,req)exceptRedirect,ex:returnself.redirect(req,ex.location)request.uri.replace(origpath,path,1)else:path=origpathifnotpathorpath=="/":path='view'try:result=self.appli.publish(path,req)exceptDirectResponse,ex:returnex.responseexceptStatusResponse,ex:returnHTTPResponse(stream=ex.content,code=ex.status,twisted_request=req._twreq,headers=req.headers_out)exceptAuthenticationError:returnself.request_auth(request=req)exceptLogOut,ex:ifself.config['auth-mode']=='cookie'andex.url:returnself.redirect(request=req,location=ex.url)# in http we have to request auth to flush current http auth# informationreturnself.request_auth(request=req,loggedout=True)exceptRedirect,ex:returnself.redirect(request=req,location=ex.location)# request may be referenced by "onetime callback", so clear its entity# cache to avoid memory usagereq.drop_entity_cache()returnHTTPResponse(twisted_request=req._twreq,code=http.OK,stream=result,headers=req.headers_out)defredirect(self,request,location):self.debug('redirecting to %s',str(location))request.headers_out.setHeader('location',str(location))# 303 See otherreturnHTTPResponse(twisted_request=request._twreq,code=303,headers=request.headers_out)defrequest_auth(self,request,loggedout=False):ifself.https_urlandrequest.base_url()!=self.https_url:returnself.redirect(request,self.https_url+'login')ifself.config['auth-mode']=='http':code=http.UNAUTHORIZEDelse:code=http.FORBIDDENifloggedout:ifrequest.https:request._base_url=self.base_urlrequest.https=Falsecontent=self.appli.loggedout_content(request)else:content=self.appli.need_login_content(request)returnHTTPResponse(twisted_request=request._twreq,stream=content,code=code,headers=request.headers_out)JSON_PATHS=set(('json',))FRAME_POST_PATHS=set(('validateform',))orig_gotLength=http.Request.gotLength@monkeypatch(http.Request)defgotLength(self,length):orig_gotLength(self,length)iflength>MAX_POST_LENGTH:# length is 0 on GETpath=self.channel._path.split('?',1)[0].rstrip('/').rsplit('/',1)[-1]self.clientproto='HTTP/1.1'# not yet initializedself.channel.persistent=0# force connection close on cleanupself.setResponseCode(http.BAD_REQUEST)ifpathinJSON_PATHS:# XXX better json path detectionself.setHeader('content-type',"application/json")body=json_dumps({'reason':'request max size exceeded'})elifpathinFRAME_POST_PATHS:# XXX better frame post path detectionself.setHeader('content-type',"text/html")body=('<script type="text/javascript">''window.parent.handleFormValidationResponse(null, null, null, %s, null);''</script>'%json_dumps((False,'request max size exceeded',None)))else:self.setHeader('content-type',"text/html")body=("<html><head><title>Processing Failed</title></head><body>""<b>request max size exceeded</b></body></html>")self.setHeader('content-length',str(len(body)))self.write(body)# see request.finish(). Done here since we get error due to not full# initialized requestself.finished=1ifnotself.queued:self._cleanup()fordinself.notifications:d.callback(None)self.notifications=[]@monkeypatch(http.Request)defrequestReceived(self,command,path,version):"""Called by channel when all data has been received. This method is not intended for users. """self.content.seek(0,0)self.args={}self.files={}self.stack=[]self.method,self.uri=command,pathself.clientproto=versionx=self.uri.split('?',1)iflen(x)==1:self.path=self.urielse:self.path,argstring=xself.args=http.parse_qs(argstring,1)# cache the client and server information, we'll need this later to be# serialized and sent with the request so CGIs will work remotelyself.client=self.channel.transport.getPeer()self.host=self.channel.transport.getHost()# Argument processingctype=self.getHeader('content-type')self._do_process_multipart=Falseifself.method=="POST"andctype:key,pdict=parse_header(ctype)ifkey=='application/x-www-form-urlencoded':self.args.update(http.parse_qs(self.content.read(),1))elifkey=='multipart/form-data':# defer this as it can be extremely time consumming# with big filesself._do_process_multipart=Trueself.process()@monkeypatch(http.Request)defprocess_multipart(self):ifnotself._do_process_multipart:returnform=FieldStorage(self.content,self.received_headers,environ={'REQUEST_METHOD':'POST'},keep_blank_values=1,strict_parsing=1)forkeyinform:value=form[key]ifisinstance(value,list):self.args[key]=[v.valueforvinvalue]elifvalue.filename:ifvalue.done!=-1:# -1 is transfer has been interruptedself.files[key]=(value.filename,value.file)else:self.files[key]=(None,None)else:self.args[key]=value.valuefromloggingimportgetLoggerfromcubicwebimportset_log_methodsLOGGER=getLogger('cubicweb.twisted')set_log_methods(CubicWebRootResource,LOGGER)defrun(config,vreg=None,debug=None):ifdebugisnotNone:config.debugmode=debugconfig.check_writeable_uid_directory(config.appdatahome)# create the siteroot_resource=CubicWebRootResource(config,vreg=vreg)website=server.Site(root_resource)# serve it via standard HTTP on port set in the configurationport=config['port']or8080reactor.listenTCP(port,website)ifnotconfig.debugmode:ifsys.platform=='win32':raiseConfigurationError("Under windows, you must use the service management ""commands (e.g : 'net start my_instance)'")fromlogilab.common.daemonimportdaemonizeLOGGER.info('instance started in the background on %s',root_resource.base_url)ifdaemonize(config['pid-file']):return# child processroot_resource.init_publisher()# before changing uidifconfig['uid']isnotNone:try:uid=int(config['uid'])exceptValueError:frompwdimportgetpwnamuid=getpwnam(config['uid']).pw_uidos.setuid(uid)root_resource.start_service()LOGGER.info('instance started on %s',root_resource.base_url)# avoid annoying warnign if not in Main Threadsignals=threading.currentThread().getName()=='MainThread'ifconfig['profile']:importcProfilecProfile.runctx('reactor.run(installSignalHandlers=%s)'%signals,globals(),locals(),config['profile'])else:reactor.run(installSignalHandlers=signals)