Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,5 @@ ENV/
*.swp

*.~undo-tree~
*.glade~
*.glade#
2 changes: 1 addition & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ checks:tests:
before_script: &before-script
- "PATH=$PATH:$HOME/.local/bin"
- sudo dnf install -y python3-gobject gtk3 python3-pytest gtksourceview4
python3-coverage xorg-x11-server-Xvfb python3-pip
python3-coverage xorg-x11-server-Xvfb python3-pip xdg-utils
- pip3 install --quiet -r ci/requirements.txt
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
- git clone https://github.com/QubesOS/qubes-core-qrexec ~/core-qrexec
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ install-autostart:
cp desktop/qubes-policy-editor-gui.desktop $(DESTDIR)/usr/share/applications/
install -d $(DESTDIR)/usr/lib/qubes -m 0755
install -m 0755 qui/devices/qubes-device-agent-autostart $(DESTDIR)/usr/lib/qubes/qubes-device-agent-autostart
cp desktop/qubes-virtual-browser.desktop $(DESTDIR)/usr/share/applications/

install-lang:
mkdir -p $(DESTDIR)/usr/share/gtksourceview-4/language-specs/
Expand Down
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Package: qubes-desktop-linux-manager
Architecture: any
Depends:
python3-qui,
xdg-utils,
${misc:Depends}
Description: Qubes UI Applications
A collection of GUI application for enhancing the Qubes UX.
Expand Down
13 changes: 13 additions & 0 deletions desktop/qubes-virtual-browser.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### Note: With this installed, typing "xdg-settings set default-web-browser qubes-virtual-browser.desktop" will make it so that in gnome-terminal
### (typing "xdg-settings set default-web-browser firefox.desktop" will put it back to normal)

[Desktop Entry]
Version=1.0
Name=Qubes Virtual Browser
Exec=/usr/bin/qubes-virtual-browser %u
Icon=qubes-manager
Terminal=false
Type=Application
Categories=Network;WebBrowser;
MimeType=text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https;
NoDisplay=true
227 changes: 215 additions & 12 deletions qubes_config/global_config.glade

Large diffs are not rendered by default.

157 changes: 144 additions & 13 deletions qubes_config/global_config/global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import qubesadmin.vm
from ..widgets.gtk_utils import show_error, show_dialog_with_icon, load_theme
from ..widgets.gtk_widgets import ProgressBarDialog, ViewportHandler
from ..widgets.utils import open_url_in_disposable
from ..widgets.utils import open_url_in_disposable, apply_feature_change
from .page_handler import PageHandler
from .policy_handler import PolicyHandler, VMSubsetPolicyHandler
from .policy_rules import RuleSimple, \
Expand All @@ -49,6 +49,7 @@

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, GObject, Gio
from gi.repository.GdkPixbuf import Pixbuf

logger = logging.getLogger('qubes-global-config')

Expand Down Expand Up @@ -189,6 +190,138 @@ def get_unsaved(self) -> str:
return "\n".join(unsaved)


class UrlPageHandler(PageHandler):
"""Handler for URL page. Requires separate handler because it combines
qubes-virtual-browser settings with qubes.OpenURL policies. """
def __init__(self, qapp: qubesadmin.Qubes,
gtk_builder: Gtk.Builder,
policy_manager: PolicyManager):
self.qapp = qapp
self.builder = gtk_builder
self.policy_manager = policy_manager
self.browser_action = qapp.domains[qapp.local_name].features.get( \
"virtual-browser-action", "")

self.virtual_browser_ask: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_ask')
self.virtual_browser_dispvm: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_dispvm')
self.virtual_browser_clipboard: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_clipboard')
self.virtual_browser_discard: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_discard')

self.virtual_browser_dispvms: Gtk.ComboBox = \
gtk_builder.get_object('virtual_browser_dispvms')
self.disposables = Gtk.ListStore(object, Pixbuf, str)
self.virtual_browser_dispvms.set_model(self.disposables)
self.renderer_icon = Gtk.CellRendererPixbuf()
self.renderer_vmname = Gtk.CellRendererText()
self.virtual_browser_dispvms.pack_start(self.renderer_icon, True)
self.virtual_browser_dispvms.pack_start(self.renderer_vmname, True)
self.virtual_browser_dispvms.add_attribute( \
self.renderer_icon, "pixbuf", 1)
self.virtual_browser_dispvms.add_attribute( \
self.renderer_vmname, "text", 2)

