cubicweb/pyramid/core.py
changeset 11631 faf279e33298
parent 11630 1400aee10df4
child 11686 41d4f0f3855c
equal deleted inserted replaced
11478:1817f8946c22 11631:faf279e33298
       
     1 import itertools
       
     2 
       
     3 from contextlib import contextmanager
       
     4 from warnings import warn
       
     5 from cgi import FieldStorage
       
     6 
       
     7 import rql
       
     8 
       
     9 from cubicweb.web.request import CubicWebRequestBase
       
    10 from cubicweb import repoapi
       
    11 
       
    12 import cubicweb
       
    13 import cubicweb.web
       
    14 from cubicweb.server import session as cwsession
       
    15 
       
    16 from pyramid import httpexceptions
       
    17 
       
    18 from cubicweb.pyramid import tools
       
    19 
       
    20 import logging
       
    21 
       
    22 log = logging.getLogger(__name__)
       
    23 
       
    24 
       
    25 CW_321 = cubicweb.__pkginfo__.numversion >= (3, 21, 0)
       
    26 
       
    27 
       
    28 class Connection(cwsession.Connection):
       
    29     """ A specialised Connection that access the session data through a
       
    30     property.
       
    31 
       
    32     This behavior makes sure the actual session data is not loaded until
       
    33     actually accessed.
       
    34     """
       
    35     def __init__(self, session, *args, **kw):
       
    36         super(Connection, self).__init__(session, *args, **kw)
       
    37         self._session = session
       
    38 
       
    39     def _get_session_data(self):
       
    40         return self._session.data
       
    41 
       
    42     def _set_session_data(self, data):
       
    43         pass
       
    44 
       
    45     _session_data = property(_get_session_data, _set_session_data)
       
    46 
       
    47 
       
    48 class Session(cwsession.Session):
       
    49     """ A Session that access the session data through a property.
       
    50 
       
    51     Along with :class:`Connection`, it avoid any load of the pyramid session
       
    52     data until it is actually accessed.
       
    53     """
       
    54     def __init__(self, pyramid_request, user, repo):
       
    55         super(Session, self).__init__(user, repo)
       
    56         self._pyramid_request = pyramid_request
       
    57 
       
    58     def get_data(self):
       
    59         if not getattr(self, '_protect_data_access', False):
       
    60             self._data_accessed = True
       
    61             return self._pyramid_request.session
       
    62 
       
    63     def set_data(self, data):
       
    64         if getattr(self, '_data_accessed', False):
       
    65             self._pyramid_request.session.clear()
       
    66             self._pyramid_request.session.update(data)
       
    67 
       
    68     data = property(get_data, set_data)
       
    69 
       
    70     def new_cnx(self):
       
    71         self._protect_data_access = True
       
    72         try:
       
    73             return Connection(self)
       
    74         finally:
       
    75             self._protect_data_access = False
       
    76 
       
    77 
       
    78 def cw_headers(request):
       
    79     return itertools.chain(
       
    80         *[[(k, item) for item in v]
       
    81           for k, v in request.cw_request.headers_out.getAllRawHeaders()])
       
    82 
       
    83 
       
    84 @contextmanager
       
    85 def cw_to_pyramid(request):
       
    86     """ Context manager to wrap a call to the cubicweb API.
       
    87 
       
    88     All CW exceptions will be transformed into their pyramid equivalent.
       
    89     When needed, some CW reponse bits may be converted too (mainly headers)"""
       
    90     try:
       
    91         yield
       
    92     except cubicweb.web.Redirect as ex:
       
    93         assert 300 <= ex.status < 400
       
    94         raise httpexceptions.status_map[ex.status](
       
    95             ex.location, headers=cw_headers(request))
       
    96     except cubicweb.web.StatusResponse as ex:
       
    97         warn('[3.16] StatusResponse is deprecated use req.status_out',
       
    98              DeprecationWarning, stacklevel=2)
       
    99         request.body = ex.content
       
   100         request.status_int = ex.status
       
   101     except cubicweb.web.Unauthorized as ex:
       
   102         raise httpexceptions.HTTPForbidden(
       
   103             request.cw_request._(
       
   104                 'You\'re not authorized to access this page. '
       
   105                 'If you think you should, please contact the site '
       
   106                 'administrator.'),
       
   107             headers=cw_headers(request))
       
   108     except cubicweb.web.Forbidden:
       
   109         raise httpexceptions.HTTPForbidden(
       
   110             request.cw_request._(
       
   111                 'This action is forbidden. '
       
   112                 'If you think it should be allowed, please contact the site '
       
   113                 'administrator.'),
       
   114             headers=cw_headers(request))
       
   115     except (rql.BadRQLQuery, cubicweb.web.RequestError) as ex:
       
   116         raise
       
   117 
       
   118 
       
   119 class CubicWebPyramidRequest(CubicWebRequestBase):
       
   120     """ A CubicWeb request that only wraps a pyramid request.
       
   121 
       
   122     :param request: A pyramid request
       
   123 
       
   124     """
       
   125     def __init__(self, request):
       
   126         self._request = request
       
   127 
       
   128         self.path = request.upath_info
       
   129 
       
   130         vreg = request.registry['cubicweb.registry']
       
   131         https = request.scheme == 'https'
       
   132 
       
   133         post = request.params.mixed()
       
   134         headers_in = request.headers
       
   135 
       
   136         super(CubicWebPyramidRequest, self).__init__(vreg, https, post,
       
   137                                                      headers=headers_in)
       
   138 
       
   139         self.content = request.body_file_seekable
       
   140 
       
   141     def setup_params(self, params):
       
   142         self.form = {}
       
   143         for param, val in params.items():
       
   144             if param in self.no_script_form_params and val:
       
   145                 val = self.no_script_form_param(param, val)
       
   146             if isinstance(val, FieldStorage) and val.file:
       
   147                 val = (val.filename, val.file)
       
   148             if param == '_cwmsgid':
       
   149                 self.set_message_id(val)
       
   150             elif param == '__message':
       
   151                 warn('[3.13] __message in request parameter is deprecated '
       
   152                      '(may only be given to .build_url). Seeing this message '
       
   153                      'usualy means your application hold some <form> where '
       
   154                      'you should replace use of __message hidden input by '
       
   155                      'form.set_message, so new _cwmsgid mechanism is properly '
       
   156                      'used',
       
   157                      DeprecationWarning)
       
   158                 self.set_message(val)
       
   159             else:
       
   160                 self.form[param] = val
       
   161 
       
   162     def is_secure(self):
       
   163         return self._request.scheme == 'https'
       
   164 
       
   165     def relative_path(self, includeparams=True):
       
   166         path = self._request.path[1:]
       
   167         if includeparams and self._request.query_string:
       
   168             return '%s?%s' % (path, self._request.query_string)
       
   169         return path
       
   170 
       
   171     def instance_uri(self):
       
   172         return self._request.application_url
       
   173 
       
   174     def get_full_path(self):
       
   175         path = self._request.path
       
   176         if self._request.query_string:
       
   177             return '%s?%s' % (path, self._request.query_string)
       
   178         return path
       
   179 
       
   180     def http_method(self):
       
   181         return self._request.method
       
   182 
       
   183     def _set_status_out(self, value):
       
   184         self._request.response.status_int = value
       
   185 
       
   186     def _get_status_out(self):
       
   187         return self._request.response.status_int
       
   188 
       
   189     status_out = property(_get_status_out, _set_status_out)
       
   190 
       
   191     @property
       
   192     def message(self):
       
   193         """Returns a '<br>' joined list of the cubicweb current message and the
       
   194         default pyramid flash queue messages.
       
   195         """
       
   196         return u'\n<br>\n'.join(
       
   197             self._request.session.pop_flash()
       
   198             + self._request.session.pop_flash('cubicweb'))
       
   199 
       
   200     def set_message(self, msg):
       
   201         self.reset_message()
       
   202         self._request.session.flash(msg, 'cubicweb')
       
   203 
       
   204     def set_message_id(self, msgid):
       
   205         self.reset_message()
       
   206         self.set_message(
       
   207             self._request.session.pop(msgid, u''))
       
   208 
       
   209     def reset_message(self):
       
   210         self._request.session.pop_flash('cubicweb')
       
   211 
       
   212 
       
   213 def render_view(request, vid, **kwargs):
       
   214     """ Helper function to render a CubicWeb view.
       
   215 
       
   216     :param request: A pyramid request
       
   217     :param vid: A CubicWeb view id
       
   218     :param **kwargs: Keyword arguments to select and instanciate the view
       
   219     :returns: The rendered view content
       
   220     """
       
   221     vreg = request.registry['cubicweb.registry']
       
   222     # XXX The select() function could, know how to handle a pyramid
       
   223     # request, and feed it directly to the views that supports it.
       
   224     # On the other hand, we could refine the View concept and decide it works
       
   225     # with a cnx, and never with a WebRequest
       
   226 
       
   227     with cw_to_pyramid(request):
       
   228         view = vreg['views'].select(vid, request.cw_request, **kwargs)
       
   229         view.set_stream()
       
   230         view.render()
       
   231         return view._stream.getvalue()
       
   232 
       
   233 
       
   234 def _cw_cnx(request):
       
   235     """ Obtains a cw session from a pyramid request
       
   236 
       
   237     The connection will be commited or rolled-back in a request finish
       
   238     callback (this is temporary, we should make use of the transaction manager
       
   239     in a later version).
       
   240 
       
   241     Not meant for direct use, use ``request.cw_cnx`` instead.
       
   242 
       
   243     :param request: A pyramid request
       
   244     :returns type: :class:`cubicweb.server.session.Connection`
       
   245     """
       
   246     session = request.cw_session
       
   247     if session is None:
       
   248         return None
       
   249 
       
   250     if CW_321:
       
   251         cnx = session.new_cnx()
       
   252 
       
   253         def commit_state(cnx):
       
   254             return cnx.commit_state
       
   255     else:
       
   256         cnx = repoapi.ClientConnection(session)
       
   257 
       
   258         def commit_state(cnx):
       
   259             return cnx._cnx.commit_state
       
   260 
       
   261     def cleanup(request):
       
   262         try:
       
   263             if (request.exception is not None and not isinstance(
       
   264                 request.exception, (
       
   265                     httpexceptions.HTTPSuccessful,
       
   266                     httpexceptions.HTTPRedirection))):
       
   267                 cnx.rollback()
       
   268             elif commit_state(cnx) == 'uncommitable':
       
   269                 cnx.rollback()
       
   270             else:
       
   271                 cnx.commit()
       
   272         finally:
       
   273             cnx.__exit__(None, None, None)
       
   274 
       
   275     request.add_finished_callback(cleanup)
       
   276     cnx.__enter__()
       
   277     return cnx
       
   278 
       
   279 
       
   280 def repo_connect(request, repo, eid):
       
   281     """A lightweight version of
       
   282     :meth:`cubicweb.server.repository.Repository.connect` that does not keep
       
   283     track of opened sessions, removing the need of closing them"""
       
   284     user = tools.cached_build_user(repo, eid)
       
   285     session = Session(request, user, repo)
       
   286     tools.cnx_attach_entity(session, user)
       
   287     # Calling the hooks should be done only once, disabling it completely for
       
   288     # now
       
   289     #with session.new_cnx() as cnx:
       
   290         #repo.hm.call_hooks('session_open', cnx)
       
   291         #cnx.commit()
       
   292     # repo._sessions[session.sessionid] = session
       
   293     return session
       
   294 
       
   295 
       
   296 def _cw_session(request):
       
   297     """Obtains a cw session from a pyramid request
       
   298 
       
   299     :param request: A pyramid request
       
   300     :returns type: :class:`cubicweb.server.session.Session`
       
   301 
       
   302     Not meant for direct use, use ``request.cw_session`` instead.
       
   303     """
       
   304     repo = request.registry['cubicweb.repository']
       
   305 
       
   306     if not request.authenticated_userid:
       
   307         eid = request.registry.get('cubicweb.anonymous_eid')
       
   308         if eid is None:
       
   309             return None
       
   310         session = repo_connect(request, repo, eid=eid)
       
   311     else:
       
   312         session = request._cw_cached_session
       
   313 
       
   314     return session
       
   315 
       
   316 
       
   317 def _cw_request(request):
       
   318     """ Obtains a CubicWeb request wrapper for the pyramid request.
       
   319 
       
   320     :param request: A pyramid request
       
   321     :return: A CubicWeb request
       
   322     :returns type: :class:`CubicWebPyramidRequest`
       
   323 
       
   324     Not meant for direct use, use ``request.cw_request`` instead.
       
   325 
       
   326     """
       
   327     req = CubicWebPyramidRequest(request)
       
   328     cnx = request.cw_cnx
       
   329     if cnx is not None:
       
   330         req.set_cnx(request.cw_cnx)
       
   331     return req
       
   332 
       
   333 
       
   334 def get_principals(login, request):
       
   335     """ Returns the group names of the authenticated user.
       
   336 
       
   337     This function is meant to be used as an authentication policy callback.
       
   338 
       
   339     It also pre-open the cubicweb session and put it in
       
   340     request._cw_cached_session for later usage by :func:`_cw_session`.
       
   341 
       
   342     .. note::
       
   343 
       
   344         If the default authentication policy is not used, make sure this
       
   345         function gets called by the active authentication policy.
       
   346 
       
   347     :param login: A cubicweb user eid
       
   348     :param request: A pyramid request
       
   349     :returns: A list of group names
       
   350     """
       
   351     repo = request.registry['cubicweb.repository']
       
   352 
       
   353     try:
       
   354         session = repo_connect(request, repo, eid=login)
       
   355         request._cw_cached_session = session
       
   356     except:
       
   357         log.exception("Failed")
       
   358         raise
       
   359 
       
   360     return session.user.groups
       
   361 
       
   362 
       
   363 def includeme(config):
       
   364     """ Enables the core features of Pyramid CubicWeb.
       
   365 
       
   366     Automatically called by the 'pyramid' command, or via
       
   367     ``config.include('cubicweb.pyramid.code')``. In the later case,
       
   368     the following registry entries must be defined first:
       
   369 
       
   370     'cubicweb.config'
       
   371         A cubicweb 'config' instance.
       
   372 
       
   373     'cubicweb.repository'
       
   374         The correponding cubicweb repository.
       
   375 
       
   376     'cubicweb.registry'
       
   377         The vreg.
       
   378     """
       
   379     repo = config.registry['cubicweb.repository']
       
   380 
       
   381     with repo.internal_cnx() as cnx:
       
   382         login = config.registry['cubicweb.config'].anonymous_user()[0]
       
   383         if login is not None:
       
   384             config.registry['cubicweb.anonymous_eid'] = cnx.find(
       
   385                 'CWUser', login=login).one().eid
       
   386 
       
   387     config.add_request_method(
       
   388         _cw_session, name='cw_session', property=True, reify=True)
       
   389     config.add_request_method(
       
   390         _cw_cnx, name='cw_cnx', property=True, reify=True)
       
   391     config.add_request_method(
       
   392         _cw_request, name='cw_request', property=True, reify=True)
       
   393 
       
   394     cwcfg = config.registry['cubicweb.config']
       
   395     for cube in cwcfg.cubes():
       
   396         pkgname = 'cubes.' + cube
       
   397         mod = __import__(pkgname)
       
   398         mod = getattr(mod, cube)
       
   399         if hasattr(mod, 'includeme'):
       
   400             config.include('cubes.' + cube)