From dd7496d7a7efc68648c91b3aaca23625138c9330 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philipp=20H=C3=B6rist?= <philipp@hoerist.com>
Date: Sat, 27 Apr 2019 12:17:16 +0200
Subject: [PATCH] Add dedicated method for parsing and opening URIs

---
 gajim/app_actions.py           |  10 ++-
 gajim/application.py           |   2 +-
 gajim/chat_control.py          |   3 +-
 gajim/chat_control_base.py     |   3 -
 gajim/common/const.py          |  16 +++++
 gajim/common/helpers.py        | 121 +++++++++++++++++++++++++--------
 gajim/common/structs.py        |  18 +++++
 gajim/conversation_textview.py |   2 +-
 gajim/groupchat_control.py     |   4 +-
 gajim/gtk/about.py             |   4 +-
 gajim/gtk/dataform.py          |   6 +-
 gajim/gtk/htmltextview.py      |  48 +++----------
 gajim/gui_interface.py         |   9 ---
 gajim/gui_menu_builder.py      |  31 +++++----
 gajim/plugins/gui.py           |   4 +-
 15 files changed, 169 insertions(+), 112 deletions(-)
 create mode 100644 gajim/common/structs.py

diff --git a/gajim/app_actions.py b/gajim/app_actions.py
index 27de3fb2ac..1fc68f77e0 100644
--- a/gajim/app_actions.py
+++ b/gajim/app_actions.py
@@ -285,13 +285,11 @@ def on_delete_motd(action, param):
 
 
 def on_contents(action, param):
-    helpers.launch_browser_mailer(
-        'url', 'https://dev.gajim.org/gajim/gajim/wikis')
+    helpers.open_uri('https://dev.gajim.org/gajim/gajim/wikis')
 
 
 def on_faq(action, param):
-    helpers.launch_browser_mailer(
-        'url', 'https://dev.gajim.org/gajim/gajim/wikis/help/gajimfaq')
+    helpers.open_uri('https://dev.gajim.org/gajim/gajim/wikis/help/gajimfaq')
 
 
 def on_keyboard_shortcuts(action, param):
@@ -351,8 +349,8 @@ def show_next_pending_event(action, param):
 
 
 def open_link(_action, param):
-    kind, link = param.get_strv()
-    helpers.launch_browser_mailer(kind, link)
+    uri = param.get_string()
+    helpers.open_uri(uri)
 
 
 def copy_text(_action, param):
diff --git a/gajim/application.py b/gajim/application.py
index 2235e4074d..fa521c1c0a 100644
--- a/gajim/application.py
+++ b/gajim/application.py
@@ -420,7 +420,7 @@ class GajimApplication(Gtk.Application):
         act.connect("activate", app_actions.copy_text)
         self.add_action(act)
 
-        act = Gio.SimpleAction.new('open-link', GLib.VariantType.new('as'))
+        act = Gio.SimpleAction.new('open-link', GLib.VariantType.new('s'))
         act.connect("activate", app_actions.open_link)
         self.add_action(act)
 
diff --git a/gajim/chat_control.py b/gajim/chat_control.py
index 17eeb298b6..9721f8d73e 100644
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -43,6 +43,7 @@ from gajim.common import ged
 from gajim.common import i18n
 from gajim.common.i18n import _
 from gajim.common.helpers import AdditionalDataDict
+from gajim.common.helpers import open_uri
 from gajim.common.contacts import GC_Contact
 from gajim.common.const import AvatarSize
 from gajim.common.const import KindConstant
@@ -651,7 +652,7 @@ class ChatControl(ChatControlBase):
                 uri = 'https://www.openstreetmap.org/?' + \
                         'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % {'lat': location['lat'],
                         'lon': location['lon']}
