web/views/basecontrollers.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Set of base controllers, which are directly plugged into the application
       
    19 object to handle publication.
       
    20 """
       
    21 
       
    22 __docformat__ = "restructuredtext en"
       
    23 from cubicweb import _
       
    24 
       
    25 from warnings import warn
       
    26 
       
    27 from six import text_type
       
    28 
       
    29 from logilab.common.deprecation import deprecated
       
    30 
       
    31 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
       
    32                       AuthenticationError, UndoTransactionException,
       
    33                       Forbidden)
       
    34 from cubicweb.utils import json_dumps
       
    35 from cubicweb.predicates import (authenticated_user, anonymous_user,
       
    36                                 match_form_params)
       
    37 from cubicweb.web import Redirect, RemoteCallFailed
       
    38 from cubicweb.web.controller import Controller, append_url_params
       
    39 from cubicweb.web.views import vid_from_rset
       
    40 import cubicweb.transaction as tx
       
    41 
       
    42 @deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead')
       
    43 def jsonize(func):
       
    44     """decorator to sets correct content_type and calls `json_dumps` on
       
    45     results
       
    46     """
       
    47     def wrapper(self, *args, **kwargs):
       
    48         self._cw.set_content_type('application/json')
       
    49         return json_dumps(func(self, *args, **kwargs))
       
    50     wrapper.__name__ = func.__name__
       
    51     return wrapper
       
    52 
       
    53 @deprecated('[3.15] xhtmlize is deprecated, use AjaxFunction appobjects instead')
       
    54 def xhtmlize(func):
       
    55     """decorator to sets correct content_type and calls `xmlize` on results"""
       
    56     def wrapper(self, *args, **kwargs):
       
    57         self._cw.set_content_type(self._cw.html_content_type())
       
    58         result = func(self, *args, **kwargs)
       
    59         return ''.join((u'<div>', result.strip(),
       
    60                         u'</div>'))
       
    61     wrapper.__name__ = func.__name__
       
    62     return wrapper
       
    63 
       
    64 @deprecated('[3.15] check_pageid is deprecated, use AjaxFunction appobjects instead')
       
    65 def check_pageid(func):
       
    66     """decorator which checks the given pageid is found in the
       
    67     user's session data
       
    68     """
       
    69     def wrapper(self, *args, **kwargs):
       
    70         data = self._cw.session.data.get(self._cw.pageid)
       
    71         if data is None:
       
    72             raise RemoteCallFailed(self._cw._('pageid-not-found'))
       
    73         return func(self, *args, **kwargs)
       
    74     return wrapper
       
    75 
       
    76 
       
    77 class LoginController(Controller):
       
    78     __regid__ = 'login'
       
    79     __select__ = anonymous_user()
       
    80 
       
    81     def publish(self, rset=None):
       
    82         """log in the instance"""
       
    83         if self._cw.vreg.config['auth-mode'] == 'http':
       
    84             # HTTP authentication
       
    85             raise AuthenticationError()
       
    86         else:
       
    87             # Cookie authentication
       
    88             return self.appli.need_login_content(self._cw)
       
    89 
       
    90 class LoginControllerForAuthed(Controller):
       
    91     __regid__ = 'login'
       
    92     __select__ = ~anonymous_user()
       
    93 
       
    94     def publish(self, rset=None):
       
    95         """log in the instance"""
       
    96         path = self._cw.form.get('postlogin_path', '')
       
    97         # Redirect expects a URL, not a path. Also path may contain a query
       
    98         # string, hence should not be given to _cw.build_url()
       
    99         raise Redirect(self._cw.base_url() + path)
       
   100 
       
   101 
       
   102 class LogoutController(Controller):
       
   103     __regid__ = 'logout'
       
   104 
       
   105     def publish(self, rset=None):
       
   106         """logout from the instance"""
       
   107         return self.appli.session_handler.logout(self._cw, self.goto_url())
       
   108 
       
   109     def goto_url(self):
       
   110         # * in http auth mode, url will be ignored
       
   111         # * in cookie mode redirecting to the index view is enough : either
       
   112         #   anonymous connection is allowed and the page will be displayed or
       
   113         #   we'll be redirected to the login form
       
   114         msg = self._cw._('you have been logged out')
       
   115         return self._cw.build_url('view', vid='loggedout')
       
   116 
       
   117 
       
   118 class ViewController(Controller):
       
   119     """standard entry point :
       
   120     - build result set
       
   121     - select and call main template
       
   122     """
       
   123     __regid__ = 'view'
       
   124     template = 'main-template'
       
   125 
       
   126     def publish(self, rset=None):
       
   127         """publish a request, returning an encoded string"""
       
   128         view, rset = self._select_view_and_rset(rset)
       
   129         view.set_http_cache_headers()
       
   130         if self._cw.is_client_cache_valid():
       
   131             return b''
       
   132         template = self.appli.main_template_id(self._cw)
       
   133         return self._cw.vreg['views'].main_template(self._cw, template,
       
   134                                                     rset=rset, view=view)
       
   135 
       
   136     def _select_view_and_rset(self, rset):
       
   137         req = self._cw
       
   138         if rset is None and not hasattr(req, '_rql_processed'):
       
   139             req._rql_processed = True
       
   140             if req.cnx:
       
   141                 rset = self.process_rql()
       
   142             else:
       
   143                 rset = None
       
   144         vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
       
   145         try:
       
   146             view = self._cw.vreg['views'].select(vid, req, rset=rset)
       
   147         except ObjectNotFound:
       
   148             self.warning("the view %s could not be found", vid)
       
   149             req.set_message(req._("The view %s could not be found") % vid)
       
   150             vid = vid_from_rset(req, rset, self._cw.vreg.schema)
       
   151             view = self._cw.vreg['views'].select(vid, req, rset=rset)
       
   152         except NoSelectableObject:
       
   153             if rset:
       
   154                 req.set_message(req._("The view %s can not be applied to this query") % vid)
       
   155             else:
       
   156                 req.set_message(req._("You have no access to this view or it can not "
       
   157                                       "be used to display the current data."))
       
   158             vid = req.form.get('fallbackvid') or vid_from_rset(req, rset, req.vreg.schema)
       
   159             view = req.vreg['views'].select(vid, req, rset=rset)
       
   160         return view, rset
       
   161 
       
   162     def execute_linkto(self, eid=None):
       
   163         """XXX __linkto parameter may cause security issue
       
   164 
       
   165         defined here since custom application controller inheriting from this
       
   166         one use this method?
       
   167         """
       
   168         req = self._cw
       
   169         if not '__linkto' in req.form:
       
   170             return
       
   171         if eid is None:
       
   172             eid = int(req.form['eid'])
       
   173         for linkto in req.list_form_param('__linkto', pop=True):
       
   174             rtype, eids, target = linkto.split(':')
       
   175             assert target in ('subject', 'object')
       
   176             eids = eids.split('_')
       
   177             if target == 'subject':
       
   178                 rql = 'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype
       
   179             else:
       
   180                 rql = 'SET Y %s X WHERE X eid %%(x)s, Y eid %%(y)s' % rtype
       
   181             for teid in eids:
       
   182                 req.execute(rql, {'x': eid, 'y': int(teid)})
       
   183 
       
   184 
       
   185 def _validation_error(req, ex):
       
   186     req.cnx.rollback()
       
   187     ex.translate(req._) # translate messages using ui language
       
   188     # XXX necessary to remove existant validation error?
       
   189     # imo (syt), it's not necessary
       
   190     req.session.data.pop(req.form.get('__errorurl'), None)
       
   191     foreid = ex.entity
       
   192     eidmap = req.data.get('eidmap', {})
       
   193     for var, eid in eidmap.items():
       
   194         if foreid == eid:
       
   195             foreid = var
       
   196             break
       
   197     return (foreid, ex.errors)
       
   198 
       
   199 
       
   200 def _validate_form(req, vreg):
       
   201     # XXX should use the `RemoteCallFailed` mechanism
       
   202     try:
       
   203         ctrl = vreg['controllers'].select('edit', req=req)
       
   204     except NoSelectableObject:
       
   205         return (False, {None: req._('not authorized')}, None)
       
   206     try:
       
   207         ctrl.publish(None)
       
   208     except ValidationError as ex:
       
   209         return (False, _validation_error(req, ex), ctrl._edited_entity)
       
   210     except Redirect as ex:
       
   211         try:
       
   212             txuuid = req.cnx.commit() # ValidationError may be raised on commit
       
   213         except ValidationError as ex:
       
   214             return (False, _validation_error(req, ex), ctrl._edited_entity)
       
   215         except Exception as ex:
       
   216             req.cnx.rollback()
       
   217             req.exception('unexpected error while validating form')
       
   218             return (False, str(ex).decode('utf-8'), ctrl._edited_entity)
       
   219         else:
       
   220             if txuuid is not None:
       
   221                 req.data['last_undoable_transaction'] = txuuid
       
   222             # complete entity: it can be used in js callbacks where we might
       
   223             # want every possible information
       
   224             if ctrl._edited_entity:
       
   225                 ctrl._edited_entity.complete()
       
   226             return (True, ex.location, ctrl._edited_entity)
       
   227     except Exception as ex:
       
   228         req.cnx.rollback()
       
   229         req.exception('unexpected error while validating form')
       
   230         return (False, text_type(ex), ctrl._edited_entity)
       
   231     return (False, '???', None)
       
   232 
       
   233 
       
   234 class FormValidatorController(Controller):
       
   235     __regid__ = 'validateform'
       
   236 
       
   237     def response(self, domid, status, args, entity):
       
   238         callback = str(self._cw.form.get('__onsuccess', 'null'))
       
   239         errback = str(self._cw.form.get('__onfailure', 'null'))
       
   240         cbargs = str(self._cw.form.get('__cbargs', 'null'))
       
   241         self._cw.set_content_type('text/html')
       
   242         jsargs = json_dumps((status, args, entity))
       
   243         return """<script type="text/javascript">
       
   244  window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);
       
   245 </script>""" %  (domid, callback, errback, jsargs, cbargs)
       
   246 
       
   247     def publish(self, rset=None):
       
   248         self._cw.ajax_request = True
       
   249         # XXX unclear why we have a separated controller here vs
       
   250         # js_validate_form on the json controller
       
   251         status, args, entity = _validate_form(self._cw, self._cw.vreg)
       
   252         domid = self._cw.form.get('__domid', 'entityForm')
       
   253         return self.response(domid, status, args, entity).encode(self._cw.encoding)
       
   254 
       
   255 
       
   256 class JSonController(Controller):
       
   257     __regid__ = 'json'
       
   258 
       
   259     def publish(self, rset=None):
       
   260         warn('[3.15] JSONController is deprecated, use AjaxController instead',
       
   261              DeprecationWarning)
       
   262         ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli)
       
   263         return ajax_controller.publish(rset)
       
   264 
       
   265 
       
   266 class MailBugReportController(Controller):
       
   267     __regid__ = 'reportbug'
       
   268     __select__ = match_form_params('description')
       
   269 
       
   270     def publish(self, rset=None):
       
   271         req = self._cw
       
   272         desc = req.form['description']
       
   273         # The description is generated and signed by cubicweb itself, check
       
   274         # description's signature so we don't want to send spam here
       
   275         sign = req.form.get('__signature', '')
       
   276         if not (sign and req.vreg.config.check_text_sign(desc, sign)):
       
   277             raise Forbidden('Invalid content')
       
   278         self.sendmail(req.vreg.config['submit-mail'],
       
   279                       req._('%s error report') % req.vreg.config.appid,
       
   280                       desc)
       
   281         raise Redirect(req.build_url(__message=req._('bug report sent')))
       
   282 
       
   283 
       
   284 class UndoController(Controller):
       
   285     __regid__ = 'undo'
       
   286     __select__ = authenticated_user() & match_form_params('txuuid')
       
   287 
       
   288     def publish(self, rset=None):
       
   289         txuuid = self._cw.form['txuuid']
       
   290         try:
       
   291             self._cw.cnx.undo_transaction(txuuid)
       
   292         except UndoTransactionException as exc:
       
   293             errors = exc.errors
       
   294             #This will cause a rollback in main_publish
       
   295             raise ValidationError(None, {None: '\n'.join(errors)})
       
   296         else :
       
   297             self.redirect() # Will raise Redirect
       
   298 
       
   299     def redirect(self, msg=None):
       
   300         req = self._cw
       
   301         msg = msg or req._("transaction undone")
       
   302         self._redirect({'_cwmsgid': req.set_redirect_message(msg)})