--- a/web/application.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,532 +0,0 @@
-# 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/>.
-"""CubicWeb web client application object"""
-
-__docformat__ = "restructuredtext en"
-
-import sys
-from time import clock, time
-from contextlib import contextmanager
-from warnings import warn
-import json
-
-from six import text_type, binary_type
-from six.moves import http_client
-
-from logilab.common.deprecation import deprecated
-
-from rql import BadRQLQuery
-
-from cubicweb import set_log_methods, cwvreg
-from cubicweb import (
- ValidationError, Unauthorized, Forbidden,
- AuthenticationError, NoSelectableObject,
- CW_EVENT_MANAGER)
-from cubicweb.repoapi import anonymous_cnx
-from cubicweb.web import LOGGER, component, cors
-from cubicweb.web import (
- StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
- RemoteCallFailed, InvalidSession, RequestError, PublishException)
-
-from cubicweb.web.request import CubicWebRequestBase
-
-# make session manager available through a global variable so the debug view can
-# print information about web session
-SESSION_MANAGER = None
-
-
-@contextmanager
-def anonymized_request(req):
- orig_cnx = req.cnx
- anon_cnx = anonymous_cnx(orig_cnx.session.repo)
- req.set_cnx(anon_cnx)
- try:
- with anon_cnx:
- yield req
- finally:
- req.set_cnx(orig_cnx)
-
-
-
-class CookieSessionHandler(object):
- """a session handler using a cookie to store the session identifier"""
-
- def __init__(self, appli):
- self.repo = appli.repo
- self.vreg = appli.vreg
- self.session_manager = self.vreg['sessions'].select('sessionmanager',
- repo=self.repo)
- global SESSION_MANAGER
- SESSION_MANAGER = self.session_manager
- if self.vreg.config.mode != 'test':
- # don't try to reset session manager during test, this leads to
- # weird failures when running multiple tests
- CW_EVENT_MANAGER.bind('after-registry-reload',
- self.reset_session_manager)
-
- def reset_session_manager(self):
- data = self.session_manager.dump_data()
- self.session_manager = self.vreg['sessions'].select('sessionmanager',
- repo=self.repo)
- self.session_manager.restore_data(data)
- global SESSION_MANAGER
- SESSION_MANAGER = self.session_manager
-
- @property
- def clean_sessions_interval(self):
- return self.session_manager.clean_sessions_interval
-
- def clean_sessions(self):
- """cleanup sessions which has not been unused since a given amount of
- time
- """
- self.session_manager.clean_sessions()
-
- def session_cookie(self, req):
- """return a string giving the name of the cookie used to store the
- session identifier.
- """
- if req.https:
- return '__%s_https_session' % self.vreg.config.appid
- return '__%s_session' % self.vreg.config.appid
-
- def get_session(self, req):
- """Return a session object corresponding to credentials held by the req
-
- Session id is searched from :
- - # form variable
- - cookie
-
- If no session id is found, try opening a new session with credentials
- found in the request.
-
- Raises AuthenticationError if no session can be found or created.
- """
- cookie = req.get_cookie()
- sessioncookie = self.session_cookie(req)
- try:
- sessionid = str(cookie[sessioncookie].value)
- session = self.get_session_by_id(req, sessionid)
- except (KeyError, InvalidSession): # no valid session cookie
- session = self.open_session(req)
- return session
-
- def get_session_by_id(self, req, sessionid):
- session = self.session_manager.get_session(req, sessionid)
- session.mtime = time()
- return session
-
- def open_session(self, req):
- session = self.session_manager.open_session(req)
- sessioncookie = self.session_cookie(req)
- secure = req.https and req.base_url().startswith('https://')
- req.set_cookie(sessioncookie, session.sessionid,
- maxage=None, secure=secure, httponly=True)
- if not session.anonymous_session:
- self.session_manager.postlogin(req, session)
- return session
-
- def logout(self, req, goto_url):
- """logout from the instance by cleaning the session and raising
- `AuthenticationError`
- """
- self.session_manager.close_session(req.session)
- req.remove_cookie(self.session_cookie(req))
- raise LogOut(url=goto_url)
-
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-class CubicWebPublisher(object):
- """the publisher is a singleton hold by the web frontend, and is responsible
- to publish HTTP request.
-
- The http server will call its main entry point ``application.handle_request``.
-
- .. automethod:: cubicweb.web.application.CubicWebPublisher.main_handle_request
-
- You have to provide both a repository and web-server config at
- initialization. In all in one instance both config will be the same.
- """
-
- def __init__(self, repo, config, session_handler_fact=CookieSessionHandler):
- self.info('starting web instance from %s', config.apphome)
- self.repo = repo
- self.vreg = repo.vreg
- # get instance's schema
- if not self.vreg.initialized:
- config.init_cubes(self.repo.get_cubes())
- self.vreg.init_properties(self.repo.properties())
- self.vreg.set_schema(self.repo.get_schema())
- # set the correct publish method
- if config['query-log-file']:
- from threading import Lock
- self._query_log = open(config['query-log-file'], 'a')
- self.handle_request = self.log_handle_request
- self._logfile_lock = Lock()
- else:
- self._query_log = None
- self.handle_request = self.main_handle_request
- # instantiate session and url resolving helpers
- self.session_handler = session_handler_fact(self)
- self.set_urlresolver()
- CW_EVENT_MANAGER.bind('after-registry-reload', self.set_urlresolver)
-
- def set_urlresolver(self):
- self.url_resolver = self.vreg['components'].select('urlpublisher',
- vreg=self.vreg)
-
- def get_session(self, req):
- """Return a session object corresponding to credentials held by the req
-
- May raise AuthenticationError.
- """
- return self.session_handler.get_session(req)
-
- # publish methods #########################################################
-
- def log_handle_request(self, req, path):
- """wrapper around _publish to log all queries executed for a given
- accessed path
- """
- def wrap_set_cnx(func):
- def wrap_execute(cnx):
- orig_execute = cnx.execute
- def execute(rql, kwargs=None, build_descr=True):
- tstart, cstart = time(), clock()
- rset = orig_execute(rql, kwargs, build_descr=build_descr)
- cnx.executed_queries.append((rql, kwargs, time() - tstart, clock() - cstart))
- return rset
- return execute
- def set_cnx(cnx):
- func(cnx)
- cnx.execute = wrap_execute(cnx)
- cnx.executed_queries = []
- return set_cnx
- req.set_cnx = wrap_set_cnx(req.set_cnx)
- try:
- return self.main_handle_request(req, path)
- finally:
- cnx = req.cnx
- if cnx:
- with self._logfile_lock:
- try:
- result = ['\n'+'*'*80]
- result.append(req.url())
- result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q
- for q in cnx.executed_queries]
- cnx.executed_queries = []
- self._query_log.write('\n'.join(result).encode(req.encoding))
- self._query_log.flush()
- except Exception:
- self.exception('error while logging queries')
-
-
-
- def main_handle_request(self, req, path):
- """Process an http request
-
- Arguments are:
- - a Request object
- - path of the request object
-
- It returns the content of the http response. HTTP header and status are
- set on the Request object.
- """
- if not isinstance(req, CubicWebRequestBase):
- warn('[3.15] Application entry point arguments are now (req, path) '
- 'not (path, req)', DeprecationWarning, 2)
- req, path = path, req
- if req.authmode == 'http':
- # activate realm-based auth
- realm = self.vreg.config['realm']
- req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
- content = b''
- try:
- try:
- session = self.get_session(req)
- from cubicweb import repoapi
- cnx = repoapi.Connection(session)
- req.set_cnx(cnx)
- except AuthenticationError:
- # Keep the dummy session set at initialisation.
- # such session with work to an some extend but raise an
- # AuthenticationError on any database access.
- import contextlib
- @contextlib.contextmanager
- def dummy():
- yield
- cnx = dummy()
- # XXX We want to clean up this approach in the future. But
- # several cubes like registration or forgotten password rely on
- # this principle.
-
- # nested try to allow LogOut to delegate logic to AuthenticationError
- # handler
- try:
- ### Try to generate the actual request content
- with cnx:
- content = self.core_handle(req, path)
- # Handle user log-out
- except LogOut as ex:
- # When authentification is handled by cookie the code that
- # raised LogOut must has invalidated the cookie. We can just
- # reload the original url without authentification
- if self.vreg.config['auth-mode'] == 'cookie' and ex.url:
- req.headers_out.setHeader('location', str(ex.url))
- if ex.status is not None:
- req.status_out = http_client.SEE_OTHER
- # When the authentification is handled by http we must
- # explicitly ask for authentification to flush current http
- # authentification information
- else:
- # Render "logged out" content.
- # assignement to ``content`` prevent standard
- # AuthenticationError code to overwrite it.
- content = self.loggedout_content(req)
- # let the explicitly reset http credential
- raise AuthenticationError()
- except Redirect as ex:
- # authentication needs redirection (eg openid)
- content = self.redirect_handler(req, ex)
- # Wrong, absent or Reseted credential
- except AuthenticationError:
- # If there is an https url configured and
- # the request does not use https, redirect to login form
- https_url = self.vreg.config['https-url']
- if https_url and req.base_url() != https_url:
- req.status_out = http_client.SEE_OTHER
- req.headers_out.setHeader('location', https_url + 'login')
- else:
- # We assume here that in http auth mode the user *May* provide
- # Authentification Credential if asked kindly.
- if self.vreg.config['auth-mode'] == 'http':
- req.status_out = http_client.UNAUTHORIZED
- # In the other case (coky auth) we assume that there is no way
- # for the user to provide them...
- # XXX But WHY ?
- else:
- req.status_out = http_client.FORBIDDEN
- # If previous error handling already generated a custom content
- # do not overwrite it. This is used by LogOut Except
- # XXX ensure we don't actually serve content
- if not content:
- content = self.need_login_content(req)
- assert isinstance(content, binary_type)
- return content
-
-
- def core_handle(self, req, path):
- """method called by the main publisher to process <path>
-
- should return a string containing the resulting page or raise a
- `NotFound` exception
-
- :type path: str
- :param path: the path part of the url to publish
-
- :type req: `web.Request`
- :param req: the request object
-
- :rtype: str
- :return: the result of the pusblished url
- """
- # don't log form values they may contains sensitive information
- self.debug('publish "%s" (%s, form params: %s)',
- path, req.session.sessionid, list(req.form))
- # remove user callbacks on a new request (except for json controllers
- # to avoid callbacks being unregistered before they could be called)
- tstart = clock()
- commited = False
- try:
- ### standard processing of the request
- try:
- # apply CORS sanity checks
- cors.process_request(req, self.vreg.config)
- ctrlid, rset = self.url_resolver.process(req, path)
- try:
- controller = self.vreg['controllers'].select(ctrlid, req,
- appli=self)
- except NoSelectableObject:
- raise Unauthorized(req._('not authorized'))
- req.update_search_state()
- result = controller.publish(rset=rset)
- except cors.CORSPreflight:
- # Return directly an empty 200
- req.status_out = 200
- result = b''
- except StatusResponse as ex:
- warn('[3.16] StatusResponse is deprecated use req.status_out',
- DeprecationWarning, stacklevel=2)
- result = ex.content
- req.status_out = ex.status
- except Redirect as ex:
- # Redirect may be raised by edit controller when everything went
- # fine, so attempt to commit
- result = self.redirect_handler(req, ex)
- if req.cnx:
- txuuid = req.cnx.commit()
- commited = True
- if txuuid is not None:
- req.data['last_undoable_transaction'] = txuuid
- ### error case
- except NotFound as ex:
- result = self.notfound_content(req)
- req.status_out = ex.status
- except ValidationError as ex:
- result = self.validation_error_handler(req, ex)
- except RemoteCallFailed as ex:
- result = self.ajax_error_handler(req, ex)
- except Unauthorized as ex:
- req.data['errmsg'] = req._('You\'re not authorized to access this page. '
- 'If you think you should, please contact the site administrator.')
- req.status_out = http_client.FORBIDDEN
- result = self.error_handler(req, ex, tb=False)
- except Forbidden as ex:
- req.data['errmsg'] = req._('This action is forbidden. '
- 'If you think it should be allowed, please contact the site administrator.')
- req.status_out = http_client.FORBIDDEN
- result = self.error_handler(req, ex, tb=False)
- except (BadRQLQuery, RequestError) as ex:
- result = self.error_handler(req, ex, tb=False)
- ### pass through exception
- except DirectResponse:
- if req.cnx:
- req.cnx.commit()
- raise
- except (AuthenticationError, LogOut):
- # the rollback is handled in the finally
- raise
- ### Last defense line
- except BaseException as ex:
- req.status_out = http_client.INTERNAL_SERVER_ERROR
- result = self.error_handler(req, ex, tb=True)
- finally:
- if req.cnx and not commited:
- try:
- req.cnx.rollback()
- except Exception:
- pass # ignore rollback error at this point
- self.add_undo_link_to_msg(req)
- self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart)
- return result
-
- # Error handlers
-
- def redirect_handler(self, req, ex):
- """handle redirect
- - comply to ex status
- - set header field
- - return empty content
- """
- self.debug('redirecting to %s', str(ex.location))
- req.headers_out.setHeader('location', str(ex.location))
- assert 300 <= ex.status < 400
- req.status_out = ex.status
- return b''
-
- def validation_error_handler(self, req, ex):
- ex.translate(req._) # translate messages using ui language
- if '__errorurl' in req.form:
- forminfo = {'error': ex,
- 'values': req.form,
- 'eidmap': req.data.get('eidmap', {})
- }
- req.session.data[req.form['__errorurl']] = forminfo
- # XXX form session key / __error_url should be differentiated:
- # session key is 'url + #<form dom id', though we usually don't want
- # the browser to move to the form since it hides the global
- # messages.
- location = req.form['__errorurl'].rsplit('#', 1)[0]
- req.headers_out.setHeader('location', str(location))
- req.status_out = http_client.SEE_OTHER
- return b''
- req.status_out = http_client.CONFLICT
- return self.error_handler(req, ex, tb=False)
-
- def error_handler(self, req, ex, tb=False):
- excinfo = sys.exc_info()
- if tb:
- self.exception(repr(ex))
- req.set_header('Cache-Control', 'no-cache')
- req.remove_header('Etag')
- req.remove_header('Content-disposition')
- req.reset_message()
- req.reset_headers()
- if req.ajax_request:
- return self.ajax_error_handler(req, ex)
- try:
- req.data['ex'] = ex
- if tb:
- req.data['excinfo'] = excinfo
- errview = self.vreg['views'].select('error', req)
- template = self.main_template_id(req)
- content = self.vreg['views'].main_template(req, template, view=errview)
- except Exception:
- content = self.vreg['views'].main_template(req, 'error-template')
- if isinstance(ex, PublishException) and ex.status is not None:
- req.status_out = ex.status
- return content
-
- def add_undo_link_to_msg(self, req):
- txuuid = req.data.get('last_undoable_transaction')
- if txuuid is not None:
- msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
- req.build_url('undo', txuuid=txuuid), req._('undo'))
- req.append_to_redirect_message(msg)
-
- def ajax_error_handler(self, req, ex):
- req.set_header('content-type', 'application/json')
- status = http_client.INTERNAL_SERVER_ERROR
- if isinstance(ex, PublishException) and ex.status is not None:
- status = ex.status
- if req.status_out < 400:
- # don't overwrite it if it's already set
- req.status_out = status
- json_dumper = getattr(ex, 'dumps', lambda : json.dumps({'reason': text_type(ex)}))
- return json_dumper().encode('utf-8')
-
- # special case handling
-
- def need_login_content(self, req):
- return self.vreg['views'].main_template(req, 'login')
-
- def loggedout_content(self, req):
- return self.vreg['views'].main_template(req, 'loggedout')
-
- def notfound_content(self, req):
- req.form['vid'] = '404'
- view = self.vreg['views'].select('404', req)
- template = self.main_template_id(req)
- return self.vreg['views'].main_template(req, template, view=view)
-
- # template stuff
-
- def main_template_id(self, req):
- template = req.form.get('__template', req.property_value('ui.main-template'))
- if template not in self.vreg['views']:
- template = 'main-template'
- return template
-
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-set_log_methods(CubicWebPublisher, LOGGER)
-set_log_methods(CookieSessionHandler, LOGGER)