default_dispvm = getattr(self.qapp, "default_dispvm", None)
for domain in qapp.domains:
if getattr(domain, "template_for_dispvms", False):
# pylint: disable=no-member
try:
icon = Gtk.IconTheme.get_default().load_icon(
getattr(domain, "icon", "qubes-manager"), 32, 0)
except gi.repository.GLib.GError:
# Use Adwaita's Qube like icon if original icon is missing
icon = Gtk.IconTheme.get_default().load_icon(
"insert-object-symbolic", 32, 0)
row = self.disposables.append([domain, icon, domain.name])
if domain.name == default_dispvm:
self.disposables[row][2] += " (Default DispVM Template)"

self._reset_virtual_browser_choice()
self.virtual_browser_dispvms.connect("changed", self._dispvm_selected)

# Allocating a list of handlers in case we want to add more in future
self.handlers: List[Union[DispvmExceptionHandler, FeatureHandler]] = [
DispvmExceptionHandler(
gtk_builder=self.builder,
qapp=self.qapp,
service_name="qubes.OpenURL",
policy_file_name="50-config-openurl",
prefix="url",
policy_manager=self.policy_manager,
)
]

def _dispvm_selected(self, combo):
# pylint: disable=unused-argument
self.virtual_browser_dispvm.set_active(True)

def _reset_virtual_browser_choice(self):
if self.browser_action is not None \
and self.browser_action.startswith('disposable:') \
and self.browser_action[11:] in self.qapp.domains:
self.virtual_browser_dispvm.set_active(True)
elif self.browser_action == 'clipboard':
self.virtual_browser_clipboard.set_active(True)
elif self.browser_action == 'discard':
self.virtual_browser_discard.set_active(True)
else:
self.virtual_browser_ask.set_active(True)

default_dispvm = getattr(self.qapp, "default_dispvm", None)
for index, disposable in enumerate(self.disposables):
if self.virtual_browser_dispvm.get_active():
if disposable[0].name == self.browser_action[11:]:
self.virtual_browser_dispvms.set_active(index)
elif disposable[0].name == default_dispvm:
self.virtual_browser_dispvms.set_active(index)

def reset(self):
for handler in self.handlers:
handler.reset()
self._reset_virtual_browser_choice()

def save(self):
for handler in self.handlers:
handler.save()
if self.virtual_browser_ask.get_active():
self.browser_action = None
elif self.virtual_browser_dispvm.get_active():
tree_iter = self.virtual_browser_dispvms.get_active_iter()
self.browser_action = "disposable:" + \
self.disposables[tree_iter][0].name
elif self.virtual_browser_clipboard.get_active():
self.browser_action = "clipboard"
elif self.virtual_browser_discard.get_active():
self.browser_action = "discard"
apply_feature_change(self.qapp.domains[self.qapp.local_name], \
"virtual-browser-action", self.browser_action)

def _virtual_browser_choice(self) -> str:
if self.virtual_browser_ask.get_active():
return ""
if self.virtual_browser_dispvm.get_active():
tree_iter = self.virtual_browser_dispvms.get_active_iter()
return "disposable:" + self.disposables[tree_iter][0].name
if self.virtual_browser_clipboard.get_active():
return "clipboard"
if self.virtual_browser_discard.get_active():
return "discard"
return ""

def get_unsaved(self) -> str:
unsaved = []
for handler in self.handlers:
unsaved_changes = handler.get_unsaved()
if unsaved_changes:
unsaved.append(unsaved_changes)
if self.browser_action != self._virtual_browser_choice():
unsaved.append(_("Qubes Virtual Browser default action"))
return "\n".join(unsaved)


