From 54ab9ce98fbf7a72ab9010698224db8f2f0a84c4 Mon Sep 17 00:00:00 2001 From: Ro Date: Mon, 10 Jun 2024 11:45:31 -0400 Subject: [PATCH] Order list of PDF viewers and return default application first (Linux). --- dangerzone/gui/logic.py | 36 +++++++++++++-- tests/gui/test_logic.py | 100 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 tests/gui/test_logic.py diff --git a/dangerzone/gui/logic.py b/dangerzone/gui/logic.py index effe8bb..d5fd611 100644 --- a/dangerzone/gui/logic.py +++ b/dangerzone/gui/logic.py @@ -4,6 +4,7 @@ import platform import shlex import subprocess import typing +from collections import OrderedDict from pathlib import Path from typing import Dict, Optional @@ -49,7 +50,7 @@ class DangerzoneGui(DangerzoneCore): # Preload font self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - # Preload list of PDF viewers on computer + # Preload ordered list of PDF viewers on computer, starting with default self.pdf_viewers = self._find_pdf_viewers() # Are we done waiting (for Docker Desktop to be installed, or for container to install) @@ -93,9 +94,22 @@ class DangerzoneGui(DangerzoneCore): log.info(Fore.YELLOW + "> " + Fore.CYAN + args_str) subprocess.Popen(args) - def _find_pdf_viewers(self) -> Dict[str, str]: - pdf_viewers: Dict[str, str] = {} + def _find_pdf_viewers(self) -> OrderedDict[str, str]: + pdf_viewers: OrderedDict[str, str] = OrderedDict() if platform.system() == "Linux": + # Opportunistically query for default pdf handler + default_pdf_viewer = None + try: + default_pdf_viewer = subprocess.check_output( + ["xdg-mime", "query", "default", "application/pdf"] + ).decode() + except (FileNotFoundError, subprocess.CalledProcessError) as e: + # Log it and continue + log.info( + "xdg-mime query failed, default PDF handler could not be found." + ) + log.debug(f"xdg-mime query failed: {e}") + # Find all .desktop files for search_path in [ "/usr/share/applications", @@ -108,14 +122,26 @@ class DangerzoneGui(DangerzoneCore): if os.path.splitext(filename)[1] == ".desktop": # See which ones can open PDFs desktop_entry = DesktopEntry(full_filename) + desktop_entry_name = desktop_entry.getName() if ( "application/pdf" in desktop_entry.getMimeTypes() - and "dangerzone" not in desktop_entry.getName().lower() + and "dangerzone" not in desktop_entry_name.lower() ): - pdf_viewers[desktop_entry.getName()] = ( + pdf_viewers[desktop_entry_name] = ( desktop_entry.getExec() ) + # Put the default entry first + if filename == default_pdf_viewer: + try: + pdf_viewers.move_to_end( + desktop_entry_name, last=False + ) + except KeyError as e: + # Should be unreachable + log.error( + f"Problem reordering applications: {e}" + ) except FileNotFoundError: pass diff --git a/tests/gui/test_logic.py b/tests/gui/test_logic.py new file mode 100644 index 0000000..a2bf6e5 --- /dev/null +++ b/tests/gui/test_logic.py @@ -0,0 +1,100 @@ +import platform +import subprocess +from unittest import mock + +import pytest + +from dangerzone.gui.logic import DangerzoneGui + +if platform.system() == "Linux": + from xdg.DesktopEntry import DesktopEntry + + +@pytest.mark.skipif(platform.system() != "Linux", reason="Linux-only test") +def test_order_mime_handers() -> None: + """ + Given a default mime handler returned by xdg-mime, + ensure it is the first item available in the list + of compatible applications. + """ + mock_app = mock.MagicMock() + dummy = mock.MagicMock() + + mock_desktop = mock.MagicMock(spec=DesktopEntry) + mock_desktop.getMimeTypes.return_value = "application/pdf" + mock_desktop.getExec.side_effect = [ + "/usr/bin/madeup-evince", + "/usr/local/bin/madeup-mupdf", + "/usr/local/bin/madeup-libredraw", + ] + mock_desktop.getName.side_effect = [ + "Evince", + "MuPDF", + "LibreOffice", + ] + + with mock.patch( + "subprocess.check_output", return_value=b"libreoffice-draw.desktop" + ) as mock_default_mime_hander, mock.patch( + "os.listdir", + side_effect=[ + ["org.gnome.Evince.desktop"], + ["org.pwmt.zathura-pdf-mupdf.desktop"], + ["libreoffice-draw.desktop"], + ], + ) as mock_list, mock.patch( + "dangerzone.gui.logic.DesktopEntry", return_value=mock_desktop + ): + dz = DangerzoneGui(mock_app, dummy) + + mock_default_mime_hander.assert_called_once_with( + ["xdg-mime", "query", "default", "application/pdf"] + ) + mock_list.assert_called() + assert len(dz.pdf_viewers) == 3 + assert dz.pdf_viewers.popitem(last=False)[0] == "LibreOffice" + + +@pytest.mark.skipif(platform.system() != "Linux", reason="Linux-only test") +def test_mime_handers_succeeds_no_default_found() -> None: + """ + Given a failure to return default mime handler, + ensure compatible applications are still returned. + """ + mock_app = mock.MagicMock() + dummy = mock.MagicMock() + + mock_desktop = mock.MagicMock(spec=DesktopEntry) + mock_desktop.getMimeTypes.return_value = "application/pdf" + mock_desktop.getExec.side_effect = [ + "/usr/bin/madeup-evince", + "/usr/local/bin/madeup-mupdf", + "/usr/local/bin/madeup-libredraw", + ] + mock_desktop.getName.side_effect = [ + "Evince", + "MuPDF", + "LibreOffice", + ] + + with mock.patch( + "subprocess.check_output", + side_effect=subprocess.CalledProcessError(1, "Oh no, xdg-mime error!)"), + ) as mock_default_mime_hander, mock.patch( + "os.listdir", + side_effect=[ + ["org.gnome.Evince.desktop"], + ["org.pwmt.zathura-pdf-mupdf.desktop"], + ["libreoffice-draw.desktop"], + ], + ) as mock_list, mock.patch( + "dangerzone.gui.logic.DesktopEntry", return_value=mock_desktop + ): + dz = DangerzoneGui(mock_app, dummy) + + mock_default_mime_hander.assert_called_once_with( + ["xdg-mime", "query", "default", "application/pdf"] + ) + mock_list.assert_called() + assert len(dz.pdf_viewers) == 3 + assert dz.pdf_viewers.popitem(last=False)[0] == "Evince"