From 6ff68f88ea66a20adfd7368034d8952401661295 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 9 Jun 2021 15:24:03 -0700 Subject: [PATCH 01/12] Refactor dangerzone to move GUI code into its own module --- dangerzone/__init__.py | 9 +- dangerzone/cli.py | 8 + dangerzone/global_common.py | 279 +----------------- dangerzone/{gui.py => gui/__init__.py} | 23 +- dangerzone/gui/common.py | 295 +++++++++++++++++++ dangerzone/{ => gui}/doc_selection_widget.py | 0 dangerzone/{ => gui}/docker_installer.py | 9 +- dangerzone/{ => gui}/main_window.py | 19 +- dangerzone/{ => gui}/settings_widget.py | 8 +- dangerzone/{ => gui}/tasks_widget.py | 11 +- dangerzone/settings.py | 8 +- dev_scripts/dangerzone-cli | 1 + pyproject.toml | 1 + 13 files changed, 350 insertions(+), 321 deletions(-) create mode 100644 dangerzone/cli.py rename dangerzone/{gui.py => gui/__init__.py} (90%) create mode 100644 dangerzone/gui/common.py rename dangerzone/{ => gui}/doc_selection_widget.py (100%) rename dangerzone/{ => gui}/docker_installer.py (97%) rename dangerzone/{ => gui}/main_window.py (84%) rename dangerzone/{ => gui}/settings_widget.py (97%) rename dangerzone/{ => gui}/tasks_widget.py (92%) create mode 120000 dev_scripts/dangerzone-cli diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 8edcba8..8021fc4 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -3,11 +3,14 @@ import sys dangerzone_version = "0.1.5" -# This is a hack for Windows and Mac to be able to run dangerzone-container, even though -# PyInstaller builds a single binary +# Depending on the filename, decide if we want to run: +# dangerzone, dangerzone-cli, or dangerzone-container + basename = os.path.basename(sys.argv[0]) + if basename == "dangerzone-container" or basename == "dangerzone-container.exe": from .container import container_main as main +elif basename == "dangerzone-cli" or basename == "dangerzone-cli.exe": + from .cli import cli_main as main else: - # If the binary isn't "dangerzone-contatiner", then launch the GUI from .gui import gui_main as main diff --git a/dangerzone/cli.py b/dangerzone/cli.py new file mode 100644 index 0000000..7265bc9 --- /dev/null +++ b/dangerzone/cli.py @@ -0,0 +1,8 @@ +import click + + +@click.command() +@click.option("--custom-container") # Use this container instead of flmcode/dangerzone +@click.argument("filename", required=False) +def cli_main(custom_container, filename): + pass diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index 0f74a6e..084e79f 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -1,26 +1,13 @@ import sys import os import inspect -import tempfile import appdirs import platform import subprocess -import shlex import pipes from PySide2 import QtCore, QtGui, QtWidgets -if platform.system() == "Darwin": - import CoreServices - import LaunchServices - import plistlib - -elif platform.system() == "Linux": - import grp - import getpass - from xdg.DesktopEntry import DesktopEntry - from .settings import Settings -from .docker_installer import is_docker_ready class GlobalCommon(object): @@ -28,19 +15,7 @@ class GlobalCommon(object): The GlobalCommon class is a singleton of shared functionality throughout the app """ - def __init__(self, app): - # Qt app - self.app = app - - # Name of input file - self.document_filename = None - - # Name of output file - self.save_filename = None - - # Preload font - self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - + def __init__(self): # App data folder self.appdata_path = appdirs.user_config_dir("dangerzone") @@ -50,9 +25,6 @@ class GlobalCommon(object): # dangerzone-container path self.dz_container_path = self.get_dangerzone_container_path() - # Preload list of PDF viewers on computer - self.pdf_viewers = self._find_pdf_viewers() - # Languages supported by tesseract self.ocr_languages = { "Afrikaans": "ar", @@ -295,187 +267,6 @@ class GlobalCommon(object): stderr=subprocess.PIPE, ) - def get_window_icon(self): - if platform.system() == "Windows": - path = self.get_resource_path("dangerzone.ico") - else: - path = self.get_resource_path("icon.png") - return QtGui.QIcon(path) - - def open_pdf_viewer(self, filename): - if self.settings.get("open_app") in self.pdf_viewers: - if platform.system() == "Darwin": - # Get the PDF reader bundle command - bundle_identifier = self.pdf_viewers[self.settings.get("open_app")] - args = ["open", "-b", bundle_identifier, filename] - - # Run - print(f"Executing: {' '.join(args)}") - subprocess.run(args) - - elif platform.system() == "Linux": - # Get the PDF reader command - args = shlex.split(self.pdf_viewers[self.settings.get("open_app")]) - # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open - for i in range(len(args)): - if ( - args[i] == "%f" - or args[i] == "%F" - or args[i] == "%u" - or args[i] == "%U" - ): - args[i] = filename - - # Open as a background process - print(f"Executing: {' '.join(args)}") - subprocess.Popen(args) - - def _find_pdf_viewers(self): - pdf_viewers = {} - - if platform.system() == "Darwin": - # Get all installed apps that can open PDFs - bundle_identifiers = LaunchServices.LSCopyAllRoleHandlersForContentType( - "com.adobe.pdf", CoreServices.kLSRolesAll - ) - for bundle_identifier in bundle_identifiers: - # Get the filesystem path of the app - res = LaunchServices.LSCopyApplicationURLsForBundleIdentifier( - bundle_identifier, None - ) - if res[0] is None: - continue - app_url = res[0][0] - app_path = str(app_url.path()) - - # Load its plist file - plist_path = os.path.join(app_path, "Contents/Info.plist") - - # Skip if there's not an Info.plist - if not os.path.exists(plist_path): - continue - - with open(plist_path, "rb") as f: - plist_data = f.read() - - plist_dict = plistlib.loads(plist_data) - - if ( - plist_dict.get("CFBundleName") - and plist_dict["CFBundleName"] != "Dangerzone" - ): - pdf_viewers[plist_dict["CFBundleName"]] = bundle_identifier - - elif platform.system() == "Linux": - # Find all .desktop files - for search_path in [ - "/usr/share/applications", - "/usr/local/share/applications", - os.path.expanduser("~/.local/share/applications"), - ]: - try: - for filename in os.listdir(search_path): - full_filename = os.path.join(search_path, filename) - if os.path.splitext(filename)[1] == ".desktop": - - # See which ones can open PDFs - desktop_entry = DesktopEntry(full_filename) - if ( - "application/pdf" in desktop_entry.getMimeTypes() - and desktop_entry.getName() != "dangerzone" - ): - pdf_viewers[ - desktop_entry.getName() - ] = desktop_entry.getExec() - - except FileNotFoundError: - pass - - return pdf_viewers - - def ensure_docker_group_preference(self): - # If the user prefers typing their password - if self.settings.get("linux_prefers_typing_password") == True: - return True - - # Get the docker group - try: - groupinfo = grp.getgrnam("docker") - except: - # Ignore if group is not found - return True - - # See if the user is in the group - username = getpass.getuser() - if username not in groupinfo.gr_mem: - # User is not in the docker group, ask if they prefer typing their password - message = "Dangerzone requires Docker

In order to use Docker, your user must be in the 'docker' group or you'll need to type your password each time you run dangerzone.

Adding your user to the 'docker' group is more convenient but less secure, and will require just typing your password once. Which do you prefer?" - return_code = Alert( - self, - message, - ok_text="I'll type my password each time", - extra_button_text="Add my user to the 'docker' group", - ).launch() - if return_code == QtWidgets.QDialog.Accepted: - # Prefers typing password - self.settings.set("linux_prefers_typing_password", True) - self.settings.save() - return True - elif return_code == 2: - # Prefers being in the docker group - self.settings.set("linux_prefers_typing_password", False) - self.settings.save() - - # Add user to the docker group - p = subprocess.run( - [ - "/usr/bin/pkexec", - "/usr/sbin/usermod", - "-a", - "-G", - "docker", - username, - ] - ) - if p.returncode == 0: - message = "Great! Now you must log out of your computer and log back in, and then you can use Dangerzone." - Alert(self, message).launch() - else: - message = "Failed to add your user to the 'docker' group, quitting." - Alert(self, message).launch() - - return False - else: - # Cancel - return False - - return True - - def ensure_docker_service_is_started(self): - if not is_docker_ready(self): - message = "Dangerzone requires Docker

Docker should be installed, but it looks like it's not running in the background.

