diff --git a/qubes_config/global_config/rule_list_widgets.py b/qubes_config/global_config/rule_list_widgets.py
index ef86762..a21eabb 100644
--- a/qubes_config/global_config/rule_list_widgets.py
+++ b/qubes_config/global_config/rule_list_widgets.py
@@ -261,7 +261,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 c6e4f2e..8cbf8ac 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
@@ -126,7 +127,7 @@ def __init__(
).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 = ""
@@ -141,8 +142,9 @@ def __init__(
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}
@@ -156,7 +158,7 @@ def __init__(
Kernel: {kernel_ver}
Xen: {xen_ver}
"""
- ).format(
+ ),
brand=self._get_data("brand"),
model=self._get_data("model"),
cpu=self._get_data("cpu"),
@@ -169,16 +171,18 @@ def __init__(
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(
@@ -201,7 +205,7 @@ def __init__(
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()
@@ -218,7 +222,7 @@ def __init__(
)
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))
diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py
index db3d834..bf03f52 100644
--- a/qubes_config/global_config/usb_devices.py
+++ b/qubes_config/global_config/usb_devices.py
@@ -554,7 +554,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 9317521..be8b626 100644
--- a/qubes_config/widgets/gtk_utils.py
+++ b/qubes_config/widgets/gtk_utils.py
@@ -88,6 +88,22 @@ def load_icon(icon_name: str, width: int = 24, height: int = 24):
return pixbuf
+def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]:
+ # pylint: disable=unidiomatic-typecheck
+ if type(s) is str:
+ return GLib.markup_escape_text(s)
+ # pylint: disable=unidiomatic-typecheck
+ if type(s) in (float, int, bool):
+ return s
+ 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):
"""
Helper function to display error messages.
@@ -184,7 +200,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 0118bf9..3ea7007 100644
--- a/qui/clipboard.py
+++ b/qui/clipboard.py
@@ -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,24 @@ 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(
- vmname=metadata["vmname"], size=size
+ 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 +155,16 @@ 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 +181,15 @@ 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 +364,11 @@ def update_clipboard_contents(
else:
self.clipboard_label.set_markup(
- _(
- "Global clipboard contents: {0} from {1}"
- ).format(size, vm)
+ markup_format(
+ _("Global clipboard contents: {0} from {1}"),
+ size,
+ vm,
+ )
)
self.icon.set_from_icon_name("edit-copy")
@@ -391,10 +402,14 @@ def setup_ui(self, *_args, **_kwargs):
help_label = Gtk.Label(xalign=0)
help_label.set_markup(
- _(
- "Use {copy} to copy and "
- "{paste} to paste."
- ).format(copy=self.copy_shortcut, paste=self.paste_shortcut)
+ markup_format(
+ _(
+ "Use {copy} to copy and "
+ "{paste} to paste."
+ ),
+ copy=self.copy_shortcut,
+ paste=self.paste_shortcut,
+ )
)
help_item = Gtk.MenuItem()
help_item.set_margin_left(10)
@@ -442,9 +457,11 @@ 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 39faf61..428034d 100644
--- a/qui/decorators.py
+++ b/qui/decorators.py
@@ -10,6 +10,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
@@ -146,12 +147,13 @@ def update_tooltip(self, netvm_changed=False, storage_changed=False):
else:
perc_storage = self.cur_storage / self.max_storage
- tooltip += _(
- "\nTemplate: {template}"
- "\nNetworking: {netvm}"
- "\nPrivate storage: {current_storage:.2f}GB/"
- "{max_storage:.2f}GB ({perc_storage:.1%})"
- ).format(
+ tooltip += markup_format(
+ _(
+ "\nTemplate: {template}"
+ "\nNetworking: {netvm}"
+ "\nPrivate storage: {current_storage:.2f}GB/"
+ "{max_storage:.2f}GB ({perc_storage:.1%})"
+ ),
template=self.template_name,
netvm=self.netvm_name,
current_storage=self.cur_storage,
@@ -193,7 +195,8 @@ def update_state(self, cpu=0, header=False):
.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)
@@ -264,8 +267,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)
@@ -296,7 +300,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 812337b..30129dc 100644
--- a/qui/devices/actionable_widgets.py
+++ b/qui/devices/actionable_widgets.py
@@ -145,7 +145,7 @@ def __init__(
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)
@@ -250,7 +250,9 @@ def __init__(
self, vm: backend.VM, device: backend.Device, variant: str = "dark"
):
super().__init__(
- "detach", "Detach from " + vm.name + "", variant
+ "detach",
+ "Detach from " + GLib.markup_escape_text(vm.name) + "",
+ variant,
)
self.vm = vm
self.device = device
@@ -383,14 +385,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)
@@ -405,7 +407,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)
@@ -447,7 +449,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
diff --git a/qui/tray/disk_space.py b/qui/tray/disk_space.py
index 6e61e0f..e17a318 100644
--- a/qui/tray/disk_space.py
+++ b/qui/tray/disk_space.py
@@ -29,7 +29,7 @@ def __init__(self, vm):
self.check_usage()
- def check_usage(self):
+ def check_usage(self) -> None:
self.problem_volumes = {}
volumes_to_check = ["private"]
if not hasattr(self.vm, "template"):
@@ -52,7 +52,7 @@ def __init__(self, qubes_app):
self.__populate_vms()
- def __populate_vms(self):
+ def __populate_vms(self) -> None:
for vm in self.qubes_app.domains:
try:
if vm.is_running():
@@ -86,10 +86,14 @@ 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)
+ _("volume {} is {:.1%} full").format(
+ 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
@@ -252,13 +256,21 @@ def __create_box(pool: PoolWrapper):
if pool.has_error:
# Pool with errors
- formatted_name = f"{pool.name}"
+ formatted_name = (
+ ''
+ + 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 = (
+ ""
+ + GLib.markup_escape_text(pool.name)
+ + ""
+ )
pool_name.set_markup(formatted_name)
pool_name.set_margin_left(20)
@@ -270,20 +282,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)
@@ -423,13 +435,15 @@ 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")
@@ -484,7 +498,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 8e5fa3d..81e816a 100644
--- a/qui/tray/domains.py
+++ b/qui/tray/domains.py
@@ -80,7 +80,7 @@ def show_error(title, text):
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)
@@ -232,7 +232,7 @@ class PreferencesItem(VMActionMenuItem):
def __init__(self, vm, icon_cache):
super().__init__(vm, icon_cache, "preferences", _("Settings"))
- def perform_action(self):
+ def perform_action(self) -> None:
# pylint: disable=consider-using-with
subprocess.Popen(["qubes-vm-settings", self.vm.name])
@@ -251,7 +251,7 @@ def __init__(self, name, path):
self.connect("activate", self.launch_log_viewer)
- def launch_log_viewer(self, *_args, **_kwargs):
+ def launch_log_viewer(self, *_args, **_kwargs) -> None:
# pylint: disable=consider-using-with
subprocess.Popen(["qubes-log-viewer", self.path])
@@ -270,7 +270,7 @@ def __init__(self, vm, icon_cache):
self.connect("activate", self.run_terminal)
- def run_terminal(self, _item):
+ def run_terminal(self, _item) -> None:
try:
self.vm.run_service("qubes.StartApp+qubes-run-terminal")
except exc.QubesException as ex:
@@ -318,11 +318,15 @@ class InternalInfoItem(Gtk.MenuItem):
def __init__(self):
super().__init__()
self.label = Gtk.Label(xalign=0)
- self.label.set_markup(_("Internal qube"))
+ self.label.set_markup(
+ "" + GLib.markup_escape_text(_("Internal qube")) + ""
+ )
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."
+ _(
+ "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."
+ )
)
self.add(self.label)
self.set_sensitive(False)
@@ -555,7 +559,9 @@ 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 37ce643..48b7355 100644
--- a/qui/tray/updates.py
+++ b/qui/tray/updates.py
@@ -15,7 +15,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
@@ -31,7 +31,7 @@ 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)
@@ -91,7 +91,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(
_(
@@ -104,15 +104,21 @@ def setup_menu(self):
if self.obsolete_vms:
self.tray_menu.append(
- TextItem(_("Some qubes are no longer supported!"))
+ TextItem(_("Some qubes are no longer supported!"))
)
obsolete_text = (
- _(
- "The following qubes are based on distributions "
- "that are no longer supported:\n"
+ 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")
)
- + ", ".join([str(vm) for vm in self.obsolete_vms])
- + _("\nInstall new templates with Template Manager")
+ + ""
)
self.tray_menu.append(
RunItem(obsolete_text, self.launch_template_manager)
diff --git a/qui/utils.py b/qui/utils.py
index 59b0062..4941d4d 100644
--- a/qui/utils.py
+++ b/qui/utils.py
@@ -24,6 +24,7 @@
import sys
import traceback
from html import escape
+from typing import Union
from qubesadmin import exc
@@ -38,7 +39,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)
@@ -65,9 +66,9 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True):
message = _(
"Whoops. A critical error in {} has occurred."
" This is most likely a bug."
- ).format(name)
+ ).format(escape(name))
if restart:
- message += _(" {} will restart itself.").format(name)
+ message += escape(_(" {} will restart itself.").format(name))
for d in done: # pylint: disable=invalid-name
try:
@@ -82,7 +83,7 @@ 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()
@@ -105,6 +106,34 @@ def check_update(vm) -> bool:
return False
+def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]:
+ if isinstance(s, str):
+ # GLib uses NUL-terminated strings
+ assert "\0" not in s, "NUL characters not supported"
+ return GLib.markup_escape_text(s)
+ # For correctness, this relies on str(s) never containing
+ # XML metacharacters, as these will not be escaped.
+ # This is guaranteed for 'float', 'int', and 'bool',
+ # but not for user-defined subclasses of these types.
+ # pylint: disable=unidiomatic-typecheck
+ if type(s) in (float, int, bool):
+ return s
+ v = str(s)
+ if GLib.markup_escape_text(v) != v:
+ raise ValueError(
+ "cannot handle subclass of 'float' or 'int' if "
+ "__str__() returns string needing escaping "
+ f"(value needing escaping is {v!r})"
+ )
+ return v
+
+
+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"""