# HG changeset patch # User Goffi # Date 1636735710 -3600 # Node ID 595e7fef41f3f01258998df7d0b6de54668f4deb # Parent fc91b78b71dbd4a1b58461561cdcb46533ea857c# Parent e35151a2cec127c076196a247810384237920924 merge bookmark @ diff -r fc91b78b71db -r 595e7fef41f3 .hgtags --- a/.hgtags Tue Sep 28 18:18:37 2021 +0200 +++ b/.hgtags Fri Nov 12 17:48:30 2021 +0100 @@ -12,3 +12,4 @@ cc16d93d4181fb5d3ee2de349b3b7b7c52fcaddf 0.7.0b1 889e8d07e95747205b4aec20a34d7dadc7f4068c 0.7.0 3821168b945985e2878a2dc4c6abbb5211bcc1c7 v0.8.0b1 +f18767efabccd4b15265bf725b7603b7220148e5 v0.8.0b1.post1 diff -r fc91b78b71db -r 595e7fef41f3 libervia/VERSION diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/_browser/bridge.py --- a/libervia/pages/_browser/bridge.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/pages/_browser/bridge.py Fri Nov 12 17:48:30 2021 +0100 @@ -23,7 +23,7 @@ else: print(f"bridge called failed: code: {xhr.response}, text: {xhr.statusText}") if errback is not None: - errback({"fullname": "InternalError", "message": xhr.statusText}) + errback({"fullname": "BridgeInternalError", "message": xhr.statusText}) def call(self, method_name, *args, callback, errback, **kwargs): xhr = window.XMLHttpRequest.new() diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/_browser/browser_meta.json --- a/libervia/pages/_browser/browser_meta.json Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/pages/_browser/browser_meta.json Fri Nov 12 17:48:30 2021 +0100 @@ -2,10 +2,10 @@ "js": { "package": { "dependencies": { - "nunjucks": "latest", - "swiper": "^6.1.1", - "moment": "latest", - "ogv": "latest" + "nunjucks": "^3.2.3", + "swiper": "^6.8.4", + "moment": "^2.29.1", + "ogv": "^1.8.4" } }, "brython_map": { diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/blog/view/page_meta.py --- a/libervia/pages/blog/view/page_meta.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/pages/blog/view/page_meta.py Fri Nov 12 17:48:30 2021 +0100 @@ -3,6 +3,7 @@ import html from libervia.server.constants import Const as C from twisted.words.protocols.jabber import jid +from twisted.web import server from sat.core.i18n import _, D_ from sat.tools.common.template import safe from sat.tools.common import uri @@ -144,7 +145,15 @@ comment_data['items'] = comments['items'] await appendComments(self, request, comments, profile) -async def getBlogItems(self, request, service, node, item_id, extra, profile): +async def getBlogItems( + self, + request: server.Request, + service: jid.JID, + node: str, + item_id, + extra: dict, + profile: str +) -> dict: try: if item_id: items_id = [item_id] @@ -211,7 +220,25 @@ target_profile = template_data.get('target_profile') if blog_items: - if not item_id: + if item_id: + template_data["previous_page_url"] = self.getURL( + service.full(), + node, + before=item_id, + page_max=1 + ) + template_data["next_page_url"] = self.getURL( + service.full(), + node, + after=item_id, + page_max=1 + ) + blog_items["rsm"] = { + "last": item_id, + "first": item_id, + } + blog_items["complete"] = False + else: self.setPagination(request, blog_items) else: if item_id: @@ -287,7 +314,9 @@ uri_args['node'] = node if item_id: uri_args['item'] = item_id - template_data['xmpp_uri'] = uri.buildXMPPUri('pubsub', subtype='microblog', **uri_args) + template_data['xmpp_uri'] = uri.buildXMPPUri( + 'pubsub', subtype='microblog', **uri_args + ) async def on_data_post(self, request): @@ -300,7 +329,7 @@ if not body: self.pageError(request, C.HTTP_BAD_REQUEST) - comment_data = {"content": body} + comment_data = {"content_rich": body} try: await self.host.bridgeCall('mbSend', service, diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/embed/page_meta.py --- a/libervia/pages/embed/page_meta.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/pages/embed/page_meta.py Fri Nov 12 17:48:30 2021 +0100 @@ -15,7 +15,7 @@ data = self.getRData(request) app_name = data["app_name"] try: - app_data = self.vhost_root.sat_apps[app_name] + app_data = self.vhost_root.libervia_apps[app_name] except KeyError: self.pageError(request, C.HTTP_BAD_REQUEST) template_data = request.template_data diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/events/_browser/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/events/_browser/__init__.py Fri Nov 12 17:48:30 2021 +0100 @@ -0,0 +1,52 @@ +from browser import DOMNode, document, aio +from javascript import JSON +from aio_bridge import Bridge, BridgeException +import dialog + +bridge = Bridge() + + +async def on_delete(evt): + evt.stopPropagation() + evt.preventDefault() + target = evt.currentTarget + item_elt = DOMNode(target.closest('.item')) + item_elt.classList.add("selected_for_deletion") + item = JSON.parse(item_elt.dataset.item) + confirmed = await dialog.Confirm( + f"Event {item['name']!r} will be deleted, are you sure?", + ok_label="delete", + ).ashow() + + if not confirmed: + item_elt.classList.remove("selected_for_deletion") + return + + try: + await bridge.interestRetract("", item['interest_id']) + except BridgeException as e: + dialog.notification.show( + f"Can't remove list {item['name']!r} from personal interests: {e}", + "error" + ) + else: + print(f"{item['name']!r} removed successfuly from list of interests") + item_elt.classList.add("state_deleted") + item_elt.bind("transitionend", lambda evt: item_elt.remove()) + if item.get("creator", False): + try: + await bridge.psNodeDelete( + item['service'], + item['node'], + ) + except BridgeException as e: + dialog.notification.show( + f"Error while deleting {item['name']!r}: {e}", + "error" + ) + else: + dialog.notification.show(f"{item['name']!r} has been deleted") + + +for elt in document.select('.action_delete'): + elt.bind("click", lambda evt: aio.run(on_delete(evt))) diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/lists/edit/page_meta.py --- a/libervia/pages/lists/edit/page_meta.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/pages/lists/edit/page_meta.py Fri Nov 12 17:48:30 2021 +0100 @@ -78,8 +78,7 @@ template_data["new_list_item_xmlui"] = list_item -@defer.inlineCallbacks -def on_data_post(self, request): +async def on_data_post(self, request): data = self.getRData(request) service = data["service"] node = data["node"] @@ -94,20 +93,21 @@ profile = self.getProfile(request) # we convert back body to XHTML - body = yield self.host.bridgeCall( + body = await self.host.bridgeCall( "syntaxConvert", posted_data['body'][0], "markdown", C.SYNTAX_XHTML, False, profile) posted_data['body'] = ['
{body}
'.format(ns=C.NS_XHTML, body=body)] extra = {'update': True} - yield self.host.bridgeCall( + await self.host.bridgeCall( "listSet", service.full(), node, posted_data, "", item_id, data_format.serialise(extra), profile ) - # we don't want to redirect to edit page on success, but to list overview data["post_redirect_page"] = ( - self.getPageByName("lists"), + self.getPageByName("list_view"), service.full(), node or "@", + item_id ) + return C.POST_NO_CONFIRM diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/lists/view/page_meta.py --- a/libervia/pages/lists/view/page_meta.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/pages/lists/view/page_meta.py Fri Nov 12 17:48:30 2021 +0100 @@ -32,6 +32,10 @@ data = self.getRData(request) template_data = request.template_data service, node = data["service"], data["node"] + submitted_node = await self.host.bridgeCall( + "psSchemaSubmittedNodeGet", + node or self.host.ns_map["tickets"] + ) profile = self.getProfile(request) or C.SERVICE_PROFILE self.checkCache(request, C.CACHE_PUBSUB, service=service, node=node, short="tickets") @@ -47,7 +51,7 @@ schema_raw = await self.host.bridgeCall( "psSchemaDictGet", service.full(), - node or self.host.ns_map["tickets"], + submitted_node, profile ) schema = data_format.deserialise(schema_raw) @@ -86,11 +90,13 @@ affiliations = await self.host.bridgeCall( "psNodeAffiliationsGet", service.full() if service else "", - node, + submitted_node, profile ) except BridgeException as e: - log.warning(f"Can't get affiliations for node {node!r} at {service}: {e}") + log.warning( + f"Can't get affiliations for node {submitted_node!r} at {service}: {e}" + ) template_data["owner"] = False else: is_owner = affiliations.get(self.getJid(request).userhost()) == 'owner' diff -r fc91b78b71db -r 595e7fef41f3 libervia/pages/lists/view_item/page_meta.py --- a/libervia/pages/lists/view_item/page_meta.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/pages/lists/view_item/page_meta.py Fri Nov 12 17:48:30 2021 +0100 @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from twisted.words.protocols.jabber import jid -from sat.core.i18n import _ +from sat.core.i18n import _, D_ from sat.tools.common import template_xmlui from sat.tools.common import uri from sat.tools.common import data_format @@ -26,6 +26,20 @@ log.warning(_("no list item id specified")) self.pageError(request, C.HTTP_BAD_REQUEST) + +def add_breadcrumb(self, request, breadcrumbs): + data = self.getRData(request) + list_url = self.getPageByName("lists").getURL(data["service"].full(), + data.get("node") or "@") + breadcrumbs.append({ + "label": D_("List"), + "url": list_url + }) + breadcrumbs.append({ + "label": D_("Item"), + }) + + async def prepare_render(self, request): data = self.getRData(request) template_data = request.template_data @@ -35,8 +49,10 @@ data.get("node", ""), data["item_id"], ) + namespace = node or self.host.ns_map["tickets"] + node = await self.host.bridgeCall("psSchemaSubmittedNodeGet", namespace) + profile = self.getProfile(request) - if profile is None: profile = C.SERVICE_PROFILE @@ -50,7 +66,7 @@ {"labels_as_list": C.BOOL_TRUE}, profile, ) - list_items, metadata = data_format.deserialise(list_raw, type_check=list) + list_items, __ = data_format.deserialise(list_raw, type_check=list) list_item = [template_xmlui.create(self.host, x) for x in list_items][0] template_data["item"] = list_item try: @@ -91,7 +107,6 @@ is_publisher = publisher.userhostJID() == session.jid.userhostJID() affiliation = None if not is_publisher: - node = node or self.host.ns_map["tickets"] affiliation = await self.host.getAffiliation(request, service, node) if is_publisher or affiliation == "owner": self.exposeToScripts( @@ -110,7 +125,7 @@ # we add xmpp: URI uri_args = {'path': service.full()} - uri_args['node'] = node or self.host.ns_map["tickets"] + uri_args['node'] = node if item_id: uri_args['item'] = item_id template_data['xmpp_uri'] = uri.buildXMPPUri('pubsub', **uri_args) diff -r fc91b78b71db -r 595e7fef41f3 libervia/server/constants.py --- a/libervia/server/constants.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/server/constants.py Fri Nov 12 17:48:30 2021 +0100 @@ -26,7 +26,8 @@ APP_NAME_ALT = APP_NAME APP_NAME_FILE = "libervia_web" CONFIG_SECTION = APP_COMPONENT.lower() - SERVICE_PROFILE = "libervia" # the SàT profile that is used for exporting the service + # the Libervia profile that is used for public operations (when nobody is connected) + SERVICE_PROFILE = "libervia" SESSION_TIMEOUT = 7200 # Session's timeout, after that the user will be disconnected HTML_DIR = "html/" diff -r fc91b78b71db -r 595e7fef41f3 libervia/server/pages.py --- a/libervia/server/pages.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/server/pages.py Fri Nov 12 17:48:30 2021 +0100 @@ -546,9 +546,14 @@ log.info(_("{page} reloaded").format(page=resource)) def checkCSRF(self, request): - csrf_token = self.host.getSessionData( + session = self.host.getSessionData( request, session_iface.ISATSession - ).csrf_token + ) + if session.profile is None: + # CSRF doesn't make sense when no user is logged + log.debug("disabling CSRF check because service profile is used") + return + csrf_token = session.csrf_token given_csrf = request.getHeader("X-Csrf-Token") if given_csrf is None: try: @@ -577,6 +582,10 @@ for name, value in kwargs.items(): if value is None: value = "null" + elif isinstance(value, str): + # FIXME: workaround for subtype used by python-dbus (dbus.String) + # to be removed when we get rid of python-dbus + value = repr(str(value)) else: value = repr(value) scripts.add(Script(content=f"var {name}={value};")) @@ -665,11 +674,12 @@ else url.encode("utf-8"), ) - def getURL(self, *args): + def getURL(self, *args: str, **kwargs: str) -> str: """retrieve URL of the page set arguments - *args(list[unicode]): argument to add to the URL as path elements - empty or None arguments will be ignored + @param *args: arguments to add to the URL as path elements empty or None + arguments will be ignored + @param **kwargs: query parameters """ url_args = [quote(a) for a in args if a] @@ -677,15 +687,29 @@ #  we check for redirection redirect_data = self.pages_redirects[self.name] args_hash = tuple(args) - for limit in range(len(args) + 1): + for limit in range(len(args), -1, -1): current_hash = args_hash[:limit] if current_hash in redirect_data: url_base = redirect_data[current_hash] remaining = args[limit:] remaining_url = "/".join(remaining) - return os.path.join("/", url_base, remaining_url) + url = urllib.parse.urljoin(url_base, remaining_url) + break + else: + url = os.path.join(self.url, *url_args) + else: + url = os.path.join(self.url, *url_args) - return os.path.join(self.url, *url_args) + if kwargs: + encoded = urllib.parse.urlencode( + {k: v for k, v in kwargs.items()} + ) + url += f"?{encoded}" + + return self.host.checkRedirection( + self.vhost_root, + url + ) def getCurrentURL(self, request): """retrieve URL used to access this page @@ -961,7 +985,7 @@ else: assert not {"rsm_max", "rsm_after", "rsm_before", C.KEY_ORDER_BY}.intersection(list(extra.keys())) - extra["rsm_max"] = str(page_max) + extra["rsm_max"] = params.get("page_max", str(page_max)) if order_by is not None: extra[C.KEY_ORDER_BY] = order_by if 'after' in params: @@ -974,13 +998,13 @@ extra['rsm_before'] = "" return extra - def setPagination(self, request, pubsub_data): + def setPagination(self, request: server.Request, pubsub_data: dict) -> None: """Add to template_data if suitable "previous_page_url" and "next_page_url" will be added using respectively "before" and "after" URL parameters - @param request(server.Request): current HTTP request - @param pubsub_data(dict): pubsub metadata + @param request: current HTTP request + @param pubsub_data: pubsub metadata """ template_data = request.template_data extra = {} @@ -996,6 +1020,11 @@ if search is not None: extra['search'] = search.strip() + # same for page_max + page_max = self.getPostedData(request, 'page_max', raise_on_missing=False) + if page_max is not None: + extra['page_max'] = page_max + if rsm.get("index", 1) > 0: # We only show previous button if it's not the first page already. # If we have no index, we default to display the button anyway @@ -1709,7 +1738,7 @@ accept_language = request.getHeader("accept-language") if not accept_language: return - accepted = {a.strip() for a in accept_language.split(',')} + accepted = [a.strip() for a in accept_language.split(',')] available = [str(l) for l in self.host.renderer.translations] for lang in accepted: lang = lang.split(';')[0].strip().lower() @@ -1785,10 +1814,13 @@ # if template_data doesn't exist, it's the beginning of the request workflow # so we fill essential data session_data = self.host.getSessionData(request, session_iface.ISATSession) + profile = session_data.profile request.template_data = { - "profile": session_data.profile, - "csrf_token": session_data.csrf_token, - "session_uuid": session_data.uuid, + "profile": profile, + # it's important to not add CSRF token and session uuid if service profile + # is used because the page may be cached, and the token then leaked + "csrf_token": "" if profile is None else session_data.csrf_token, + "session_uuid": "public" if profile is None else session_data.uuid, "breadcrumbs": [] } diff -r fc91b78b71db -r 595e7fef41f3 libervia/server/restricted_bridge.py --- a/libervia/server/restricted_bridge.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/server/restricted_bridge.py Fri Nov 12 17:48:30 2021 +0100 @@ -16,8 +16,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from sat.tools.common import data_format +from sat.core import exceptions from libervia.server.constants import Const as C -from sat.tools.common import data_format class RestrictedBridge: @@ -31,6 +32,13 @@ self.host = host self.security_limit = C.SECURITY_LIMIT + def noServiceProfile(self, profile): + """Raise an error if service profile is used""" + if profile == C.SERVICE_PROFILE: + raise exceptions.PermissionError( + "This action is not allowed for service profile" + ) + async def getContacts(self, profile): return await self.host.bridgeCall("getContacts", profile) @@ -47,14 +55,17 @@ "identitiesBaseGet", profile) async def psNodeDelete(self, service_s, node, profile): + self.noServiceProfile(profile) return await self.host.bridgeCall( "psNodeDelete", service_s, node, profile) async def psNodeAffiliationsSet(self, service_s, node, affiliations, profile): + self.noServiceProfile(profile) return await self.host.bridgeCall( "psNodeAffiliationsSet", service_s, node, affiliations, profile) async def psItemRetract(self, service_s, node, item_id, notify, profile): + self.noServiceProfile(profile) return await self.host.bridgeCall( "psItemRetract", service_s, node, item_id, notify, profile) @@ -63,24 +74,28 @@ "mbPreview", service_s, node, data, profile) async def listSet(self, service_s, node, values, schema, item_id, extra, profile): + self.noServiceProfile(profile) return await self.host.bridgeCall( "listSet", service_s, node, values, "", item_id, "", profile) async def fileHTTPUploadGetSlot( self, filename, size, content_type, upload_jid, profile): + self.noServiceProfile(profile) return await self.host.bridgeCall( "fileHTTPUploadGetSlot", filename, size, content_type, upload_jid, profile) async def fileSharingDelete( self, service_jid, path, namespace, profile): + self.noServiceProfile(profile) return await self.host.bridgeCall( "fileSharingDelete", service_jid, path, namespace, profile) async def interestsRegisterFileSharing( self, service, repos_type, namespace, path, name, extra_s, profile ): + self.noServiceProfile(profile) if extra_s: # we only allow "thumb_url" here extra = data_format.deserialise(extra_s) @@ -97,12 +112,14 @@ async def interestRetract( self, service_jid, item_id, profile ): + self.noServiceProfile(profile) return await self.host.bridgeCall( "interestRetract", service_jid, item_id, profile) async def psInvite( self, invitee_jid_s, service_s, node, item_id, name, extra_s, profile ): + self.noServiceProfile(profile) return await self.host.bridgeCall( "psInvite", invitee_jid_s, service_s, node, item_id, name, extra_s, profile ) @@ -111,6 +128,7 @@ self, invitee_jid_s, service_s, repos_type, namespace, path, name, extra_s, profile ): + self.noServiceProfile(profile) if extra_s: # we only allow "thumb_url" here extra = data_format.deserialise(extra_s) @@ -127,6 +145,7 @@ async def FISAffiliationsSet( self, service_s, namespace, path, affiliations, profile ): + self.noServiceProfile(profile) return await self.host.bridgeCall( "FISAffiliationsSet", service_s, namespace, path, affiliations, profile ) @@ -134,6 +153,7 @@ async def invitationSimpleCreate( self, invitee_email, invitee_name, url_template, extra_s, profile ): + self.noServiceProfile(profile) return await self.host.bridgeCall( "invitationSimpleCreate", invitee_email, invitee_name, url_template, extra_s, profile diff -r fc91b78b71db -r 595e7fef41f3 libervia/server/server.py --- a/libervia/server/server.py Tue Sep 28 18:18:37 2021 +0200 +++ b/libervia/server/server.py Fri Nov 12 17:48:30 2021 +0100 @@ -23,6 +23,7 @@ import urllib.request, urllib.error import time import copy +from typing import Optional from pathlib import Path from twisted.application import service from twisted.internet import reactor, defer, inotify @@ -179,6 +180,7 @@ def getChildForRequest(self, request): return super().getChildForRequest(request) + class LiberviaRootResource(ProtectedFile): """Specialized resource for Libervia root @@ -234,8 +236,8 @@ self.pages_redirects = {} self.cached_urls = {} self.main_menu = None - # map SàT application names => data - self.sat_apps = {} + # map Libervia application names => data + self.libervia_apps = {} self.build_path = host.getBuildPath(site_name) self.build_path.mkdir(parents=True, exist_ok=True) self.dev_build_path = host.getBuildPath(site_name, dev=True) @@ -295,7 +297,7 @@ await self.host.bridgeCall( "applicationStart", app_name, data_format.serialise(extra) ) - app_data = self.sat_apps[app_name] = data_format.deserialise( + app_data = self.libervia_apps[app_name] = data_format.deserialise( await self.host.bridgeCall( "applicationExposedGet", app_name, "", "")) @@ -337,168 +339,184 @@ self.redirections = {} self.inv_redirections = {} # new URL to old URL map - for old, new_data in url_redirections.items(): - # new_data can be a dictionary or a unicode url - if isinstance(new_data, dict): - # new_data dict must contain either "url", "page" or "path" key - # (exclusive) - # if "path" is used, a file url is constructed with it - if len({"path", "url", "page"}.intersection(list(new_data.keys()))) != 1: - raise ValueError( - 'You must have one and only one of "url", "page" or "path" key ' - 'in your url_redirections_dict data') - if "url" in new_data: - new = new_data["url"] - elif "page" in new_data: - new = new_data - new["type"] = "page" - new.setdefault("path_args", []) - if not isinstance(new["path_args"], list): - log.error( - _('"path_args" in redirection of {old} must be a list. ' - 'Ignoring the redirection'.format(old=old))) - continue - new.setdefault("query_args", {}) - if not isinstance(new["query_args"], dict): - log.error( - _( - '"query_args" in redirection of {old} must be a ' - 'dictionary. Ignoring the redirection'.format(old=old))) - continue - new["path_args"] = [quote(a) for a in new["path_args"]] - # we keep an inversed dict of page redirection - # (page/path_args => redirecting URL) - # so getURL can return the redirecting URL if the same arguments - # are used # making the URL consistent - args_hash = tuple(new["path_args"]) - self.pages_redirects.setdefault(new_data["page"], {})[ - args_hash - ] = old + for old, new_data_list in url_redirections.items(): + # several redirections can be used for one path by using a list. + # The redirection will be done using first item of the list, and all items + # will be used for inverse redirection. + # e.g. if a => [b, c], a will redirect to c, and b and c will both be + # equivalent to a + if not isinstance(new_data_list, list): + new_data_list = [new_data_list] + for new_data in new_data_list: + # new_data can be a dictionary or a unicode url + if isinstance(new_data, dict): + # new_data dict must contain either "url", "page" or "path" key + # (exclusive) + # if "path" is used, a file url is constructed with it + if (( + len( + {"path", "url", "page"}.intersection(list(new_data.keys())) + ) != 1 + )): + raise ValueError( + 'You must have one and only one of "url", "page" or "path" ' + 'key in your url_redirections_dict data' + ) + if "url" in new_data: + new = new_data["url"] + elif "page" in new_data: + new = new_data + new["type"] = "page" + new.setdefault("path_args", []) + if not isinstance(new["path_args"], list): + log.error( + _('"path_args" in redirection of {old} must be a list. ' + 'Ignoring the redirection'.format(old=old))) + continue + new.setdefault("query_args", {}) + if not isinstance(new["query_args"], dict): + log.error( + _( + '"query_args" in redirection of {old} must be a ' + 'dictionary. Ignoring the redirection' + ).format(old=old) + ) + continue + new["path_args"] = [quote(a) for a in new["path_args"]] + # we keep an inversed dict of page redirection + # (page/path_args => redirecting URL) + # so getURL can return the redirecting URL if the same arguments + # are used # making the URL consistent + args_hash = tuple(new["path_args"]) + self.pages_redirects.setdefault(new_data["page"], {}).setdefault( + args_hash, + old + ) - # we need lists in query_args because it will be used - # as it in request.path_args - for k, v in new["query_args"].items(): - if isinstance(v, str): - new["query_args"][k] = [v] - elif "path" in new_data: - new = "file:{}".format(urllib.parse.quote(new_data["path"])) - elif isinstance(new_data, str): - new = new_data - new_data = {} - else: - log.error( - _("ignoring invalid redirection value: {new_data}").format( - new_data=new_data + # we need lists in query_args because it will be used + # as it in request.path_args + for k, v in new["query_args"].items(): + if isinstance(v, str): + new["query_args"][k] = [v] + elif "path" in new_data: + new = "file:{}".format(urllib.parse.quote(new_data["path"])) + elif isinstance(new_data, str): + new = new_data + new_data = {} + else: + log.error( + _("ignoring invalid redirection value: {new_data}").format( + new_data=new_data + ) ) - ) - continue + continue - # some normalization - if not old.strip(): - # root URL special case - old = "" - elif not old.startswith("/"): - log.error(_("redirected url must start with '/', got {value}. Ignoring") - .format(value=old)) - continue - else: - old = self._normalizeURL(old) + # some normalization + if not old.strip(): + # root URL special case + old = "" + elif not old.startswith("/"): + log.error( + _("redirected url must start with '/', got {value}. Ignoring") + .format(value=old) + ) + continue + else: + old = self._normalizeURL(old) - if isinstance(new, dict): - # dict are handled differently, they contain data - # which ared use dynamically when the request is done - self.redirections[old] = new - if not old: - if new["type"] == "page": - log.info( - _("Root URL redirected to page {name}").format( - name=new["page"] + if isinstance(new, dict): + # dict are handled differently, they contain data + # which ared use dynamically when the request is done + self.redirections.setdefault(old, new) + if not old: + if new["type"] == "page": + log.info( + _("Root URL redirected to page {name}").format( + name=new["page"] + ) ) - ) - else: - if new["type"] == "page": - page = self.getPageByName(new["page"]) - url = page.getURL(*new.get("path_args", [])) - self.inv_redirections[url] = old - continue + else: + if new["type"] == "page": + page = self.getPageByName(new["page"]) + url = page.getURL(*new.get("path_args", [])) + self.inv_redirections[url] = old + continue - # at this point we have a redirection URL in new, we can parse it - new_url = urllib.parse.urlsplit(new) + # at this point we have a redirection URL in new, we can parse it + new_url = urllib.parse.urlsplit(new) - # we handle the known URL schemes - if new_url.scheme == "xmpp": - location = self.getPagePathFromURI(new) - if location is None: - log.warning( - _("ignoring redirection, no page found to handle this URI: " - "{uri}").format(uri=new)) - continue - request_data = self._getRequestData(location) - if old: + # we handle the known URL schemes + if new_url.scheme == "xmpp": + location = self.getPagePathFromURI(new) + if location is None: + log.warning( + _("ignoring redirection, no page found to handle this URI: " + "{uri}").format(uri=new)) + continue + request_data = self._getRequestData(location) self.inv_redirections[location] = old - elif new_url.scheme in ("", "http", "https"): - # direct redirection - if new_url.netloc: - raise NotImplementedError( - "netloc ({netloc}) is not implemented yet for " - "url_redirections_dict, it is not possible to redirect to an " - "external website".format(netloc=new_url.netloc)) - location = urllib.parse.urlunsplit( - ("", "", new_url.path, new_url.query, new_url.fragment) - ) - request_data = self._getRequestData(location) - if old: + elif new_url.scheme in ("", "http", "https"): + # direct redirection + if new_url.netloc: + raise NotImplementedError( + "netloc ({netloc}) is not implemented yet for " + "url_redirections_dict, it is not possible to redirect to an " + "external website".format(netloc=new_url.netloc)) + location = urllib.parse.urlunsplit( + ("", "", new_url.path, new_url.query, new_url.fragment) + ) + request_data = self._getRequestData(location) self.inv_redirections[location] = old - elif new_url.scheme == "file": - # file or directory - if new_url.netloc: - raise NotImplementedError( - "netloc ({netloc}) is not implemented for url redirection to " - "file system, it is not possible to redirect to an external " - "host".format( - netloc=new_url.netloc)) - path = urllib.parse.unquote(new_url.path) - if not os.path.isabs(path): - raise ValueError( - "file redirection must have an absolute path: e.g. " - "file:/path/to/my/file") - # for file redirection, we directly put child here - resource_class = ( - ProtectedFile if new_data.get("protected", True) else static.File - ) - res = resource_class(path, defaultType="application/octet-stream") - self.addResourceToPath(old, res) - log.info("[{host_name}] Added redirection from /{old} to file system " - "path {path}".format(host_name=self.host_name, - old=old, - path=path)) + elif new_url.scheme == "file": + # file or directory + if new_url.netloc: + raise NotImplementedError( + "netloc ({netloc}) is not implemented for url redirection to " + "file system, it is not possible to redirect to an external " + "host".format( + netloc=new_url.netloc)) + path = urllib.parse.unquote(new_url.path) + if not os.path.isabs(path): + raise ValueError( + "file redirection must have an absolute path: e.g. " + "file:/path/to/my/file") + # for file redirection, we directly put child here + resource_class = ( + ProtectedFile if new_data.get("protected", True) else static.File + ) + res = resource_class(path, defaultType="application/octet-stream") + self.addResourceToPath(old, res) + log.info("[{host_name}] Added redirection from /{old} to file system " + "path {path}".format(host_name=self.host_name, + old=old, + path=path)) - # we don't want to use redirection system, so we continue here - continue - - elif new_url.scheme == "sat-app": - # a SàT application - - app_name = urllib.parse.unquote(new_url.path).lower().strip() - extra = {"url_prefix": f"/{old}"} - try: - await self._startApp(app_name, extra) - except Exception as e: - log.warning(_( - "Can't launch {app_name!r} for path /{old}: {e}").format( - app_name=app_name, old=old, e=e)) + # we don't want to use redirection system, so we continue here continue - log.info("[{host_name}] Added redirection from /{old} to application " - "{app_name}".format( - host_name=self.host_name, - old=old, - app_name=app_name)) + elif new_url.scheme == "libervia-app": + # a Libervia application - # normal redirection system is not used here - continue + app_name = urllib.parse.unquote(new_url.path).lower().strip() + extra = {"url_prefix": f"/{old}"} + try: + await self._startApp(app_name, extra) + except Exception as e: + log.warning(_( + "Can't launch {app_name!r} for path /{old}: {e}").format( + app_name=app_name, old=old, e=e)) + continue + + log.info("[{host_name}] Added redirection from /{old} to application " + "{app_name}".format( + host_name=self.host_name, + old=old, + app_name=app_name)) + + # normal redirection system is not used here + continue elif new_url.scheme == "proxy": # a reverse proxy host, port = new_url.hostname, new_url.port @@ -521,18 +539,18 @@ # normal redirection system is not used here continue - else: - raise NotImplementedError( - "{scheme}: scheme is not managed for url_redirections_dict".format( - scheme=new_url.scheme + else: + raise NotImplementedError( + "{scheme}: scheme is not managed for url_redirections_dict".format( + scheme=new_url.scheme + ) ) - ) - self.redirections[old] = request_data - if not old: - log.info(_("[{host_name}] Root URL redirected to {uri}") - .format(host_name=self.host_name, - uri=request_data[1])) + self.redirections.setdefault(old, request_data) + if not old: + log.info(_("[{host_name}] Root URL redirected to {uri}") + .format(host_name=self.host_name, + uri=request_data[1])) # the default root URL, if not redirected if not "" in self.redirections: @@ -554,8 +572,8 @@ log.error(msg) raise ValueError(msg) page_name, url = menu - elif menu.startswith("sat-app:"): - app_name = menu[8:].strip().lower() + elif menu.startswith("libervia-app:"): + app_name = menu[13:].strip().lower() app_data = await self._startApp(app_name) front_url = app_data['front_url'] options = self.host.options @@ -746,7 +764,7 @@ # if nothing was found, we try our luck with redirections # XXX: we want redirections to happen only if everything else failed path_elt = request.prepath + request.postpath - for idx in range(len(path_elt), 0, -1): + for idx in range(len(path_elt), -1, -1): test_url = b"/".join(path_elt[:idx]).decode('utf-8').lower() if test_url in self.redirections: request_data = self.redirections[test_url] @@ -1660,57 +1678,59 @@ """ ext_data = self.base_url_ext_data url_path = request.URLPath() - if not ext_data.scheme or not ext_data.netloc: - #  ext_data is not specified, we check headers - if request.requestHeaders.hasHeader("x-forwarded-host"): - # we are behing a proxy - # we fill proxy_scheme and proxy_netloc value - proxy_host = request.requestHeaders.getRawHeaders("x-forwarded-host")[0] - try: - proxy_server = request.requestHeaders.getRawHeaders( - "x-forwarded-server" - )[0] - except TypeError: - # no x-forwarded-server found, we use proxy_host - proxy_netloc = proxy_host - else: - # if the proxy host has a port, we use it with server name - proxy_port = urllib.parse.urlsplit("//{}".format(proxy_host)).port - proxy_netloc = ( - "{}:{}".format(proxy_server, proxy_port) - if proxy_port is not None - else proxy_server - ) - try: - proxy_scheme = request.requestHeaders.getRawHeaders( - "x-forwarded-proto" - )[0] - except TypeError: - proxy_scheme = None - else: - proxy_scheme, proxy_netloc = None, None + + try: + forwarded = request.requestHeaders.getRawHeaders( + "forwarded" + )[0] + except TypeError: + # we try deprecated headers + try: + proxy_netloc = request.requestHeaders.getRawHeaders( + "x-forwarded-host" + )[0] + except TypeError: + proxy_netloc = None + try: + proxy_scheme = request.requestHeaders.getRawHeaders( + "x-forwarded-proto" + )[0] + except TypeError: + proxy_scheme = None else: - proxy_scheme, proxy_netloc = None, None + fwd_data = { + k.strip(): v.strip() + for k,v in (d.split("=") for d in forwarded.split(";")) + } + proxy_netloc = fwd_data.get("host") + proxy_scheme = fwd_data.get("proto") return urllib.parse.SplitResult( - ext_data.scheme or proxy_scheme or url_path.scheme.decode("utf-8"), - ext_data.netloc or proxy_netloc or url_path.netloc.decode("utf-8"), + ext_data.scheme or proxy_scheme or url_path.scheme.decode(), + ext_data.netloc or proxy_netloc or url_path.netloc.decode(), ext_data.path or "/", "", "", ) - def getExtBaseURL(self, request, path="", query="", fragment="", scheme=None): + def getExtBaseURL( + self, + request: server.Request, + path: str = "", + query: str = "", + fragment: str = "", + scheme: Optional[str] = None, + ) -> str: """Get external URL according to given elements external URL is the URL seen by external user - @param path(unicode): same as for urlsplit.urlsplit + @param path: same as for urlsplit.urlsplit path will be prefixed to follow found external URL if suitable - @param params(unicode): same as for urlsplit.urlsplit - @param query(unicode): same as for urlsplit.urlsplit - @param fragment(unicode): same as for urlsplit.urlsplit - @param scheme(unicode, None): if not None, will override scheme from base URL - @return (unicode): external URL + @param params: same as for urlsplit.urlsplit + @param query: same as for urlsplit.urlsplit + @param fragment: same as for urlsplit.urlsplit + @param scheme: if not None, will override scheme from base URL + @return: external URL """ split_result = self.getExtBaseURLData(request) return urllib.parse.urlunsplit( @@ -1723,23 +1743,23 @@ ) ) - def checkRedirection(self, vhost_root, url): + def checkRedirection(self, vhost_root: LiberviaRootResource, url_path: str) -> str: """check is a part of the URL prefix is redirected then replace it - @param vhost_root(web_resource.Resource): root of this virtual host - @param url(unicode): url to check - @return (unicode): possibly redirected URL which should link to the same location + @param vhost_root: root of this virtual host + @param url_path: path of the url to check + @return: possibly redirected URL which should link to the same location """ inv_redirections = vhost_root.inv_redirections - url_parts = url.strip("/").split("/") - for idx in range(len(url), 0, -1): + url_parts = url_path.strip("/").split("/") + for idx in range(len(url_parts), -1, -1): test_url = "/" + "/".join(url_parts[:idx]) if test_url in inv_redirections: rem_url = url_parts[idx:] return os.path.join( "/", "/".join([inv_redirections[test_url]] + rem_url) ) - return url + return url_path ## Sessions ## diff -r fc91b78b71db -r 595e7fef41f3 twisted/plugins/libervia_server.py --- a/twisted/plugins/libervia_server.py Tue Sep 28 18:18:37 2021 +0200 +++ b/twisted/plugins/libervia_server.py Fri Nov 12 17:48:30 2021 +0100 @@ -31,6 +31,7 @@ except ImportError: pass +import re import os.path import libervia import sat @@ -48,7 +49,9 @@ import configparser -if libervia.__version__ != sat.__version__: +RE_VER_POST = re.compile(r"\.post[0-9]+") + +if RE_VER_POST.sub("", libervia.__version__) != RE_VER_POST.sub("", sat.__version__): import sys sys.stderr.write(