-                helpers.launch_browser_mailer('url', uri)
+                open_uri(uri)
 
     def on_location_eventbox_leave_notify_event(self, widget, event):
         """
diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py
index d66c639c5e..5ec2c90bb8 100644
--- a/gajim/chat_control_base.py
+++ b/gajim/chat_control_base.py
@@ -167,9 +167,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         if self.parent_win:
             self.parent_win.redraw_tab(self)
 
-    def status_url_clicked(self, widget, url):
-        helpers.launch_browser_mailer('url', url)
-
     def setup_seclabel(self):
         self.seclabel_combo.hide()
         self.seclabel_combo.set_no_show_all(True)
diff --git a/gajim/common/const.py b/gajim/common/const.py
index de43a49a04..e50e0f5a3f 100644
--- a/gajim/common/const.py
+++ b/gajim/common/const.py
@@ -184,6 +184,22 @@ class Display(Enum):
     QUARTZ = 'GdkQuartzDisplay'
 
 
+class URIType(Enum):
+    UNKNOWN = 'unknown'
+    XMPP = 'xmpp'
+    MAIL = 'mail'
+    GEO = 'geo'
+    WEB = 'web'
+    FILE = 'file'
+    AT = 'at'
+
+
+class URIAction(Enum):
+    MESSAGE = 'message'
+    JOIN = 'join'
+    SUBSCRIBE = 'subscribe'
+
+
 EME_MESSAGES = {
     'urn:xmpp:otr:0':
         _('This message was encrypted with OTR '
diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py
index 8fa9c4cfb9..9c848203f3 100644
--- a/gajim/common/helpers.py
+++ b/gajim/common/helpers.py
@@ -65,6 +65,9 @@ from gajim.common.i18n import _
 from gajim.common.i18n import ngettext
 from gajim.common.const import ShowConstant
 from gajim.common.const import Display
+from gajim.common.const import URIType
+from gajim.common.const import URIAction
+from gajim.common.structs import URI
 
 if app.is_installed('PYCURL'):
     import pycurl
@@ -629,35 +632,6 @@ def get_contact_dict_for_account(account):
             contacts_dict[name] = contact
     return contacts_dict
 
-def launch_browser_mailer(kind, uri):
-    # kind = 'url' or 'mail'
-    if kind == 'url' and uri.startswith('file://'):
-        launch_file_manager(uri)
-        return
-    if kind in ('mail', 'sth_at_sth') and not uri.startswith('mailto:'):
-        uri = 'mailto:' + uri
-
-    if kind == 'url' and uri.startswith('www.'):
-        uri = 'http://' + uri
-
-    if not app.config.get('autodetect_browser_mailer'):
-        if kind == 'url':
-            command = app.config.get('custombrowser')
-        elif kind in ('mail', 'sth_at_sth'):
-            command = app.config.get('custommailapp')
-        if command == '': # if no app is configured
-            return
-
-        command = build_command(command, uri)
-        try:
-            exec_command(command)
-        except Exception:
-            pass
-
-    else:
-        webbrowser.open(uri)
-
-
 def launch_file_manager(path_to_open):
     if os.name == 'nt':
         try:
@@ -1502,3 +1476,92 @@ def delay_execution(milliseconds):
                 milliseconds, timeout_wrapper)
         return func_wrapper
     return delay_execution_decorator
+
+
+def parse_uri(uri):
+    if uri.startswith('xmpp:'):
+        uri = uri[5:]
+        if '?' in uri:
+            jid, action = uri.split('?')
+            try:
+                return URI(type=URIType.XMPP,
+                           action=URIAction(action),
+                           data=jid)
+            except ValueError:
+                # Unknown action
+                pass
+
+        return URI(type=URIType.XMPP, action=URIAction.MESSAGE, data=uri)
+
+    if uri.startswith('mailto:'):
+        uri = uri[7:]
+        return URI(type=URIType.MAIL, data=uri)
+
+    if app.interface.sth_at_sth_dot_sth_re.match(uri):
+        return URI(type=URIType.AT, data=uri)
+
+    if uri.startswith('geo:'):
+        location = uri[4:]
+        lat, _, lon = location.partition(',')
+        if not lon:
+            return URI(type=URIType.UNKNOWN, data=uri)
+
+        uri = ('https://www.openstreetmap.org/?'
+               'mlat=%s&mlon=%s&zoom=16') % (lat, lon)
+        return URI(type=URIType.GEO, data=uri)
+
+    if uri.startswith('file://'):
+        return URI(type=URIType.FILE, data=uri)
+
+    return URI(type=URIType.WEB, data=uri)
+
+
+def open_uri(uri, account=None):
+    if not isinstance(uri, URI):
+        uri = parse_uri(uri)
+
+    if uri.type == URIType.FILE:
+        launch_file_manager(uri.data)
+
+    elif uri.type == URIType.MAIL:
+        uri = 'mailto:%s' % uri.data
+        if not app.config.get('autodetect_browser_mailer'):
+            open_uri_with_custom('custommailapp', 'mailto:%s' % uri)
+        else:
+            webbrowser.open(uri)
+
+    elif uri.type in (URIType.WEB, URIType.GEO):
+        if not app.config.get('autodetect_browser_mailer'):
+            open_uri_with_custom('custombrowser', uri.data)
+        else:
+            webbrowser.open(uri.data)
+
+    elif uri.type == URIType.AT:
+        app.interface.new_chat_from_jid(account, uri.data)
+
+    elif uri.type == URIType.XMPP:
+        if account is None:
+            log.warning('Account must be specified to open XMPP uri')
+            return
+
+        if uri.action == URIAction.JOIN:
+            app.interface.join_gc_minimal(account, uri.data)
+        elif uri.action == URIAction.MESSAGE:
+            app.interface.new_chat_from_jid(account, uri.data)
+        else:
+            log.warning('Cant open URI: %s', uri)
+
+    else:
+        log.warning('Cant open URI: %s', uri)
+
+
+def open_uri_with_custom(config_app, uri):
+    command = app.config.get(config_app)
+    if not command:
+        log.warning('No custom application set')
+        return
+    command = build_command(command, uri)
+    try:
+        exec_command(command)
+    except Exception:
+        pass
diff --git a/gajim/common/structs.py b/gajim/common/structs.py
new file mode 100644
index 0000000000..9d1dd421fd
--- /dev/null
+++ b/gajim/common/structs.py
@@ -0,0 +1,18 @@
+# This file is part of Gajim.
+#
+# Gajim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation; version 3 only.
+#
+# Gajim is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
+
+from collections import namedtuple
+
+URI = namedtuple('URI', 'type action data')
+URI.__new__.__defaults__ = (None, None)  # type: ignore
diff --git a/gajim/conversation_textview.py b/gajim/conversation_textview.py
index ba0f5721ab..d1cd712ca8 100644
--- a/gajim/conversation_textview.py
+++ b/gajim/conversation_textview.py
@@ -518,7 +518,7 @@ class ConversationTextview(GObject.GObject):
         """
         Basically it filters out the widget instance
         """
-        helpers.launch_browser_mailer('url', link)
+        helpers.open_uri(link)
 
     def on_textview_populate_popup(self, textview, menu):
         """
diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py
index 74097fbd78..4bc00ce295 100644
--- a/gajim/groupchat_control.py
+++ b/gajim/groupchat_control.py
@@ -55,7 +55,7 @@ from gajim.common.caps_cache import muc_caps_cache
 from gajim.common import events
 from gajim.common import app
 from gajim.common import helpers
-from gajim.common.helpers import launch_browser_mailer
+from gajim.common.helpers import open_uri
 from gajim.common.helpers import AdditionalDataDict
 from gajim.common import ged
 from gajim.common.i18n import _
@@ -3106,5 +3106,5 @@ class SubjectPopover(Gtk.Popover):
     def _on_activate_link(_label, uri):
         # We have to use this, because the default GTK handler
         # is not cross-platform compatible
-        launch_browser_mailer(None, uri)
+        open_uri(uri)
         return Gdk.EVENT_STOP
diff --git a/gajim/gtk/about.py b/gajim/gtk/about.py
index 0bc5c81053..c95ab8304a 100644
--- a/gajim/gtk/about.py
+++ b/gajim/gtk/about.py
@@ -19,7 +19,7 @@ from gi.repository import Gtk
 from gi.repository import GObject
 
 from gajim.common import app
-from gajim.common.helpers import launch_browser_mailer
+from gajim.common.helpers import open_uri
 from gajim.common.i18n import _
 from gajim.common.const import DEVS_CURRENT
 from gajim.common.const import DEVS_PAST
@@ -75,7 +75,7 @@ class AboutDialog(Gtk.AboutDialog):
     def _on_activate_link(_label, uri):
         # We have to use this, because the default GTK handler
         # is not cross-platform compatible
-        launch_browser_mailer(None, uri)
+        open_uri(uri)
         return Gdk.EVENT_STOP
 
     def _connect_link_handler(self, parent):
diff --git a/gajim/gtk/dataform.py b/gajim/gtk/dataform.py
index 85a25f51c3..467c9f278d 100644
--- a/gajim/gtk/dataform.py
+++ b/gajim/gtk/dataform.py
@@ -21,7 +21,7 @@ from gajim.gtkgui_helpers import scale_pixbuf_from_data
 
 from gajim.common import app
 from gajim.common.i18n import _
-from gajim.common.helpers import launch_browser_mailer
+from gajim.common.helpers import open_uri
 from gajim.common.modules.dataforms import extend_form
 
 from gajim.gtk.util import MultiLineLabel