Click Ok to try starting the docker service. You will have to type your login password." - if Alert(self, message).launch() == QtWidgets.QDialog.Accepted: - p = subprocess.run( - [ - "/usr/bin/pkexec", - self.get_resource_path("enable_docker_service.sh"), - ] - ) - if p.returncode == 0: - # Make sure docker is now ready - if is_docker_ready(self): - return True - else: - message = "Restarting docker appeared to work, but the service still isn't responding, quitting." - Alert(self, message).launch() - else: - message = "Failed to start the docker service, quitting." - Alert(self, message).launch() - - return False - - return True - def get_subprocess_startupinfo(self): if platform.system() == "Windows": startupinfo = subprocess.STARTUPINFO() @@ -483,71 +274,3 @@ class GlobalCommon(object): return startupinfo else: return None - - -class Alert(QtWidgets.QDialog): - def __init__(self, common, message, ok_text="Ok", extra_button_text=None): - super(Alert, self).__init__() - self.common = common - - self.setWindowTitle("dangerzone") - self.setWindowIcon(self.common.get_window_icon()) - self.setModal(True) - - flags = ( - QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.WindowTitleHint - | QtCore.Qt.WindowSystemMenuHint - | QtCore.Qt.WindowCloseButtonHint - | QtCore.Qt.WindowStaysOnTopHint - ) - self.setWindowFlags(flags) - - logo = QtWidgets.QLabel() - logo.setPixmap( - QtGui.QPixmap.fromImage( - QtGui.QImage(self.common.get_resource_path("icon.png")) - ) - ) - - label = QtWidgets.QLabel() - label.setText(message) - label.setWordWrap(True) - - message_layout = QtWidgets.QHBoxLayout() - message_layout.addWidget(logo) - message_layout.addSpacing(10) - message_layout.addWidget(label, stretch=1) - - ok_button = QtWidgets.QPushButton(ok_text) - ok_button.clicked.connect(self.clicked_ok) - if extra_button_text: - extra_button = QtWidgets.QPushButton(extra_button_text) - extra_button.clicked.connect(self.clicked_extra) - cancel_button = QtWidgets.QPushButton("Cancel") - cancel_button.clicked.connect(self.clicked_cancel) - - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addStretch() - buttons_layout.addWidget(ok_button) - if extra_button_text: - buttons_layout.addWidget(extra_button) - buttons_layout.addWidget(cancel_button) - - layout = QtWidgets.QVBoxLayout() - layout.addLayout(message_layout) - layout.addSpacing(10) - layout.addLayout(buttons_layout) - self.setLayout(layout) - - def clicked_ok(self): - self.done(QtWidgets.QDialog.Accepted) - - def clicked_extra(self): - self.done(2) - - def clicked_cancel(self): - self.done(QtWidgets.QDialog.Rejected) - - def launch(self): - return self.exec_() diff --git a/dangerzone/gui.py b/dangerzone/gui/__init__.py similarity index 90% rename from dangerzone/gui.py rename to dangerzone/gui/__init__.py index 4fe1107..a2c59a8 100644 --- a/dangerzone/gui.py +++ b/dangerzone/gui/__init__.py @@ -3,21 +3,19 @@ import sys import signal import platform import click -import time import uuid -import subprocess from PySide2 import QtCore, QtWidgets -from .global_common import GlobalCommon +from .common import GuiCommon from .main_window import MainWindow from .docker_installer import ( is_docker_installed, is_docker_ready, - launch_docker_windows, DockerInstaller, AuthorizationFailed, ) -from .container import container_runtime +from ..global_common import GlobalCommon +from ..container import container_runtime # For some reason, Dangerzone segfaults if I inherit from QApplication directly, so instead @@ -59,8 +57,9 @@ def gui_main(custom_container, filename): app_wrapper = ApplicationWrapper() app = app_wrapper.app - # GlobalCommon object - global_common = GlobalCommon(app) + # Common objects + global_common = GlobalCommon() + gui_common = GuiCommon(app, global_common) if custom_container: # Do we have this container? @@ -89,10 +88,10 @@ def gui_main(custom_container, filename): # If we're using Linux and docker, see if we need to add the user to the docker group or if the user prefers typing their password if platform.system() == "Linux": - if not global_common.ensure_docker_group_preference(): + if not gui_common.ensure_docker_group_preference(): return try: - if not global_common.ensure_docker_service_is_started(): + if not gui_common.ensure_docker_service_is_started(): click.echo("Failed to start docker service") return except AuthorizationFailed: @@ -101,10 +100,10 @@ def gui_main(custom_container, filename): # See if we need to install Docker... if (platform.system() == "Darwin" or platform.system() == "Windows") and ( - not is_docker_installed(global_common) or not is_docker_ready(global_common) + not is_docker_installed() or not is_docker_ready(global_common) ): click.echo("Docker is either not installed or not running") - docker_installer = DockerInstaller(global_common) + docker_installer = DockerInstaller(gui_common) docker_installer.start() return @@ -124,7 +123,7 @@ def gui_main(custom_container, filename): window = windows[list(windows.keys())[0]] else: window_id = uuid.uuid4().hex - window = MainWindow(global_common, window_id) + window = MainWindow(global_common, gui_common, window_id) window.delete_window.connect(delete_window) windows[window_id] = window diff --git a/dangerzone/gui/common.py b/dangerzone/gui/common.py new file mode 100644 index 0000000..982121e --- /dev/null +++ b/dangerzone/gui/common.py @@ -0,0 +1,295 @@ +import os +import platform +import subprocess +import shlex +from PySide2 import QtCore, QtGui, QtWidgets + +if platform.system() == "Darwin": + import CoreServices + import LaunchServices + import plistlib + +elif platform.system() == "Linux": + import grp + import getpass + from xdg.DesktopEntry import DesktopEntry + +from .docker_installer import is_docker_ready +from ..settings import Settings + + +class GuiCommon(object): + """ + The GuiCommon class is a singleton of shared functionality for the GUI + """ + + def __init__(self, app, global_common): + # Qt app + self.app = app + + # Global common singleton + self.global_common = global_common + + # Preload font + self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + + # Preload list of PDF viewers on computer + self.pdf_viewers = self._find_pdf_viewers() + + def get_window_icon(self): + if platform.system() == "Windows": + path = self.global_common.get_resource_path("dangerzone.ico") + else: + path = self.global_common.get_resource_path("icon.png") + return QtGui.QIcon(path) + + def open_pdf_viewer(self, filename): + if self.global_common.settings.get("open_app") in self.pdf_viewers: + if platform.system() == "Darwin": + # Get the PDF reader bundle command + bundle_identifier = self.pdf_viewers[self.global_common.settings.get("open_app")] + args = ["open", "-b", bundle_identifier, filename] + + # Run + print(f"Executing: {' '.join(args)}") + subprocess.run(args) + + elif platform.system() == "Linux": + # Get the PDF reader command + args = shlex.split(self.pdf_viewers[self.global_common.settings.get("open_app")]) + # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open + for i in range(len(args)): + if ( + args[i] == "%f" + or args[i] == "%F" + or args[i] == "%u" + or args[i] == "%U" + ): + args[i] = filename + + # Open as a background process + print(f"Executing: {' '.join(args)}") + subprocess.Popen(args) + + def _find_pdf_viewers(self): + pdf_viewers = {} + + if platform.system() == "Darwin": + # Get all installed apps that can open PDFs + bundle_identifiers = LaunchServices.LSCopyAllRoleHandlersForContentType( + "com.adobe.pdf", CoreServices.kLSRolesAll + ) + for bundle_identifier in bundle_identifiers: + # Get the filesystem path of the app + res = LaunchServices.LSCopyApplicationURLsForBundleIdentifier( + bundle_identifier, None + ) + if res[0] is None: + continue + app_url = res[0][0] + app_path = str(app_url.path()) + + # Load its plist file + plist_path = os.path.join(app_path, "Contents/Info.plist") + + # Skip if there's not an Info.plist + if not os.path.exists(plist_path): + continue + + with open(plist_path, "rb") as f: + plist_data = f.read() + + plist_dict = plistlib.loads(plist_data) + + if ( + plist_dict.get("CFBundleName") + and plist_dict["CFBundleName"] != "Dangerzone" + ): + pdf_viewers[plist_dict["CFBundleName"]] = bundle_identifier + + elif platform.system() == "Linux": + # Find all .desktop files + for search_path in [ + "/usr/share/applications", + "/usr/local/share/applications", + os.path.expanduser("~/.local/share/applications"), + ]: + try: + for filename in os.listdir(search_path): + full_filename = os.path.join(search_path, filename) + if os.path.splitext(filename)[1] == ".desktop": + + # See which ones can open PDFs + desktop_entry = DesktopEntry(full_filename) + if ( + "application/pdf" in desktop_entry.getMimeTypes() + and desktop_entry.getName() != "dangerzone" + ): + pdf_viewers[ + desktop_entry.getName() + ] = desktop_entry.getExec() + + except FileNotFoundError: + pass + + return pdf_viewers + + def ensure_docker_group_preference(self): + # If the user prefers typing their password + if self.global_common.settings.get("linux_prefers_typing_password") == True: + return True + + # Get the docker group + try: + groupinfo = grp.getgrnam("docker") + except: + # Ignore if group is not found + return True + + # See if the user is in the group + username = getpass.getuser() + if username not in groupinfo.gr_mem: + # User is not in the docker group, ask if they prefer typing their password + message = "Dangerzone requires Docker

In order to use Docker, your user must be in the 'docker' group or you'll need to type your password each time you run dangerzone.

