Mercurial > libervia-web
comparison libervia/server/pages.py @ 1124:28e3eb3bb217
files reorganisation and installation rework:
- files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory)
- VERSION file is now used, as for other SàT projects
- replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly
- removed check for data_dir if it's empty
- installation tested working in virtual env
- libervia launching script is now in bin/libervia
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 25 Aug 2018 17:59:48 +0200 |
parents | src/server/pages.py@cdd389ef97bc |
children | 9234f29053b0 |
comparison
equal
deleted
inserted
replaced
1123:63a4b8fe9782 | 1124:28e3eb3bb217 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 from twisted.web import server | |
20 from twisted.web import resource as web_resource | |
21 from twisted.web import util as web_util | |
22 from twisted.internet import defer | |
23 from twisted.words.protocols.jabber import jid | |
24 from twisted.python import failure | |
25 | |
26 from sat.core.i18n import _ | |
27 from sat.core import exceptions | |
28 from sat.tools.common import uri as common_uri | |
29 from sat.tools.common import date_utils | |
30 from sat.core.log import getLogger | |
31 | |
32 log = getLogger(__name__) | |
33 from libervia.server.constants import Const as C | |
34 from libervia.server import session_iface | |
35 from libervia.server.utils import quote, SubPage | |
36 import libervia | |
37 | |
38 from collections import namedtuple | |
39 import uuid | |
40 import os.path | |
41 import urllib | |
42 import time | |
43 import hashlib | |
44 | |
45 WebsocketMeta = namedtuple("WebsocketMeta", ("url", "token", "debug")) | |
46 | |
47 | |
48 class CacheBase(object): | |
49 def __init__(self): | |
50 self._created = time.time() | |
51 self._last_access = self._created | |
52 | |
53 @property | |
54 def created(self): | |
55 return self._created | |
56 | |
57 @property | |
58 def last_access(self): | |
59 return self._last_access | |
60 | |
61 @last_access.setter | |
62 def last_access(self, timestamp): | |
63 self._last_access = timestamp | |
64 | |
65 | |
66 class CachePage(CacheBase): | |
67 def __init__(self, rendered): | |
68 super(CachePage, self).__init__() | |
69 self._created = time.time() | |
70 self._last_access = self._created | |
71 self._rendered = rendered | |
72 self._hash = hashlib.sha256(rendered).hexdigest() | |
73 | |
74 @property | |
75 def rendered(self): | |
76 return self._rendered | |
77 | |
78 @property | |
79 def hash(self): | |
80 return self._hash | |
81 | |
82 | |
83 class CacheURL(CacheBase): | |
84 def __init__(self, request): | |
85 super(CacheURL, self).__init__() | |
86 try: | |
87 self._data = request.data.copy() | |
88 except AttributeError: | |
89 self._data = {} | |
90 self._template_data = request.template_data.copy() | |
91 self._prepath = request.prepath[:] | |
92 self._postpath = request.postpath[:] | |
93 del self._template_data["csrf_token"] | |
94 | |
95 def use(self, request): | |
96 self.last_access = time.time() | |
97 request.data = self._data.copy() | |
98 request.template_data.update(self._template_data) | |
99 request.prepath = self._prepath[:] | |
100 request.postpath = self._postpath[:] | |
101 | |
102 | |
103 class LiberviaPage(web_resource.Resource): | |
104 isLeaf = True # we handle subpages ourself | |
105 named_pages = {} | |
106 uri_callbacks = {} | |
107 signals_handlers = {} | |
108 pages_redirects = {} | |
109 cache = {} | |
110 cached_urls = {} | |
111 # Set of tuples (service/node/sub_id) of nodes subscribed for caching | |
112 # sub_id can be empty string if not handled by service | |
113 cache_pubsub_sub = set() | |
114 main_menu = None | |
115 | |
116 def __init__( | |
117 self, | |
118 host, | |
119 root_dir, | |
120 url, | |
121 name=None, | |
122 redirect=None, | |
123 access=None, | |
124 dynamic=False, | |
125 parse_url=None, | |
126 prepare_render=None, | |
127 render=None, | |
128 template=None, | |
129 on_data_post=None, | |
130 on_data=None, | |
131 on_signal=None, | |
132 url_cache=False, | |
133 ): | |
134 """initiate LiberviaPages | |
135 | |
136 LiberviaPages are the main resources of Libervia, using easy to set python files | |
137 The arguments are the variables found in page_meta.py | |
138 @param host(Libervia): the running instance of Libervia | |
139 @param root_dir(unicode): aboslute file path of the page | |
140 @param url(unicode): relative URL to the page | |
141 this URL may not be valid, as pages may require path arguments | |
142 @param name(unicode, None): if not None, a unique name to identify the page | |
143 can then be used for e.g. redirection | |
144 "/" is not allowed in names (as it can be used to construct URL paths) | |
145 @param redirect(unicode, None): if not None, this page will be redirected. A redirected | |
146 parameter is used as in self.pageRedirect. parse_url will not be skipped | |
147 using this redirect parameter is called "full redirection" | |
148 using self.pageRedirect is called "partial redirection" (because some rendering method | |
149 can still be used, e.g. parse_url) | |
150 @param access(unicode, None): permission needed to access the page | |
151 None means public access. | |
152 Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, | |
153 and if "settings/blog" is public, it still can only be accessed by admins. | |
154 see C.PAGES_ACCESS_* for details | |
155 @param dynamic(bool): if True, activate websocket for bidirectional communication | |
156 @param parse_url(callable, None): if set it will be called to handle the URL path | |
157 after this method, the page will be rendered if noting is left in path (request.postpath) | |
158 else a the request will be transmitted to a subpage | |
159 @param prepare_render(callable, None): if set, will be used to prepare the rendering | |
160 that often means gathering data using the bridge | |
161 @param render(callable, None): if not template is set, this method will be called and | |
162 what it returns will be rendered. | |
163 This method is mutually exclusive with template and must return a unicode string. | |
164 @param template(unicode, None): path to the template to render. | |
165 This method is mutually exclusive with render | |
166 @param on_data_post(callable, None): method to call when data is posted | |
167 None if not post is handled | |
168 on_data_post can return a string with following value: | |
169 - C.POST_NO_CONFIRM: confirm flag will not be set | |
170 @param on_data(callable, None): method to call when dynamic data is sent | |
171 this method is used with Libervia's websocket mechanism | |
172 @param on_signal(callable, None): method to call when a registered signal is received | |
173 this method is used with Libervia's websocket mechanism | |
174 """ | |
175 | |
176 web_resource.Resource.__init__(self) | |
177 self.host = host | |
178 self.root_dir = root_dir | |
179 self.url = url | |
180 self.name = name | |
181 if name is not None: | |
182 if name in self.named_pages: | |
183 raise exceptions.ConflictError( | |
184 _(u'a Libervia page named "{}" already exists'.format(name)) | |
185 ) | |
186 if u"/" in name: | |
187 raise ValueError(_(u'"/" is not allowed in page names')) | |
188 if not name: | |
189 raise ValueError(_(u"a page name can't be empty")) | |
190 self.named_pages[name] = self | |
191 if access is None: | |
192 access = C.PAGES_ACCESS_PUBLIC | |
193 if access not in ( | |
194 C.PAGES_ACCESS_PUBLIC, | |
195 C.PAGES_ACCESS_PROFILE, | |
196 C.PAGES_ACCESS_NONE, | |
197 ): | |
198 raise NotImplementedError( | |
199 _(u"{} access is not implemented yet").format(access) | |
200 ) | |
201 self.access = access | |
202 self.dynamic = dynamic | |
203 if redirect is not None: | |
204 # only page access and name make sense in case of full redirection | |
205 # so we check that rendering methods/values are not set | |
206 if not all( | |
207 lambda x: x is not None | |
208 for x in (parse_url, prepare_render, render, template) | |
209 ): | |
210 raise ValueError( | |
211 _( | |
212 u"you can't use full page redirection with other rendering method," | |
213 u"check self.pageRedirect if you need to use them" | |
214 ) | |
215 ) | |
216 self.redirect = redirect | |
217 else: | |
218 self.redirect = None | |
219 self.parse_url = parse_url | |
220 self.prepare_render = prepare_render | |
221 self.template = template | |
222 self.render_method = render | |
223 self.on_data_post = on_data_post | |
224 self.on_data = on_data | |
225 self.on_signal = on_signal | |
226 self.url_cache = url_cache | |
227 if access == C.PAGES_ACCESS_NONE: | |
228 # none pages just return a 404, no further check is needed | |
229 return | |
230 if template is not None and render is not None: | |
231 log.error(_(u"render and template methods can't be used at the same time")) | |
232 if parse_url is not None and not callable(parse_url): | |
233 log.error(_(u"parse_url must be a callable")) | |
234 | |
235 # if not None, next rendering will be cached | |
236 # it must then contain a list of the the keys to use (without the page instance) | |
237 # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] | |
238 self._do_cache = None | |
239 | |
240 def __unicode__(self): | |
241 return u"LiberviaPage {name} at {url}".format( | |
242 name=self.name or u"<anonymous>", url=self.url | |
243 ) | |
244 | |
245 def __str__(self): | |
246 return self.__unicode__().encode("utf-8") | |
247 | |
248 @classmethod | |
249 def importPages(cls, host, parent=None, path=None): | |
250 """Recursively import Libervia pages""" | |
251 if path is None: | |
252 path = [] | |
253 if parent is None: | |
254 root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR) | |
255 parent = host | |
256 else: | |
257 root_dir = parent.root_dir | |
258 for d in os.listdir(root_dir): | |
259 dir_path = os.path.join(root_dir, d) | |
260 if not os.path.isdir(dir_path): | |
261 continue | |
262 meta_path = os.path.join(dir_path, C.PAGES_META_FILE) | |
263 if os.path.isfile(meta_path): | |
264 page_data = {} | |
265 new_path = path + [d] | |
266 # we don't want to force the presence of __init__.py | |
267 # so we use execfile instead of import. | |
268 # TODO: when moved to Python 3, __init__.py is not mandatory anymore | |
269 # so we can switch to import | |
270 execfile(meta_path, page_data) | |
271 resource = LiberviaPage( | |
272 host, | |
273 dir_path, | |
274 u"/" + u"/".join(new_path), | |
275 name=page_data.get("name"), | |
276 redirect=page_data.get("redirect"), | |
277 access=page_data.get("access"), | |
278 dynamic=page_data.get("dynamic", False), | |
279 parse_url=page_data.get("parse_url"), | |
280 prepare_render=page_data.get("prepare_render"), | |
281 render=page_data.get("render"), | |
282 template=page_data.get("template"), | |
283 on_data_post=page_data.get("on_data_post"), | |
284 on_data=page_data.get("on_data"), | |
285 on_signal=page_data.get("on_signal"), | |
286 url_cache=page_data.get("url_cache", False), | |
287 ) | |
288 parent.putChild(d, resource) | |
289 log.info(u"Added /{path} page".format(path=u"[...]/".join(new_path))) | |
290 if "uri_handlers" in page_data: | |
291 if not isinstance(page_data, dict): | |
292 log.error(_(u"uri_handlers must be a dict")) | |
293 else: | |
294 for uri_tuple, cb_name in page_data["uri_handlers"].iteritems(): | |
295 if len(uri_tuple) != 2 or not isinstance(cb_name, basestring): | |
296 log.error(_(u"invalid uri_tuple")) | |
297 continue | |
298 log.info(_(u"setting {}/{} URIs handler").format(*uri_tuple)) | |
299 try: | |
300 cb = page_data[cb_name] | |
301 except KeyError: | |
302 log.error( | |
303 _(u"missing {name} method to handle {1}/{2}").format( | |
304 name=cb_name, *uri_tuple | |
305 ) | |
306 ) | |
307 continue | |
308 else: | |
309 resource.registerURI(uri_tuple, cb) | |
310 | |
311 LiberviaPage.importPages(host, resource, new_path) | |
312 | |
313 @classmethod | |
314 def setMenu(cls, menus): | |
315 main_menu = [] | |
316 for menu in menus: | |
317 if not menu: | |
318 msg = _(u"menu item can't be empty") | |
319 log.error(msg) | |
320 raise ValueError(msg) | |
321 elif isinstance(menu, list): | |
322 if len(menu) != 2: | |
323 msg = _( | |
324 u"menu item as list must be in the form [page_name, absolue URL]" | |
325 ) | |
326 log.error(msg) | |
327 raise ValueError(msg) | |
328 page_name, url = menu | |
329 else: | |
330 page_name = menu | |
331 try: | |
332 url = cls.getPageByName(page_name).url | |
333 except KeyError as e: | |
334 log.error( | |
335 _( | |
336 u"Can'find a named page ({msg}), please check menu_json in configuration." | |
337 ).format(msg=e) | |
338 ) | |
339 raise e | |
340 main_menu.append((page_name, url)) | |
341 cls.main_menu = main_menu | |
342 | |
343 def registerURI(self, uri_tuple, get_uri_cb): | |
344 """register a URI handler | |
345 | |
346 @param uri_tuple(tuple[unicode, unicode]): type or URIs handler | |
347 type/subtype as returned by tools/common/parseXMPPUri | |
348 or type/None to handle all subtypes | |
349 @param get_uri_cb(callable): method which take uri_data dict as only argument | |
350 and return absolute path with correct arguments or None if the page | |
351 can't handle this URL | |
352 """ | |
353 if uri_tuple in self.uri_callbacks: | |
354 log.info( | |
355 _(u"{}/{} URIs are already handled, replacing by the new handler").format( | |
356 *uri_tuple | |
357 ) | |
358 ) | |
359 self.uri_callbacks[uri_tuple] = (self, get_uri_cb) | |
360 | |
361 def registerSignal(self, request, signal, check_profile=True): | |
362 r"""register a signal handler | |
363 | |
364 the page must be dynamic | |
365 when signal is received, self.on_signal will be called with: | |
366 - request | |
367 - signal name | |
368 - signal arguments | |
369 signal handler will be removed when connection with dynamic page will be lost | |
370 @param signal(unicode): name of the signal | |
371 last arg of signal must be profile, as it will be checked to filter signals | |
372 @param check_profile(bool): if True, signal profile (which MUST be last arg) will be | |
373 checked against session profile. | |
374 /!\ if False, profile will not be checked/filtered, be sure to know what you are doing | |
375 if you unset this option /!\ | |
376 """ | |
377 # FIXME: add a timeout, if socket is not opened before it, signal handler must be removed | |
378 if not self.dynamic: | |
379 log.error(_(u"You can't register signal if page is not dynamic")) | |
380 return | |
381 LiberviaPage.signals_handlers.setdefault(signal, {})[id(request)] = ( | |
382 self, | |
383 request, | |
384 check_profile, | |
385 ) | |
386 request._signals_registered.append(signal) | |
387 | |
388 @classmethod | |
389 def getPagePathFromURI(cls, uri): | |
390 """Retrieve page URL from xmpp: URI | |
391 | |
392 @param uri(unicode): URI with a xmpp: scheme | |
393 @return (unicode,None): absolute path (starting from root "/") to page handling the URI | |
394 None is returned if no page has been registered for this URI | |
395 """ | |
396 uri_data = common_uri.parseXMPPUri(uri) | |
397 try: | |
398 page, cb = cls.uri_callbacks[uri_data["type"], uri_data["sub_type"]] | |
399 except KeyError: | |
400 url = None | |
401 else: | |
402 url = cb(page, uri_data) | |
403 if url is None: | |
404 # no handler found | |
405 # we try to find a more generic one | |
406 try: | |
407 page, cb = cls.uri_callbacks[uri_data["type"], None] | |
408 except KeyError: | |
409 pass | |
410 else: | |
411 url = cb(page, uri_data) | |
412 return url | |
413 | |
414 @classmethod | |
415 def getPageByName(cls, name): | |
416 """retrieve page instance from its name | |
417 | |
418 @param name(unicode): name of the page | |
419 @return (LiberviaPage): page instance | |
420 @raise KeyError: the page doesn't exist | |
421 """ | |
422 return cls.named_pages[name] | |
423 | |
424 def getPageRedirectURL(self, request, page_name=u"login", url=None): | |
425 """generate URL for a page with redirect_url parameter set | |
426 | |
427 mainly used for login page with redirection to current page | |
428 @param request(server.Request): current HTTP request | |
429 @param page_name(unicode): name of the page to go | |
430 @param url(None, unicode): url to redirect to | |
431 None to use request path (i.e. current page) | |
432 @return (unicode): URL to use | |
433 """ | |
434 return u"{root_url}?redirect_url={redirect_url}".format( | |
435 root_url=self.getPageByName(page_name).url, | |
436 redirect_url=urllib.quote_plus(request.uri) | |
437 if url is None | |
438 else url.encode("utf-8"), | |
439 ) | |
440 | |
441 def getURL(self, *args): | |
442 """retrieve URL of the page set arguments | |
443 | |
444 *args(list[unicode]): argument to add to the URL as path elements | |
445 empty or None arguments will be ignored | |
446 """ | |
447 url_args = [quote(a) for a in args if a] | |
448 | |
449 if self.name is not None and self.name in self.pages_redirects: | |
450 # we check for redirection | |
451 redirect_data = self.pages_redirects[self.name] | |
452 args_hash = tuple(args) | |
453 for limit in xrange(len(args) + 1): | |
454 current_hash = args_hash[:limit] | |
455 if current_hash in redirect_data: | |
456 url_base = redirect_data[current_hash] | |
457 remaining = args[limit:] | |
458 remaining_url = "/".join(remaining) | |
459 return os.path.join("/", url_base, remaining_url) | |
460 | |
461 return os.path.join(self.url, *url_args) | |
462 | |
463 def getCurrentURL(self, request): | |
464 """retrieve URL used to access this page | |
465 | |
466 @return(unicode): current URL | |
467 """ | |
468 # we get url in the following way (splitting request.path instead of using | |
469 # request.prepath) because request.prepath may have been modified by | |
470 # redirection (if redirection args have been specified), while path reflect | |
471 # the real request | |
472 | |
473 # we ignore empty path elements (i.e. double '/' or '/' at the end) | |
474 path_elts = [p for p in request.path.split("/") if p] | |
475 | |
476 if request.postpath: | |
477 if not request.postpath[-1]: | |
478 # we remove trailing slash | |
479 request.postpath = request.postpath[:-1] | |
480 if request.postpath: | |
481 # getSubPageURL must return subpage from the point where | |
482 # the it is called, so we have to remove remanining | |
483 # path elements | |
484 path_elts = path_elts[: -len(request.postpath)] | |
485 | |
486 return u"/" + "/".join(path_elts).decode("utf-8") | |
487 | |
488 def getParamURL(self, request, **kwargs): | |
489 """use URL of current request but modify the parameters in query part | |
490 | |
491 **kwargs(dict[str, unicode]): argument to use as query parameters | |
492 @return (unicode): constructed URL | |
493 """ | |
494 current_url = self.getCurrentURL(request) | |
495 if kwargs: | |
496 encoded = urllib.urlencode( | |
497 {k: v.encode("utf-8") for k, v in kwargs.iteritems()} | |
498 ).decode("utf-8") | |
499 current_url = current_url + u"?" + encoded | |
500 return current_url | |
501 | |
502 def getSubPageByName(self, subpage_name, parent=None): | |
503 """retrieve a subpage and its path using its name | |
504 | |
505 @param subpage_name(unicode): name of the sub page | |
506 it must be a direct children of parent page | |
507 @param parent(LiberviaPage, None): parent page | |
508 None to use current page | |
509 @return (tuple[str, LiberviaPage]): page subpath and instance | |
510 @raise exceptions.NotFound: no page has been found | |
511 """ | |
512 if parent is None: | |
513 parent = self | |
514 for path, child in parent.children.iteritems(): | |
515 try: | |
516 child_name = child.name | |
517 except AttributeError: | |
518 # LiberviaPages have a name, but maybe this is an other Resource | |
519 continue | |
520 if child_name == subpage_name: | |
521 return path, child | |
522 raise exceptions.NotFound(_(u"requested sub page has not been found")) | |
523 | |
524 def getSubPageURL(self, request, page_name, *args): | |
525 """retrieve a page in direct children and build its URL according to request | |
526 | |
527 request's current path is used as base (at current parsing point, | |
528 i.e. it's more prepath than path). | |
529 Requested page is checked in children and an absolute URL is then built | |
530 by the resulting combination. | |
531 This method is useful to construct absolute URLs for children instead of | |
532 using relative path, which may not work in subpages, and are linked to the | |
533 names of directories (i.e. relative URL will break if subdirectory is renamed | |
534 while getSubPageURL won't as long as page_name is consistent). | |
535 Also, request.path is used, keeping real path used by user, | |
536 and potential redirections. | |
537 @param request(server.Request): current HTTP request | |
538 @param page_name(unicode): name of the page to retrieve | |
539 it must be a direct children of current page | |
540 @param *args(list[unicode]): arguments to add as path elements | |
541 if an arg is None, it will be ignored | |
542 @return (unicode): absolute URL to the sub page | |
543 """ | |
544 current_url = self.getCurrentURL(request) | |
545 path, child = self.getSubPageByName(page_name) | |
546 return os.path.join( | |
547 u"/", current_url, path, *[quote(a) for a in args if a is not None] | |
548 ) | |
549 | |
550 def getURLByNames(self, named_path): | |
551 """retrieve URL from pages names and arguments | |
552 | |
553 @param named_path(list[tuple[unicode, list[unicode]]]): path to the page as a list | |
554 of tuples of 2 items: | |
555 - first item is page name | |
556 - second item is list of path arguments of this page | |
557 @return (unicode): URL to the requested page with given path arguments | |
558 @raise exceptions.NotFound: one of the page was not found | |
559 """ | |
560 current_page = None | |
561 path = [] | |
562 for page_name, page_args in named_path: | |
563 if current_page is None: | |
564 current_page = self.getPageByName(page_name) | |
565 path.append(current_page.getURL(*page_args)) | |
566 else: | |
567 sub_path, current_page = self.getSubPageByName( | |
568 page_name, parent=current_page | |
569 ) | |
570 path.append(sub_path) | |
571 if page_args: | |
572 path.extend([quote(a) for a in page_args]) | |
573 return self.host.checkRedirection(u"/".join(path)) | |
574 | |
575 def getURLByPath(self, *args): | |
576 """generate URL by path | |
577 | |
578 this method as a similar effect as getURLByNames, but it is more readable | |
579 by using SubPage to get pages instead of using tuples | |
580 @param *args: path element: | |
581 - if unicode, will be used as argument | |
582 - if util.SubPage instance, must be the name of a subpage | |
583 @return (unicode): generated path | |
584 """ | |
585 args = list(args) | |
586 if not args: | |
587 raise ValueError("You must specify path elements") | |
588 # root page is the one needed to construct the base of the URL | |
589 # if first arg is not a SubPage instance, we use current page | |
590 if not isinstance(args[0], SubPage): | |
591 root = self | |
592 else: | |
593 root = self.getPageByName(args.pop(0)) | |
594 # we keep track of current page to check subpage | |
595 current_page = root | |
596 url_elts = [] | |
597 arguments = [] | |
598 while True: | |
599 while args and not isinstance(args[0], SubPage): | |
600 arguments.append(quote(args.pop(0))) | |
601 if not url_elts: | |
602 url_elts.append(root.getURL(*arguments)) | |
603 else: | |
604 url_elts.extend(arguments) | |
605 if not args: | |
606 break | |
607 else: | |
608 path, current_page = current_page.getSubPageByName(args.pop(0)) | |
609 arguments = [path] | |
610 return self.host.checkRedirection(u"/".join(url_elts)) | |
611 | |
612 def getChildWithDefault(self, path, request): | |
613 # we handle children ourselves | |
614 raise exceptions.InternalError( | |
615 u"this method should not be used with LiberviaPage" | |
616 ) | |
617 | |
618 def nextPath(self, request): | |
619 """get next URL path segment, and update request accordingly | |
620 | |
621 will move first segment of postpath in prepath | |
622 @param request(server.Request): current HTTP request | |
623 @return (unicode): unquoted segment | |
624 @raise IndexError: there is no segment left | |
625 """ | |
626 pathElement = request.postpath.pop(0) | |
627 request.prepath.append(pathElement) | |
628 return urllib.unquote(pathElement).decode("utf-8") | |
629 | |
630 def _filterPathValue(self, value, handler, name, request): | |
631 """Modify a path value according to handler (see [getPathArgs])""" | |
632 if handler in (u"@", u"@jid") and value == u"@": | |
633 value = None | |
634 | |
635 if handler in (u"", u"@"): | |
636 if value is None: | |
637 return u"" | |
638 elif handler in (u"jid", u"@jid"): | |
639 if value: | |
640 try: | |
641 return jid.JID(value) | |
642 except RuntimeError: | |
643 log.warning(_(u"invalid jid argument: {value}").format(value=value)) | |
644 self.pageError(request, C.HTTP_BAD_REQUEST) | |
645 else: | |
646 return u"" | |
647 else: | |
648 return handler(self, value, name, request) | |
649 | |
650 return value | |
651 | |
652 def getPathArgs(self, request, names, min_args=0, **kwargs): | |
653 """get several path arguments at once | |
654 | |
655 Arguments will be put in request data. | |
656 Missing arguments will have None value | |
657 @param names(list[unicode]): list of arguments to get | |
658 @param min_args(int): if less than min_args are found, PageError is used with C.HTTP_BAD_REQUEST | |
659 use 0 to ignore | |
660 @param **kwargs: special value or optional callback to use for arguments | |
661 names of the arguments must correspond to those in names | |
662 special values may be: | |
663 - '': use empty string instead of None when no value is specified | |
664 - '@': if value of argument is empty or '@', empty string will be used | |
665 - 'jid': value must be converted to jid.JID if it exists, else empty string is used | |
666 - '@jid': if value of arguments is empty or '@', empty string will be used, else it will be converted to jid | |
667 """ | |
668 data = self.getRData(request) | |
669 | |
670 for idx, name in enumerate(names): | |
671 if name[0] == u"*": | |
672 value = data[name[1:]] = [] | |
673 while True: | |
674 try: | |
675 value.append(self.nextPath(request)) | |
676 except IndexError: | |
677 idx -= 1 | |
678 break | |
679 else: | |
680 idx += 1 | |
681 else: | |
682 try: | |
683 value = data[name] = self.nextPath(request) | |
684 except IndexError: | |
685 data[name] = None | |
686 idx -= 1 | |
687 break | |
688 | |
689 values_count = idx + 1 | |
690 if values_count < min_args: | |
691 log.warning( | |
692 _( | |
693 u"Missing arguments in URL (got {count}, expected at least {min_args})" | |
694 ).format(count=values_count, min_args=min_args) | |
695 ) | |
696 self.pageError(request, C.HTTP_BAD_REQUEST) | |
697 | |
698 for name in names[values_count:]: | |
699 data[name] = None | |
700 | |
701 for name, handler in kwargs.iteritems(): | |
702 if name[0] == "*": | |
703 data[name] = [ | |
704 self._filterPathValue(v, handler, name, request) for v in data[name] | |
705 ] | |
706 else: | |
707 data[name] = self._filterPathValue(data[name], handler, name, request) | |
708 | |
709 ## Cache handling ## | |
710 | |
711 def _setCacheHeaders(self, request, cache): | |
712 """Set ETag and Last-Modified HTTP headers, used for caching""" | |
713 request.setHeader("ETag", cache.hash) | |
714 last_modified = self.host.getHTTPDate(cache.created) | |
715 request.setHeader("Last-Modified", last_modified) | |
716 | |
717 def _checkCacheHeaders(self, request, cache): | |
718 """Check if a cache condition is set on the request | |
719 | |
720 if condition is valid, C.HTTP_NOT_MODIFIED is returned | |
721 """ | |
722 etag_match = request.getHeader("If-None-Match") | |
723 if etag_match is not None: | |
724 if cache.hash == etag_match: | |
725 self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True) | |
726 else: | |
727 modified_match = request.getHeader("If-Modified-Since") | |
728 if modified_match is not None: | |
729 modified = date_utils.date_parse(modified_match) | |
730 if modified >= int(cache.created): | |
731 self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True) | |
732 | |
733 def checkCacheSubscribeCb(self, sub_id, service, node): | |
734 self.cache_pubsub_sub.add((service, node, sub_id)) | |
735 | |
736 def checkCacheSubscribeEb(self, failure_, service, node): | |
737 log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_)) | |
738 # FIXME: cache must be marked as unusable here | |
739 | |
740 def psNodeWatchAddEb(self, failure_, service, node): | |
741 log.warning(_(u"Can't add node watched: {msg}").format(msg=failure_)) | |
742 | |
743 def checkCache(self, request, cache_type, **kwargs): | |
744 """check if a page is in cache and return cached version if suitable | |
745 | |
746 this method may perform extra operation to handle cache (e.g. subscribing to a | |
747 pubsub node) | |
748 @param request(server.Request): current HTTP request | |
749 @param cache_type(int): on of C.CACHE_* const. | |
750 @param **kwargs: args according to cache_type: | |
751 C.CACHE_PUBSUB: | |
752 service: pubsub service | |
753 node: pubsub node | |
754 short: short name of feature (needed if node is empty to find namespace) | |
755 | |
756 """ | |
757 if request.postpath: | |
758 # we are not on the final page, no need to go further | |
759 return | |
760 | |
761 profile = self.getProfile(request) or C.SERVICE_PROFILE | |
762 | |
763 if cache_type == C.CACHE_PUBSUB: | |
764 service, node = kwargs["service"], kwargs["node"] | |
765 if not node: | |
766 try: | |
767 short = kwargs["short"] | |
768 node = self.host.ns_map[short] | |
769 except KeyError: | |
770 log.warning( | |
771 _( | |
772 u'Can\'t use cache for empty node without namespace set, please ensure to set "short" and that it is registered' | |
773 ) | |
774 ) | |
775 return | |
776 if profile != C.SERVICE_PROFILE: | |
777 # only service profile is cache for now | |
778 return | |
779 try: | |
780 cache = self.cache[profile][cache_type][service][node][request.uri][self] | |
781 except KeyError: | |
782 # no cache yet, let's subscribe to the pubsub node | |
783 d1 = self.host.bridgeCall( | |
784 "psSubscribe", service.full(), node, {}, profile | |
785 ) | |
786 d1.addCallback(self.checkCacheSubscribeCb, service, node) | |
787 d1.addErrback(self.checkCacheSubscribeEb, service, node) | |
788 d2 = self.host.bridgeCall("psNodeWatchAdd", service.full(), node, profile) | |
789 d2.addErrback(self.psNodeWatchAddEb, service, node) | |
790 self._do_cache = [self, profile, cache_type, service, node, request.uri] | |
791 # we don't return the Deferreds as it is not needed to wait for | |
792 # the subscription to continue with page rendering | |
793 return | |
794 | |
795 else: | |
796 raise exceptions.InternalError(u"Unknown cache_type") | |
797 log.debug(u"using cache for {page}".format(page=self)) | |
798 cache.last_access = time.time() | |
799 self._setCacheHeaders(request, cache) | |
800 self._checkCacheHeaders(request, cache) | |
801 request.write(cache.rendered) | |
802 request.finish() | |
803 raise failure.Failure(exceptions.CancelError(u"cache is used")) | |
804 | |
805 def _cacheURL(self, dummy, request, profile): | |
806 self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) | |
807 | |
808 @classmethod | |
809 def onNodeEvent(cls, host, service, node, event_type, items, profile): | |
810 """Invalidate cache for all pages linked to this node""" | |
811 try: | |
812 cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node] | |
813 except KeyError: | |
814 log.info( | |
815 _( | |
816 u"Removing subscription for {service}/{node}: " | |
817 u"the page is not cached" | |
818 ).format(service=service, node=node) | |
819 ) | |
820 d1 = host.bridgeCall("psUnsubscribe", service, node, profile) | |
821 d1.addErrback( | |
822 lambda failure_: log.warning( | |
823 _(u"Can't unsubscribe from {service}/{node}: {msg}").format( | |
824 service=service, node=node, msg=failure_ | |
825 ) | |
826 ) | |
827 ) | |
828 d2 = host.bridgeCall("psNodeWatchAdd", service, node, profile) | |
829 # TODO: check why the page is not in cache, remove subscription? | |
830 d2.addErrback( | |
831 lambda failure_: log.warning( | |
832 _(u"Can't remove watch for {service}/{node}: {msg}").format( | |
833 service=service, node=node, msg=failure_ | |
834 ) | |
835 ) | |
836 ) | |
837 else: | |
838 cache.clear() | |
839 | |
840 @classmethod | |
841 def onSignal(cls, host, signal, *args): | |
842 """Generic method which receive registered signals | |
843 | |
844 if a callback is registered for this signal, call it | |
845 @param host: Libervia instance | |
846 @param signal(unicode): name of the signal | |
847 @param *args: args of the signals | |
848 """ | |
849 for page, request, check_profile in cls.signals_handlers.get( | |
850 signal, {} | |
851 ).itervalues(): | |
852 if check_profile: | |
853 signal_profile = args[-1] | |
854 request_profile = page.getProfile(request) | |
855 if not request_profile: | |
856 # if you want to use signal without session, unset check_profile | |
857 # (be sure to know what you are doing) | |
858 log.error(_(u"no session started, signal can't be checked")) | |
859 continue | |
860 if signal_profile != request_profile: | |
861 # we ignore the signal, it's not for our profile | |
862 continue | |
863 if request._signals_cache is not None: | |
864 # socket is not yet opened, we cache the signal | |
865 request._signals_cache.append((request, signal, args)) | |
866 log.debug( | |
867 u"signal [{signal}] cached: {args}".format(signal=signal, args=args) | |
868 ) | |
869 else: | |
870 page.on_signal(page, request, signal, *args) | |
871 | |
872 def onSocketOpen(self, request): | |
873 """Called for dynamic pages when socket has just been opened | |
874 | |
875 we send all cached signals | |
876 """ | |
877 assert request._signals_cache is not None | |
878 cache = request._signals_cache | |
879 request._signals_cache = None | |
880 for request, signal, args in cache: | |
881 self.on_signal(self, request, signal, *args) | |
882 | |
883 def onSocketClose(self, request): | |
884 """Called for dynamic pages when socket has just been closed | |
885 | |
886 we remove signal handler | |
887 """ | |
888 for signal in request._signals_registered: | |
889 try: | |
890 del LiberviaPage.signals_handlers[signal][id(request)] | |
891 except KeyError: | |
892 log.error( | |
893 _( | |
894 u"Can't find signal handler for [{signal}], this should not happen" | |
895 ).format(signal=signal) | |
896 ) | |
897 else: | |
898 log.debug(_(u"Removed signal handler")) | |
899 | |
900 def delegateToResource(self, request, resource): | |
901 """continue workflow with Twisted Resource""" | |
902 buf = resource.render(request) | |
903 if buf == server.NOT_DONE_YET: | |
904 pass | |
905 else: | |
906 request.write(buf) | |
907 request.finish() | |
908 raise failure.Failure(exceptions.CancelError(u"resource delegation")) | |
909 | |
910 def HTTPRedirect(self, request, url): | |
911 """redirect to an URL using HTTP redirection | |
912 | |
913 @param request(server.Request): current HTTP request | |
914 @param url(unicode): url to redirect to | |
915 """ | |
916 web_util.redirectTo(url.encode("utf-8"), request) | |
917 request.finish() | |
918 raise failure.Failure(exceptions.CancelError(u"HTTP redirection is used")) | |
919 | |
920 def redirectOrContinue(self, request, redirect_arg=u"redirect_url"): | |
921 """helper method to redirect a page to an url given as arg | |
922 | |
923 if the arg is not present, the page will continue normal workflow | |
924 @param request(server.Request): current HTTP request | |
925 @param redirect_arg(unicode): argument to use to get redirection URL | |
926 @interrupt: redirect the page to requested URL | |
927 @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used | |
928 """ | |
929 try: | |
930 url = request.args["redirect_url"][0] | |
931 except (KeyError, IndexError): | |
932 pass | |
933 else: | |
934 # a redirection is requested | |
935 if not url or url[0] != u"/": | |
936 # we only want local urls | |
937 self.pageError(request, C.HTTP_BAD_REQUEST) | |
938 else: | |
939 self.HTTPRedirect(request, url) | |
940 | |
941 def pageRedirect(self, page_path, request, skip_parse_url=True, path_args=None): | |
942 """redirect a page to a named page | |
943 | |
944 the workflow will continue with the workflow of the named page, | |
945 skipping named page's parse_url method if it exist. | |
946 If you want to do a HTTP redirection, use HTTPRedirect | |
947 @param page_path(unicode): path to page (elements are separated by "/"): | |
948 if path starts with a "/": | |
949 path is a full path starting from root | |
950 else: | |
951 - first element is name as registered in name variable | |
952 - following element are subpages path | |
953 e.g.: "blog" redirect to page named "blog" | |
954 "blog/atom.xml" redirect to atom.xml subpage of "blog" | |
955 "/common/blog/atom.xml" redirect to the page at the fiven full path | |
956 @param request(server.Request): current HTTP request | |
957 @param skip_parse_url(bool): if True, parse_url method on redirect page will be skipped | |
958 @param path_args(list[unicode], None): path arguments to use in redirected page | |
959 @raise KeyError: there is no known page with this name | |
960 """ | |
961 # FIXME: render non LiberviaPage resources | |
962 path = page_path.rstrip(u"/").split(u"/") | |
963 if not path[0]: | |
964 redirect_page = self.host.root | |
965 else: | |
966 redirect_page = self.named_pages[path[0]] | |
967 | |
968 for subpage in path[1:]: | |
969 if redirect_page is self.host.root: | |
970 redirect_page = redirect_page.children[subpage] | |
971 else: | |
972 redirect_page = redirect_page.original.children[subpage] | |
973 | |
974 if path_args is not None: | |
975 args = [quote(a) for a in path_args] | |
976 request.postpath = args + request.postpath | |
977 | |
978 if self._do_cache: | |
979 # if cache is needed, it will be handled by final page | |
980 redirect_page._do_cache = self._do_cache | |
981 self._do_cache = None | |
982 | |
983 redirect_page.renderPage(request, skip_parse_url=skip_parse_url) | |
984 raise failure.Failure(exceptions.CancelError(u"page redirection is used")) | |
985 | |
986 def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False): | |
987 """generate an error page and terminate the request | |
988 | |
989 @param request(server.Request): HTTP request | |
990 @param core(int): error code to use | |
991 @param no_body: don't write body if True | |
992 """ | |
993 request.setResponseCode(code) | |
994 if no_body: | |
995 request.finish() | |
996 else: | |
997 template = u"error/" + unicode(code) + ".html" | |
998 | |
999 rendered = self.host.renderer.render( | |
1000 template, | |
1001 root_path="/templates/", | |
1002 error_code=code, | |
1003 **request.template_data | |
1004 ) | |
1005 | |
1006 self.writeData(rendered, request) | |
1007 raise failure.Failure(exceptions.CancelError(u"error page is used")) | |
1008 | |
1009 def writeData(self, data, request): | |
1010 """write data to transport and finish the request""" | |
1011 if data is None: | |
1012 self.pageError(request) | |
1013 data_encoded = data.encode("utf-8") | |
1014 | |
1015 if self._do_cache is not None: | |
1016 redirected_page = self._do_cache.pop(0) | |
1017 cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache) | |
1018 page_cache = cache[redirected_page] = CachePage(data_encoded) | |
1019 self._setCacheHeaders(request, page_cache) | |
1020 log.debug( | |
1021 _(u"{page} put in cache for [{profile}]").format( | |
1022 page=self, profile=self._do_cache[0] | |
1023 ) | |
1024 ) | |
1025 self._do_cache = None | |
1026 self._checkCacheHeaders(request, page_cache) | |
1027 | |
1028 request.write(data_encoded) | |
1029 request.finish() | |
1030 | |
1031 def _subpagesHandler(self, dummy, request): | |
1032 """render subpage if suitable | |
1033 | |
1034 this method checks if there is still an unmanaged part of the path | |
1035 and check if it corresponds to a subpage. If so, it render the subpage | |
1036 else it render a NoResource. | |
1037 If there is no unmanaged part of the segment, current page workflow is pursued | |
1038 """ | |
1039 if request.postpath: | |
1040 subpage = self.nextPath(request) | |
1041 try: | |
1042 child = self.children[subpage] | |
1043 except KeyError: | |
1044 self.pageError(request) | |
1045 else: | |
1046 child.render(request) | |
1047 raise failure.Failure(exceptions.CancelError(u"subpage page is used")) | |
1048 | |
1049 def _prepare_dynamic(self, dummy, request): | |
1050 # we need to activate dynamic page | |
1051 # we set data for template, and create/register token | |
1052 socket_token = unicode(uuid.uuid4()) | |
1053 socket_url = self.host.getWebsocketURL(request) | |
1054 socket_debug = C.boolConst(self.host.debug) | |
1055 request.template_data["websocket"] = WebsocketMeta( | |
1056 socket_url, socket_token, socket_debug | |
1057 ) | |
1058 self.host.registerWSToken(socket_token, self, request) | |
1059 # we will keep track of handlers to remove | |
1060 request._signals_registered = [] | |
1061 # we will cache registered signals until socket is opened | |
1062 request._signals_cache = [] | |
1063 | |
1064 def _prepare_render(self, dummy, request): | |
1065 return defer.maybeDeferred(self.prepare_render, self, request) | |
1066 | |
1067 def _render_method(self, dummy, request): | |
1068 return defer.maybeDeferred(self.render_method, self, request) | |
1069 | |
1070 def _render_template(self, dummy, request): | |
1071 template_data = request.template_data | |
1072 | |
1073 # if confirm variable is set in case of successfuly data post | |
1074 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
1075 if session_data.popPageFlag(self, C.FLAG_CONFIRM): | |
1076 template_data[u"confirm"] = True | |
1077 | |
1078 return self.host.renderer.render( | |
1079 self.template, | |
1080 root_path="/templates/", | |
1081 media_path="/" + C.MEDIA_DIR, | |
1082 cache_path=session_data.cache_dir, | |
1083 main_menu=LiberviaPage.main_menu, | |
1084 **template_data | |
1085 ) | |
1086 | |
1087 def _renderEb(self, failure_, request): | |
1088 """don't raise error on CancelError""" | |
1089 failure_.trap(exceptions.CancelError) | |
1090 | |
1091 def _internalError(self, failure_, request): | |
1092 """called if an error is not catched""" | |
1093 log.error( | |
1094 _(u"Uncatched error for HTTP request on {url}: {msg}").format( | |
1095 url=request.URLPath(), msg=failure_ | |
1096 ) | |
1097 ) | |
1098 self.pageError(request, C.HTTP_INTERNAL_ERROR) | |
1099 | |
1100 def _on_data_post_redirect(self, ret, request): | |
1101 """called when page's on_data_post has been done successfuly | |
1102 | |
1103 This will do a Post/Redirect/Get pattern. | |
1104 this method redirect to the same page or to request.data['post_redirect_page'] | |
1105 post_redirect_page can be either a page or a tuple with page as first item, then a list of unicode arguments to append to the url. | |
1106 if post_redirect_page is not used, initial request.uri (i.e. the same page as where the data have been posted) will be used for redirection. | |
1107 HTTP status code "See Other" (303) is used as it is the recommanded code in this case. | |
1108 @param ret(None, unicode, iterable): on_data_post return value | |
1109 see LiberviaPage.__init__ on_data_post docstring | |
1110 """ | |
1111 if ret is None: | |
1112 ret = () | |
1113 elif isinstance(ret, basestring): | |
1114 ret = (ret,) | |
1115 else: | |
1116 ret = tuple(ret) | |
1117 raise NotImplementedError( | |
1118 _(u"iterable in on_data_post return value is not used yet") | |
1119 ) | |
1120 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
1121 request_data = self.getRData(request) | |
1122 if "post_redirect_page" in request_data: | |
1123 redirect_page_data = request_data["post_redirect_page"] | |
1124 if isinstance(redirect_page_data, tuple): | |
1125 redirect_page = redirect_page_data[0] | |
1126 redirect_page_args = redirect_page_data[1:] | |
1127 redirect_uri = redirect_page.getURL(*redirect_page_args) | |
1128 else: | |
1129 redirect_page = redirect_page_data | |
1130 redirect_uri = redirect_page.url | |
1131 else: | |
1132 redirect_page = self | |
1133 redirect_uri = request.uri | |
1134 | |
1135 if not C.POST_NO_CONFIRM in ret: | |
1136 session_data.setPageFlag(redirect_page, C.FLAG_CONFIRM) | |
1137 request.setResponseCode(C.HTTP_SEE_OTHER) | |
1138 request.setHeader("location", redirect_uri) | |
1139 request.finish() | |
1140 raise failure.Failure(exceptions.CancelError(u"Post/Redirect/Get is used")) | |
1141 | |
1142 def _on_data_post(self, dummy, request): | |
1143 csrf_token = self.host.getSessionData( | |
1144 request, session_iface.ISATSession | |
1145 ).csrf_token | |
1146 try: | |
1147 given_csrf = self.getPostedData(request, u"csrf_token") | |
1148 except KeyError: | |
1149 given_csrf = None | |
1150 if given_csrf is None or given_csrf != csrf_token: | |
1151 log.warning( | |
1152 _(u"invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( | |
1153 url=request.uri, ip=request.getClientIP() | |
1154 ) | |
1155 ) | |
1156 self.pageError(request, C.HTTP_UNAUTHORIZED) | |
1157 d = defer.maybeDeferred(self.on_data_post, self, request) | |
1158 d.addCallback(self._on_data_post_redirect, request) | |
1159 return d | |
1160 | |
1161 def getPostedData(self, request, keys, multiple=False): | |
1162 """get data from a POST request or from URL's query part and decode it | |
1163 | |
1164 @param request(server.Request): request linked to the session | |
1165 @param keys(unicode, iterable[unicode]): name of the value(s) to get | |
1166 unicode to get one value | |
1167 iterable to get more than one | |
1168 @param multiple(bool): True if multiple values are possible/expected | |
1169 if False, the first value is returned | |
1170 @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s) | |
1171 @raise KeyError: one specific key has been requested, and it is missing | |
1172 """ | |
1173 # FIXME: request.args is already unquoting the value, it seems we are doing double unquote | |
1174 if isinstance(keys, basestring): | |
1175 keys = [keys] | |
1176 get_first = True | |
1177 else: | |
1178 get_first = False | |
1179 | |
1180 ret = [] | |
1181 for key in keys: | |
1182 gen = (urllib.unquote(v).decode("utf-8") for v in request.args.get(key, [])) | |
1183 if multiple: | |
1184 ret.append(gen) | |
1185 else: | |
1186 try: | |
1187 ret.append(next(gen)) | |
1188 except StopIteration: | |
1189 raise KeyError(key) | |
1190 | |
1191 return ret[0] if get_first else ret | |
1192 | |
1193 def getAllPostedData(self, request, except_=(), multiple=True): | |
1194 """get all posted data | |
1195 | |
1196 @param request(server.Request): request linked to the session | |
1197 @param except_(iterable[unicode]): key of values to ignore | |
1198 csrf_token will always be ignored | |
1199 @param multiple(bool): if False, only the first values are returned | |
1200 @return (dict[unicode, list[unicode]]): post values | |
1201 """ | |
1202 except_ = tuple(except_) + (u"csrf_token",) | |
1203 ret = {} | |
1204 for key, values in request.args.iteritems(): | |
1205 key = urllib.unquote(key).decode("utf-8") | |
1206 if key in except_: | |
1207 continue | |
1208 if not multiple: | |
1209 ret[key] = urllib.unquote(values[0]).decode("utf-8") | |
1210 else: | |
1211 ret[key] = [urllib.unquote(v).decode("utf-8") for v in values] | |
1212 return ret | |
1213 | |
1214 def getProfile(self, request): | |
1215 """helper method to easily get current profile | |
1216 | |
1217 @return (unicode, None): current profile | |
1218 None if no profile session is started | |
1219 """ | |
1220 sat_session = self.host.getSessionData(request, session_iface.ISATSession) | |
1221 return sat_session.profile | |
1222 | |
1223 def getRData(self, request): | |
1224 """helper method to get request data dict | |
1225 | |
1226 this dictionnary if for the request only, it is not saved in session | |
1227 It is mainly used to pass data between pages/methods called during request workflow | |
1228 @return (dict): request data | |
1229 """ | |
1230 try: | |
1231 return request.data | |
1232 except AttributeError: | |
1233 request.data = {} | |
1234 return request.data | |
1235 | |
1236 def _checkAccess(self, data, request): | |
1237 """Check access according to self.access | |
1238 | |
1239 if access is not granted, show a HTTP_UNAUTHORIZED pageError and stop request, | |
1240 else return data (so it can be inserted in deferred chain | |
1241 """ | |
1242 if self.access == C.PAGES_ACCESS_PUBLIC: | |
1243 pass | |
1244 elif self.access == C.PAGES_ACCESS_PROFILE: | |
1245 profile = self.getProfile(request) | |
1246 if not profile: | |
1247 # no session started | |
1248 if not self.host.options["allow_registration"]: | |
1249 # registration not allowed, access is not granted | |
1250 self.pageError(request, C.HTTP_UNAUTHORIZED) | |
1251 else: | |
1252 # registration allowed, we redirect to login page | |
1253 login_url = self.getPageRedirectURL(request) | |
1254 self.HTTPRedirect(request, login_url) | |
1255 | |
1256 return data | |
1257 | |
1258 def renderPartial(self, request, template, template_data): | |
1259 """Render a template to be inserted in dynamic page | |
1260 | |
1261 this is NOT the normal page rendering method, it is used only to update | |
1262 dynamic pages | |
1263 @param template(unicode): path of the template to render | |
1264 @param template_data(dict): template_data to use | |
1265 """ | |
1266 if not self.dynamic: | |
1267 raise exceptions.InternalError( | |
1268 _(u"renderPartial must only be used with dynamic pages") | |
1269 ) | |
1270 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
1271 | |
1272 return self.host.renderer.render( | |
1273 template, | |
1274 root_path="/templates/", | |
1275 media_path="/" + C.MEDIA_DIR, | |
1276 cache_path=session_data.cache_dir, | |
1277 main_menu=LiberviaPage.main_menu, | |
1278 **template_data | |
1279 ) | |
1280 | |
1281 def renderAndUpdate( | |
1282 self, request, template, selectors, template_data_update, update_type="append" | |
1283 ): | |
1284 """Helper method to render a partial page element and update the page | |
1285 | |
1286 this is NOT the normal page rendering method, it is used only to update | |
1287 dynamic pages | |
1288 @param request(server.Request): current HTTP request | |
1289 @param template: same as for [renderPartial] | |
1290 @param selectors: CSS selectors to use | |
1291 @param template_data_update: template data to use | |
1292 template data cached in request will be copied then updated | |
1293 with this data | |
1294 @parap update_type(unicode): one of: | |
1295 append: append rendered element to selected element | |
1296 """ | |
1297 template_data = request.template_data.copy() | |
1298 template_data.update(template_data_update) | |
1299 html = self.renderPartial(request, template, template_data) | |
1300 request.sendData(u"dom", selectors=selectors, update_type=update_type, html=html) | |
1301 | |
1302 def renderPage(self, request, skip_parse_url=False): | |
1303 """Main method to handle the workflow of a LiberviaPage""" | |
1304 | |
1305 # template_data are the variables passed to template | |
1306 if not hasattr(request, "template_data"): | |
1307 session_data = self.host.getSessionData(request, session_iface.ISATSession) | |
1308 csrf_token = session_data.csrf_token | |
1309 request.template_data = { | |
1310 u"profile": session_data.profile, | |
1311 u"csrf_token": csrf_token, | |
1312 } | |
1313 | |
1314 # XXX: here is the code which need to be executed once | |
1315 # at the beginning of the request hanling | |
1316 if request.postpath and not request.postpath[-1]: | |
1317 # we don't differenciate URLs finishing with '/' or not | |
1318 del request.postpath[-1] | |
1319 | |
1320 d = defer.Deferred() | |
1321 d.addCallback(self._checkAccess, request) | |
1322 | |
1323 if self.redirect is not None: | |
1324 d.addCallback( | |
1325 lambda dummy: self.pageRedirect( | |
1326 self.redirect, request, skip_parse_url=False | |
1327 ) | |
1328 ) | |
1329 | |
1330 if self.parse_url is not None and not skip_parse_url: | |
1331 if self.url_cache: | |
1332 profile = self.getProfile(request) | |
1333 try: | |
1334 cache_url = self.cached_urls[profile][request.uri] | |
1335 except KeyError: | |
1336 # no cache for this URI yet | |
1337 # we do normal URL parsing, and then the cache | |
1338 d.addCallback(self.parse_url, request) | |
1339 d.addCallback(self._cacheURL, request, profile) | |
1340 else: | |
1341 log.debug(_(u"using URI cache for {page}").format(page=self)) | |
1342 cache_url.use(request) | |
1343 else: | |
1344 d.addCallback(self.parse_url, request) | |
1345 | |
1346 d.addCallback(self._subpagesHandler, request) | |
1347 | |
1348 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): | |
1349 # only HTTP GET and POST are handled so far | |
1350 d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) | |
1351 | |
1352 if request.method == C.HTTP_METHOD_POST: | |
1353 if self.on_data_post is None: | |
1354 # if we don't have on_data_post, the page was not expecting POST | |
1355 # so we return an error | |
1356 d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) | |
1357 else: | |
1358 d.addCallback(self._on_data_post, request) | |
1359 # by default, POST follow normal behaviour after on_data_post is called | |
1360 # this can be changed by a redirection or other method call in on_data_post | |
1361 | |
1362 if self.dynamic: | |
1363 d.addCallback(self._prepare_dynamic, request) | |
1364 | |
1365 if self.prepare_render: | |
1366 d.addCallback(self._prepare_render, request) | |
1367 | |
1368 if self.template: | |
1369 d.addCallback(self._render_template, request) | |
1370 elif self.render_method: | |
1371 d.addCallback(self._render_method, request) | |
1372 | |
1373 d.addCallback(self.writeData, request) | |
1374 d.addErrback(self._renderEb, request) | |
1375 d.addErrback(self._internalError, request) | |
1376 d.callback(self) | |
1377 return server.NOT_DONE_YET | |
1378 | |
1379 def render_GET(self, request): | |
1380 return self.renderPage(request) | |
1381 | |
1382 def render_POST(self, request): | |
1383 return self.renderPage(request) |