From be5c363451d67ab9d59b619c487f53b88d502cf3 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 25 Nov 2024 18:41:50 -0500 Subject: [PATCH] Fix markup injection issues This fixes some (theoretical) markup injection problems. I believe none of the strings that I escape here will ever contain "<" or "&", but it is always safer to escape. --- .../global_config/rule_list_widgets.py | 2 +- .../global_config/thisdevice_handler.py | 20 ++++++----- qubes_config/global_config/usb_devices.py | 2 +- qubes_config/widgets/gtk_utils.py | 14 +++++++- qui/clipboard.py | 34 +++++++++---------- qui/decorators.py | 15 ++++---- qui/devices/actionable_widgets.py | 12 +++---- qui/tray/disk_space.py | 28 ++++++++------- qui/tray/domains.py | 11 +++--- qui/tray/updates.py | 18 +++++----- qui/updater/progress_page.py | 2 +- qui/utils.py | 22 +++++++++--- 12 files changed, 107 insertions(+), 73 deletions(-) diff --git a/qubes_config/global_config/rule_list_widgets.py b/qubes_config/global_config/rule_list_widgets.py index a81e2548..c43f3761 100644 --- a/qubes_config/global_config/rule_list_widgets.py +++ b/qubes_config/global_config/rule_list_widgets.py @@ -240,7 +240,7 @@ def _combobox_changed(self, *_args): self.callback() def _format_new_value(self, new_value): - self.name_widget.set_markup(f'{self.choices[new_value]}') + self.name_widget.set_text(f'{self.choices[new_value]}') if self.verb_description: self.additional_text_widget.set_text( self.verb_description.get_verb_for_action_and_target( diff --git a/qubes_config/global_config/thisdevice_handler.py b/qubes_config/global_config/thisdevice_handler.py index 1212803d..05f6f43a 100644 --- a/qubes_config/global_config/thisdevice_handler.py +++ b/qubes_config/global_config/thisdevice_handler.py @@ -23,13 +23,14 @@ import qubesadmin.vm from ..widgets.gtk_utils import show_error, load_icon, copy_to_global_clipboard +from ..widgets.gtk_utils import markup_format from .page_handler import PageHandler from .policy_manager import PolicyManager import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, GLib import gettext t = gettext.translation("desktop-linux-manager", fallback=True) @@ -101,7 +102,7 @@ def __init__(self, ['qubes-hcl-report', '-y']).decode() except subprocess.CalledProcessError as ex: label_text += _("Failed to load system data: {ex}\n").format( - ex=str(ex)) + ex=GLib.markup_escape_text(str(ex))) self.hcl_check = "" try: @@ -115,7 +116,7 @@ def __init__(self, label_text += _("Failed to load system data.\n") self.data_label.get_style_context().add_class('red_code') - label_text += _("""Brand: {brand} + label_text += markup_format("""Brand: {brand} Model: {model} CPU: {cpu} @@ -128,7 +129,8 @@ def __init__(self, BIOS: {bios} Kernel: {kernel_ver} Xen: {xen_ver} -""").format(brand=self._get_data('brand'), +""", + brand=self._get_data('brand'), model=self._get_data('model'), cpu=self._get_data('cpu'), chipset=self._get_data('chipset'), @@ -139,15 +141,15 @@ def __init__(self, kernel_ver=self._get_version('kernel'), xen_ver=self._get_version('xen')) self.set_state(self.compat_hvm_image, self._get_data('hvm')) - self.compat_hvm_label.set_markup(f"HVM: {self._get_data('hvm')}") + self.compat_hvm_label.set_markup(markup_format("HVM: {}", self._get_data('hvm'))) self.set_state(self.compat_iommu_image, self._get_data('iommu')) self.compat_iommu_label.set_markup( - f"I/O MMU: {self._get_data('iommu')}") + markup_format("I/O MMU: {}", self._get_data('iommu'))) self.set_state(self.compat_hap_image, self._get_data('slat')) self.compat_hap_label.set_markup( - f"HAP/SLAT: {self._get_data('slat')}") + markup_format("HAP/SLAT: {}", self._get_data('slat'))) self.set_state(self.compat_tpm_image, 'yes' if self._get_data('tpm') == '1.2' else 'maybe') @@ -166,7 +168,7 @@ def __init__(self, self.set_state(self.compat_remapping_image, self._get_data('remap')) self.compat_remapping_label.set_markup( - f"Remapping: {self._get_data('remap')}") + markup_format(_("Remapping: {}"), self._get_data('remap'))) self.set_policy_state() @@ -178,7 +180,7 @@ def __init__(self, _("PV qubes: {num_pvs} found").format(num_pvs=len(pv_vms))) self.compat_pv_tooltip.set_tooltip_markup( _("The following qubes have PV virtualization mode:\n - ") + - '\n - '.join([vm.name for vm in pv_vms])) + '\n - '.join([GLib.markup_escape_text(vm.name) for vm in pv_vms])) self.compat_pv_tooltip.set_visible(bool(pv_vms)) self.data_label.set_markup(label_text) diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py index 57c1722a..5a97ece8 100644 --- a/qubes_config/global_config/usb_devices.py +++ b/qubes_config/global_config/usb_devices.py @@ -488,7 +488,7 @@ def load_rules_for_usb_qube(self): def disable_u2f(self, reason: str): self.problem_fatal_box.set_visible(True) self.problem_fatal_box.show_all() - self.problem_fatal_label.set_markup(reason) + self.problem_fatal_label.set_text(reason) self.enable_check.set_active(False) self.enable_check.set_sensitive(False) self.box.set_visible(False) diff --git a/qubes_config/widgets/gtk_utils.py b/qubes_config/widgets/gtk_utils.py index 0a333eed..e37b0b3b 100644 --- a/qubes_config/widgets/gtk_utils.py +++ b/qubes_config/widgets/gtk_utils.py @@ -84,6 +84,18 @@ def load_icon(icon_name: str, width: int = 24, height: int = 24): pixbuf.fill(0x000) return pixbuf +def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]: + if type(s) is str: + return GLib.markup_escape_text(s) + elif type(s) in (float, int, bool): + return s + else: # Neither escapable nor known safe to passthrough + raise TypeError(f"Unsupported input type {type(s)}") + +def markup_format(s, *args, **kwargs) -> str: + escaped_args = [_escape_str(i) for i in args] + escaped_kwargs = {k: _escape_str(v) for k, v in kwargs.items()} + return s.format(*escaped_args, **escaped_kwargs) def show_error(parent, title, text): """ @@ -175,7 +187,7 @@ def show_dialog( if isinstance(text, str): label: Gtk.Label = Gtk.Label() - label.set_markup(text) + label.set_text(text) label.set_line_wrap_mode(Gtk.WrapMode.WORD) label.set_max_width_chars(200) label.set_xalign(0) diff --git a/qui/clipboard.py b/qui/clipboard.py index 0118bf94..592ac72f 100644 --- a/qui/clipboard.py +++ b/qui/clipboard.py @@ -38,7 +38,7 @@ import gi gi.require_version("Gtk", "3.0") # isort:skip -from gi.repository import Gtk, Gio, Gdk # isort:skip +from gi.repository import Gtk, Gio, Gdk, GLib # isort:skip import gbulb import pyinotify @@ -48,7 +48,7 @@ t = gettext.translation("desktop-linux-manager", fallback=True) _ = t.gettext -from .utils import run_asyncio_and_show_errors +from .utils import run_asyncio_and_show_errors, markup_format gbulb.install() @@ -127,19 +127,19 @@ def _copy(self, metadata: dict) -> None: size = clipboard_formatted_size(metadata["sent_size"]) if metadata["malformed_request"]: - body = ERROR_MALFORMED_DATA.format(vmname=metadata["vmname"]) + body = markup_format(ERROR_MALFORMED_DATA, vmname=metadata["vmname"]) icon = "dialog-error" elif ( metadata["qrexec_clipboard"] and metadata["sent_size"] >= metadata["buffer_size"] ): # Microsoft Windows clipboard case - body = WARNING_POSSIBLE_TRUNCATION.format( + body = markup_format(WARNING_POSSIBLE_TRUNCATION, vmname=metadata["vmname"], size=size ) icon = "dialog-warning" elif metadata["oversized_request"]: - body = ERROR_OVERSIZED_DATA.format( + body = markup_format(ERROR_OVERSIZED_DATA, vmname=metadata["vmname"], size=size, limit=clipboard_formatted_size(metadata["buffer_size"]), @@ -150,13 +150,13 @@ def _copy(self, metadata: dict) -> None: and metadata["cleared"] and metadata["sent_size"] == 0 ): - body = WARNING_EMPTY_CLIPBOARD.format(vmname=metadata["vmname"]) + body = markup_format(WARNING_EMPTY_CLIPBOARD, vmname=metadata["vmname"]) icon = "dialog-warning" elif not metadata["successful"]: - body = ERROR_ON_COPY.format(vmname=metadata["vmname"]) + body = markup_format(ERROR_ON_COPY, vmname=metadata["vmname"]) icon = "dialog-error" else: - body = MSG_COPY_SUCCESS.format( + body = markup_format(MSG_COPY_SUCCESS, vmname=metadata["vmname"], size=size, shortcut=self.gtk_app.paste_shortcut, @@ -173,14 +173,14 @@ def _copy(self, metadata: dict) -> None: def _paste(self, metadata: dict) -> None: """Sends Paste notification via Gio.Notification.""" if not metadata["successful"] or metadata["malformed_request"]: - body = ERROR_ON_PASTE.format(vmname=metadata["vmname"]) + body = markup_format(ERROR_ON_PASTE, vmname=metadata["vmname"]) body += MSG_WIPED icon = "dialog-error" elif ( "protocol_version_xside" in metadata.keys() and metadata["protocol_version_xside"] >= 0x00010008 ): - body = MSG_PASTE_SUCCESS_METADATA.format( + body = markup_format(MSG_PASTE_SUCCESS_METADATA, size=clipboard_formatted_size(metadata["sent_size"]), vmname=metadata["vmname"], ) @@ -355,9 +355,9 @@ def update_clipboard_contents( else: self.clipboard_label.set_markup( - _( + markup_format(_( "Global clipboard contents: {0} from {1}" - ).format(size, vm) + ), size, vm) ) self.icon.set_from_icon_name("edit-copy") @@ -391,10 +391,10 @@ def setup_ui(self, *_args, **_kwargs): help_label = Gtk.Label(xalign=0) help_label.set_markup( - _( + markup_format(_( "Use {copy} to copy and " "{paste} to paste." - ).format(copy=self.copy_shortcut, paste=self.paste_shortcut) + ), copy=self.copy_shortcut, paste=self.paste_shortcut) ) help_item = Gtk.MenuItem() help_item.set_margin_left(10) @@ -442,9 +442,9 @@ def copy_dom0_clipboard(self, *_args, **_kwargs): '"protocol_version_xside":65544,\n' '"protocol_version_vmside":65544,\n' "}}\n".format( - xevent_timestamp=str(Gtk.get_current_event_time()), - sent_size=os.path.getsize(DATA), - buffer_size="256000", + xevent_timestamp=json.dumps(Gtk.get_current_event_time()), + sent_size=json.dumps(os.path.getsize(DATA)), + buffer_size=json.dumps(256000), ) ) except Exception: # pylint: disable=broad-except diff --git a/qui/decorators.py b/qui/decorators.py index 8a76fb6a..dcfd6a38 100644 --- a/qui/decorators.py +++ b/qui/decorators.py @@ -9,6 +9,7 @@ from gi.repository import Gtk, Pango, GLib, GdkPixbuf # isort:skip from qubesadmin import exc from qubesadmin.utils import size_to_human +from .utils import markup_format import gettext t = gettext.translation("desktop-linux-manager", fallback=True) @@ -137,10 +138,10 @@ def update_tooltip(self, perc_storage = self.cur_storage / self.max_storage tooltip += \ - _("\nTemplate: {template}" + markup_format(_("\nTemplate: {template}" "\nNetworking: {netvm}" "\nPrivate storage: {current_storage:.2f}GB/" - "{max_storage:.2f}GB ({perc_storage:.1%})").format( + "{max_storage:.2f}GB ({perc_storage:.1%})"), template=self.template_name, netvm=self.netvm_name, current_storage=self.cur_storage, @@ -177,7 +178,8 @@ def update_state(self, cpu=0, header=False): else: color = self.cpu_label.get_style_context() \ .get_color(Gtk.StateFlags.INSENSITIVE).to_color() - markup = f'0%' + escaped_color = GLib.markup_escape_text(color.to_string()) + markup = f'0%' self.cpu_label.set_markup(markup) @@ -249,8 +251,9 @@ def device_hbox(device) -> Gtk.Box: name_label = Gtk.Label(xalign=0) name = f"{device.backend_domain}:{device.port_id} - {device.description}" if device.attachments: - dev_list = ", ".join(list(device.attachments)) - name_label.set_markup(f'{name} ({dev_list})') + dev_list = GLib.markup_escape_text(", ".join(list(device.attachments))) + name_escaped = GLib.markup_escape_text(name) + name_label.set_markup(f'{name_escaped} ({dev_list})') else: name_label.set_text(name) name_label.set_max_width_chars(64) @@ -281,7 +284,7 @@ def device_domain_hbox(vm, attached: bool) -> Gtk.Box: name = Gtk.Label(xalign=0) if attached: - name.set_markup(f'{vm.vm_name}') + name.set_markup(f'{GLib.markup_escape_text(vm.vm_name)}') else: name.set_text(vm.vm_name) diff --git a/qui/devices/actionable_widgets.py b/qui/devices/actionable_widgets.py index 266bd8a1..d5e94bbe 100644 --- a/qui/devices/actionable_widgets.py +++ b/qui/devices/actionable_widgets.py @@ -130,7 +130,7 @@ def __init__(self, vm: backend.VM, size: int = 18, variant: str = 'dark', self.backend_label = Gtk.Label(xalign=0) backend_label: str = vm.name if name_extension: - backend_label += ": " + name_extension + backend_label += ": " + GLib.markup_escape_text(name_extension) self.backend_label.set_markup(backend_label) self.pack_start(self.backend_icon, False, False, 4) @@ -227,7 +227,7 @@ class DetachWidget(ActionableWidget, SimpleActionWidget): """Detach device from a VM""" def __init__(self, vm: backend.VM, device: backend.Device, variant: str = 'dark'): - super().__init__('detach', 'Detach from ' + vm.name + '', + super().__init__('detach', 'Detach from ' + GLib.markup_escape_text(vm.name) + '', variant) self.vm = vm self.device = device @@ -345,14 +345,14 @@ def __init__(self, device: backend.Device, variant: str = 'dark'): super().__init__(orientation=Gtk.Orientation.VERTICAL) # FUTURE: this is proposed layout for new API # self.device_label = Gtk.Label() - # self.device_label.set_markup(device.name) + # self.device_label.set_text(device.name) # self.device_label.get_style_context().add_class('device_name') # self.edit_icon = VariantIcon('edit', 'dark', 24) # self.detailed_description_label = Gtk.Label() # self.detailed_description_label.set_text(device.description) # self.backend_icon = VariantIcon(device.vm_icon, 'dark', 24) # self.backend_label = Gtk.Label(xalign=0) - # self.backend_label.set_markup(str(device.backend_domain)) + # self.backend_label.set_text(str(device.backend_domain)) # # self.title_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) # self.title_box.add(self.device_label) @@ -367,7 +367,7 @@ def __init__(self, device: backend.Device, variant: str = 'dark'): # self.add(self.attachment_box) self.device_label = Gtk.Label() - self.device_label.set_markup(device.name) + self.device_label.set_text(device.name) self.device_label.get_style_context().add_class('device_name') self.device_label.set_xalign(Gtk.Align.CENTER) self.device_label.set_halign(Gtk.Align.CENTER) @@ -408,7 +408,7 @@ def __init__(self, device: backend.Device, variant: str = 'dark'): self.device_label = Gtk.Label(xalign=0) - label_markup = device.name + label_markup = GLib.markup_escape_text(device.name) if (device.connection_timestamp and int(time.monotonic() - device.connection_timestamp) < 120): label_markup += ' NEW' diff --git a/qui/tray/disk_space.py b/qui/tray/disk_space.py index 8d8c8094..d70d917a 100644 --- a/qui/tray/disk_space.py +++ b/qui/tray/disk_space.py @@ -85,9 +85,9 @@ def __create_widgets(vm_usage): for volume_name, usage in vm_usage.problem_volumes.items(): # pylint: disable=consider-using-f-string label_contents.append(_('volume {} is {:.1%} full').format( - volume_name, usage)) + GLib.markup_escape_text(volume_name), usage)) - label_text = f"{vm.name}: " + ", ".join(label_contents) + label_text = f"{GLib.markup_escape_text(vm.name)}: " + ", ".join(label_contents) label_widget.set_markup(label_text) return vm, icon_img, label_widget @@ -242,13 +242,13 @@ def __create_box(pool: PoolWrapper): if pool.has_error: # Pool with errors formatted_name = \ - f'{pool.name}' + f'{GLib.markup_escape_text(pool.name)}' elif pool.size and 'included_in' not in pool.config: # normal pool - formatted_name = f'{pool.name}' + formatted_name = f'{GLib.markup_escape_text(pool.name)}' else: # pool without data or included in another pool - formatted_name = f'{pool.name}' + formatted_name = f'{GLib.markup_escape_text(pool.name)}' pool_name.set_markup(formatted_name) pool_name.set_margin_left(20) @@ -260,20 +260,20 @@ def __create_box(pool: PoolWrapper): if pool.has_error: error_desc = Gtk.Label(xalign=0) - error_desc.set_markup("Error accessing pool data") + error_desc.set_text("Error accessing pool data") error_desc.set_margin_left(40) name_box.pack_start(error_desc, True, True, 0) return name_box, percentage_box, usage_box data_name = Gtk.Label(xalign=0) - data_name.set_markup("data") + data_name.set_text("data") data_name.set_margin_left(40) name_box.pack_start(data_name, True, True, 0) if pool.metadata_perc: metadata_name = Gtk.Label(xalign=0) - metadata_name.set_markup("metadata") + metadata_name.set_text("metadata") metadata_name.set_margin_left(40) name_box.pack_start(metadata_name, True, True, 0) @@ -407,11 +407,13 @@ def set_icon_state(self, pool_warning=None, vm_warning=None): self.icon.set_from_icon_name("dialog-warning") text = _("Qubes Disk Space Monitor\n\nWARNING!") if pool_warning: - text += _('\nYou are running out of disk space.\n') + \ - ''.join(pool_warning) + text += GLib.markup_escape_text( + _('\nYou are running out of disk space.\n') + + ''.join(pool_warning)) if vm_warning: - text += _('\nThe following qubes are running out of space: ') \ - + ', '.join([x.vm.name for x in vm_warning]) + text += GLib.markup_escape_text(_( + '\nThe following qubes are running out of space: ') + + ', '.join([x.vm.name for x in vm_warning])) self.icon.set_tooltip_markup(text) else: self.icon.set_from_icon_name("drive-harddisk") @@ -465,7 +467,7 @@ def make_menu(self, _unused, _event): @staticmethod def make_title_item(text): label = Gtk.Label(xalign=0) - label.set_markup(_("{}").format(text)) + label.set_markup("{}").format(GLib.markup_escape_text(text)) menu_item = Gtk.MenuItem() menu_item.add(label) menu_item.set_sensitive(False) diff --git a/qui/tray/domains.py b/qui/tray/domains.py index 1eae1310..b5929429 100644 --- a/qui/tray/domains.py +++ b/qui/tray/domains.py @@ -74,7 +74,7 @@ def show_error(title, text): dialog = Gtk.MessageDialog( None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK) dialog.set_title(title) - dialog.set_markup(text) + dialog.set_markup(GLib.markup_escape_text(text)) dialog.connect("response", lambda *x: dialog.destroy()) GLib.idle_add(dialog.show) @@ -284,12 +284,11 @@ class InternalInfoItem(Gtk.MenuItem): def __init__(self): super().__init__() self.label = Gtk.Label(xalign=0) - self.label.set_markup(_( - 'Internal qube')) - self.set_tooltip_text( + self.label.set_markup("" + GLib.markup_escape_text(_("Internal qubes")) + "") + self.set_tooltip_text(_( 'Internal qubes are used by the operating system. Do not modify' ' them or run programs in them unless you really ' - 'know what you are doing.') + 'know what you are doing.')) self.add(self.label) self.set_sensitive(False) @@ -510,7 +509,7 @@ def update_state(self, state): colormap = {'Paused': 'grey', 'Crashed': 'red', 'Transient': 'red'} if state in colormap: self.name.label.set_markup( - f'{self.vm.name}') + f'{GLib.markup_escape_text(self.vm.name)}') else: self.name.label.set_label(self.vm.name) diff --git a/qui/tray/updates.py b/qui/tray/updates.py index 8533dea3..656a7e1d 100644 --- a/qui/tray/updates.py +++ b/qui/tray/updates.py @@ -14,7 +14,7 @@ import gi # isort:skip gi.require_version('Gtk', '3.0') # isort:skip -from gi.repository import Gtk, Gio # isort:skip +from gi.repository import Gtk, Gio, GLib # isort:skip import gbulb gbulb.install() @@ -28,7 +28,8 @@ class TextItem(Gtk.MenuItem): def __init__(self, text): super().__init__() title_label = Gtk.Label() - title_label.set_markup(text) + title_label.set_markup('' + GLib.markup_escape_text(_(text)) + + '') title_label.set_halign(Gtk.Align.CENTER) title_label.set_justify(Gtk.Justification.CENTER) self.set_margin_left(10) @@ -87,7 +88,7 @@ def setup_menu(self): self.tray_menu.set_reserve_toggle_size(False) if self.vms_needing_update: - self.tray_menu.append(TextItem(_("Qube updates available!"))) + self.tray_menu.append(TextItem("Qube updates available!")) self.tray_menu.append(RunItem( _("Updates for {} qubes are available!\n" "Launch updater").format( @@ -95,11 +96,12 @@ def setup_menu(self): if self.obsolete_vms: self.tray_menu.append(TextItem( - _("Some qubes are no longer supported!"))) - obsolete_text = _("The following qubes are based on distributions " - "that are no longer supported:\n")\ - + ", ".join([str(vm) for vm in self.obsolete_vms])\ - + _("\nInstall new templates with Template Manager") + "Some qubes are no longer supported!")) + obsolete_text = GLib.markup_escape_text(_( + "The following qubes are based on distributions " + "that are no longer supported:\n") + + ", ".join([str(vm) for vm in self.obsolete_vms])) \ + + "\n" + GLib.markup_escape_text(_("Install new templates with Template Manager")) + "" self.tray_menu.append( RunItem(obsolete_text, self.launch_template_manager)) diff --git a/qui/updater/progress_page.py b/qui/updater/progress_page.py index ac69e2b1..3d352db8 100644 --- a/qui/updater/progress_page.py +++ b/qui/updater/progress_page.py @@ -539,7 +539,7 @@ def set_active_row(self, row): else: self.details_label.set_text(l("Details for") + " ") self.qube_icon.set_from_pixbuf(self.active_row.icon) - self.qube_label.set_markup(" " + str(self.active_row.color_name)) + self.qube_label.set_text(" " + str(self.active_row.color_name)) self.update_buffer() self.qube_icon.set_visible(row_activated) diff --git a/qui/utils.py b/qui/utils.py index ffbdf26b..c0b15048 100644 --- a/qui/utils.py +++ b/qui/utils.py @@ -37,7 +37,7 @@ import gi # isort:skip gi.require_version('Gtk', '3.0') # isort:skip -from gi.repository import Gtk # isort:skip +from gi.repository import Gtk, GLib # isort:skip with importlib.resources.files('qui').joinpath('eol.json').open() as stream: EOL_DATES = json.load(stream) @@ -60,9 +60,9 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True): exit_code = 0 message = _("Whoops. A critical error in {} has occurred." - " This is most likely a bug.").format(name) + " This is most likely a bug.").format(escape(name)) if restart: - message += _(" {} will restart itself.").format(name) + message += _(" {} will restart itself.").format(escape(name)) for d in done: # pylint: disable=invalid-name try: @@ -76,12 +76,13 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True): exc_value_descr = escape(str(exc_value)) traceback_descr = escape(traceback.format_exc(limit=10)) exc_description = "\n{}: {}\n{}".format( - exc_type.__name__, exc_value_descr, traceback_descr) + escape(exc_type.__name__), exc_value_descr, traceback_descr) dialog.format_secondary_markup(exc_description) dialog.run() exit_code = 1 return exit_code + def check_update(vm) -> bool: """Return true if the given template/standalone vm is updated or not updateable or skipped. default returns true""" @@ -96,6 +97,19 @@ def check_update(vm) -> bool: return True return False +def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]: + if type(s) is str: + return GLIb.markup_escape_text(s) + elif type(s) in (float, int, bool): + return s + else: + raise TypeError(f"Unsupported input type {type(s)}") + +def markup_format(s, *args, **kwargs) -> str: + escaped_args = [_escape_str(i) for i in args] + escaped_kwargs = {k: _escape_str(v) for k, v in kwargs.items()} + return s.format(*escaped_args, **escaped_kwargs) + def check_support(vm) -> bool: """Return true if the given template/standalone vm is still supported, by default returns true"""