@@ -588,9 +588,7 @@ class FakeDataFormWidget(Gtk.ScrolledWindow):
             button = Gtk.Button(label='Register')
             button.set_halign(Gtk.Align.CENTER)
             button.get_style_context().add_class('suggested-action')
-            button.connect('clicked',
-                           lambda *args: launch_browser_mailer('url',
-                                                               redirect_url))
+            button.connect('clicked', lambda *args: open_uri(redirect_url))
             self._grid.attach(button, 0, self._row_count, 2, 1)
         else:
             self._add_fields()
diff --git a/gajim/gtk/htmltextview.py b/gajim/gtk/htmltextview.py
index 1575c28165..2fb439486e 100644
--- a/gajim/gtk/htmltextview.py
+++ b/gajim/gtk/htmltextview.py
@@ -49,6 +49,8 @@ from gajim.common import app
 from gajim.common import helpers
 from gajim.common.i18n import _
 from gajim.common.const import StyleAttr
+from gajim.common.helpers import open_uri
+from gajim.common.helpers import parse_uri
 from gajim.gtk.util import load_icon
 from gajim.gtk.util import get_cursor
 
@@ -877,8 +879,8 @@ class HtmlTextView(Gtk.TextView):
             self._cursor_changed = False
         return False
 
-    def show_context_menu(self, _event, kind, text):
-        menu = get_conv_context_menu(self.account, kind, text)
+    def show_context_menu(self, uri):
+        menu = get_conv_context_menu(self.account, uri)
         if menu is None:
             return
 
@@ -904,53 +906,21 @@ class HtmlTextView(Gtk.TextView):
 
             # Detect XHTML-IM link
             word = getattr(texttag, 'href', None)
-            if word:
-                if word.startswith('xmpp'):
-                    kind = 'xmpp'
-                elif word.startswith('mailto:'):
-                    kind = 'mail'
-                elif app.interface.sth_at_sth_dot_sth_re.match(word):
-                    # it's a JID or mail
-                    kind = 'sth_at_sth'
-            else:
+            if not word:
                 word = self.get_buffer().get_text(begin_iter, end_iter, True)
 
+            uri = parse_uri(word)
             if event.button.button == 3: # right click
-                self.show_context_menu(event, kind, word)
+                self.show_context_menu(uri)
                 return True
 
             self.plugin_modified = False
             app.plugin_manager.extension_point(
-                'hyperlink_handler', word, kind, self,
-                self.get_toplevel())
+                'hyperlink_handler', uri, self, self.get_toplevel())
             if self.plugin_modified:
                 return
 
-            # we launch the correct application
-            if kind == 'xmpp':
-                word = word[5:]
-                if '?' in word:
-                    (jid, action) = word.split('?')
-                    if action == 'join':
-                        app.interface.join_gc_minimal(self.account, jid)
-                    else:
-                        app.interface.new_chat_from_jid(self.account, jid)
-                else:
-                    app.interface.new_chat_from_jid(self.account, word)
-
-            # handle geo:-URIs
-            elif word[:4] == 'geo:':
-                location = word[4:]
-                lat, _, lon = location.partition(',')
-                if lon == '':
-                    return
-                uri = 'https://www.openstreetmap.org/?' \
-                      'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % \
-                      {'lat': lat, 'lon': lon}
-                helpers.launch_browser_mailer(kind, uri)
-            else:
-                # other URIs
-                helpers.launch_browser_mailer(kind, word)
+            open_uri(uri, account=self.account)
 
     def display_html(self, html, textview, conv_textview, iter_=None):
         buffer_ = self.get_buffer()
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index 208d50a70f..455c21b0af 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -1957,10 +1957,6 @@ class Interface:
         self.systray_enabled = False
         self.systray.hide_icon()
 
