diff options
-rw-r--r-- | gajim/common/const.py | 13 | ||||
-rw-r--r-- | gajim/common/helpers.py | 83 | ||||
-rw-r--r-- | gajim/common/structs.py | 4 | ||||
-rw-r--r-- | gajim/gtk/menus.py | 4 | ||||
-rw-r--r-- | gajim/gtk/start_chat.py | 9 | ||||
-rw-r--r-- | test/no_gui/test_styling.py | 2 |
6 files changed, 74 insertions, 41 deletions
diff --git a/gajim/common/const.py b/gajim/common/const.py index 278a04edd..5cc9cb4b7 100644 --- a/gajim/common/const.py +++ b/gajim/common/const.py @@ -15,6 +15,7 @@ from __future__ import annotations from typing import Any +from typing import Optional from typing import Union from typing import NamedTuple @@ -226,10 +227,18 @@ class URIType(Enum): TEL = 'tel' -class URIAction(Enum): +# https://xmpp.org/registrar/querytypes.html +class XmppUriQuery(Enum): + NONE = '' MESSAGE = 'message' JOIN = 'join' - SUBSCRIBE = 'subscribe' + + @staticmethod + def from_str(s: str) -> Optional[XmppUriQuery]: + try: + return XmppUriQuery(s) + except ValueError: + return None class MUCJoinedState(Enum): diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 25f9c75bc..438cf3456 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -94,7 +94,7 @@ from gajim.common.i18n import get_rfc5646_lang from gajim.common.const import SHOW_STRING from gajim.common.const import SHOW_STRING_MNEMONIC from gajim.common.const import URIType -from gajim.common.const import URIAction +from gajim.common.const import XmppUriQuery from gajim.common.const import GIO_TLS_ERRORS from gajim.common.const import SHOW_LIST from gajim.common.const import CONSONANTS @@ -936,26 +936,30 @@ def catch_exceptions(func): return func_wrapper -def parse_uri_actions(uri: str) -> tuple[str, dict[str, str]]: - uri = uri[5:] - if '?' not in uri: - return 'message', {'jid': uri} +def parse_xmpp_uri_query(pct_iquerycomp: str) -> tuple[str, dict[str, str]]: + ''' + Parses 'mess%61ge;b%6Fdy=Hello%20%F0%9F%8C%90%EF%B8%8F' into + ('message', {'body': 'Hello 🌐️'}), empty string into ('', {}). + ''' + # Syntax and terminology from + # <https://rfc-editor.org/rfc/rfc5122#section-2.2>; the pct_ prefix means + # percent encoding not being undone yet. - jid, action = uri.split('?', 1) - data = {'jid': jid} - if ';' in action: - action, keys = action.split(';', 1) - action_keys = keys.split(';') - for key in action_keys: - if key.startswith('subject='): - data['subject'] = unquote(key[8:]) + if not pct_iquerycomp: + return '', {} - elif key.startswith('body='): - data['body'] = unquote(key[5:]) + pct_iquerytype, _, pct_ipairs = pct_iquerycomp.partition(';') + iquerytype = unquote(pct_iquerytype, errors='strict') + if not pct_ipairs: + return iquerytype, {} - elif key.startswith('thread='): - data['thread'] = key[7:] - return action, data + pairs: dict[str, str] = {} + for pct_ipair in pct_ipairs.split(';'): + pct_ikey, _, pct_ival = pct_ipair.partition('=') + pairs[ + unquote(pct_ikey, errors='strict') + ] = unquote(pct_ival, errors='replace') + return iquerytype, pairs def parse_uri(uri: str) -> URI: @@ -979,15 +983,24 @@ def parse_uri(uri: str) -> URI: return URI(URIType.WEB, uri, data=uri) if scheme == 'xmpp': - action, data = parse_uri_actions(uri) + if not urlparts.path.startswith('/'): + pct_jid = urlparts.path + else: + pct_jid = urlparts.path[1:] + + data: dict[str, str] = {} try: + data['jid'] = unquote(pct_jid, errors='strict') validate_jid(data['jid']) - return URI(URIType.XMPP, uri, - action=URIAction(action), - data=data) - except ValueError: - # Unknown action - return URI(URIType.UNKNOWN, uri) + qtype, qparams = parse_xmpp_uri_query(urlparts.query) + except ValueError as err: + data['error'] = str(err) + return URI(URIType.INVALID, uri, data=data) + + return URI(URIType.XMPP, uri, + query_type=qtype, + query_params=qparams, + data=data) if scheme == 'mailto': data = uri[7:] @@ -1051,19 +1064,29 @@ def open_uri(uri: Union[URI, str], account: Optional[str] = None) -> None: if isinstance(uri.data, dict): jid = uri.data['jid'] - message = uri.data.get('body') else: log.warning('Cant open URI: %s', uri) return - if uri.action == URIAction.JOIN: + qtype, qparams = XmppUriQuery.from_str(uri.query_type), uri.query_params + if not qtype: + log.info('open_uri: can\'t "%s": ' + 'unsupported query type in %s', uri.query_type, uri) + # From <rfc5122#section-2.5>: + # > If the processing application does not understand [...] the + # > specified query type, it MUST ignore the query component and + # > treat the IRI/URI as consisting of, for example, + # > <xmpp:example-node@example.com> rather than + # > <xmpp:example-node@example.com?query>." + qtype, qparams = XmppUriQuery.NONE, {} + + if qtype == XmppUriQuery.JOIN: app.app.activate_action( 'groupchat-join', GLib.Variant('as', [account, jid])) - elif uri.action == URIAction.MESSAGE: - app.window.start_chat_from_jid(account, jid, message=message) else: - log.warning('Cant open URI: %s', uri) + message = qparams.get('body') + app.window.start_chat_from_jid(account, jid, message=message) else: log.warning('Cant open URI: %s', uri) diff --git a/gajim/common/structs.py b/gajim/common/structs.py index 5fd732bfc..b6d7364d6 100644 --- a/gajim/common/structs.py +++ b/gajim/common/structs.py @@ -42,7 +42,6 @@ from gajim.common.const import MUCJoinedState from gajim.common.const import KindConstant from gajim.common.const import PresenceShowExt from gajim.common.const import URIType -from gajim.common.const import URIAction _T = TypeVar('_T') @@ -51,7 +50,8 @@ _T = TypeVar('_T') class URI(NamedTuple): type: URIType source: str - action: Optional[URIAction] = None + query_type: str = '' + query_params: dict[str, str] = {} data: Optional[Union[dict[str, str], str]] = None diff --git a/gajim/gtk/menus.py b/gajim/gtk/menus.py index 824b0c10f..2d9258082 100644 --- a/gajim/gtk/menus.py +++ b/gajim/gtk/menus.py @@ -38,7 +38,7 @@ from gajim.common.helpers import jid_is_blocked from gajim.common.i18n import _ from gajim.common.i18n import get_short_lang_code from gajim.common.const import URIType -from gajim.common.const import URIAction +from gajim.common.const import XmppUriQuery from gajim.common.structs import URI from gajim.common.structs import VariantMixin from gajim.common.modules.contacts import GroupchatContact @@ -281,7 +281,7 @@ def get_conv_action_context_menu(account: str, def get_conv_uri_context_menu(account: str, uri: URI) -> Optional[Gtk.Menu]: if uri.type == URIType.XMPP: - if uri.action == URIAction.JOIN: + if XmppUriQuery.from_str(uri.query_type) == XmppUriQuery.JOIN: context_menu = [ ('copy-text', _('Copy XMPP Address')), ('groupchat-join', _('Join Groupchat')), diff --git a/gajim/gtk/start_chat.py b/gajim/gtk/start_chat.py index 7d4a5259a..df16f5571 100644 --- a/gajim/gtk/start_chat.py +++ b/gajim/gtk/start_chat.py @@ -40,6 +40,8 @@ from nbxmpp.task import Task from gajim.common import app from gajim.common.const import Direction +from gajim.common.const import URIType +from gajim.common.helpers import parse_uri from gajim.common.helpers import validate_jid from gajim.common.helpers import to_user_string from gajim.common.helpers import get_group_chat_nick @@ -549,10 +551,9 @@ class StartChatDialog(Gtk.ApplicationWindow): return search_text = search_entry.get_text() - if search_text.startswith('xmpp:'): - search_text = search_text.removeprefix('xmpp:') - search_text = search_text.removesuffix('?join') - search_entry.set_text(search_text) + uri = parse_uri(search_text) + if uri.type == URIType.XMPP: + search_entry.set_text(uri.data['jid']) return if '@' in search_text: diff --git a/test/no_gui/test_styling.py b/test/no_gui/test_styling.py index e80584a01..af6ed9f83 100644 --- a/test/no_gui/test_styling.py +++ b/test/no_gui/test_styling.py @@ -295,7 +295,7 @@ URIS = [ 'xmpp:asd@at', 'xmpp:asd@asd.at', 'xmpp:asd-asd@asd.asdasd.at.', - #'xmpp:me@%5B::1%5D', FIXME: unescape before validating domainpart + 'xmpp:me@%5B::1%5D', 'xmpp:myself@127.13.42.69', 'xmpp:myself@127.13.42.69/localhost', 'xmpp:%23room%25irc.example@biboumi.xmpp.example', |