Adding your user to the 'docker' group is more convenient but less secure, and will require just typing your password once. Which do you prefer?" + return_code = Alert( + self, + self.global_common, + message, + ok_text="I'll type my password each time", + extra_button_text="Add my user to the 'docker' group", + ).launch() + if return_code == QtWidgets.QDialog.Accepted: + # Prefers typing password + self.global_common.settings.set("linux_prefers_typing_password", True) + self.global_common.settings.save() + return True + elif return_code == 2: + # Prefers being in the docker group + self.global_common.settings.set("linux_prefers_typing_password", False) + self.global_common.settings.save() + + # Add user to the docker group + p = subprocess.run( + [ + "/usr/bin/pkexec", + "/usr/sbin/usermod", + "-a", + "-G", + "docker", + username, + ] + ) + if p.returncode == 0: + message = "Great! Now you must log out of your computer and log back in, and then you can use Dangerzone." + Alert(self, self.global_common, message).launch() + else: + message = "Failed to add your user to the 'docker' group, quitting." + Alert(self, self.global_common, message).launch() + + return False + else: + # Cancel + return False + + return True + + def ensure_docker_service_is_started(self): + if not is_docker_ready(self): + message = "Dangerzone requires Docker

Docker should be installed, but it looks like it's not running in the background.