-    @staticmethod
-    def on_launch_browser_mailer(widget, url, kind):
-        helpers.launch_browser_mailer(kind, url)
-
     def process_connections(self):
         """
         Called each foo (200) milliseconds. Check for idlequeue timeouts
@@ -2381,11 +2377,6 @@ class Interface:
             app.config.get_per('accounts', account, 'active'):
                 app.connections[account] = Connection(account)
 
-        # gtk hooks
-#        Gtk.about_dialog_set_email_hook(self.on_launch_browser_mailer, 'mail')
-#        Gtk.about_dialog_set_url_hook(self.on_launch_browser_mailer, 'url')
-#        Gtk.link_button_set_uri_hook(self.on_launch_browser_mailer, 'url')
-
         self.instances = {}
 
         for a in app.connections:
diff --git a/gajim/gui_menu_builder.py b/gajim/gui_menu_builder.py
index eb1cb6f34f..a3793ff7ad 100644
--- a/gajim/gui_menu_builder.py
+++ b/gajim/gui_menu_builder.py
@@ -25,6 +25,8 @@ from gajim.common import app
 from gajim.common import helpers
 from gajim.common.i18n import ngettext
 from gajim.common.i18n import _
+from gajim.common.const import URIType
+from gajim.common.const import URIAction
 
 from gajim.gtk.util import get_builder
 
@@ -918,9 +920,9 @@ def get_encryption_menu(control_id, type_id, zeroconf=False):
     return menu
 
 
-def get_conv_context_menu(account, kind, text):
-    if kind == 'xmpp':
-        if '?join' in text:
+def get_conv_context_menu(account, uri):
+    if uri.type == URIType.XMPP:
+        if uri.action == URIAction.JOIN:
             context_menu = [
                 ('copy-text', _('Copy JID')),
                 ('-join-groupchat', _('Join Groupchat')),
@@ -932,19 +934,25 @@ def get_conv_context_menu(account, kind, text):
                 ('-add-contact', _('Add to Roster…')),
             ]
 
-    elif kind == 'url':
+    elif uri.type == URIType.WEB:
         context_menu = [
             ('copy-text', _('Copy Link Location')),
             ('open-link', _('Open Link in Browser')),
         ]
 
-    elif kind == 'mail':
+    elif uri.type == URIType.MAIL:
         context_menu = [
             ('copy-text', _('Copy Email Address')),
             ('open-link', _('Open Email Composer')),
         ]
 
-    elif kind == 'sth_at_sth':
+    elif uri.type == URIType.GEO:
+        context_menu = [
+            ('copy-text', _('Copy Location')),
+            ('open-link', _('Show Location')),
+        ]
+
+    elif uri.type == URIType.AT:
         context_menu = [
             ('copy-text', _('Copy JID/Email')),
             ('open-link', _('Open Email Composer')),
@@ -953,6 +961,7 @@ def get_conv_context_menu(account, kind, text):
             ('-add-contact', _('Add to Roster…')),
         ]
     else:
+        log.warning('No handler for URI type: %s', uri)
         return
 
     menu = Gtk.Menu()
@@ -962,19 +971,15 @@ def get_conv_context_menu(account, kind, text):
         menuitem.set_label(label)
 
         if action.startswith('-'):
-            text = text.replace('xmpp:', '')
-            text = text.split('?')[0]
             action = 'app.%s%s' % (account, action)
         else:
             action = 'app.%s' % action
         menuitem.set_action_name(action)
 
-        if action == 'app.open-link':
-            value = GLib.Variant.new_strv([kind, text])
-        elif action == 'app.copy-text':
-            value = GLib.Variant.new_string(text)
+        if action in ('app.open-link', 'app.copy-text'):
+            value = GLib.Variant.new_string(uri.data)
         else:
-            value = GLib.Variant.new_strv([account, text])
+            value = GLib.Variant.new_strv([account, uri.data])
         menuitem.set_action_target_value(value)
         menuitem.show()
         menu.append(menuitem)
diff --git a/gajim/plugins/gui.py b/gajim/plugins/gui.py
index 2b7efe5be2..6b447cb50b 100644
--- a/gajim/plugins/gui.py
+++ b/gajim/plugins/gui.py
@@ -32,7 +32,7 @@ from gi.repository import Gdk
 from gajim.common import app
 from gajim.common import configpaths
 from gajim.common.exceptions import PluginsystemError
-from gajim.common.helpers import launch_browser_mailer
+from gajim.common.helpers import open_uri
 
 from gajim.plugins.helpers import log_calls
 from gajim.plugins.helpers import GajimPluginActivateException
@@ -250,7 +250,7 @@ class PluginsWindow:
     @log_calls('PluginsWindow')
     def on_install_plugin_button_clicked(self, widget):
         if app.is_flatpak():
-            launch_browser_mailer('url', 'https://dev.gajim.org/gajim/gajim/wikis/help/flathub')
+            open_uri('https://dev.gajim.org/gajim/gajim/wikis/help/flathub')
             return
 
         def show_warn_dialog():
-- 
GitLab