class GlobalConfig(Gtk.Application):
"""
Main Gtk.Application for new qube widget.
Expand Down Expand Up @@ -403,14 +536,12 @@ def perform_setup(self):
policy_manager=self.policy_manager
)
self.progress_bar_dialog.update_progress(page_progress)
self.handlers['url'] = DispvmExceptionHandler(
gtk_builder=self.builder,
qapp=self.qapp,
service_name="qubes.OpenURL",
policy_file_name="50-config-openurl",
prefix="url",
policy_manager=self.policy_manager,
)

self.handlers['url'] = UrlPageHandler(
qapp=self.qapp,
gtk_builder=self.builder,
policy_manager=self.policy_manager
)
self.progress_bar_dialog.update_progress(page_progress)

self.handlers['thisdevice'] = ThisDeviceHandler(self.qapp,
Expand Down Expand Up @@ -452,7 +583,7 @@ def _handle_urls(self):
label.connect("activate-link", self._activate_link)

def _activate_link(self, _widget, url):
open_url_in_disposable(url, self.qapp)
open_url_in_disposable(url)
return True

def get_current_page(self) -> Optional[PageHandler]:
Expand Down Expand Up @@ -520,9 +651,9 @@ def _ask_unsaved(self, description: str) -> Gtk.ResponseType:
label_3 = Gtk.Label()
label_3.set_text(_("Do you want to save changes?"))
label_3.set_xalign(0)
box.pack_start(label_1, False, False, 10)
box.pack_start(label_2, False, False, 10)
box.pack_start(label_3, False, False, 10)
box.pack_start(label_1, False, False, 10) # pylint: disable=no-member
box.pack_start(label_2, False, False, 10) # pylint: disable=no-member
box.pack_start(label_3, False, False, 10) # pylint: disable=no-member

response = show_dialog_with_icon(
parent=self.main_window, title=_("Unsaved changes"), text=box,
Expand Down
4 changes: 1 addition & 3 deletions qubes_config/policy_editor/policy_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import gi
import importlib.resources

import qubesadmin
from qrexec.policy.admin_client import PolicyClient
from qrexec.policy.parser import StringPolicy
from qrexec.exc import PolicySyntaxError
Expand Down Expand Up @@ -246,8 +245,7 @@ def perform_setup(self):

@staticmethod
def _open_docs(_widget, url):
qapp = qubesadmin.Qubes()
open_url_in_disposable(url, qapp)
open_url_in_disposable(url)
return True

def setup_actions(self):
Expand Down
9 changes: 6 additions & 3 deletions qubes_config/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ def test_qapp_impl():
'config.default.qubes-update-check': None,
'config-usbvm-name': None,
'gui-default-secure-copy-sequence': None,
'gui-default-secure-paste-sequence': None
'gui-default-secure-paste-sequence': None,
'virtual-browser-action': None
}, [])
add_expected_vm(qapp, 'sys-net', 'AppVM',
{'provides_network': ('bool', False, 'True')},
Expand Down Expand Up @@ -373,7 +374,8 @@ def test_qapp_simple(): # pylint: disable=redefined-outer-name
'config.default.qubes-update-check': None,
'config-usbvm-name': None,
'gui-default-secure-copy-sequence': None,
'gui-default-secure-paste-sequence': None
'gui-default-secure-paste-sequence': None,
'virtual-browser-action': None
}, [])
add_expected_vm(qapp, 'sys-net', 'AppVM',
{'provides_network': ('bool', False, 'True')},
Expand Down Expand Up @@ -427,7 +429,8 @@ def test_qapp_broken(): # pylint: disable=redefined-outer-name
'config.default.qubes-update-check': None,
'config-usbvm-name': None,
'gui-default-secure-copy-sequence': None,
'gui-default-secure-paste-sequence': None
'gui-default-secure-paste-sequence': None,
'virtual-browser-action': None
}, [])

#
Expand Down
12 changes: 5 additions & 7 deletions qubes_config/widgets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,15 @@ def compare_rule_lists(rule_list_1: List[Rule],
return False
return True

def _open_url_in_dvm(url, default_dvm: qubesadmin.vm.QubesVM):
def _open_url(url):
subprocess.run(
['qvm-run', '-p', '--service', f'--dispvm={default_dvm}',
'qubes.OpenURL'], input=url.encode(), check=False,
['qubes-virtual-browser', url.encode()], input=None, check=False,
stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)

def open_url_in_disposable(url: str, qapp: qubesadmin.Qubes):
def open_url_in_disposable(url: str):
"""Open provided url in disposable qube based on default disposable
template"""
default_dvm = qapp.default_dispvm
open_thread = threading.Thread(group=None,
target=_open_url_in_dvm,
args=[url, default_dvm])
target=_open_url,
args=[url])
open_thread.start()
Loading