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)}) |
|