diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/basecontrollers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/web/views/basecontrollers.py Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,302 @@ +# copyright 2003-2013 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 . +"""Set of base controllers, which are directly plugged into the application +object to handle publication. +""" + +__docformat__ = "restructuredtext en" +from cubicweb import _ + +from warnings import warn + +from six import text_type + +from logilab.common.deprecation import deprecated + +from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError, + AuthenticationError, UndoTransactionException, + Forbidden) +from cubicweb.utils import json_dumps +from cubicweb.predicates import (authenticated_user, anonymous_user, + match_form_params) +from cubicweb.web import Redirect, RemoteCallFailed +from cubicweb.web.controller import Controller, append_url_params +from cubicweb.web.views import vid_from_rset +import cubicweb.transaction as tx + +@deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead') +def jsonize(func): + """decorator to sets correct content_type and calls `json_dumps` on + results + """ + def wrapper(self, *args, **kwargs): + self._cw.set_content_type('application/json') + return json_dumps(func(self, *args, **kwargs)) + wrapper.__name__ = func.__name__ + return wrapper + +@deprecated('[3.15] xhtmlize is deprecated, use AjaxFunction appobjects instead') +def xhtmlize(func): + """decorator to sets correct content_type and calls `xmlize` on results""" + def wrapper(self, *args, **kwargs): + self._cw.set_content_type(self._cw.html_content_type()) + result = func(self, *args, **kwargs) + return ''.join((u'
', result.strip(), + u'
')) + wrapper.__name__ = func.__name__ + return wrapper + +@deprecated('[3.15] check_pageid is deprecated, use AjaxFunction appobjects instead') +def check_pageid(func): + """decorator which checks the given pageid is found in the + user's session data + """ + def wrapper(self, *args, **kwargs): + data = self._cw.session.data.get(self._cw.pageid) + if data is None: + raise RemoteCallFailed(self._cw._('pageid-not-found')) + return func(self, *args, **kwargs) + return wrapper + + +class LoginController(Controller): + __regid__ = 'login' + __select__ = anonymous_user() + + def publish(self, rset=None): + """log in the instance""" + if self._cw.vreg.config['auth-mode'] == 'http': + # HTTP authentication + raise AuthenticationError() + else: + # Cookie authentication + return self.appli.need_login_content(self._cw) + +class LoginControllerForAuthed(Controller): + __regid__ = 'login' + __select__ = ~anonymous_user() + + def publish(self, rset=None): + """log in the instance""" + path = self._cw.form.get('postlogin_path', '') + # Redirect expects a URL, not a path. Also path may contain a query + # string, hence should not be given to _cw.build_url() + raise Redirect(self._cw.base_url() + path) + + +class LogoutController(Controller): + __regid__ = 'logout' + + def publish(self, rset=None): + """logout from the instance""" + return self.appli.session_handler.logout(self._cw, self.goto_url()) + + def goto_url(self): + # * in http auth mode, url will be ignored + # * in cookie mode redirecting to the index view is enough : either + # anonymous connection is allowed and the page will be displayed or + # we'll be redirected to the login form + msg = self._cw._('you have been logged out') + return self._cw.build_url('view', vid='loggedout') + + +class ViewController(Controller): + """standard entry point : + - build result set + - select and call main template + """ + __regid__ = 'view' + template = 'main-template' + + def publish(self, rset=None): + """publish a request, returning an encoded string""" + view, rset = self._select_view_and_rset(rset) + view.set_http_cache_headers() + if self._cw.is_client_cache_valid(): + return b'' + template = self.appli.main_template_id(self._cw) + return self._cw.vreg['views'].main_template(self._cw, template, + rset=rset, view=view) + + def _select_view_and_rset(self, rset): + req = self._cw + if rset is None and not hasattr(req, '_rql_processed'): + req._rql_processed = True + if req.cnx: + rset = self.process_rql() + else: + rset = None + vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema) + try: + view = self._cw.vreg['views'].select(vid, req, rset=rset) + except ObjectNotFound: + self.warning("the view %s could not be found", vid) + req.set_message(req._("The view %s could not be found") % vid) + vid = vid_from_rset(req, rset, self._cw.vreg.schema) + view = self._cw.vreg['views'].select(vid, req, rset=rset) + except NoSelectableObject: + if rset: + req.set_message(req._("The view %s can not be applied to this query") % vid) + else: + req.set_message(req._("You have no access to this view or it can not " + "be used to display the current data.")) + vid = req.form.get('fallbackvid') or vid_from_rset(req, rset, req.vreg.schema) + view = req.vreg['views'].select(vid, req, rset=rset) + return view, rset + + def execute_linkto(self, eid=None): + """XXX __linkto parameter may cause security issue + + defined here since custom application controller inheriting from this + one use this method? + """ + req = self._cw + if not '__linkto' in req.form: + return + if eid is None: + eid = int(req.form['eid']) + for linkto in req.list_form_param('__linkto', pop=True): + rtype, eids, target = linkto.split(':') + assert target in ('subject', 'object') + eids = eids.split('_') + if target == 'subject': + rql = 'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype + else: + rql = 'SET Y %s X WHERE X eid %%(x)s, Y eid %%(y)s' % rtype + for teid in eids: + req.execute(rql, {'x': eid, 'y': int(teid)}) + + +def _validation_error(req, ex): + req.cnx.rollback() + ex.translate(req._) # translate messages using ui language + # XXX necessary to remove existant validation error? + # imo (syt), it's not necessary + req.session.data.pop(req.form.get('__errorurl'), None) + foreid = ex.entity + eidmap = req.data.get('eidmap', {}) + for var, eid in eidmap.items(): + if foreid == eid: + foreid = var + break + return (foreid, ex.errors) + + +def _validate_form(req, vreg): + # XXX should use the `RemoteCallFailed` mechanism + try: + ctrl = vreg['controllers'].select('edit', req=req) + except NoSelectableObject: + return (False, {None: req._('not authorized')}, None) + try: + ctrl.publish(None) + except ValidationError as ex: + return (False, _validation_error(req, ex), ctrl._edited_entity) + except Redirect as ex: + try: + txuuid = req.cnx.commit() # ValidationError may be raised on commit + except ValidationError as ex: + return (False, _validation_error(req, ex), ctrl._edited_entity) + except Exception as ex: + req.cnx.rollback() + req.exception('unexpected error while validating form') + return (False, str(ex).decode('utf-8'), ctrl._edited_entity) + else: + if txuuid is not None: + req.data['last_undoable_transaction'] = txuuid + # complete entity: it can be used in js callbacks where we might + # want every possible information + if ctrl._edited_entity: + ctrl._edited_entity.complete() + return (True, ex.location, ctrl._edited_entity) + except Exception as ex: + req.cnx.rollback() + req.exception('unexpected error while validating form') + return (False, text_type(ex), ctrl._edited_entity) + return (False, '???', None) + + +class FormValidatorController(Controller): + __regid__ = 'validateform' + + def response(self, domid, status, args, entity): + callback = str(self._cw.form.get('__onsuccess', 'null')) + errback = str(self._cw.form.get('__onfailure', 'null')) + cbargs = str(self._cw.form.get('__cbargs', 'null')) + self._cw.set_content_type('text/html') + jsargs = json_dumps((status, args, entity)) + return """""" % (domid, callback, errback, jsargs, cbargs) + + def publish(self, rset=None): + self._cw.ajax_request = True + # XXX unclear why we have a separated controller here vs + # js_validate_form on the json controller + status, args, entity = _validate_form(self._cw, self._cw.vreg) + domid = self._cw.form.get('__domid', 'entityForm') + return self.response(domid, status, args, entity).encode(self._cw.encoding) + + +class JSonController(Controller): + __regid__ = 'json' + + def publish(self, rset=None): + warn('[3.15] JSONController is deprecated, use AjaxController instead', + DeprecationWarning) + ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli) + return ajax_controller.publish(rset) + + +class MailBugReportController(Controller): + __regid__ = 'reportbug' + __select__ = match_form_params('description') + + def publish(self, rset=None): + req = self._cw + desc = req.form['description'] + # The description is generated and signed by cubicweb itself, check + # description's signature so we don't want to send spam here + sign = req.form.get('__signature', '') + if not (sign and req.vreg.config.check_text_sign(desc, sign)): + raise Forbidden('Invalid content') + self.sendmail(req.vreg.config['submit-mail'], + req._('%s error report') % req.vreg.config.appid, + desc) + raise Redirect(req.build_url(__message=req._('bug report sent'))) + + +class UndoController(Controller): + __regid__ = 'undo' + __select__ = authenticated_user() & match_form_params('txuuid') + + def publish(self, rset=None): + txuuid = self._cw.form['txuuid'] + try: + self._cw.cnx.undo_transaction(txuuid) + except UndoTransactionException as exc: + errors = exc.errors + #This will cause a rollback in main_publish + raise ValidationError(None, {None: '\n'.join(errors)}) + else : + self.redirect() # Will raise Redirect + + def redirect(self, msg=None): + req = self._cw + msg = msg or req._("transaction undone") + self._redirect({'_cwmsgid': req.set_redirect_message(msg)})