[web login] fix after login redirection: redirect expect an url not a relative path. Breaks on cases like '?vid=aview'. Closes #2423575
# copyright 2003-2012 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/>.
"""Set of base controllers, which are directly plugged into the application
object to handle publication.
"""
__docformat__ = "restructuredtext en"
_ = unicode
from warnings import warn
from logilab.common.deprecation import deprecated
from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
AuthenticationError, typed_eid, UndoTransactionException)
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((self._cw.document_surrounding_div(), result.strip(),
u'</div>'))
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 expect an url, not a path. Also path may contains 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')
# force base_url so on dual http/https configuration, we generate an url
# on the http version of the site
return self._cw.build_url('view', vid='loggedout',
base_url=self._cw.vreg.config['base-url'])
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)
self.add_to_breadcrumbs(view)
self.validate_cache(view)
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 add_to_breadcrumbs(self, view):
# update breadcrumbs **before** validating cache, unless the view
# specifies explicitly it should not be added to breadcrumb or the
# view is a binary view
if view.add_to_breadcrumbs and not view.binary:
self._cw.update_breadcrumbs()
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 = typed_eid(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': typed_eid(teid)})
def _validation_error(req, ex):
req.cnx.rollback()
# 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, ex:
return (False, _validation_error(req, ex), ctrl._edited_entity)
except Redirect, ex:
try:
txuuid = req.cnx.commit() # ValidationError may be raised on commit
except ValidationError, ex:
return (False, _validation_error(req, ex), ctrl._edited_entity)
except Exception, 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, ex:
req.cnx.rollback()
req.exception('unexpected error while validating form')
return (False, str(ex).decode('utf-8'), 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 """<script type="text/javascript">
window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);
</script>""" % (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').encode(
self._cw.encoding)
return self.response(domid, status, args, entity)
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)
# XXX move to massmailing
class MailBugReportController(Controller):
__regid__ = 'reportbug'
__select__ = match_form_params('description')
def publish(self, rset=None):
req = self._cw
self.sendmail(req.vreg.config['submit-mail'],
req._('%s error report') % req.vreg.config.appid,
req.form['description'])
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, 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._return_to_lastpage( dict(_cwmsgid= req.set_redirect_message(msg)) )