Mercurial > libervia-web
comparison libervia/web/server/pages.py @ 1518:eb00d593801d
refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 16:49:28 +0200 |
parents | libervia/server/pages.py@16228994ca3b |
children | 66c1a90da1bc |
comparison
equal
deleted
inserted
replaced
1517:b8ed9726525b | 1518:eb00d593801d |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia: a Salut à Toi frontend | |
4 # Copyright (C) 2011-2021 Jérôme Poisson <goffi@goffi.org> | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from __future__ import annotations | |
20 import copy | |
21 from functools import reduce | |
22 import hashlib | |
23 import json | |
24 import os.path | |
25 from pathlib import Path | |
26 import time | |
27 import traceback | |
28 from typing import List, Optional, Union | |
29 import urllib.error | |
30 import urllib.parse | |
31 import urllib.request | |
32 import uuid | |
33 | |
34 from twisted.internet import defer | |
35 from twisted.python import failure | |
36 from twisted.python.filepath import FilePath | |
37 from twisted.web import server | |
38 from twisted.web import resource as web_resource | |
39 from twisted.web import util as web_util | |
40 from twisted.words.protocols.jabber import jid | |
41 | |
42 from libervia.backend.core import exceptions | |
43 from libervia.backend.core.i18n import _ | |
44 from libervia.backend.core.log import getLogger | |
45 from libervia.backend.tools.common import date_utils | |
46 from libervia.backend.tools.common import utils | |
47 from libervia.backend.tools.common import data_format | |
48 from libervia.backend.tools.utils import as_deferred | |
49 from libervia.frontends.bridge.bridge_frontend import BridgeException | |
50 | |
51 from . import session_iface | |
52 from .classes import WebsocketMeta | |
53 from .classes import Script | |
54 from .constants import Const as C | |
55 from .resources import LiberviaRootResource | |
56 from .utils import SubPage, quote | |
57 | |
58 log = getLogger(__name__) | |
59 | |
60 | |
61 class CacheBase(object): | |
62 def __init__(self): | |
63 self._created = time.time() | |
64 self._last_access = self._created | |
65 | |
66 @property | |
67 def created(self): | |
68 return self._created | |
69 | |
70 @property | |
71 def last_access(self): | |
72 return self._last_access | |
73 | |
74 @last_access.setter | |
75 def last_access(self, timestamp): | |
76 self._last_access = timestamp | |
77 | |
78 | |
79 class CachePage(CacheBase): | |
80 def __init__(self, rendered): | |
81 super(CachePage, self).__init__() | |
82 self._created = time.time() | |
83 self._last_access = self._created | |
84 self._rendered = rendered | |
85 self._hash = hashlib.sha256(rendered).hexdigest() | |
86 | |
87 @property | |
88 def rendered(self): | |
89 return self._rendered | |
90 | |
91 @property | |
92 def hash(self): | |
93 return self._hash | |
94 | |
95 | |
96 class CacheURL(CacheBase): | |
97 def __init__(self, request): | |
98 super(CacheURL, self).__init__() | |
99 try: | |
100 self._data = copy.deepcopy(request.data) | |
101 except AttributeError: | |
102 self._data = {} | |
103 self._template_data = copy.deepcopy(request.template_data) | |
104 self._prepath = request.prepath[:] | |
105 self._postpath = request.postpath[:] | |
106 del self._template_data["csrf_token"] | |
107 | |
108 def use(self, request): | |
109 self.last_access = time.time() | |
110 request.data = copy.deepcopy(self._data) | |
111 request.template_data.update(copy.deepcopy(self._template_data)) | |
112 request.prepath = self._prepath[:] | |
113 request.postpath = self._postpath[:] | |
114 | |
115 | |
116 class LiberviaPage(web_resource.Resource): | |
117 isLeaf = True # we handle subpages ourself | |
118 cache = {} | |
119 # Set of tuples (service/node/sub_id) of nodes subscribed for caching | |
120 # sub_id can be empty string if not handled by service | |
121 cache_pubsub_sub = set() | |
122 | |
123 def __init__( | |
124 self, host, vhost_root, root_dir, url, name=None, label=None, redirect=None, | |
125 access=None, dynamic=True, parse_url=None, add_breadcrumb=None, | |
126 prepare_render=None, render=None, template=None, on_data_post=None, on_data=None, | |
127 url_cache=False, replace_on_conflict=False | |
128 ): | |
129 """Initiate LiberviaPage instance | |
130 | |
131 LiberviaPages are the main resources of Libervia, using easy to set python files | |
132 The non mandatory arguments are the variables found in page_meta.py | |
133 @param host(Libervia): the running instance of Libervia | |
134 @param vhost_root(web_resource.Resource): root resource of the virtual host which | |
135 handle this page. | |
136 @param root_dir(Path): absolute file path of the page | |
137 @param url(unicode): relative URL to the page | |
138 this URL may not be valid, as pages may require path arguments | |
139 @param name(unicode, None): if not None, a unique name to identify the page | |
140 can then be used for e.g. redirection | |
141 "/" is not allowed in names (as it can be used to construct URL paths) | |
142 @param redirect(unicode, None): if not None, this page will be redirected. | |
143 A redirected parameter is used as in self.page_redirect. | |
144 parse_url will not be skipped | |
145 using this redirect parameter is called "full redirection" | |
146 using self.page_redirect is called "partial redirection" (because some | |
147 rendering method can still be used, e.g. parse_url) | |
148 @param access(unicode, None): permission needed to access the page | |
149 None means public access. | |
150 Pages inherit from parent pages: e.g. if a "settings" page is restricted | |
151 to admins, and if "settings/blog" is public, it still can only be accessed by | |
152 admins. See C.PAGES_ACCESS_* for details | |
153 @param dynamic(bool): if True, activate websocket for bidirectional communication | |
154 @param parse_url(callable, None): if set it will be called to handle the URL path | |
155 after this method, the page will be rendered if noting is left in path | |
156 (request.postpath) else a the request will be transmitted to a subpage | |
157 @param add_breadcrumb(callable, None): if set, manage the breadcrumb data for this | |
158 page, otherwise it will be set automatically from page name or label. | |
159 @param prepare_render(callable, None): if set, will be used to prepare the | |
160 rendering. That often means gathering data using the bridge | |
161 @param render(callable, None): if template is not set, this method will be | |
162 called and what it returns will be rendered. | |
163 This method is mutually exclusive with template and must return a unicode | |
164 string. | |
165 @param template(unicode, None): path to the template to render. | |
166 This method is mutually exclusive with render | |
167 @param on_data_post(callable, None): method to call when data is posted | |
168 None if data post is not handled | |
169 "continue" if data post is not handled there, and we must not interrupt | |
170 workflow (i.e. it's handled in "render" method). | |
171 otherwise, on_data_post can return a string with following value: | |
172 - C.POST_NO_CONFIRM: confirm flag will not be set | |
173 on_data_post can raise following exceptions: | |
174 - exceptions.DataError: value is incorrect, message will be displayed | |
175 as a notification | |
176 @param on_data(callable, None): method to call when dynamic data is sent | |
177 this method is used with Libervia's websocket mechanism | |
178 @param url_cache(boolean): if set, result of parse_url is cached (per profile). | |
179 Useful when costly calls (e.g. network) are done while parsing URL. | |
180 @param replace_on_conflict(boolean): if True, don't raise ConflictError if a | |
181 page of this name already exists, but replace it | |
182 """ | |
183 | |
184 web_resource.Resource.__init__(self) | |
185 self.host = host | |
186 self.vhost_root = vhost_root | |
187 self.root_dir = root_dir | |
188 self.url = url | |
189 self.name = name | |
190 self.label = label | |
191 self.dyn_data = {} | |
192 if name is not None: | |
193 if (name in self.named_pages | |
194 and not (replace_on_conflict and self.named_pages[name].url == url)): | |
195 raise exceptions.ConflictError( | |
196 _('a Libervia page named "{}" already exists'.format(name))) | |
197 if "/" in name: | |
198 raise ValueError(_('"/" is not allowed in page names')) | |
199 if not name: | |
200 raise ValueError(_("a page name can't be empty")) | |
201 self.named_pages[name] = self | |
202 if access is None: | |
203 access = C.PAGES_ACCESS_PUBLIC | |
204 if access not in ( | |
205 C.PAGES_ACCESS_PUBLIC, | |
206 C.PAGES_ACCESS_PROFILE, | |
207 C.PAGES_ACCESS_NONE, | |
208 ): | |
209 raise NotImplementedError( | |
210 _("{} access is not implemented yet").format(access) | |
211 ) | |
212 self.access = access | |
213 self.dynamic = dynamic | |
214 if redirect is not None: | |
215 # only page access and name make sense in case of full redirection | |
216 # so we check that rendering methods/values are not set | |
217 if not all( | |
218 lambda x: x is not None | |
219 for x in (parse_url, prepare_render, render, template) | |
220 ): | |
221 raise ValueError( | |
222 _("you can't use full page redirection with other rendering" | |
223 "method, check self.page_redirect if you need to use them")) | |
224 self.redirect = redirect | |
225 else: | |
226 self.redirect = None | |
227 self.parse_url = parse_url | |
228 self.add_breadcrumb = add_breadcrumb | |
229 self.prepare_render = prepare_render | |
230 self.template = template | |
231 self.render_method = render | |
232 self.on_data_post = on_data_post | |
233 self.on_data = on_data | |
234 self.url_cache = url_cache | |
235 if access == C.PAGES_ACCESS_NONE: | |
236 # none pages just return a 404, no further check is needed | |
237 return | |
238 if template is not None and render is not None: | |
239 log.error(_("render and template methods can't be used at the same time")) | |
240 | |
241 # if not None, next rendering will be cached | |
242 # it must then contain a list of the the keys to use (without the page instance) | |
243 # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] | |
244 self._do_cache = None | |
245 | |
246 def __str__(self): | |
247 return "LiberviaPage {name} at {url} (vhost: {vhost_root})".format( | |
248 name=self.name or "<anonymous>", url=self.url, vhost_root=self.vhost_root) | |
249 | |
250 @property | |
251 def named_pages(self): | |
252 return self.vhost_root.named_pages | |
253 | |
254 @property | |
255 def uri_callbacks(self): | |
256 return self.vhost_root.uri_callbacks | |
257 | |
258 @property | |
259 def pages_redirects(self): | |
260 return self.vhost_root.pages_redirects | |
261 | |
262 @property | |
263 def cached_urls(self): | |
264 return self.vhost_root.cached_urls | |
265 | |
266 @property | |
267 def main_menu(self): | |
268 return self.vhost_root.main_menu | |
269 | |
270 @property | |
271 def default_theme(self): | |
272 return self.vhost_root.default_theme | |
273 | |
274 | |
275 @property | |
276 def site_themes(self): | |
277 return self.vhost_root.site_themes | |
278 | |
279 @staticmethod | |
280 def create_page(host, meta_path, vhost_root, url_elts, replace_on_conflict=False): | |
281 """Create a LiberviaPage instance | |
282 | |
283 @param meta_path(Path): path to the page_meta.py file | |
284 @param vhost_root(resource.Resource): root resource of the virtual host | |
285 @param url_elts(list[unicode]): list of path element from root site to this page | |
286 @param replace_on_conflict(bool): same as for [LiberviaPage] | |
287 @return (tuple[dict, LiberviaPage]): tuple with: | |
288 - page_data: dict containing data of the page | |
289 - libervia_page: created resource | |
290 """ | |
291 dir_path = meta_path.parent | |
292 page_data = {"__name__": ".".join(["page"] + url_elts)} | |
293 # we don't want to force the presence of __init__.py | |
294 # so we use execfile instead of import. | |
295 # TODO: when moved to Python 3, __init__.py is not mandatory anymore | |
296 # so we can switch to import | |
297 exec(compile(open(meta_path, "rb").read(), meta_path, 'exec'), page_data) | |
298 return page_data, LiberviaPage( | |
299 host=host, | |
300 vhost_root=vhost_root, | |
301 root_dir=dir_path, | |
302 url="/" + "/".join(url_elts), | |
303 name=page_data.get("name"), | |
304 label=page_data.get("label"), | |
305 redirect=page_data.get("redirect"), | |
306 access=page_data.get("access"), | |
307 dynamic=page_data.get("dynamic", True), | |
308 parse_url=page_data.get("parse_url"), | |
309 add_breadcrumb=page_data.get("add_breadcrumb"), | |
310 prepare_render=page_data.get("prepare_render"), | |
311 render=page_data.get("render"), | |
312 template=page_data.get("template"), | |
313 on_data_post=page_data.get("on_data_post"), | |
314 on_data=page_data.get("on_data"), | |
315 url_cache=page_data.get("url_cache", False), | |
316 replace_on_conflict=replace_on_conflict | |
317 ) | |
318 | |
319 @staticmethod | |
320 def create_browser_data( | |
321 vhost_root, | |
322 resource: Optional[LiberviaPage], | |
323 browser_path: Path, | |
324 path_elts: Optional[List[str]], | |
325 engine: str = "brython" | |
326 ) -> None: | |
327 """create and store data for browser dynamic code""" | |
328 dyn_data = { | |
329 "path": browser_path, | |
330 "url_hash": ( | |
331 hashlib.sha256('/'.join(path_elts).encode()).hexdigest() | |
332 if path_elts is not None else None | |
333 ), | |
334 } | |
335 browser_meta_path = browser_path / C.PAGES_BROWSER_META_FILE | |
336 if browser_meta_path.is_file(): | |
337 with browser_meta_path.open() as f: | |
338 browser_meta = json.load(f) | |
339 utils.recursive_update(vhost_root.browser_modules, browser_meta) | |
340 if resource is not None: | |
341 utils.recursive_update(resource.dyn_data, browser_meta) | |
342 | |
343 init_path = browser_path / '__init__.py' | |
344 if init_path.is_file(): | |
345 vhost_root.browser_modules.setdefault( | |
346 engine, []).append(dyn_data) | |
347 if resource is not None: | |
348 resource.dyn_data[engine] = dyn_data | |
349 elif path_elts is None: | |
350 try: | |
351 next(browser_path.glob('*.py')) | |
352 except StopIteration: | |
353 # no python file, nothing for Brython | |
354 pass | |
355 else: | |
356 vhost_root.browser_modules.setdefault( | |
357 engine, []).append(dyn_data) | |
358 | |
359 | |
360 @classmethod | |
361 def import_pages(cls, host, vhost_root, root_path=None, _parent=None, _path=None, | |
362 _extra_pages=False): | |
363 """Recursively import Libervia pages | |
364 | |
365 @param host(Libervia): Libervia instance | |
366 @param vhost_root(LiberviaRootResource): root of this VirtualHost | |
367 @param root_path(Path, None): use this root path instead of vhost_root's one | |
368 Used to add default site pages to external sites | |
369 @param _parent(Resource, None): _parent page. Do not set yourself, this is for | |
370 internal use only | |
371 @param _path(list(unicode), None): current path. Do not set yourself, this is for | |
372 internal use only | |
373 @param _extra_pages(boolean): set to True when extra pages are used (i.e. | |
374 root_path is set). Do not set yourself, this is for internal use only | |
375 """ | |
376 if _path is None: | |
377 _path = [] | |
378 if _parent is None: | |
379 if root_path is None: | |
380 root_dir = vhost_root.site_path / C.PAGES_DIR | |
381 else: | |
382 root_dir = root_path / C.PAGES_DIR | |
383 _extra_pages = True | |
384 _parent = vhost_root | |
385 root_browser_path = root_dir / C.PAGES_BROWSER_DIR | |
386 if root_browser_path.is_dir(): | |
387 cls.create_browser_data(vhost_root, None, root_browser_path, None) | |
388 else: | |
389 root_dir = _parent.root_dir | |
390 | |
391 for d in os.listdir(root_dir): | |
392 dir_path = root_dir / d | |
393 if not dir_path.is_dir(): | |
394 continue | |
395 if _extra_pages and d in _parent.children: | |
396 log.debug(_("[{host_name}] {path} is already present, ignoring it") | |
397 .format(host_name=vhost_root.host_name, path='/'.join(_path+[d]))) | |
398 continue | |
399 meta_path = dir_path / C.PAGES_META_FILE | |
400 if meta_path.is_file(): | |
401 new_path = _path + [d] | |
402 try: | |
403 page_data, resource = cls.create_page( | |
404 host, meta_path, vhost_root, new_path) | |
405 except exceptions.ConflictError as e: | |
406 if _extra_pages: | |
407 # extra pages are discarded if there is already an existing page | |
408 continue | |
409 else: | |
410 raise e | |
411 _parent.putChild(str(d).encode(), resource) | |
412 log_msg = ("[{host_name}] Added /{path} page".format( | |
413 host_name=vhost_root.host_name, | |
414 path="[…]/".join(new_path))) | |
415 if _extra_pages: | |
416 log.debug(log_msg) | |
417 else: | |
418 log.info(log_msg) | |
419 if "uri_handlers" in page_data: | |
420 if not isinstance(page_data, dict): | |
421 log.error(_("uri_handlers must be a dict")) | |
422 else: | |
423 for uri_tuple, cb_name in page_data["uri_handlers"].items(): | |
424 if len(uri_tuple) != 2 or not isinstance(cb_name, str): | |
425 log.error(_("invalid uri_tuple")) | |
426 continue | |
427 if not _extra_pages: | |
428 log.info(_("setting {}/{} URIs handler") | |
429 .format(*uri_tuple)) | |
430 try: | |
431 cb = page_data[cb_name] | |
432 except KeyError: | |
433 log.error(_("missing {name} method to handle {1}/{2}") | |
434 .format(name=cb_name, *uri_tuple)) | |
435 continue | |
436 else: | |
437 resource.register_uri(uri_tuple, cb) | |
438 | |
439 LiberviaPage.import_pages( | |
440 host, vhost_root, _parent=resource, _path=new_path, | |
441 _extra_pages=_extra_pages) | |
442 # now we check if there is some code for browser | |
443 browser_path = dir_path / C.PAGES_BROWSER_DIR | |
444 if browser_path.is_dir(): | |
445 cls.create_browser_data(vhost_root, resource, browser_path, new_path) | |
446 | |
447 @classmethod | |
448 def on_file_change( | |
449 cls, | |
450 host, | |
451 file_path: FilePath, | |
452 flags: List[str], | |
453 site_root: LiberviaRootResource, | |
454 site_path: Path | |
455 ) -> None: | |
456 """Method triggered by file_watcher when something is changed in files | |
457 | |
458 This method is used in dev mode to reload pages when needed | |
459 @param file_path: path of the file which triggered the event | |
460 @param flags: human readable flags of the event (from | |
461 internet.inotify) | |
462 @param site_root: root of the site | |
463 @param site_path: absolute path of the site | |
464 """ | |
465 if flags == ['create']: | |
466 return | |
467 path = Path(file_path.path.decode()) | |
468 base_name = path.name | |
469 if base_name != "page_meta.py": | |
470 # we only handle libervia pages | |
471 return | |
472 | |
473 log.debug("{flags} event(s) received for {file_path}".format( | |
474 flags=", ".join(flags), file_path=file_path)) | |
475 | |
476 dir_path = path.parent | |
477 | |
478 if dir_path == site_path: | |
479 return | |
480 | |
481 if not site_path in dir_path.parents: | |
482 raise exceptions.InternalError("watched file should be in a subdirectory of site path") | |
483 | |
484 path_elts = list(dir_path.relative_to(site_path).parts) | |
485 | |
486 if path_elts[0] == C.PAGES_DIR: | |
487 # a page has been modified | |
488 del path_elts[0] | |
489 if not path_elts: | |
490 # we need at least one element to parse | |
491 return | |
492 # we retrieve page by starting from site root and finding each path element | |
493 parent = page = site_root | |
494 new_page = False | |
495 for idx, child_name in enumerate(path_elts): | |
496 child_name = child_name.encode() | |
497 try: | |
498 try: | |
499 page = page.original.children[child_name] | |
500 except AttributeError: | |
501 page = page.children[child_name] | |
502 except KeyError: | |
503 if idx != len(path_elts)-1: | |
504 # a page has been created in a subdir when one or more | |
505 # page_meta.py are missing on the way | |
506 log.warning(_("Can't create a page at {path}, missing parents") | |
507 .format(path=path)) | |
508 return | |
509 new_page = True | |
510 else: | |
511 if idx<len(path_elts)-1: | |
512 try: | |
513 parent = page.original | |
514 except AttributeError: | |
515 parent = page | |
516 | |
517 try: | |
518 # we (re)create a page with the new/modified code | |
519 __, resource = cls.create_page(host, path, site_root, path_elts, | |
520 replace_on_conflict=True) | |
521 if not new_page: | |
522 try: | |
523 resource.children = page.original.children | |
524 except AttributeError: | |
525 # FIXME: this .original handling madness is due to EncodingResourceWrapper | |
526 # EncodingResourceWrapper should probably be removed | |
527 resource.children = page.children | |
528 except Exception as e: | |
529 log.warning(_("Can't create page: {reason}").format(reason=e)) | |
530 else: | |
531 url_elt = path_elts[-1].encode() | |
532 if not new_page: | |
533 # the page was already existing, we remove it | |
534 del parent.children[url_elt] | |
535 # we can now add the new page | |
536 parent.putChild(url_elt, resource) | |
537 | |
538 # is there any browser data to create? | |
539 browser_path = resource.root_dir / C.PAGES_BROWSER_DIR | |
540 if browser_path.is_dir(): | |
541 cls.create_browser_data( | |
542 resource.vhost_root, | |
543 resource, | |
544 browser_path, | |
545 resource.url.split('/') | |
546 ) | |
547 | |
548 if new_page: | |
549 log.info(_("{page} created").format(page=resource)) | |
550 else: | |
551 log.info(_("{page} reloaded").format(page=resource)) | |
552 | |
553 def check_csrf(self, request): | |
554 session = self.host.get_session_data( | |
555 request, session_iface.IWebSession | |
556 ) | |
557 if session.profile is None: | |
558 # CSRF doesn't make sense when no user is logged | |
559 log.debug("disabling CSRF check because service profile is used") | |
560 return | |
561 csrf_token = session.csrf_token | |
562 given_csrf = request.getHeader("X-Csrf-Token") | |
563 if given_csrf is None: | |
564 try: | |
565 given_csrf = self.get_posted_data(request, "csrf_token") | |
566 except KeyError: | |
567 pass | |
568 if given_csrf is None or given_csrf != csrf_token: | |
569 log.warning( | |
570 _("invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( | |
571 url=request.uri, ip=request.getClientIP() | |
572 ) | |
573 ) | |
574 self.page_error(request, C.HTTP_FORBIDDEN) | |
575 | |
576 def expose_to_scripts( | |
577 self, | |
578 request: server.Request, | |
579 **kwargs: str | |
580 ) -> None: | |
581 """Make a local variable available to page script as a global variable | |
582 | |
583 No check is done for conflicting name, use this carefully | |
584 """ | |
585 template_data = request.template_data | |
586 scripts = template_data.setdefault("scripts", utils.OrderedSet()) | |
587 for name, value in kwargs.items(): | |
588 if value is None: | |
589 value = "null" | |
590 elif isinstance(value, str): | |
591 # FIXME: workaround for subtype used by python-dbus (dbus.String) | |
592 # to be removed when we get rid of python-dbus | |
593 value = repr(str(value)) | |
594 else: | |
595 value = repr(value) | |
596 scripts.add(Script(content=f"var {name}={value};")) | |
597 | |
598 def register_uri(self, uri_tuple, get_uri_cb): | |
599 """Register a URI handler | |
600 | |
601 @param uri_tuple(tuple[unicode, unicode]): type or URIs handler | |
602 type/subtype as returned by tools/common/parse_xmpp_uri | |
603 or type/None to handle all subtypes | |
604 @param get_uri_cb(callable): method which take uri_data dict as only argument | |
605 and return absolute path with correct arguments or None if the page | |
606 can't handle this URL | |
607 """ | |
608 if uri_tuple in self.uri_callbacks: | |
609 log.info(_("{}/{} URIs are already handled, replacing by the new handler") | |
610 .format( *uri_tuple)) | |
611 self.uri_callbacks[uri_tuple] = (self, get_uri_cb) | |
612 | |
613 def config_get(self, key, default=None, value_type=None): | |
614 return self.host.config_get(self.vhost_root, key=key, default=default, | |
615 value_type=value_type) | |
616 | |
617 def get_build_path(self, session_data): | |
618 return session_data.cache_dir + self.vhost.site_name | |
619 | |
620 def get_page_by_name(self, name): | |
621 return self.vhost_root.get_page_by_name(name) | |
622 | |
623 def get_page_path_from_uri(self, uri): | |
624 return self.vhost_root.get_page_path_from_uri(uri) | |
625 | |
626 def get_page_redirect_url(self, request, page_name="login", url=None): | |
627 """generate URL for a page with redirect_url parameter set | |
628 | |
629 mainly used for login page with redirection to current page | |
630 @param request(server.Request): current HTTP request | |
631 @param page_name(unicode): name of the page to go | |
632 @param url(None, unicode): url to redirect to | |
633 None to use request path (i.e. current page) | |
634 @return (unicode): URL to use | |
635 """ | |
636 return "{root_url}?redirect_url={redirect_url}".format( | |
637 root_url=self.get_page_by_name(page_name).url, | |
638 redirect_url=urllib.parse.quote_plus(request.uri) | |
639 if url is None | |
640 else url.encode("utf-8"), | |
641 ) | |
642 | |
643 def get_url(self, *args: str, **kwargs: str) -> str: | |
644 """retrieve URL of the page set arguments | |
645 | |
646 @param *args: arguments to add to the URL as path elements empty or None | |
647 arguments will be ignored | |
648 @param **kwargs: query parameters | |
649 """ | |
650 url_args = [quote(a) for a in args if a] | |
651 | |
652 if self.name is not None and self.name in self.pages_redirects: | |
653 # we check for redirection | |
654 redirect_data = self.pages_redirects[self.name] | |
655 args_hash = tuple(args) | |
656 for limit in range(len(args), -1, -1): | |
657 current_hash = args_hash[:limit] | |
658 if current_hash in redirect_data: | |
659 url_base = redirect_data[current_hash] | |
660 remaining = args[limit:] | |
661 remaining_url = "/".join(remaining) | |
662 url = urllib.parse.urljoin(url_base, remaining_url) | |
663 break | |
664 else: | |
665 url = os.path.join(self.url, *url_args) | |
666 else: | |
667 url = os.path.join(self.url, *url_args) | |
668 | |
669 if kwargs: | |
670 encoded = urllib.parse.urlencode( | |
671 {k: v for k, v in kwargs.items()} | |
672 ) | |
673 url += f"?{encoded}" | |
674 | |
675 return self.host.check_redirection( | |
676 self.vhost_root, | |
677 url | |
678 ) | |
679 | |
680 def get_current_url(self, request): | |
681 """retrieve URL used to access this page | |
682 | |
683 @return(unicode): current URL | |
684 """ | |
685 # we get url in the following way (splitting request.path instead of using | |
686 # request.prepath) because request.prepath may have been modified by | |
687 # redirection (if redirection args have been specified), while path reflect | |
688 # the real request | |
689 | |
690 # we ignore empty path elements (i.e. double '/' or '/' at the end) | |
691 path_elts = [p for p in request.path.decode('utf-8').split("/") if p] | |
692 | |
693 if request.postpath: | |
694 if not request.postpath[-1]: | |
695 # we remove trailing slash | |
696 request.postpath = request.postpath[:-1] | |
697 if request.postpath: | |
698 # get_sub_page_url must return subpage from the point where | |
699 # the it is called, so we have to remove remanining | |
700 # path elements | |
701 path_elts = path_elts[: -len(request.postpath)] | |
702 | |
703 return "/" + "/".join(path_elts) | |
704 | |
705 def get_param_url(self, request, **kwargs): | |
706 """use URL of current request but modify the parameters in query part | |
707 | |
708 **kwargs(dict[str, unicode]): argument to use as query parameters | |
709 @return (unicode): constructed URL | |
710 """ | |
711 current_url = self.get_current_url(request) | |
712 if kwargs: | |
713 encoded = urllib.parse.urlencode( | |
714 {k: v for k, v in kwargs.items()} | |
715 ) | |
716 current_url = current_url + "?" + encoded | |
717 return current_url | |
718 | |
719 def get_sub_page_by_name(self, subpage_name, parent=None): | |
720 """retrieve a subpage and its path using its name | |
721 | |
722 @param subpage_name(unicode): name of the sub page | |
723 it must be a direct children of parent page | |
724 @param parent(LiberviaPage, None): parent page | |
725 None to use current page | |
726 @return (tuple[str, LiberviaPage]): page subpath and instance | |
727 @raise exceptions.NotFound: no page has been found | |
728 """ | |
729 if parent is None: | |
730 parent = self | |
731 for path, child in parent.children.items(): | |
732 try: | |
733 child_name = child.name | |
734 except AttributeError: | |
735 # LiberviaPages have a name, but maybe this is an other Resource | |
736 continue | |
737 if child_name == subpage_name: | |
738 return path.decode('utf-8'), child | |
739 raise exceptions.NotFound( | |
740 _("requested sub page has not been found ({subpage_name})").format( | |
741 subpage_name=subpage_name)) | |
742 | |
743 def get_sub_page_url(self, request, page_name, *args): | |
744 """retrieve a page in direct children and build its URL according to request | |
745 | |
746 request's current path is used as base (at current parsing point, | |
747 i.e. it's more prepath than path). | |
748 Requested page is checked in children and an absolute URL is then built | |
749 by the resulting combination. | |
750 This method is useful to construct absolute URLs for children instead of | |
751 using relative path, which may not work in subpages, and are linked to the | |
752 names of directories (i.e. relative URL will break if subdirectory is renamed | |
753 while get_sub_page_url won't as long as page_name is consistent). | |
754 Also, request.path is used, keeping real path used by user, | |
755 and potential redirections. | |
756 @param request(server.Request): current HTTP request | |
757 @param page_name(unicode): name of the page to retrieve | |
758 it must be a direct children of current page | |
759 @param *args(list[unicode]): arguments to add as path elements | |
760 if an arg is None, it will be ignored | |
761 @return (unicode): absolute URL to the sub page | |
762 """ | |
763 current_url = self.get_current_url(request) | |
764 path, child = self.get_sub_page_by_name(page_name) | |
765 return os.path.join( | |
766 "/", current_url, path, *[quote(a) for a in args if a is not None] | |
767 ) | |
768 | |
769 def get_url_by_names(self, named_path): | |
770 """Retrieve URL from pages names and arguments | |
771 | |
772 @param named_path(list[tuple[unicode, list[unicode]]]): path to the page as a list | |
773 of tuples of 2 items: | |
774 - first item is page name | |
775 - second item is list of path arguments of this page | |
776 @return (unicode): URL to the requested page with given path arguments | |
777 @raise exceptions.NotFound: one of the page was not found | |
778 """ | |
779 current_page = None | |
780 path = [] | |
781 for page_name, page_args in named_path: | |
782 if current_page is None: | |
783 current_page = self.get_page_by_name(page_name) | |
784 path.append(current_page.get_url(*page_args)) | |
785 else: | |
786 sub_path, current_page = self.get_sub_page_by_name( | |
787 page_name, parent=current_page | |
788 ) | |
789 path.append(sub_path) | |
790 if page_args: | |
791 path.extend([quote(a) for a in page_args]) | |
792 return self.host.check_redirection(self.vhost_root, "/".join(path)) | |
793 | |
794 def get_url_by_path(self, *args): | |
795 """Generate URL by path | |
796 | |
797 this method as a similar effect as get_url_by_names, but it is more readable | |
798 by using SubPage to get pages instead of using tuples | |
799 @param *args: path element: | |
800 - if unicode, will be used as argument | |
801 - if util.SubPage instance, must be the name of a subpage | |
802 @return (unicode): generated path | |
803 """ | |
804 args = list(args) | |
805 if not args: | |
806 raise ValueError("You must specify path elements") | |
807 # root page is the one needed to construct the base of the URL | |
808 # if first arg is not a SubPage instance, we use current page | |
809 if not isinstance(args[0], SubPage): | |
810 root = self | |
811 else: | |
812 root = self.get_page_by_name(args.pop(0)) | |
813 # we keep track of current page to check subpage | |
814 current_page = root | |
815 url_elts = [] | |
816 arguments = [] | |
817 while True: | |
818 while args and not isinstance(args[0], SubPage): | |
819 arguments.append(quote(args.pop(0))) | |
820 if not url_elts: | |
821 url_elts.append(root.get_url(*arguments)) | |
822 else: | |
823 url_elts.extend(arguments) | |
824 if not args: | |
825 break | |
826 else: | |
827 path, current_page = current_page.get_sub_page_by_name(args.pop(0)) | |
828 arguments = [path] | |
829 return self.host.check_redirection(self.vhost_root, "/".join(url_elts)) | |
830 | |
831 def getChildWithDefault(self, path, request): | |
832 # we handle children ourselves | |
833 raise exceptions.InternalError( | |
834 "this method should not be used with LiberviaPage" | |
835 ) | |
836 | |
837 def next_path(self, request): | |
838 """get next URL path segment, and update request accordingly | |
839 | |
840 will move first segment of postpath in prepath | |
841 @param request(server.Request): current HTTP request | |
842 @return (unicode): unquoted segment | |
843 @raise IndexError: there is no segment left | |
844 """ | |
845 pathElement = request.postpath.pop(0) | |
846 request.prepath.append(pathElement) | |
847 return urllib.parse.unquote(pathElement.decode('utf-8')) | |
848 | |
849 def _filter_path_value(self, value, handler, name, request): | |
850 """Modify a path value according to handler (see [get_path_args])""" | |
851 if handler in ("@", "@jid") and value == "@": | |
852 value = None | |
853 | |
854 if handler in ("", "@"): | |
855 if value is None: | |
856 return "" | |
857 elif handler in ("jid", "@jid"): | |
858 if value: | |
859 try: | |
860 return jid.JID(value) | |
861 except (RuntimeError, jid.InvalidFormat): | |
862 log.warning(_("invalid jid argument: {value}").format(value=value)) | |
863 self.page_error(request, C.HTTP_BAD_REQUEST) | |
864 else: | |
865 return "" | |
866 else: | |
867 return handler(self, value, name, request) | |
868 | |
869 return value | |
870 | |
871 def get_path_args(self, request, names, min_args=0, **kwargs): | |
872 """get several path arguments at once | |
873 | |
874 Arguments will be put in request data. | |
875 Missing arguments will have None value | |
876 @param names(list[unicode]): list of arguments to get | |
877 @param min_args(int): if less than min_args are found, PageError is used with | |
878 C.HTTP_BAD_REQUEST | |
879 Use 0 to ignore | |
880 @param **kwargs: special value or optional callback to use for arguments | |
881 names of the arguments must correspond to those in names | |
882 special values may be: | |
883 - '': use empty string instead of None when no value is specified | |
884 - '@': if value of argument is empty or '@', empty string will be used | |
885 - 'jid': value must be converted to jid.JID if it exists, else empty | |
886 string is used | |
887 - '@jid': if value of arguments is empty or '@', empty string will be | |
888 used, else it will be converted to jid | |
889 """ | |
890 data = self.get_r_data(request) | |
891 | |
892 for idx, name in enumerate(names): | |
893 if name[0] == "*": | |
894 value = data[name[1:]] = [] | |
895 while True: | |
896 try: | |
897 value.append(self.next_path(request)) | |
898 except IndexError: | |
899 idx -= 1 | |
900 break | |
901 else: | |
902 idx += 1 | |
903 else: | |
904 try: | |
905 value = data[name] = self.next_path(request) | |
906 except IndexError: | |
907 data[name] = None | |
908 idx -= 1 | |
909 break | |
910 | |
911 values_count = idx + 1 | |
912 if values_count < min_args: | |
913 log.warning(_("Missing arguments in URL (got {count}, expected at least " | |
914 "{min_args})").format(count=values_count, min_args=min_args)) | |
915 self.page_error(request, C.HTTP_BAD_REQUEST) | |
916 | |
917 for name in names[values_count:]: | |
918 data[name] = None | |
919 | |
920 for name, handler in kwargs.items(): | |
921 if name[0] == "*": | |
922 data[name] = [ | |
923 self._filter_path_value(v, handler, name, request) for v in data[name] | |
924 ] | |
925 else: | |
926 data[name] = self._filter_path_value(data[name], handler, name, request) | |
927 | |
928 ## Pagination/Filtering ## | |
929 | |
930 def get_pubsub_extra(self, request, page_max=10, params=None, extra=None, | |
931 order_by=C.ORDER_BY_CREATION): | |
932 """Set extra dict to retrieve PubSub items corresponding to URL parameters | |
933 | |
934 Following parameters are used: | |
935 - after: set rsm_after with ID of item | |
936 - before: set rsm_before with ID of item | |
937 @param request(server.Request): current HTTP request | |
938 @param page_max(int): required number of items per page | |
939 @param params(None, dict[unicode, list[unicode]]): params as returned by | |
940 self.get_all_posted_data. | |
941 None to parse URL automatically | |
942 @param extra(None, dict): extra dict to use, or None to use a new one | |
943 @param order_by(unicode, None): key to order by | |
944 None to not specify order | |
945 @return (dict): fill extra data | |
946 """ | |
947 if params is None: | |
948 params = self.get_all_posted_data(request, multiple=False) | |
949 if extra is None: | |
950 extra = {} | |
951 else: | |
952 assert not {"rsm_max", "rsm_after", "rsm_before", | |
953 C.KEY_ORDER_BY}.intersection(list(extra.keys())) | |
954 extra["rsm_max"] = params.get("page_max", str(page_max)) | |
955 if order_by is not None: | |
956 extra[C.KEY_ORDER_BY] = order_by | |
957 if 'after' in params: | |
958 extra['rsm_after'] = params['after'] | |
959 elif 'before' in params: | |
960 extra['rsm_before'] = params['before'] | |
961 else: | |
962 # RSM returns list in order (oldest first), but we want most recent first | |
963 # so we start by the end | |
964 extra['rsm_before'] = "" | |
965 return extra | |
966 | |
967 def set_pagination(self, request: server.Request, pubsub_data: dict) -> None: | |
968 """Add to template_data if suitable | |
969 | |
970 "previous_page_url" and "next_page_url" will be added using respectively | |
971 "before" and "after" URL parameters | |
972 @param request: current HTTP request | |
973 @param pubsub_data: pubsub metadata | |
974 """ | |
975 template_data = request.template_data | |
976 extra = {} | |
977 try: | |
978 rsm = pubsub_data["rsm"] | |
979 last_id = rsm["last"] | |
980 except KeyError: | |
981 # no pagination available | |
982 return | |
983 | |
984 # if we have a search query, we must keep it | |
985 search = self.get_posted_data(request, 'search', raise_on_missing=False) | |
986 if search is not None: | |
987 extra['search'] = search.strip() | |
988 | |
989 # same for page_max | |
990 page_max = self.get_posted_data(request, 'page_max', raise_on_missing=False) | |
991 if page_max is not None: | |
992 extra['page_max'] = page_max | |
993 | |
994 if rsm.get("index", 1) > 0: | |
995 # We only show previous button if it's not the first page already. | |
996 # If we have no index, we default to display the button anyway | |
997 # as we can't know if we are on the first page or not. | |
998 first_id = rsm["first"] | |
999 template_data['previous_page_url'] = self.get_param_url( | |
1000 request, before=first_id, **extra) | |
1001 if not pubsub_data["complete"]: | |
1002 # we also show the page next button if complete is None because we | |
1003 # can't know where we are in the feed in this case. | |
1004 template_data['next_page_url'] = self.get_param_url( | |
1005 request, after=last_id, **extra) | |
1006 | |
1007 | |
1008 ## Cache handling ## | |
1009 | |
1010 def _set_cache_headers(self, request, cache): | |
1011 """Set ETag and Last-Modified HTTP headers, used for caching""" | |
1012 request.setHeader("ETag", cache.hash) | |
1013 last_modified = self.host.get_http_date(cache.created) | |
1014 request.setHeader("Last-Modified", last_modified) | |
1015 | |
1016 def _check_cache_headers(self, request, cache): | |
1017 """Check if a cache condition is set on the request | |
1018 | |
1019 if condition is valid, C.HTTP_NOT_MODIFIED is returned | |
1020 """ | |
1021 etag_match = request.getHeader("If-None-Match") | |
1022 if etag_match is not None: | |
1023 if cache.hash == etag_match: | |
1024 self.page_error(request, C.HTTP_NOT_MODIFIED, no_body=True) | |
1025 else: | |
1026 modified_match = request.getHeader("If-Modified-Since") | |
1027 if modified_match is not None: | |
1028 modified = date_utils.date_parse(modified_match) | |
1029 if modified >= int(cache.created): | |
1030 self.page_error(request, C.HTTP_NOT_MODIFIED, no_body=True) | |
1031 | |
1032 def check_cache_subscribe_cb(self, sub_id, service, node): | |
1033 self.cache_pubsub_sub.add((service, node, sub_id)) | |
1034 | |
1035 def check_cache_subscribe_eb(self, failure_, service, node): | |
1036 log.warning(_("Can't subscribe to node: {msg}").format(msg=failure_)) | |
1037 # FIXME: cache must be marked as unusable here | |
1038 | |
1039 def ps_node_watch_add_eb(self, failure_, service, node): | |
1040 log.warning(_("Can't add node watched: {msg}").format(msg=failure_)) | |
1041 | |
1042 def use_cache(self, request: server.Request) -> bool: | |
1043 """Indicate if the cache should be used | |
1044 | |
1045 test request header to see if it is requested to skip the cache | |
1046 @return: True if cache should be used | |
1047 """ | |
1048 return request.getHeader('cache-control') != 'no-cache' | |
1049 | |
1050 def check_cache(self, request, cache_type, **kwargs): | |
1051 """check if a page is in cache and return cached version if suitable | |
1052 | |
1053 this method may perform extra operation to handle cache (e.g. subscribing to a | |
1054 pubsub node) | |
1055 @param request(server.Request): current HTTP request | |
1056 @param cache_type(int): on of C.CACHE_* const. | |
1057 @param **kwargs: args according to cache_type: | |
1058 C.CACHE_PUBSUB: | |
1059 service: pubsub service | |
1060 node: pubsub node | |
1061 short: short name of feature (needed if node is empty to find namespace) | |
1062 | |
1063 """ | |
1064 if request.postpath: | |
1065 # we are not on the final page, no need to go further | |
1066 return | |
1067 | |
1068 if request.uri != request.path: | |
1069 # we don't cache page with query arguments as there can be a lot of variants | |
1070 # influencing page results (e.g. search terms) | |
1071 log.debug("ignoring cache due to query arguments") | |
1072 | |
1073 no_cache = not self.use_cache(request) | |
1074 | |
1075 profile = self.get_profile(request) or C.SERVICE_PROFILE | |
1076 | |
1077 if cache_type == C.CACHE_PUBSUB: | |
1078 service, node = kwargs["service"], kwargs["node"] | |
1079 if not node: | |
1080 try: | |
1081 short = kwargs["short"] | |
1082 node = self.host.ns_map[short] | |
1083 except KeyError: | |
1084 log.warning(_('Can\'t use cache for empty node without namespace ' | |
1085 'set, please ensure to set "short" and that it is ' | |
1086 'registered')) | |
1087 return | |
1088 if profile != C.SERVICE_PROFILE: | |
1089 # only service profile is cached for now | |
1090 return | |
1091 session_data = self.host.get_session_data(request, session_iface.IWebSession) | |
1092 locale = session_data.locale | |
1093 if locale == C.DEFAULT_LOCALE: | |
1094 # no need to duplicate cache here | |
1095 locale = None | |
1096 try: | |
1097 cache = (self.cache[profile][cache_type][service][node] | |
1098 [self.vhost_root][request.uri][locale][self]) | |
1099 except KeyError: | |
1100 # no cache yet, let's subscribe to the pubsub node | |
1101 d1 = self.host.bridge_call( | |
1102 "ps_subscribe", service.full(), node, "", profile | |
1103 ) | |
1104 d1.addCallback(self.check_cache_subscribe_cb, service, node) | |
1105 d1.addErrback(self.check_cache_subscribe_eb, service, node) | |
1106 d2 = self.host.bridge_call("ps_node_watch_add", service.full(), node, profile) | |
1107 d2.addErrback(self.ps_node_watch_add_eb, service, node) | |
1108 self._do_cache = [self, profile, cache_type, service, node, | |
1109 self.vhost_root, request.uri, locale] | |
1110 # we don't return the Deferreds as it is not needed to wait for | |
1111 # the subscription to continue with page rendering | |
1112 return | |
1113 else: | |
1114 if no_cache: | |
1115 del (self.cache[profile][cache_type][service][node] | |
1116 [self.vhost_root][request.uri][locale][self]) | |
1117 log.debug(f"cache removed for {self}") | |
1118 return | |
1119 | |
1120 else: | |
1121 raise exceptions.InternalError("Unknown cache_type") | |
1122 log.debug("using cache for {page}".format(page=self)) | |
1123 cache.last_access = time.time() | |
1124 self._set_cache_headers(request, cache) | |
1125 self._check_cache_headers(request, cache) | |
1126 request.write(cache.rendered) | |
1127 request.finish() | |
1128 raise failure.Failure(exceptions.CancelError("cache is used")) | |
1129 | |
1130 def _cache_url(self, request, profile): | |
1131 self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) | |
1132 | |
1133 @classmethod | |
1134 def on_node_event(cls, host, service, node, event_type, items, profile): | |
1135 """Invalidate cache for all pages linked to this node""" | |
1136 try: | |
1137 cache = cls.cache[profile][C.CACHE_PUBSUB][jid.JID(service)][node] | |
1138 except KeyError: | |
1139 log.info(_( | |
1140 "Removing subscription for {service}/{node}: " | |
1141 "the page is not cached").format(service=service, node=node)) | |
1142 d1 = host.bridge_call("ps_unsubscribe", service, node, profile) | |
1143 d1.addErrback( | |
1144 lambda failure_: log.warning( | |
1145 _("Can't unsubscribe from {service}/{node}: {msg}").format( | |
1146 service=service, node=node, msg=failure_))) | |
1147 d2 = host.bridge_call("ps_node_watch_add", service, node, profile) | |
1148 # TODO: check why the page is not in cache, remove subscription? | |
1149 d2.addErrback( | |
1150 lambda failure_: log.warning( | |
1151 _("Can't remove watch for {service}/{node}: {msg}").format( | |
1152 service=service, node=node, msg=failure_))) | |
1153 else: | |
1154 cache.clear() | |
1155 | |
1156 # identities | |
1157 | |
1158 async def fill_missing_identities( | |
1159 self, | |
1160 request: server.Request, | |
1161 entities: List[Union[str, jid.JID, None]], | |
1162 ) -> None: | |
1163 """Check if all entities have an identity cache, get missing ones from backend | |
1164 | |
1165 @param request: request with a plugged profile | |
1166 @param entities: entities to check, None or empty strings will be filtered | |
1167 """ | |
1168 entities = {str(e) for e in entities if e} | |
1169 profile = self.get_profile(request) or C.SERVICE_PROFILE | |
1170 identities = self.host.get_session_data( | |
1171 request, | |
1172 session_iface.IWebSession | |
1173 ).identities | |
1174 for e in entities: | |
1175 if e not in identities: | |
1176 id_raw = await self.host.bridge_call( | |
1177 'identity_get', e, [], True, profile) | |
1178 identities[e] = data_format.deserialise(id_raw) | |
1179 | |
1180 # signals, server => browser communication | |
1181 | |
1182 def delegate_to_resource(self, request, resource): | |
1183 """continue workflow with Twisted Resource""" | |
1184 buf = resource.render(request) | |
1185 if buf == server.NOT_DONE_YET: | |
1186 pass | |
1187 else: | |
1188 request.write(buf) | |
1189 request.finish() | |
1190 raise failure.Failure(exceptions.CancelError("resource delegation")) | |
1191 | |
1192 def http_redirect(self, request, url): | |
1193 """redirect to an URL using HTTP redirection | |
1194 | |
1195 @param request(server.Request): current HTTP request | |
1196 @param url(unicode): url to redirect to | |
1197 """ | |
1198 web_util.redirectTo(url.encode("utf-8"), request) | |
1199 request.finish() | |
1200 raise failure.Failure(exceptions.CancelError("HTTP redirection is used")) | |
1201 | |
1202 def redirect_or_continue(self, request, redirect_arg="redirect_url"): | |
1203 """Helper method to redirect a page to an url given as arg | |
1204 | |
1205 if the arg is not present, the page will continue normal workflow | |
1206 @param request(server.Request): current HTTP request | |
1207 @param redirect_arg(unicode): argument to use to get redirection URL | |
1208 @interrupt: redirect the page to requested URL | |
1209 @interrupt page_error(C.HTTP_BAD_REQUEST): empty or non local URL is used | |
1210 """ | |
1211 redirect_arg = redirect_arg.encode('utf-8') | |
1212 try: | |
1213 url = request.args[redirect_arg][0].decode('utf-8') | |
1214 except (KeyError, IndexError): | |
1215 pass | |
1216 else: | |
1217 # a redirection is requested | |
1218 if not url or url[0] != "/": | |
1219 # we only want local urls | |
1220 self.page_error(request, C.HTTP_BAD_REQUEST) | |
1221 else: | |
1222 self.http_redirect(request, url) | |
1223 | |
1224 def page_redirect(self, page_path, request, skip_parse_url=True, path_args=None): | |
1225 """redirect a page to a named page | |
1226 | |
1227 the workflow will continue with the workflow of the named page, | |
1228 skipping named page's parse_url method if it exist. | |
1229 If you want to do a HTTP redirection, use http_redirect | |
1230 @param page_path(unicode): path to page (elements are separated by "/"): | |
1231 if path starts with a "/": | |
1232 path is a full path starting from root | |
1233 else: | |
1234 - first element is name as registered in name variable | |
1235 - following element are subpages path | |
1236 e.g.: "blog" redirect to page named "blog" | |
1237 "blog/atom.xml" redirect to atom.xml subpage of "blog" | |
1238 "/common/blog/atom.xml" redirect to the page at the given full path | |
1239 @param request(server.Request): current HTTP request | |
1240 @param skip_parse_url(bool): if True, parse_url method on redirect page will be | |
1241 skipped | |
1242 @param path_args(list[unicode], None): path arguments to use in redirected page | |
1243 @raise KeyError: there is no known page with this name | |
1244 """ | |
1245 # FIXME: render non LiberviaPage resources | |
1246 path = page_path.rstrip("/").split("/") | |
1247 if not path[0]: | |
1248 redirect_page = self.vhost_root | |
1249 else: | |
1250 redirect_page = self.named_pages[path[0]] | |
1251 | |
1252 for subpage in path[1:]: | |
1253 subpage = subpage.encode('utf-8') | |
1254 if redirect_page is self.vhost_root: | |
1255 redirect_page = redirect_page.children[subpage] | |
1256 else: | |
1257 redirect_page = redirect_page.original.children[subpage] | |
1258 | |
1259 if path_args is not None: | |
1260 args = [quote(a).encode() for a in path_args] | |
1261 request.postpath = args + request.postpath | |
1262 | |
1263 if self._do_cache: | |
1264 # if cache is needed, it will be handled by final page | |
1265 redirect_page._do_cache = self._do_cache | |
1266 self._do_cache = None | |
1267 | |
1268 defer.ensureDeferred( | |
1269 redirect_page.render_page(request, skip_parse_url=skip_parse_url) | |
1270 ) | |
1271 raise failure.Failure(exceptions.CancelError("page redirection is used")) | |
1272 | |
1273 def page_error(self, request, code=C.HTTP_NOT_FOUND, no_body=False): | |
1274 """generate an error page and terminate the request | |
1275 | |
1276 @param request(server.Request): HTTP request | |
1277 @param core(int): error code to use | |
1278 @param no_body: don't write body if True | |
1279 """ | |
1280 if self._do_cache is not None: | |
1281 # we don't want to cache error pages | |
1282 self._do_cache = None | |
1283 request.setResponseCode(code) | |
1284 if no_body: | |
1285 request.finish() | |
1286 else: | |
1287 template = "error/" + str(code) + ".html" | |
1288 template_data = request.template_data | |
1289 session_data = self.host.get_session_data(request, session_iface.IWebSession) | |
1290 if session_data.locale is not None: | |
1291 template_data['locale'] = session_data.locale | |
1292 if self.vhost_root.site_name: | |
1293 template_data['site'] = self.vhost_root.site_name | |
1294 | |
1295 rendered = self.host.renderer.render( | |
1296 template, | |
1297 theme=session_data.theme or self.default_theme, | |
1298 media_path=f"/{C.MEDIA_DIR}", | |
1299 build_path=f"/{C.BUILD_DIR}/", | |
1300 site_themes=self.site_themes, | |
1301 error_code=code, | |
1302 **template_data | |
1303 ) | |
1304 | |
1305 self.write_data(rendered, request) | |
1306 raise failure.Failure(exceptions.CancelError("error page is used")) | |
1307 | |
1308 def write_data(self, data, request): | |
1309 """write data to transport and finish the request""" | |
1310 if data is None: | |
1311 self.page_error(request) | |
1312 data_encoded = data.encode("utf-8") | |
1313 | |
1314 if self._do_cache is not None: | |
1315 redirected_page = self._do_cache.pop(0) | |
1316 cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache) | |
1317 page_cache = cache[redirected_page] = CachePage(data_encoded) | |
1318 self._set_cache_headers(request, page_cache) | |
1319 log.debug(_("{page} put in cache for [{profile}]") | |
1320 .format( page=self, profile=self._do_cache[0])) | |
1321 self._do_cache = None | |
1322 self._check_cache_headers(request, page_cache) | |
1323 | |
1324 try: | |
1325 request.write(data_encoded) | |
1326 except AttributeError: | |
1327 log.warning(_("Can't write page, the request has probably been cancelled " | |
1328 "(browser tab closed or reloaded)")) | |
1329 return | |
1330 request.finish() | |
1331 | |
1332 def _subpages_handler(self, request): | |
1333 """render subpage if suitable | |
1334 | |
1335 this method checks if there is still an unmanaged part of the path | |
1336 and check if it corresponds to a subpage. If so, it render the subpage | |
1337 else it render a NoResource. | |
1338 If there is no unmanaged part of the segment, current page workflow is pursued | |
1339 """ | |
1340 if request.postpath: | |
1341 subpage = self.next_path(request).encode('utf-8') | |
1342 try: | |
1343 child = self.children[subpage] | |
1344 except KeyError: | |
1345 self.page_error(request) | |
1346 else: | |
1347 child.render(request) | |
1348 raise failure.Failure(exceptions.CancelError("subpage page is used")) | |
1349 | |
1350 def _prepare_dynamic(self, request): | |
1351 session_data = self.host.get_session_data(request, session_iface.IWebSession) | |
1352 # we need to activate dynamic page | |
1353 # we set data for template, and create/register token | |
1354 # socket_token = str(uuid.uuid4()) | |
1355 socket_url = self.host.get_websocket_url(request) | |
1356 # as for CSRF, it is important to not let the socket token if we use the service | |
1357 # profile, as those pages can be cached, and then the token leaked. | |
1358 socket_token = '' if session_data.profile is None else session_data.ws_token | |
1359 socket_debug = C.bool_const(self.host.debug) | |
1360 request.template_data["websocket"] = WebsocketMeta( | |
1361 socket_url, socket_token, socket_debug | |
1362 ) | |
1363 # we will keep track of handlers to remove | |
1364 request._signals_registered = [] | |
1365 # we will cache registered signals until socket is opened | |
1366 request._signals_cache = [] | |
1367 | |
1368 def _render_template(self, request): | |
1369 template_data = request.template_data | |
1370 | |
1371 # if confirm variable is set in case of successfuly data post | |
1372 session_data = self.host.get_session_data(request, session_iface.IWebSession) | |
1373 template_data['identities'] = session_data.identities | |
1374 if session_data.pop_page_flag(self, C.FLAG_CONFIRM): | |
1375 template_data["confirm"] = True | |
1376 notifs = session_data.pop_page_notifications(self) | |
1377 if notifs: | |
1378 template_data["notifications"] = notifs | |
1379 if session_data.jid is not None: | |
1380 template_data["own_jid"] = session_data.jid | |
1381 if session_data.locale is not None: | |
1382 template_data['locale'] = session_data.locale | |
1383 if session_data.guest: | |
1384 template_data['guest_session'] = True | |
1385 if self.vhost_root.site_name: | |
1386 template_data['site'] = self.vhost_root.site_name | |
1387 if self.dyn_data: | |
1388 for data in self.dyn_data.values(): | |
1389 try: | |
1390 scripts = data['scripts'] | |
1391 except KeyError: | |
1392 pass | |
1393 else: | |
1394 template_data.setdefault('scripts', utils.OrderedSet()).update(scripts) | |
1395 template_data.update(data.get('template', {})) | |
1396 data_common = self.vhost_root.dyn_data_common | |
1397 common_scripts = data_common['scripts'] | |
1398 if common_scripts: | |
1399 template_data.setdefault('scripts', utils.OrderedSet()).update(common_scripts) | |
1400 if "template" in data_common: | |
1401 for key, value in data_common["template"].items(): | |
1402 if key not in template_data: | |
1403 template_data[key] = value | |
1404 | |
1405 theme = session_data.theme or self.default_theme | |
1406 self.expose_to_scripts( | |
1407 request, | |
1408 cache_path=session_data.cache_dir, | |
1409 templates_root_url=str(self.vhost_root.get_front_url(theme)), | |
1410 profile=session_data.profile) | |
1411 | |
1412 uri = request.uri.decode() | |
1413 try: | |
1414 template_data["current_page"] = next( | |
1415 m[0] for m in self.main_menu if uri.startswith(m[1]) | |
1416 ) | |
1417 except StopIteration: | |
1418 pass | |
1419 | |
1420 return self.host.renderer.render( | |
1421 self.template, | |
1422 theme=theme, | |
1423 site_themes=self.site_themes, | |
1424 page_url=self.get_url(), | |
1425 media_path=f"/{C.MEDIA_DIR}", | |
1426 build_path=f"/{C.BUILD_DIR}/", | |
1427 cache_path=session_data.cache_dir, | |
1428 main_menu=self.main_menu, | |
1429 **template_data) | |
1430 | |
1431 def _on_data_post_redirect(self, ret, request): | |
1432 """called when page's on_data_post has been done successfuly | |
1433 | |
1434 This will do a Post/Redirect/Get pattern. | |
1435 this method redirect to the same page or to request.data['post_redirect_page'] | |
1436 post_redirect_page can be either a page or a tuple with page as first item, then | |
1437 a list of unicode arguments to append to the url. | |
1438 if post_redirect_page is not used, initial request.uri (i.e. the same page as | |
1439 where the data have been posted) will be used for redirection. | |
1440 HTTP status code "See Other" (303) is used as it is the recommanded code in | |
1441 this case. | |
1442 @param ret(None, unicode, iterable): on_data_post return value | |
1443 see LiberviaPage.__init__ on_data_post docstring | |
1444 """ | |
1445 if ret is None: | |
1446 ret = () | |
1447 elif isinstance(ret, str): | |
1448 ret = (ret,) | |
1449 else: | |
1450 ret = tuple(ret) | |
1451 raise NotImplementedError( | |
1452 _("iterable in on_data_post return value is not used yet") | |
1453 ) | |
1454 session_data = self.host.get_session_data(request, session_iface.IWebSession) | |
1455 request_data = self.get_r_data(request) | |
1456 if "post_redirect_page" in request_data: | |
1457 redirect_page_data = request_data["post_redirect_page"] | |
1458 if isinstance(redirect_page_data, tuple): | |
1459 redirect_page = redirect_page_data[0] | |
1460 redirect_page_args = redirect_page_data[1:] | |
1461 redirect_uri = redirect_page.get_url(*redirect_page_args) | |
1462 else: | |
1463 redirect_page = redirect_page_data | |
1464 redirect_uri = redirect_page.url | |
1465 else: | |
1466 redirect_page = self | |
1467 redirect_uri = request.uri | |
1468 | |
1469 if not C.POST_NO_CONFIRM in ret: | |
1470 session_data.set_page_flag(redirect_page, C.FLAG_CONFIRM) | |
1471 request.setResponseCode(C.HTTP_SEE_OTHER) | |
1472 request.setHeader(b"location", redirect_uri) | |
1473 request.finish() | |
1474 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) | |
1475 | |
1476 async def _on_data_post(self, request): | |
1477 self.check_csrf(request) | |
1478 try: | |
1479 ret = await as_deferred(self.on_data_post, self, request) | |
1480 except exceptions.DataError as e: | |
1481 # something is wrong with the posted data, we re-display the page with a | |
1482 # warning notification | |
1483 session_data = self.host.get_session_data(request, session_iface.IWebSession) | |
1484 session_data.set_page_notification(self, str(e), C.LVL_WARNING) | |
1485 request.setResponseCode(C.HTTP_SEE_OTHER) | |
1486 request.setHeader("location", request.uri) | |
1487 request.finish() | |
1488 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) | |
1489 else: | |
1490 if ret != "continue": | |
1491 self._on_data_post_redirect(ret, request) | |
1492 | |
1493 def get_posted_data( | |
1494 self, | |
1495 request: server.Request, | |
1496 keys, | |
1497 multiple: bool = False, | |
1498 raise_on_missing: bool = True, | |
1499 strip: bool = True | |
1500 ): | |
1501 """Get data from a POST request or from URL's query part and decode it | |
1502 | |
1503 @param request: request linked to the session | |
1504 @param keys(unicode, iterable[unicode]): name of the value(s) to get | |
1505 unicode to get one value | |
1506 iterable to get more than one | |
1507 @param multiple: True if multiple values are possible/expected | |
1508 if False, the first value is returned | |
1509 @param raise_on_missing: raise KeyError on missing key if True | |
1510 else use None for missing values | |
1511 @param strip: if True, apply "strip()" on values | |
1512 @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): | |
1513 values received for this(these) key(s) | |
1514 @raise KeyError: one specific key has been requested, and it is missing | |
1515 """ | |
1516 # FIXME: request.args is already unquoting the value, it seems we are doing | |
1517 # double unquote | |
1518 if isinstance(keys, str): | |
1519 keys = [keys] | |
1520 | |
1521 keys = [k.encode('utf-8') for k in keys] | |
1522 | |
1523 ret = [] | |
1524 for key in keys: | |
1525 gen = (urllib.parse.unquote(v.decode("utf-8")) | |
1526 for v in request.args.get(key, [])) | |
1527 if multiple: | |
1528 ret.append(gen.strip() if strip else gen) | |
1529 else: | |
1530 try: | |
1531 v = next(gen) | |
1532 except StopIteration: | |
1533 if raise_on_missing: | |
1534 raise KeyError(key) | |
1535 else: | |
1536 ret.append(None) | |
1537 else: | |
1538 ret.append(v.strip() if strip else v) | |
1539 | |
1540 if len(keys) == 1: | |
1541 return ret[0] | |
1542 else: | |
1543 return ret | |
1544 | |
1545 def get_all_posted_data(self, request, except_=(), multiple=True): | |
1546 """get all posted data | |
1547 | |
1548 @param request(server.Request): request linked to the session | |
1549 @param except_(iterable[unicode]): key of values to ignore | |
1550 csrf_token will always be ignored | |
1551 @param multiple(bool): if False, only the first values are returned | |
1552 @return (dict[unicode, list[unicode]]): post values | |
1553 """ | |
1554 except_ = tuple(except_) + ("csrf_token",) | |
1555 ret = {} | |
1556 for key, values in request.args.items(): | |
1557 key = key.decode('utf-8') | |
1558 key = urllib.parse.unquote(key) | |
1559 if key in except_: | |
1560 continue | |
1561 values = [v.decode('utf-8') for v in values] | |
1562 if not multiple: | |
1563 ret[key] = urllib.parse.unquote(values[0]) | |
1564 else: | |
1565 ret[key] = [urllib.parse.unquote(v) for v in values] | |
1566 return ret | |
1567 | |
1568 def get_profile(self, request): | |
1569 """Helper method to easily get current profile | |
1570 | |
1571 @return (unicode, None): current profile | |
1572 None if no profile session is started | |
1573 """ | |
1574 web_session = self.host.get_session_data(request, session_iface.IWebSession) | |
1575 return web_session.profile | |
1576 | |
1577 def get_jid(self, request): | |
1578 """Helper method to easily get current jid | |
1579 | |
1580 @return: current jid | |
1581 """ | |
1582 web_session = self.host.get_session_data(request, session_iface.IWebSession) | |
1583 return web_session.jid | |
1584 | |
1585 | |
1586 def get_r_data(self, request): | |
1587 """Helper method to get request data dict | |
1588 | |
1589 this dictionnary if for the request only, it is not saved in session | |
1590 It is mainly used to pass data between pages/methods called during request | |
1591 workflow | |
1592 @return (dict): request data | |
1593 """ | |
1594 try: | |
1595 return request.data | |
1596 except AttributeError: | |
1597 request.data = {} | |
1598 return request.data | |
1599 | |
1600 def get_page_data(self, request, key): | |
1601 """Helper method to retrieve reload resistant data""" | |
1602 web_session = self.host.get_session_data(request, session_iface.IWebSession) | |
1603 return web_session.get_page_data(self, key) | |
1604 | |
1605 def set_page_data(self, request, key, value): | |
1606 """Helper method to set reload resistant data""" | |
1607 web_session = self.host.get_session_data(request, session_iface.IWebSession) | |
1608 return web_session.set_page_data(self, key, value) | |
1609 | |
1610 def handle_search(self, request, extra): | |
1611 """Manage Full-Text Search | |
1612 | |
1613 Check if "search" query argument is present, and add MAM filter for it if | |
1614 necessary. | |
1615 If used, the "search" variable will also be available in template data, thus | |
1616 frontend can display some information about it. | |
1617 """ | |
1618 search = self.get_posted_data(request, 'search', raise_on_missing=False) | |
1619 if search is not None: | |
1620 search = search.strip() | |
1621 if search: | |
1622 try: | |
1623 extra[f'mam_filter_{self.host.ns_map["fulltextmam"]}'] = search | |
1624 except KeyError: | |
1625 log.warning(_("Full-text search is not available")) | |
1626 else: | |
1627 request.template_data['search'] = search | |
1628 | |
1629 def _check_access(self, request): | |
1630 """Check access according to self.access | |
1631 | |
1632 if access is not granted, show a HTTP_FORBIDDEN page_error and stop request, | |
1633 else return data (so it can be inserted in deferred chain | |
1634 """ | |
1635 if self.access == C.PAGES_ACCESS_PUBLIC: | |
1636 pass | |
1637 elif self.access == C.PAGES_ACCESS_PROFILE: | |
1638 profile = self.get_profile(request) | |
1639 if not profile: | |
1640 # registration allowed, we redirect to login page | |
1641 login_url = self.get_page_redirect_url(request) | |
1642 self.http_redirect(request, login_url) | |
1643 | |
1644 def set_best_locale(self, request): | |
1645 """Guess the best locale when it is not specified explicitly by user | |
1646 | |
1647 This method will check "accept-language" header, and set locale to first | |
1648 matching value with available translations. | |
1649 """ | |
1650 accept_language = request.getHeader("accept-language") | |
1651 if not accept_language: | |
1652 return | |
1653 accepted = [a.strip() for a in accept_language.split(',')] | |
1654 available = [str(l) for l in self.host.renderer.translations] | |
1655 for lang in accepted: | |
1656 lang = lang.split(';')[0].strip().lower() | |
1657 if not lang: | |
1658 continue | |
1659 for a in available: | |
1660 if a.lower().startswith(lang): | |
1661 session_data = self.host.get_session_data(request, | |
1662 session_iface.IWebSession) | |
1663 session_data.locale = a | |
1664 return | |
1665 | |
1666 async def render_page(self, request, skip_parse_url=False): | |
1667 """Main method to handle the workflow of a LiberviaPage""" | |
1668 # template_data are the variables passed to template | |
1669 if not hasattr(request, "template_data"): | |
1670 # if template_data doesn't exist, it's the beginning of the request workflow | |
1671 # so we fill essential data | |
1672 session_data = self.host.get_session_data(request, session_iface.IWebSession) | |
1673 profile = session_data.profile | |
1674 request.template_data = { | |
1675 "profile": profile, | |
1676 # it's important to not add CSRF token and session uuid if service profile | |
1677 # is used because the page may be cached, and the token then leaked | |
1678 "csrf_token": "" if profile is None else session_data.csrf_token, | |
1679 "session_uuid": "public" if profile is None else session_data.uuid, | |
1680 "breadcrumbs": [] | |
1681 } | |
1682 | |
1683 # XXX: here is the code which need to be executed once | |
1684 # at the beginning of the request hanling | |
1685 if request.postpath and not request.postpath[-1]: | |
1686 # we don't differenciate URLs finishing with '/' or not | |
1687 del request.postpath[-1] | |
1688 | |
1689 # i18n | |
1690 key_lang = C.KEY_LANG.encode() | |
1691 if key_lang in request.args: | |
1692 try: | |
1693 locale = request.args.pop(key_lang)[0].decode() | |
1694 except IndexError: | |
1695 log.warning("empty lang received") | |
1696 else: | |
1697 if "/" in locale: | |
1698 # "/" is refused because locale may sometime be used to access | |
1699 # path, if localised documents are available for instance | |
1700 log.warning(_('illegal char found in locale ("/"), hack ' | |
1701 'attempt? locale={locale}').format(locale=locale)) | |
1702 locale = None | |
1703 session_data.locale = locale | |
1704 | |
1705 # if locale is not specified, we try to find one requested by browser | |
1706 if session_data.locale is None: | |
1707 self.set_best_locale(request) | |
1708 | |
1709 # theme | |
1710 key_theme = C.KEY_THEME.encode() | |
1711 if key_theme in request.args: | |
1712 theme = request.args.pop(key_theme)[0].decode() | |
1713 if key_theme != session_data.theme: | |
1714 if theme not in self.site_themes: | |
1715 log.warning(_( | |
1716 "Theme {theme!r} doesn't exist for {vhost}" | |
1717 .format(theme=theme, vhost=self.vhost_root))) | |
1718 else: | |
1719 session_data.theme = theme | |
1720 try: | |
1721 | |
1722 try: | |
1723 self._check_access(request) | |
1724 | |
1725 if self.redirect is not None: | |
1726 self.page_redirect(self.redirect, request, skip_parse_url=False) | |
1727 | |
1728 if self.parse_url is not None and not skip_parse_url: | |
1729 if self.url_cache: | |
1730 profile = self.get_profile(request) | |
1731 try: | |
1732 cache_url = self.cached_urls[profile][request.uri] | |
1733 except KeyError: | |
1734 # no cache for this URI yet | |
1735 # we do normal URL parsing, and then the cache | |
1736 await as_deferred(self.parse_url, self, request) | |
1737 self._cache_url(request, profile) | |
1738 else: | |
1739 log.debug(f"using URI cache for {self}") | |
1740 cache_url.use(request) | |
1741 else: | |
1742 await as_deferred(self.parse_url, self, request) | |
1743 | |
1744 if self.add_breadcrumb is None: | |
1745 label = ( | |
1746 self.label | |
1747 or self.name | |
1748 or self.url[self.url.rfind('/')+1:] | |
1749 ) | |
1750 breadcrumb = { | |
1751 "url": self.url, | |
1752 "label": label.title(), | |
1753 } | |
1754 request.template_data["breadcrumbs"].append(breadcrumb) | |
1755 else: | |
1756 await as_deferred( | |
1757 self.add_breadcrumb, | |
1758 self, | |
1759 request, | |
1760 request.template_data["breadcrumbs"] | |
1761 ) | |
1762 | |
1763 self._subpages_handler(request) | |
1764 | |
1765 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): | |
1766 # only HTTP GET and POST are handled so far | |
1767 self.page_error(request, C.HTTP_BAD_REQUEST) | |
1768 | |
1769 if request.method == C.HTTP_METHOD_POST: | |
1770 if self.on_data_post == 'continue': | |
1771 pass | |
1772 elif self.on_data_post is None: | |
1773 # if we don't have on_data_post, the page was not expecting POST | |
1774 # so we return an error | |
1775 self.page_error(request, C.HTTP_BAD_REQUEST) | |
1776 else: | |
1777 await self._on_data_post(request) | |
1778 # by default, POST follow normal behaviour after on_data_post is called | |
1779 # this can be changed by a redirection or other method call in on_data_post | |
1780 | |
1781 if self.dynamic: | |
1782 self._prepare_dynamic(request) | |
1783 | |
1784 if self.prepare_render: | |
1785 await as_deferred(self.prepare_render, self, request) | |
1786 | |
1787 if self.template: | |
1788 rendered = self._render_template(request) | |
1789 elif self.render_method: | |
1790 rendered = await as_deferred(self.render_method, self, request) | |
1791 else: | |
1792 raise exceptions.InternalError( | |
1793 "No method set to render page, please set a template or use a " | |
1794 "render method" | |
1795 ) | |
1796 | |
1797 self.write_data(rendered, request) | |
1798 | |
1799 except failure.Failure as f: | |
1800 # we have to unpack the Failure to catch the right Exception | |
1801 raise f.value | |
1802 | |
1803 except exceptions.CancelError: | |
1804 pass | |
1805 except BridgeException as e: | |
1806 if e.condition == 'not-allowed': | |
1807 log.warning("not allowed exception catched") | |
1808 self.page_error(request, C.HTTP_FORBIDDEN) | |
1809 elif e.condition == 'item-not-found' or e.classname == 'NotFound': | |
1810 self.page_error(request, C.HTTP_NOT_FOUND) | |
1811 elif e.condition == 'remote-server-not-found': | |
1812 self.page_error(request, C.HTTP_NOT_FOUND) | |
1813 elif e.condition == 'forbidden': | |
1814 if self.get_profile(request) is None: | |
1815 log.debug("access forbidden, we're redirecting to log-in page") | |
1816 self.http_redirect(request, self.get_page_redirect_url(request)) | |
1817 else: | |
1818 self.page_error(request, C.HTTP_FORBIDDEN) | |
1819 else: | |
1820 log.error( | |
1821 _("Uncatched bridge exception for HTTP request on {url}: {e}\n" | |
1822 "page name: {name}\npath: {path}\nURL: {full_url}\n{tb}") | |
1823 .format( | |
1824 url=self.url, | |
1825 e=e, | |
1826 name=self.name or "", | |
1827 path=self.root_dir, | |
1828 full_url=request.URLPath(), | |
1829 tb=traceback.format_exc(), | |
1830 ) | |
1831 ) | |
1832 try: | |
1833 self.page_error(request, C.HTTP_INTERNAL_ERROR) | |
1834 except exceptions.CancelError: | |
1835 pass | |
1836 except Exception as e: | |
1837 log.error( | |
1838 _("Uncatched error for HTTP request on {url}: {e}\npage name: " | |
1839 "{name}\npath: {path}\nURL: {full_url}\n{tb}") | |
1840 .format( | |
1841 url=self.url, | |
1842 e=e, | |
1843 name=self.name or "", | |
1844 path=self.root_dir, | |
1845 full_url=request.URLPath(), | |
1846 tb=traceback.format_exc(), | |
1847 ) | |
1848 ) | |
1849 try: | |
1850 self.page_error(request, C.HTTP_INTERNAL_ERROR) | |
1851 except exceptions.CancelError: | |
1852 pass | |
1853 | |
1854 def render_GET(self, request): | |
1855 defer.ensureDeferred(self.render_page(request)) | |
1856 return server.NOT_DONE_YET | |
1857 | |
1858 def render_POST(self, request): | |
1859 defer.ensureDeferred(self.render_page(request)) | |
1860 return server.NOT_DONE_YET |