Click Ok to try starting the docker service. You will have to type your login password." + if ( + Alert(self, self.global_common, message).launch() + == QtWidgets.QDialog.Accepted + ): + p = subprocess.run( + [ + "/usr/bin/pkexec", + self.global_common.get_resource_path( + "enable_docker_service.sh" + ), + ] + ) + if p.returncode == 0: + # Make sure docker is now ready + if is_docker_ready(self): + return True + else: + message = "Restarting docker appeared to work, but the service still isn't responding, quitting." + Alert(self, self.global_common, message).launch() + else: + message = "Failed to start the docker service, quitting." + Alert(self, self.global_common, message).launch() + + return False + + return True + + +class Alert(QtWidgets.QDialog): + def __init__( + self, global_common, gui_common, message, ok_text="Ok", extra_button_text=None + ): + super(Alert, self).__init__() + self.global_common = global_common + self.gui_common = gui_common + + self.setWindowTitle("dangerzone") + self.setWindowIcon(self.gui_common.get_window_icon()) + self.setModal(True) + + flags = ( + QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowSystemMenuHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + self.setWindowFlags(flags) + + logo = QtWidgets.QLabel() + logo.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(self.global_common.get_resource_path("icon.png")) + ) + ) + + label = QtWidgets.QLabel() + label.setText(message) + label.setWordWrap(True) + + message_layout = QtWidgets.QHBoxLayout() + message_layout.addWidget(logo) + message_layout.addSpacing(10) + message_layout.addWidget(label, stretch=1) + + ok_button = QtWidgets.QPushButton(ok_text) + ok_button.clicked.connect(self.clicked_ok) + if extra_button_text: + extra_button = QtWidgets.QPushButton(extra_button_text) + extra_button.clicked.connect(self.clicked_extra) + cancel_button = QtWidgets.QPushButton("Cancel") + cancel_button.clicked.connect(self.clicked_cancel) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addStretch() + buttons_layout.addWidget(ok_button) + if extra_button_text: + buttons_layout.addWidget(extra_button) + buttons_layout.addWidget(cancel_button) + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(message_layout) + layout.addSpacing(10) + layout.addLayout(buttons_layout) + self.setLayout(layout) + + def clicked_ok(self): + self.done(QtWidgets.QDialog.Accepted) + + def clicked_extra(self): + self.done(2) + + def clicked_cancel(self): + self.done(QtWidgets.QDialog.Rejected) + + def launch(self): + return self.exec_() diff --git a/dangerzone/doc_selection_widget.py b/dangerzone/gui/doc_selection_widget.py similarity index 100% rename from dangerzone/doc_selection_widget.py rename to dangerzone/gui/doc_selection_widget.py diff --git a/dangerzone/docker_installer.py b/dangerzone/gui/docker_installer.py similarity index 97% rename from dangerzone/docker_installer.py rename to dangerzone/gui/docker_installer.py index 6def8ee..150b2f2 100644 --- a/dangerzone/docker_installer.py +++ b/dangerzone/gui/docker_installer.py @@ -8,14 +8,14 @@ import time import platform from PySide2 import QtCore, QtGui, QtWidgets -from .container import container_runtime +from ..container import container_runtime class AuthorizationFailed(Exception): pass -def is_docker_installed(global_common): +def is_docker_installed(): if platform.system() == "Darwin": # Does the docker binary exist? if os.path.isdir("/Applications/Docker.app") and os.path.exists( @@ -55,12 +55,11 @@ def launch_docker_windows(global_common): class DockerInstaller(QtWidgets.QDialog): - def __init__(self, global_common): + def __init__(self, gui_common): super(DockerInstaller, self).__init__() - self.global_common = global_common self.setWindowTitle("dangerzone") - self.setWindowIcon(self.global_common.get_window_icon()) + self.setWindowIcon(gui_common.get_window_icon()) # self.setMinimumHeight(170) label = QtWidgets.QLabel() diff --git a/dangerzone/main_window.py b/dangerzone/gui/main_window.py similarity index 84% rename from dangerzone/main_window.py rename to dangerzone/gui/main_window.py index 209af08..4477f6c 100644 --- a/dangerzone/main_window.py +++ b/dangerzone/gui/main_window.py @@ -6,20 +6,21 @@ from PySide2 import QtCore, QtGui, QtWidgets from .doc_selection_widget import DocSelectionWidget from .settings_widget import SettingsWidget from .tasks_widget import TasksWidget -from .common import Common +from ..common import Common class MainWindow(QtWidgets.QMainWindow): delete_window = QtCore.Signal(str) - def __init__(self, global_common, window_id): + def __init__(self, global_common, gui_common, window_id): super(MainWindow, self).__init__() self.global_common = global_common + self.gui_common = gui_common self.window_id = window_id self.common = Common() self.setWindowTitle("dangerzone") - self.setWindowIcon(self.global_common.get_window_icon()) + self.setWindowIcon(self.gui_common.get_window_icon()) self.setMinimumWidth(600) self.setMinimumHeight(400) @@ -32,7 +33,7 @@ class MainWindow(QtWidgets.QMainWindow): ) ) header_label = QtWidgets.QLabel("dangerzone") - header_label.setFont(self.global_common.fixed_font) + header_label.setFont(self.gui_common.fixed_font) header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }") header_layout = QtWidgets.QHBoxLayout() header_layout.addStretch() @@ -47,7 +48,9 @@ class MainWindow(QtWidgets.QMainWindow): self.doc_selection_widget.show() # Settings - self.settings_widget = SettingsWidget(self.global_common, self.common) + self.settings_widget = SettingsWidget( + self.global_common, self.gui_common, self.common + ) self.doc_selection_widget.document_selected.connect( self.settings_widget.document_selected ) @@ -59,7 +62,9 @@ class MainWindow(QtWidgets.QMainWindow): ) # Tasks - self.tasks_widget = TasksWidget(self.global_common, self.common) + self.tasks_widget = TasksWidget( + self.global_common, self.gui_common, self.common + ) self.tasks_widget.close_window.connect(self.close) self.doc_selection_widget.document_selected.connect( self.tasks_widget.document_selected @@ -93,4 +98,4 @@ class MainWindow(QtWidgets.QMainWindow): self.delete_window.emit(self.window_id) if platform.system() != "Darwin": - self.global_common.app.quit() + self.gui_common.app.quit() diff --git a/dangerzone/settings_widget.py b/dangerzone/gui/settings_widget.py similarity index 97% rename from dangerzone/settings_widget.py rename to dangerzone/gui/settings_widget.py index 4150653..759818f 100644 --- a/dangerzone/settings_widget.py +++ b/dangerzone/gui/settings_widget.py @@ -1,5 +1,4 @@ import os -import subprocess import platform from PySide2 import QtCore, QtGui, QtWidgets @@ -8,9 +7,10 @@ class SettingsWidget(QtWidgets.QWidget): start_clicked = QtCore.Signal() close_window = QtCore.Signal() - def __init__(self, global_common, common): + def __init__(self, global_common, gui_common, common): super(SettingsWidget, self).__init__() self.global_common = global_common + self.gui_common = gui_common self.common = common # Dangerous document label @@ -49,8 +49,8 @@ class SettingsWidget(QtWidgets.QWidget): ) self.open_checkbox.clicked.connect(self.update_ui) self.open_combobox = QtWidgets.QComboBox() - for k in self.global_common.pdf_viewers: - self.open_combobox.addItem(k, self.global_common.pdf_viewers[k]) + for k in self.gui_common.pdf_viewers: + self.open_combobox.addItem(k, self.gui_common.pdf_viewers[k]) open_layout = QtWidgets.QHBoxLayout() open_layout.addWidget(self.open_checkbox) open_layout.addWidget(self.open_combobox) diff --git a/dangerzone/tasks_widget.py b/dangerzone/gui/tasks_widget.py similarity index 92% rename from dangerzone/tasks_widget.py rename to dangerzone/gui/tasks_widget.py index 4688d36..ab77f47 100644 --- a/dangerzone/tasks_widget.py +++ b/dangerzone/gui/tasks_widget.py @@ -5,15 +5,16 @@ import platform import subprocess from PySide2 import QtCore, QtGui, QtWidgets -from .tasks import PullImageTask, ConvertToPixels, ConvertToPDF +from ..tasks import PullImageTask, ConvertToPixels, ConvertToPDF class TasksWidget(QtWidgets.QWidget): close_window = QtCore.Signal() - def __init__(self, global_common, common): + def __init__(self, global_common, gui_common, common): super(TasksWidget, self).__init__() self.global_common = global_common + self.gui_common = gui_common self.common = common # Dangerous document label @@ -31,7 +32,7 @@ class TasksWidget(QtWidgets.QWidget): self.task_details.setStyleSheet( "QLabel { background-color: #ffffff; font-size: 12px; padding: 10px; }" ) - self.task_details.setFont(self.global_common.fixed_font) + self.task_details.setFont(self.gui_common.fixed_font) self.task_details.setAlignment(QtCore.Qt.AlignTop) self.details_scrollarea = QtWidgets.QScrollArea() @@ -111,7 +112,7 @@ class TasksWidget(QtWidgets.QWidget): # Open if self.global_common.settings.get("open"): - self.global_common.open_pdf_viewer(dest_filename) + self.gui_common.open_pdf_viewer(dest_filename) # Clean up self.common.pixel_dir.cleanup() @@ -122,7 +123,7 @@ class TasksWidget(QtWidgets.QWidget): # In macOS, just close the window self.close_window.emit() else: - self.global_common.app.quit() + self.gui_common.app.quit() def scroll_to_bottom(self, minimum, maximum): self.details_scrollarea.verticalScrollBar().setValue(maximum) diff --git a/dangerzone/settings.py b/dangerzone/settings.py index 556f63a..4242b1f 100644 --- a/dangerzone/settings.py +++ b/dangerzone/settings.py @@ -7,18 +7,12 @@ class Settings: def __init__(self, common): self.common = common self.settings_filename = os.path.join(self.common.appdata_path, "settings.json") - - if len(self.common.pdf_viewers) == 0: - default_pdf_viewer = None - else: - default_pdf_viewer = list(self.common.pdf_viewers)[0] - self.default_settings = { "save": True, "ocr": True, "ocr_language": "English", "open": True, - "open_app": default_pdf_viewer, + "open_app": None, "update_container": True, "linux_prefers_typing_password": None, } diff --git a/dev_scripts/dangerzone-cli b/dev_scripts/dangerzone-cli new file mode 120000 index 0000000..2fe47df --- /dev/null +++ b/dev_scripts/dangerzone-cli @@ -0,0 +1 @@ +dangerzone \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9aca820..0803dbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ black = "^21.5b2" [tool.poetry.scripts] dangerzone = 'dangerzone:main' dangerzone-container = 'dangerzone:main' +dangerzone-cli = 'dangerzone:main' [build-system] requires = ["poetry>=1.1.4"] From 8aaf7ebcf1e54ab2d3b8f9ab63550361cf906a44 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 9 Jun 2021 16:15:19 -0700 Subject: [PATCH 02/12] Start implementing CLI args, and start with validating custom containers and pulling the latest container --- dangerzone/cli.py | 63 +++++++++++++++++++++++++++++++--- dangerzone/global_common.py | 23 +++++++++++++ dangerzone/gui/__init__.py | 23 +++---------- dangerzone/{ => gui}/tasks.py | 0 dangerzone/gui/tasks_widget.py | 2 +- 5 files changed, 87 insertions(+), 24 deletions(-) rename dangerzone/{ => gui}/tasks.py (100%) diff --git a/dangerzone/cli.py b/dangerzone/cli.py index 7265bc9..2b6b385 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -1,8 +1,63 @@ import click +from .global_common import GlobalCommon + + +def exec_container(global_common, args): + output = "" + + with global_common.exec_dangerzone_container(args) as p: + for line in p.stdout: + output += line.decode() + print(line.decode(), end="") + + stderr = p.stderr.read().decode() + print(stderr) + + if p.returncode == 126 or p.returncode == 127: + click.echo(f"Authorization failed") + elif p.returncode == 0: + click.echo(f"Return code: {p.returncode}") + + print("") + return p.returncode, output, stderr + @click.command() -@click.option("--custom-container") # Use this container instead of flmcode/dangerzone -@click.argument("filename", required=False) -def cli_main(custom_container, filename): - pass +@click.option("--custom-container", help="Use a custom container") +@click.option("--safe-pdf-filename", help="Default is filename ending with -safe.pdf") +@click.option("--ocr-lang", help="Language to OCR, defaults to none") +@click.option( + "--skip-update", + is_flag=True, + help="Don't update flmcode/dangerzone container", +) +@click.argument("filename", required=True) +def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filename): + global_common = GlobalCommon() + + # Make sure custom container exists + if custom_container: + success, error_message = global_common.container_exists(custom_container) + if not success: + click.echo(error_message) + return + + global_common.custom_container = custom_container + else: + if skip_update: + # Make sure flmcode/dangerzone exists + success, error_message = global_common.container_exists( + "flmcode/dangerzone" + ) + if not success: + click.echo( + "You don't have the flmcode/dangerzone container so you can't use --skip-update" + ) + return + + if not skip_update: + click.echo("Pulling container image (this might take a few minutes)") + returncode, _, _ = exec_container(global_common, ["pull"]) + if returncode != 0: + return diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index 084e79f..ef0440c 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -274,3 +274,26 @@ class GlobalCommon(object): return startupinfo else: return None + + def container_exists(self, container_name): + # Do we have this container? + with self.exec_dangerzone_container( + ["ls", "--container-name", container_name] + ) as p: + stdout_data, _ = p.communicate() + if stdout_data.startswith(b"Executing: "): + stdout_data = b"\n".join(stdout_data.split(b"\n")[1:]) + + # The user canceled, or permission denied + if p.returncode == 126 or p.returncode == 127: + return False, "Authorization failed" + return + elif p.returncode != 0: + return False, "Container error" + return + + # Check the output + if container_name.encode() not in stdout_data: + return False, f"Container '{container_name}' not found" + + return True, True diff --git a/dangerzone/gui/__init__.py b/dangerzone/gui/__init__.py index a2c59a8..65ccddd 100644 --- a/dangerzone/gui/__init__.py +++ b/dangerzone/gui/__init__.py @@ -15,7 +15,6 @@ from .docker_installer import ( AuthorizationFailed, ) from ..global_common import GlobalCommon -from ..container import container_runtime # For some reason, Dangerzone segfaults if I inherit from QApplication directly, so instead @@ -62,24 +61,10 @@ def gui_main(custom_container, filename): gui_common = GuiCommon(app, global_common) if custom_container: - # Do we have this container? - with global_common.exec_dangerzone_container( - ["ls", "--container-name", custom_container] - ) as p: - stdout_data, stderr_data = p.communicate() - - # The user canceled, or permission denied - if p.returncode == 126 or p.returncode == 127: - click.echo("Authorization failed") - return - elif p.returncode != 0: - click.echo("Container error") - return - - # Check the output - if custom_container.encode() not in stdout_data: - click.echo(f"Container '{custom_container}' not found") - return + success, error_message = global_common.container_exists(custom_container) + if not success: + click.echo(error_message) + return global_common.custom_container = custom_container diff --git a/dangerzone/tasks.py b/dangerzone/gui/tasks.py similarity index 100% rename from dangerzone/tasks.py rename to dangerzone/gui/tasks.py diff --git a/dangerzone/gui/tasks_widget.py b/dangerzone/gui/tasks_widget.py index ab77f47..94a00a4 100644 --- a/dangerzone/gui/tasks_widget.py +++ b/dangerzone/gui/tasks_widget.py @@ -5,7 +5,7 @@ import platform import subprocess from PySide2 import QtCore, QtGui, QtWidgets -from ..tasks import PullImageTask, ConvertToPixels, ConvertToPDF +from .tasks import PullImageTask, ConvertToPixels, ConvertToPDF class TasksWidget(QtWidgets.QWidget): From 73d412501cbbb4b9f4dd6a0069917de25958771d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 9 Jun 2021 16:32:06 -0700 Subject: [PATCH 03/12] Move the ConvertToPixels task validation into global_common so the CLI and GUI can share it --- dangerzone/cli.py | 28 ++++++++++++++ dangerzone/global_common.py | 73 ++++++++++++++++++++++++++++++++++++ dangerzone/gui/tasks.py | 75 +++---------------------------------- 3 files changed, 107 insertions(+), 69 deletions(-) diff --git a/dangerzone/cli.py b/dangerzone/cli.py index 2b6b385..c9aa893 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -1,6 +1,7 @@ import click from .global_common import GlobalCommon +from .common import Common def exec_container(global_common, args): @@ -35,6 +36,7 @@ def exec_container(global_common, args): @click.argument("filename", required=True) def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filename): global_common = GlobalCommon() + common = Common() # Make sure custom container exists if custom_container: @@ -56,8 +58,34 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam ) return + # Pull the latest image if not skip_update: click.echo("Pulling container image (this might take a few minutes)") returncode, _, _ = exec_container(global_common, ["pull"]) if returncode != 0: return + + # Document to pixels + click.echo("Converting document to pixels") + returncode, output, _ = exec_container( + global_common, + [ + "documenttopixels", + "--document-filename", + common.document_filename, + "--pixel-dir", + common.pixel_dir.name, + "--container-name", + global_common.get_container_name(), + ], + ) + + if returncode != 0: + return + + success, error_message = global_common.validate_convert_to_pixel_output( + common, output + ) + if not success: + click.echo(error_message) + return diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index ef0440c..2a4f6ad 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -276,6 +276,10 @@ class GlobalCommon(object): return None def container_exists(self, container_name): + """ + Check if container_name is a valid container. Returns a tuple like: + (success (boolean), error_message (str)) + """ # Do we have this container? with self.exec_dangerzone_container( ["ls", "--container-name", container_name] @@ -297,3 +301,72 @@ class GlobalCommon(object): return False, f"Container '{container_name}' not found" return True, True + + def validate_convert_to_pixel_output(self, common, output): + """ + Take the output from the convert to pixels tasks and validate it. Returns + a tuple like: (success (boolean), error_message (str)) + """ + max_image_width = 10000 + max_image_height = 10000 + + # Did we hit an error? + for line in output.split("\n"): + if ( + "failed:" in line + or "The document format is not supported" in line + or "Error" in line + ): + return False, output + + # How many pages was that? + num_pages = None + for line in output.split("\n"): + if line.startswith("Document has "): + num_pages = line.split(" ")[2] + break + if not num_pages or not num_pages.isdigit() or int(num_pages) <= 0: + return False, "Invalid number of pages returned" + num_pages = int(num_pages) + + # Make sure we have the files we expect + expected_filenames = [] + for i in range(1, num_pages + 1): + expected_filenames += [ + f"page-{i}.rgb", + f"page-{i}.width", + f"page-{i}.height", + ] + expected_filenames.sort() + actual_filenames = os.listdir(common.pixel_dir.name) + actual_filenames.sort() + + if expected_filenames != actual_filenames: + return ( + False, + f"We expected these files:\n{expected_filenames}\n\nBut we got these files:\n{actual_filenames}", + ) + + # Make sure the files are the correct sizes + for i in range(1, num_pages + 1): + with open(f"{common.pixel_dir.name}/page-{i}.width") as f: + w_str = f.read().strip() + with open(f"{common.pixel_dir.name}/page-{i}.height") as f: + h_str = f.read().strip() + w = int(w_str) + h = int(h_str) + if ( + not w_str.isdigit() + or not h_str.isdigit() + or w <= 0 + or w > max_image_width + or h <= 0 + or h > max_image_height + ): + return False, f"Page {i} has invalid geometry" + + # Make sure the RGB file is the correct size + if os.path.getsize(f"{common.pixel_dir.name}/page-{i}.rgb") != w * h * 3: + return False, f"Page {i} has an invalid RGB file size" + + return True, True diff --git a/dangerzone/gui/tasks.py b/dangerzone/gui/tasks.py index bf950a3..3dc5ade 100644 --- a/dangerzone/gui/tasks.py +++ b/dangerzone/gui/tasks.py @@ -65,10 +65,6 @@ class ConvertToPixels(TaskBase): self.global_common = global_common self.common = common - self.max_image_width = 10000 - self.max_image_height = 10000 - self.max_image_size = self.max_image_width * self.max_image_height * 3 - def run(self): self.update_label.emit("Converting document to pixels") args = [ @@ -80,76 +76,17 @@ class ConvertToPixels(TaskBase): "--container-name", self.global_common.get_container_name(), ] - returncode, output, stderr = self.exec_container(args) + returncode, output, _ = self.exec_container(args) if returncode != 0: return - # Did we hit an error? - for line in output.split("\n"): - if ( - "failed:" in line - or "The document format is not supported" in line - or "Error" in line - ): - self.task_failed.emit(output) - return - - # How many pages was that? - num_pages = None - for line in output.split("\n"): - if line.startswith("Document has "): - num_pages = line.split(" ")[2] - break - if not num_pages or not num_pages.isdigit() or int(num_pages) <= 0: - self.task_failed.emit("Invalid number of pages returned") + success, error_message = self.global_common.validate_convert_to_pixel_output( + self.common, output + ) + if not success: + self.task_failed.emit(error_message) return - num_pages = int(num_pages) - - # Make sure we have the files we expect - expected_filenames = [] - for i in range(1, num_pages + 1): - expected_filenames += [ - f"page-{i}.rgb", - f"page-{i}.width", - f"page-{i}.height", - ] - expected_filenames.sort() - actual_filenames = os.listdir(self.common.pixel_dir.name) - actual_filenames.sort() - - if expected_filenames != actual_filenames: - self.task_failed.emit( - f"We expected these files:\n{expected_filenames}\n\nBut we got these files:\n{actual_filenames}" - ) - return - - # Make sure the files are the correct sizes - for i in range(1, num_pages + 1): - with open(f"{self.common.pixel_dir.name}/page-{i}.width") as f: - w_str = f.read().strip() - with open(f"{self.common.pixel_dir.name}/page-{i}.height") as f: - h_str = f.read().strip() - w = int(w_str) - h = int(h_str) - if ( - not w_str.isdigit() - or not h_str.isdigit() - or w <= 0 - or w > self.max_image_width - or h <= 0 - or h > self.max_image_height - ): - self.task_failed.emit(f"Page {i} has invalid geometry") - return - - # Make sure the RGB file is the correct size - if ( - os.path.getsize(f"{self.common.pixel_dir.name}/page-{i}.rgb") - != w * h * 3 - ): - self.task_failed.emit(f"Page {i} has an invalid RGB file size") - return self.task_finished.emit() From 1144fd9f87e3f1451752147445647a0a87401290 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 9 Jun 2021 17:00:39 -0700 Subject: [PATCH 04/12] Make the CLI fully functional --- .gitignore | 2 +- dangerzone/cli.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b4cc8ea..465d3eb 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,4 @@ dmypy.json deb_dist .DS_Store install/windows/Dangerzone.wxs -test-docs/sample-safe.pdf \ No newline at end of file +test_docs/sample-safe.pdf \ No newline at end of file diff --git a/dangerzone/cli.py b/dangerzone/cli.py index c9aa893..a9ed940 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -1,3 +1,5 @@ +import os +import shutil import click from .global_common import GlobalCommon @@ -38,7 +40,66 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam global_common = GlobalCommon() common = Common() - # Make sure custom container exists + # Validate filename + valid = True + try: + with open(os.path.abspath(filename), "rb") as f: + pass + except: + valid = False + + if not valid: + click.echo("Invalid filename") + return + + common.document_filename = os.path.abspath(filename) + + # Validate safe PDF output filename + if safe_pdf_filename: + valid = True + if not safe_pdf_filename.endswith(".pdf"): + click.echo("Safe PDF filename must end in '.pdf'") + return + + try: + with open(os.path.abspath(safe_pdf_filename), "wb") as f: + pass + except: + valid = False + + if not valid: + click.echo("Safe PDF filename is not writable") + return + + common.save_filename = os.path.abspath(safe_pdf_filename) + + else: + common.save_filename = ( + f"{os.path.splitext(common.document_filename)[0]}-safe.pdf" + ) + try: + with open(common.save_filename, "wb") as f: + pass + except: + click.echo( + f"Output filename {common.save_filename} is not writable, use --safe-pdf-filename" + ) + return + + # Validate OCR language + if ocr_lang: + valid = False + for lang in global_common.ocr_languages: + if global_common.ocr_languages[lang] == ocr_lang: + valid = True + break + if not valid: + click.echo("Invalid OCR language code. Valid language codes:") + for lang in global_common.ocr_languages: + click.echo(f"{global_common.ocr_languages[lang]}: {lang}") + return + + # Validate custom container if custom_container: success, error_message = global_common.container_exists(custom_container) if not success: @@ -65,7 +126,7 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam if returncode != 0: return - # Document to pixels + # Convert to pixels click.echo("Converting document to pixels") returncode, output, _ = exec_container( global_common, @@ -89,3 +150,37 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam if not success: click.echo(error_message) return + + # Convert to PDF + click.echo("Converting pixels to safe PDF") + + if ocr_lang: + ocr = "1" + else: + ocr = "0" + ocr_lang = "" + + returncode, _, _ = exec_container( + global_common, + [ + "pixelstopdf", + "--pixel-dir", + common.pixel_dir.name, + "--safe-dir", + common.safe_dir.name, + "--container-name", + global_common.get_container_name(), + "--ocr", + ocr, + "--ocr-lang", + ocr_lang, + ], + ) + + if returncode != 0: + return + + # Save the safe PDF + source_filename = f"{common.safe_dir.name}/safe-output-compressed.pdf" + shutil.move(source_filename, common.save_filename) + print(f"Success! {common.save_filename}") From 918e5fa306207cf98de1ba9e1979ea39efe3495c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 9 Jun 2021 17:31:06 -0700 Subject: [PATCH 05/12] Display banner --- RELEASE.md | 2 +- dangerzone/__init__.py | 2 - dangerzone/cli.py | 167 ++++++++++++++++++++++++++++++++++++ dangerzone/global_common.py | 4 + poetry.lock | 2 +- pyproject.toml | 1 + setup.py | 11 ++- share/version.txt | 1 + 8 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 share/version.txt diff --git a/RELEASE.md b/RELEASE.md index b2022d8..b745021 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -7,7 +7,7 @@ This section documents the release process. Unless you're a dangerzone developer Before making a release, all of these should be complete: * Update `version` in `pyproject.toml` -* Update `dangerzone_version` in `dangerzone/__init__.py` +* Update in `share/version.txt` * Update version and download links in `README.md` * CHANGELOG.md should be updated to include a list of all major changes since the last release * There must be a PGP-signed git tag for the version, e.g. for dangerzone 0.1.0, the tag must be `v0.1.0` diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 8021fc4..d89750d 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -1,8 +1,6 @@ import os import sys -dangerzone_version = "0.1.5" - # Depending on the filename, decide if we want to run: # dangerzone, dangerzone-cli, or dangerzone-container diff --git a/dangerzone/cli.py b/dangerzone/cli.py index a9ed940..27f66ca 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -1,9 +1,174 @@ import os import shutil import click +import colorama +from colorama import Fore, Back, Style from .global_common import GlobalCommon from .common import Common +from dangerzone import global_common + + +def display_banner(global_common): + """ + Raw ASCII art example: + ╭──────────────────────────╮ + │ ▄██▄ │ + │ ██████ │ + │ ███▀▀▀██ │ + │ ███ ████ │ + │ ███ ██████ │ + │ ███ ▀▀▀▀████ │ + │ ███████ ▄██████ │ + │ ███████ ▄█████████ │ + │ ████████████████████ │ + │ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ + │ │ + │ Dangerzone v0.1.5 │ + │ https://dangerzone.rocks │ + ╰──────────────────────────╯ + """ + colorama.init(autoreset=True) + print(Fore.YELLOW + Style.DIM + "╭──────────────────────────╮") + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ▄██▄ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███▀▀▀██ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ▀▀▀▀████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███████ ▄██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███████ ▄█████████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ████████████████████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print(Fore.YELLOW + Style.DIM + "│ │") + left_spaces = (15 - len(global_common.version) - 1) // 2 + right_spaces = left_spaces + if left_spaces + len(global_common.version) + 1 + right_spaces < 15: + right_spaces += 1 + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTWHITE_EX + + Style.RESET_ALL + + Style.BRIGHT + + f"{' '*left_spaces}Dangerzone v{global_common.version}{' '*right_spaces}" + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTWHITE_EX + + Style.RESET_ALL + + " https://dangerzone.rocks " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print(Fore.YELLOW + Style.DIM + "╰──────────────────────────╯") def exec_container(global_common, args): @@ -40,6 +205,8 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam global_common = GlobalCommon() common = Common() + display_banner(global_common) + # Validate filename valid = True try: diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index 2a4f6ad..47a21e2 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -16,6 +16,10 @@ class GlobalCommon(object): """ def __init__(self): + # Version + with open(self.get_resource_path("version.txt")) as f: + self.version = f.read().strip() + # App data folder self.appdata_path = appdirs.user_config_dir("dangerzone") diff --git a/poetry.lock b/poetry.lock index 27e525f..94a767c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -374,7 +374,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "26e6acc883dad4194c45e399fadaabc12730fdd3a7b2264737e4e81838183eee" +content-hash = "72592722794667cf7cb6bea727b31eb60d710c90d488be9d10a13bbbcaa56688" [metadata.files] altgraph = [ diff --git a/pyproject.toml b/pyproject.toml index 0803dbb..860a048 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ wmi = {version = "*", platform = "win32"} pyxdg = {version = "*", platform = "linux"} pyobjc-core = {version = "*", platform = "darwin"} pyobjc-framework-launchservices = {version = "*", platform = "darwin"} +colorama = "^0.4.4" [tool.poetry.dev-dependencies] pyinstaller = {version = "*", platform = "darwin"} diff --git a/setup.py b/setup.py index 21a4741..2edcca2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,9 @@ import setuptools import os import sys -from dangerzone import dangerzone_version + +with open("share/version.txt") as f: + version = f.read().strip() def file_list(path): @@ -15,7 +17,7 @@ def file_list(path): setuptools.setup( name="dangerzone", - version=dangerzone_version, + version=version, author="Micah Lee", author_email="micah.lee@theintercept.com", license="MIT", @@ -23,7 +25,10 @@ setuptools.setup( url="https://github.com/firstlookmedia/dangerzone", packages=["dangerzone"], data_files=[ - ("share/applications", ["install/linux/media.firstlook.dangerzone.desktop"],), + ( + "share/applications", + ["install/linux/media.firstlook.dangerzone.desktop"], + ), ( "share/icons/hicolor/64x64/apps", ["install/linux/media.firstlook.dangerzone.png"], diff --git a/share/version.txt b/share/version.txt new file mode 100644 index 0000000..2f45361 --- /dev/null +++ b/share/version.txt @@ -0,0 +1 @@ +0.2 \ No newline at end of file From 05b5d0bb3ea6e56922f17b2968cb124d74dbf3db Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 9 Jun 2021 17:35:00 -0700 Subject: [PATCH 06/12] Set black background for banner --- dangerzone/cli.py | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/dangerzone/cli.py b/dangerzone/cli.py index 27f66ca..db4d11e 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -29,9 +29,10 @@ def display_banner(global_common): ╰──────────────────────────╯ """ colorama.init(autoreset=True) - print(Fore.YELLOW + Style.DIM + "╭──────────────────────────╮") + print(Back.BLACK + Fore.YELLOW + Style.DIM + "╭──────────────────────────╮") print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -42,7 +43,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -53,7 +55,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -64,7 +67,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -75,7 +79,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -86,7 +91,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -97,7 +103,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -108,7 +115,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -119,7 +127,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -130,7 +139,8 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" + Fore.LIGHTYELLOW_EX @@ -140,17 +150,19 @@ def display_banner(global_common): + Style.DIM + "│" ) - print(Fore.YELLOW + Style.DIM + "│ │") + print(Back.BLACK + Fore.YELLOW + Style.DIM + "│ │") left_spaces = (15 - len(global_common.version) - 1) // 2 right_spaces = left_spaces if left_spaces + len(global_common.version) + 1 + right_spaces < 15: right_spaces += 1 print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" - + Fore.LIGHTWHITE_EX + Style.RESET_ALL + + Back.BLACK + + Fore.LIGHTWHITE_EX + Style.BRIGHT + f"{' '*left_spaces}Dangerzone v{global_common.version}{' '*right_spaces}" + Fore.YELLOW @@ -158,11 +170,13 @@ def display_banner(global_common): + "│" ) print( - Fore.YELLOW + Back.BLACK + + Fore.YELLOW + Style.DIM + "│" - + Fore.LIGHTWHITE_EX + Style.RESET_ALL + + Back.BLACK + + Fore.LIGHTWHITE_EX + " https://dangerzone.rocks " + Fore.YELLOW + Style.DIM From 429f5dcf436b4d9380b4b2811a2b2f99652ea1b2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 9 Jun 2021 17:36:09 -0700 Subject: [PATCH 07/12] Set one more part of the banner to have a black background --- dangerzone/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dangerzone/cli.py b/dangerzone/cli.py index db4d11e..cf297cf 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -182,7 +182,7 @@ def display_banner(global_common): + Style.DIM + "│" ) - print(Fore.YELLOW + Style.DIM + "╰──────────────────────────╯") + print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯") def exec_container(global_common, args): From 46c73329a5caf3fb488c45aabb83b228c0d3c493 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 10 Jun 2021 10:24:28 -0700 Subject: [PATCH 08/12] Switch from termcolor to colorama --- BUILD.md | 13 ++++++++++--- dangerzone/gui/tasks.py | 4 ++-- install/linux/build_rpm.py | 2 +- stdeb.cfg | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/BUILD.md b/BUILD.md index 8126674..c68b2da 100644 --- a/BUILD.md +++ b/BUILD.md @@ -5,7 +5,7 @@ Install dependencies: ```sh -sudo apt install -y dh-python python3 python3-stdeb python3-pyside2.qtcore python3-pyside2.qtgui python3-pyside2.qtwidgets python3-appdirs python3-click python3-xdg python3-requests python3-termcolor +sudo apt install -y dh-python python3 python3-stdeb python3-pyside2.qtcore python3-pyside2.qtgui python3-pyside2.qtwidgets python3-appdirs python3-click python3-xdg python3-requests python3-colorama ``` You also need docker, either by installing the [Docker snap package](https://snapcraft.io/docker), installing the `docker.io` package, or by installing `docker-ce` by following [these instructions for Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/) or [for Debian](https://docs.docker.com/install/linux/docker-ce/debian/). @@ -27,7 +27,7 @@ Create a .deb: Install dependencies: ```sh -sudo dnf install -y rpm-build python3 python3-pyside2 python3-appdirs python3-click python3-pyxdg python3-requests python3-termcolor +sudo dnf install -y rpm-build python3 python3-pyside2 python3-appdirs python3-click python3-pyxdg python3-requests python3-colorama ``` You also need docker, either by installing the `docker` package, or by installing `docker-ce` by following [these instructions](https://docs.docker.com/install/linux/docker-ce/fedora/). @@ -61,7 +61,14 @@ poetry install Run from source tree: ``` -poetry run dangerzone +# start a shell in the virtual environment +poetry shell + +# run the CLI +./dev_scripts/dangerzone-cli --help + +# run the GUI +./dev_scripts/dangerzone ``` To create an app bundle, use the `build_app.py` script: diff --git a/dangerzone/gui/tasks.py b/dangerzone/gui/tasks.py index 3dc5ade..4ccb2cb 100644 --- a/dangerzone/gui/tasks.py +++ b/dangerzone/gui/tasks.py @@ -4,7 +4,7 @@ import os import pipes import platform from PySide2 import QtCore, QtWidgets, QtGui -from termcolor import cprint +from colorama import Style class TaskBase(QtCore.QThread): @@ -27,7 +27,7 @@ class TaskBase(QtCore.QThread): self.update_details.emit(output) stderr = p.stderr.read().decode() - cprint(stderr, attrs=["dark"]) + print(Style.DIM + stderr + Style.RESET_ALL) self.update_details.emit(output) if p.returncode == 126 or p.returncode == 127: diff --git a/install/linux/build_rpm.py b/install/linux/build_rpm.py index 42e582a..0d6c2b8 100755 --- a/install/linux/build_rpm.py +++ b/install/linux/build_rpm.py @@ -31,7 +31,7 @@ def main(): print("* Building RPM package") subprocess.run( - "python3 setup.py bdist_rpm --requires='python3-pyside2,python3-appdirs,python3-click,python3-pyxdg,python3-requests,python3-termcolor,(docker or docker-ce)'", + "python3 setup.py bdist_rpm --requires='python3-pyside2,python3-appdirs,python3-click,python3-pyxdg,python3-requests,python3-colorama,(docker or docker-ce)'", shell=True, cwd=root, check=True, diff --git a/stdeb.cfg b/stdeb.cfg index cfae021..df13e23 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [DEFAULT] Package3: dangerzone -Depends3: python3, python3-pyside2.qtcore, python3-pyside2.qtgui, python3-pyside2.qtwidgets, python3-appdirs, python3-click, python3-xdg, python3-requests, python3-termcolor +Depends3: python3, python3-pyside2.qtcore, python3-pyside2.qtgui, python3-pyside2.qtwidgets, python3-appdirs, python3-click, python3-xdg, python3-requests, python3-colorama Build-Depends: dh-python, python3, python3-all Suite: bionic X-Python3-Version: >= 3.6 \ No newline at end of file From 38ea24393ab41968de168b6040c6b1d941f4e319 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 10 Jun 2021 11:39:26 -0700 Subject: [PATCH 09/12] Beautiful CLI colors and formatting --- dangerzone/__init__.py | 3 +++ dangerzone/cli.py | 37 ++++++++++++++++++++++++++----------- dangerzone/container.py | 4 +--- dangerzone/global_common.py | 9 +++++---- dangerzone/gui/tasks.py | 5 ----- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index d89750d..2426cb6 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -1,5 +1,8 @@ import os import sys +import colorama + +colorama.init(autoreset=True) # Depending on the filename, decide if we want to run: # dangerzone, dangerzone-cli, or dangerzone-container diff --git a/dangerzone/cli.py b/dangerzone/cli.py index cf297cf..796e7f3 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -28,7 +28,6 @@ def display_banner(global_common): │ https://dangerzone.rocks │ ╰──────────────────────────╯ """ - colorama.init(autoreset=True) print(Back.BLACK + Fore.YELLOW + Style.DIM + "╭──────────────────────────╮") print( Back.BLACK @@ -185,23 +184,38 @@ def display_banner(global_common): print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯") +def print_header(s): + click.echo("") + click.echo(Style.BRIGHT + Fore.LIGHTWHITE_EX + s) + + def exec_container(global_common, args): output = "" with global_common.exec_dangerzone_container(args) as p: for line in p.stdout: output += line.decode() - print(line.decode(), end="") + + # Hack to add colors to the command executing + if line.startswith(b"\xe2\x80\xa3 "): + print( + Fore.WHITE + "\u2023 " + Fore.LIGHTCYAN_EX + line.decode()[2:], + end="", + ) + else: + print(" " + line.decode(), end="") stderr = p.stderr.read().decode() - print(stderr) + if len(stderr) > 0: + print("") + for line in stderr.strip().split("\n"): + print(" " + Style.DIM + line) - if p.returncode == 126 or p.returncode == 127: - click.echo(f"Authorization failed") - elif p.returncode == 0: + if p.returncode != 0: click.echo(f"Return code: {p.returncode}") + if p.returncode == 126 or p.returncode == 127: + click.echo(f"Authorization failed") - print("") return p.returncode, output, stderr @@ -302,13 +316,13 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam # Pull the latest image if not skip_update: - click.echo("Pulling container image (this might take a few minutes)") + print_header("Pulling container image (this might take a few minutes)") returncode, _, _ = exec_container(global_common, ["pull"]) if returncode != 0: return # Convert to pixels - click.echo("Converting document to pixels") + print_header("Converting document to pixels") returncode, output, _ = exec_container( global_common, [ @@ -333,7 +347,7 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam return # Convert to PDF - click.echo("Converting pixels to safe PDF") + print_header("Converting pixels to safe PDF") if ocr_lang: ocr = "1" @@ -364,4 +378,5 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam # Save the safe PDF source_filename = f"{common.safe_dir.name}/safe-output-compressed.pdf" shutil.move(source_filename, common.save_filename) - print(f"Success! {common.save_filename}") + print_header("Safe PDF created successfully") + click.echo(common.save_filename) diff --git a/dangerzone/container.py b/dangerzone/container.py index 3a745fd..ae9cbdb 100644 --- a/dangerzone/container.py +++ b/dangerzone/container.py @@ -3,7 +3,6 @@ import platform import subprocess import sys import pipes -import getpass import shutil # What is the container runtime for this platform? @@ -26,8 +25,7 @@ def exec_container(args): args = [container_runtime] + args args_str = " ".join(pipes.quote(s) for s in args) - sys.stdout.write(f"Executing: {args_str}\n\n") - sys.stdout.flush() + print("\u2023 " + args_str) # ‣ with subprocess.Popen( args, diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index 47a21e2..628cfe6 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -5,7 +5,7 @@ import appdirs import platform import subprocess import pipes -from PySide2 import QtCore, QtGui, QtWidgets +from colorama import Fore, Style from .settings import Settings @@ -263,7 +263,7 @@ class GlobalCommon(object): # Execute dangerzone-container args_str = " ".join(pipes.quote(s) for s in args) - print(f"Executing: {args_str}") + print(Fore.YELLOW + "\u2023 " + Fore.CYAN + args_str) # ‣ return subprocess.Popen( args, startupinfo=self.get_subprocess_startupinfo(), @@ -289,8 +289,9 @@ class GlobalCommon(object): ["ls", "--container-name", container_name] ) as p: stdout_data, _ = p.communicate() - if stdout_data.startswith(b"Executing: "): - stdout_data = b"\n".join(stdout_data.split(b"\n")[1:]) + lines = stdout_data.split(b"\n") + if b"\u2023 " in lines[0]: # ‣ + stdout_data = b"\n".join(lines[1:]) # The user canceled, or permission denied if p.returncode == 126 or p.returncode == 127: diff --git a/dangerzone/gui/tasks.py b/dangerzone/gui/tasks.py index 4ccb2cb..9ff5448 100644 --- a/dangerzone/gui/tasks.py +++ b/dangerzone/gui/tasks.py @@ -1,8 +1,3 @@ -import subprocess -import time -import os -import pipes -import platform from PySide2 import QtCore, QtWidgets, QtGui from colorama import Style From 429d1e3f08183f2a5f19f172361bdf057563f86c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 10 Jun 2021 12:03:24 -0700 Subject: [PATCH 10/12] Display banner and pretty terminal output in GUI mode too --- dangerzone/__init__.py | 3 - dangerzone/cli.py | 179 +---------------------------------- dangerzone/container.py | 1 + dangerzone/global_common.py | 181 +++++++++++++++++++++++++++++++++++- dangerzone/gui/__init__.py | 2 + dangerzone/gui/common.py | 16 +++- dangerzone/gui/tasks.py | 18 +++- 7 files changed, 211 insertions(+), 189 deletions(-) diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 2426cb6..d89750d 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -1,8 +1,5 @@ import os import sys -import colorama - -colorama.init(autoreset=True) # Depending on the filename, decide if we want to run: # dangerzone, dangerzone-cli, or dangerzone-container diff --git a/dangerzone/cli.py b/dangerzone/cli.py index 796e7f3..f94ef14 100644 --- a/dangerzone/cli.py +++ b/dangerzone/cli.py @@ -1,187 +1,10 @@ import os import shutil import click -import colorama from colorama import Fore, Back, Style from .global_common import GlobalCommon from .common import Common -from dangerzone import global_common - - -def display_banner(global_common): - """ - Raw ASCII art example: - ╭──────────────────────────╮ - │ ▄██▄ │ - │ ██████ │ - │ ███▀▀▀██ │ - │ ███ ████ │ - │ ███ ██████ │ - │ ███ ▀▀▀▀████ │ - │ ███████ ▄██████ │ - │ ███████ ▄█████████ │ - │ ████████████████████ │ - │ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ - │ │ - │ Dangerzone v0.1.5 │ - │ https://dangerzone.rocks │ - ╰──────────────────────────╯ - """ - print(Back.BLACK + Fore.YELLOW + Style.DIM + "╭──────────────────────────╮") - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ▄██▄ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ██████ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ███▀▀▀██ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ███ ████ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ███ ██████ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ███ ▀▀▀▀████ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ███████ ▄██████ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ███████ ▄█████████ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ████████████████████ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Fore.LIGHTYELLOW_EX - + Style.NORMAL - + " ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print(Back.BLACK + Fore.YELLOW + Style.DIM + "│ │") - left_spaces = (15 - len(global_common.version) - 1) // 2 - right_spaces = left_spaces - if left_spaces + len(global_common.version) + 1 + right_spaces < 15: - right_spaces += 1 - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Style.RESET_ALL - + Back.BLACK - + Fore.LIGHTWHITE_EX - + Style.BRIGHT - + f"{' '*left_spaces}Dangerzone v{global_common.version}{' '*right_spaces}" - + Fore.YELLOW - + Style.DIM - + "│" - ) - print( - Back.BLACK - + Fore.YELLOW - + Style.DIM - + "│" - + Style.RESET_ALL - + Back.BLACK - + Fore.LIGHTWHITE_EX - + " https://dangerzone.rocks " - + Fore.YELLOW - + Style.DIM - + "│" - ) - print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯") def print_header(s): @@ -233,7 +56,7 @@ def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filenam global_common = GlobalCommon() common = Common() - display_banner(global_common) + global_common.display_banner() # Validate filename valid = True diff --git a/dangerzone/container.py b/dangerzone/container.py index ae9cbdb..a0e1a5a 100644 --- a/dangerzone/container.py +++ b/dangerzone/container.py @@ -26,6 +26,7 @@ def exec_container(args): args_str = " ".join(pipes.quote(s) for s in args) print("\u2023 " + args_str) # ‣ + sys.stdout.flush() with subprocess.Popen( args, diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index 628cfe6..e19680d 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -5,7 +5,8 @@ import appdirs import platform import subprocess import pipes -from colorama import Fore, Style +import colorama +from colorama import Fore, Back, Style from .settings import Settings @@ -20,6 +21,9 @@ class GlobalCommon(object): with open(self.get_resource_path("version.txt")) as f: self.version = f.read().strip() + # Initialize terminal colors + colorama.init(autoreset=True) + # App data folder self.appdata_path = appdirs.user_config_dir("dangerzone") @@ -196,6 +200,181 @@ class GlobalCommon(object): # Load settings self.settings = Settings(self) + def display_banner(self): + """ + Raw ASCII art example: + ╭──────────────────────────╮ + │ ▄██▄ │ + │ ██████ │ + │ ███▀▀▀██ │ + │ ███ ████ │ + │ ███ ██████ │ + │ ███ ▀▀▀▀████ │ + │ ███████ ▄██████ │ + │ ███████ ▄█████████ │ + │ ████████████████████ │ + │ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ + │ │ + │ Dangerzone v0.1.5 │ + │ https://dangerzone.rocks │ + ╰──────────────────────────╯ + """ + + print(Back.BLACK + Fore.YELLOW + Style.DIM + "╭──────────────────────────╮") + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ▄██▄ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███▀▀▀██ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ▀▀▀▀████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███████ ▄██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███████ ▄█████████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ████████████████████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print(Back.BLACK + Fore.YELLOW + Style.DIM + "│ │") + left_spaces = (15 - len(self.version) - 1) // 2 + right_spaces = left_spaces + if left_spaces + len(self.version) + 1 + right_spaces < 15: + right_spaces += 1 + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Style.RESET_ALL + + Back.BLACK + + Fore.LIGHTWHITE_EX + + Style.BRIGHT + + f"{' '*left_spaces}Dangerzone v{self.version}{' '*right_spaces}" + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Style.RESET_ALL + + Back.BLACK + + Fore.LIGHTWHITE_EX + + " https://dangerzone.rocks " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯") + def get_container_name(self): if self.custom_container: return self.custom_container diff --git a/dangerzone/gui/__init__.py b/dangerzone/gui/__init__.py index 65ccddd..2e79083 100644 --- a/dangerzone/gui/__init__.py +++ b/dangerzone/gui/__init__.py @@ -60,6 +60,8 @@ def gui_main(custom_container, filename): global_common = GlobalCommon() gui_common = GuiCommon(app, global_common) + global_common.display_banner() + if custom_container: success, error_message = global_common.container_exists(custom_container) if not success: diff --git a/dangerzone/gui/common.py b/dangerzone/gui/common.py index 982121e..28b614f 100644 --- a/dangerzone/gui/common.py +++ b/dangerzone/gui/common.py @@ -2,7 +2,9 @@ import os import platform import subprocess import shlex +import pipes from PySide2 import QtCore, QtGui, QtWidgets +from colorama import Fore if platform.system() == "Darwin": import CoreServices @@ -47,16 +49,21 @@ class GuiCommon(object): if self.global_common.settings.get("open_app") in self.pdf_viewers: if platform.system() == "Darwin": # Get the PDF reader bundle command - bundle_identifier = self.pdf_viewers[self.global_common.settings.get("open_app")] + bundle_identifier = self.pdf_viewers[ + self.global_common.settings.get("open_app") + ] args = ["open", "-b", bundle_identifier, filename] # Run - print(f"Executing: {' '.join(args)}") + args_str = " ".join(pipes.quote(s) for s in args) + print(Fore.YELLOW + "\u2023 " + Fore.CYAN + args_str) # ‣ subprocess.run(args) elif platform.system() == "Linux": # Get the PDF reader command - args = shlex.split(self.pdf_viewers[self.global_common.settings.get("open_app")]) + args = shlex.split( + self.pdf_viewers[self.global_common.settings.get("open_app")] + ) # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open for i in range(len(args)): if ( @@ -68,7 +75,8 @@ class GuiCommon(object): args[i] = filename # Open as a background process - print(f"Executing: {' '.join(args)}") + args_str = " ".join(pipes.quote(s) for s in args) + print(Fore.YELLOW + "\u2023 " + Fore.CYAN + args_str) # ‣ subprocess.Popen(args) def _find_pdf_viewers(self): diff --git a/dangerzone/gui/tasks.py b/dangerzone/gui/tasks.py index 9ff5448..5b97f34 100644 --- a/dangerzone/gui/tasks.py +++ b/dangerzone/gui/tasks.py @@ -1,5 +1,5 @@ from PySide2 import QtCore, QtWidgets, QtGui -from colorama import Style +from colorama import Style, Fore class TaskBase(QtCore.QThread): @@ -18,11 +18,23 @@ class TaskBase(QtCore.QThread): with self.global_common.exec_dangerzone_container(args) as p: for line in p.stdout: output += line.decode() - print(line.decode(), end="") + + if line.startswith(b"\xe2\x80\xa3 "): + print( + Fore.WHITE + "\u2023 " + Fore.LIGHTCYAN_EX + line.decode()[2:], + end="", + ) + else: + print(" " + line.decode(), end="") + self.update_details.emit(output) stderr = p.stderr.read().decode() - print(Style.DIM + stderr + Style.RESET_ALL) + if len(stderr) > 0: + print("") + for line in stderr.strip().split("\n"): + print(" " + Style.DIM + line) + self.update_details.emit(output) if p.returncode == 126 or p.returncode == 127: From a711ec1dedf3a6f397f49294fbe5aa954991e787 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 10 Jun 2021 14:41:26 -0700 Subject: [PATCH 11/12] Make the temp directories world-readable so that docker containers can access them regardless of which user created them --- dangerzone/common.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dangerzone/common.py b/dangerzone/common.py index d50a51f..d8630d6 100644 --- a/dangerzone/common.py +++ b/dangerzone/common.py @@ -1,4 +1,5 @@ import os +import stat import platform import tempfile @@ -30,6 +31,20 @@ class Common(object): prefix=os.path.join(cache_dir, "safe-") ) + # Make the folders world-readable to ensure that the container has permission + # to access it even if it's owned by root or someone else + permissions = ( + stat.S_IRUSR + | stat.S_IWUSR + | stat.S_IXUSR + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH + ) + os.chmod(self.pixel_dir.name, permissions) + os.chmod(self.safe_dir.name, permissions) + # Name of input and out files self.document_filename = None self.save_filename = None From 05f00ca53fd36fee2e036045bb0a042cf6279789 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 10 Jun 2021 14:46:31 -0700 Subject: [PATCH 12/12] Fix bugs with testing if docker is ready --- dangerzone/gui/common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dangerzone/gui/common.py b/dangerzone/gui/common.py index 28b614f..cbf29bf 100644 --- a/dangerzone/gui/common.py +++ b/dangerzone/gui/common.py @@ -202,7 +202,7 @@ class GuiCommon(object): return True def ensure_docker_service_is_started(self): - if not is_docker_ready(self): + if not is_docker_ready(self.global_common): message = "Dangerzone requires Docker

Docker should be installed, but it looks like it's not running in the background.

Click Ok to try starting the docker service. You will have to type your login password." if ( Alert(self, self.global_common, message).launch() @@ -218,7 +218,7 @@ class GuiCommon(object): ) if p.returncode == 0: # Make sure docker is now ready - if is_docker_ready(self): + if is_docker_ready(self.global_common): return True else: message = "Restarting docker appeared to work, but the service still isn't responding, quitting." @@ -234,7 +234,7 @@ class GuiCommon(object): class Alert(QtWidgets.QDialog): def __init__( - self, global_common, gui_common, message, ok_text="Ok", extra_button_text=None + self, gui_common, global_common, message, ok_text="Ok", extra_button_text=None ): super(Alert, self).__init__() self.global_common = global_common