Merge pull request #116 from firstlookmedia/53_cli_support

Add command line-only support and pretty terminal output
This commit is contained in:
Micah Lee 2021-06-10 14:56:01 -07:00 committed by GitHub
commit 07935a273b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 891 additions and 420 deletions

2
.gitignore vendored
View file

@ -133,4 +133,4 @@ dmypy.json
deb_dist deb_dist
.DS_Store .DS_Store
install/windows/Dangerzone.wxs install/windows/Dangerzone.wxs
test-docs/sample-safe.pdf test_docs/sample-safe.pdf

View file

@ -5,7 +5,7 @@
Install dependencies: Install dependencies:
```sh ```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/). 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: Install dependencies:
```sh ```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/). 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: 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: To create an app bundle, use the `build_app.py` script:

View file

@ -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: Before making a release, all of these should be complete:
* Update `version` in `pyproject.toml` * 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` * 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 * 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` * 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`

View file

@ -1,13 +1,14 @@
import os import os
import sys import sys
dangerzone_version = "0.1.5" # Depending on the filename, decide if we want to run:
# dangerzone, dangerzone-cli, or dangerzone-container
# This is a hack for Windows and Mac to be able to run dangerzone-container, even though
# PyInstaller builds a single binary
basename = os.path.basename(sys.argv[0]) basename = os.path.basename(sys.argv[0])
if basename == "dangerzone-container" or basename == "dangerzone-container.exe": if basename == "dangerzone-container" or basename == "dangerzone-container.exe":
from .container import container_main as main from .container import container_main as main
elif basename == "dangerzone-cli" or basename == "dangerzone-cli.exe":
from .cli import cli_main as main
else: else:
# If the binary isn't "dangerzone-contatiner", then launch the GUI
from .gui import gui_main as main from .gui import gui_main as main

205
dangerzone/cli.py Normal file
View file

@ -0,0 +1,205 @@
import os
import shutil
import click
from colorama import Fore, Back, Style
from .global_common import GlobalCommon
from .common import Common
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()
# 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()
if len(stderr) > 0:
print("")
for line in stderr.strip().split("\n"):
print(" " + Style.DIM + line)
if p.returncode != 0:
click.echo(f"Return code: {p.returncode}")
if p.returncode == 126 or p.returncode == 127:
click.echo(f"Authorization failed")
return p.returncode, output, stderr
@click.command()
@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()
common = Common()
global_common.display_banner()
# 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:
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
# Pull the latest image
if not skip_update:
print_header("Pulling container image (this might take a few minutes)")
returncode, _, _ = exec_container(global_common, ["pull"])
if returncode != 0:
return
# Convert to pixels
print_header("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
# Convert to PDF
print_header("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_header("Safe PDF created successfully")
click.echo(common.save_filename)

View file

@ -1,4 +1,5 @@
import os import os
import stat
import platform import platform
import tempfile import tempfile
@ -30,6 +31,20 @@ class Common(object):
prefix=os.path.join(cache_dir, "safe-") 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 # Name of input and out files
self.document_filename = None self.document_filename = None
self.save_filename = None self.save_filename = None

View file

@ -3,7 +3,6 @@ import platform
import subprocess import subprocess
import sys import sys
import pipes import pipes
import getpass
import shutil import shutil
# What is the container runtime for this platform? # What is the container runtime for this platform?
@ -26,7 +25,7 @@ def exec_container(args):
args = [container_runtime] + args args = [container_runtime] + args
args_str = " ".join(pipes.quote(s) for s in args) args_str = " ".join(pipes.quote(s) for s in args)
sys.stdout.write(f"Executing: {args_str}\n\n") print("\u2023 " + args_str) # ‣
sys.stdout.flush() sys.stdout.flush()
with subprocess.Popen( with subprocess.Popen(

View file

@ -1,26 +1,14 @@
import sys import sys
import os import os
import inspect import inspect
import tempfile
import appdirs import appdirs
import platform import platform
import subprocess import subprocess
import shlex
import pipes import pipes
from PySide2 import QtCore, QtGui, QtWidgets import colorama
from colorama import Fore, Back, Style
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 .settings import Settings
from .docker_installer import is_docker_ready
class GlobalCommon(object): class GlobalCommon(object):
@ -28,18 +16,13 @@ class GlobalCommon(object):
The GlobalCommon class is a singleton of shared functionality throughout the app The GlobalCommon class is a singleton of shared functionality throughout the app
""" """
def __init__(self, app): def __init__(self):
# Qt app # Version
self.app = app with open(self.get_resource_path("version.txt")) as f:
self.version = f.read().strip()
# Name of input file # Initialize terminal colors
self.document_filename = None colorama.init(autoreset=True)
# Name of output file
self.save_filename = None
# Preload font
self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
# App data folder # App data folder
self.appdata_path = appdirs.user_config_dir("dangerzone") self.appdata_path = appdirs.user_config_dir("dangerzone")
@ -50,9 +33,6 @@ class GlobalCommon(object):
# dangerzone-container path # dangerzone-container path
self.dz_container_path = self.get_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 # Languages supported by tesseract
self.ocr_languages = { self.ocr_languages = {
"Afrikaans": "ar", "Afrikaans": "ar",
@ -220,6 +200,181 @@ class GlobalCommon(object):
# Load settings # Load settings
self.settings = Settings(self) 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): def get_container_name(self):
if self.custom_container: if self.custom_container:
return self.custom_container return self.custom_container
@ -287,7 +442,7 @@ class GlobalCommon(object):
# Execute dangerzone-container # Execute dangerzone-container
args_str = " ".join(pipes.quote(s) for s in args) 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( return subprocess.Popen(
args, args,
startupinfo=self.get_subprocess_startupinfo(), startupinfo=self.get_subprocess_startupinfo(),
@ -295,187 +450,6 @@ class GlobalCommon(object):
stderr=subprocess.PIPE, 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 = "<b>Dangerzone requires Docker</b><br><br>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.<br><br><b>Adding your user to the 'docker' group is more convenient but less secure</b>, 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 = "<b>Dangerzone requires Docker</b><br><br>Docker should be installed, but it looks like it's not running in the background.<br><br>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): def get_subprocess_startupinfo(self):
if platform.system() == "Windows": if platform.system() == "Windows":
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
@ -484,70 +458,99 @@ class GlobalCommon(object):
else: else:
return None 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]
) as p:
stdout_data, _ = p.communicate()
lines = stdout_data.split(b"\n")
if b"\u2023 " in lines[0]: # ‣
stdout_data = b"\n".join(lines[1:])
class Alert(QtWidgets.QDialog): # The user canceled, or permission denied
def __init__(self, common, message, ok_text="Ok", extra_button_text=None): if p.returncode == 126 or p.returncode == 127:
super(Alert, self).__init__() return False, "Authorization failed"
self.common = common return
elif p.returncode != 0:
return False, "Container error"
return
self.setWindowTitle("dangerzone") # Check the output
self.setWindowIcon(self.common.get_window_icon()) if container_name.encode() not in stdout_data:
self.setModal(True) return False, f"Container '{container_name}' not found"
flags = ( return True, True
QtCore.Qt.CustomizeWindowHint
| QtCore.Qt.WindowTitleHint
| QtCore.Qt.WindowSystemMenuHint
| QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowStaysOnTopHint
)
self.setWindowFlags(flags)
logo = QtWidgets.QLabel() def validate_convert_to_pixel_output(self, common, output):
logo.setPixmap( """
QtGui.QPixmap.fromImage( Take the output from the convert to pixels tasks and validate it. Returns
QtGui.QImage(self.common.get_resource_path("icon.png")) 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}",
) )
label = QtWidgets.QLabel() # Make sure the files are the correct sizes
label.setText(message) for i in range(1, num_pages + 1):
label.setWordWrap(True) 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"
message_layout = QtWidgets.QHBoxLayout() # Make sure the RGB file is the correct size
message_layout.addWidget(logo) if os.path.getsize(f"{common.pixel_dir.name}/page-{i}.rgb") != w * h * 3:
message_layout.addSpacing(10) return False, f"Page {i} has an invalid RGB file size"
message_layout.addWidget(label, stretch=1)
ok_button = QtWidgets.QPushButton(ok_text) return True, True
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_()

View file

@ -3,21 +3,18 @@ import sys
import signal import signal
import platform import platform
import click import click
import time
import uuid import uuid
import subprocess
from PySide2 import QtCore, QtWidgets from PySide2 import QtCore, QtWidgets
from .global_common import GlobalCommon from .common import GuiCommon
from .main_window import MainWindow from .main_window import MainWindow
from .docker_installer import ( from .docker_installer import (
is_docker_installed, is_docker_installed,
is_docker_ready, is_docker_ready,
launch_docker_windows,
DockerInstaller, DockerInstaller,
AuthorizationFailed, AuthorizationFailed,
) )
from .container import container_runtime from ..global_common import GlobalCommon
# For some reason, Dangerzone segfaults if I inherit from QApplication directly, so instead # For some reason, Dangerzone segfaults if I inherit from QApplication directly, so instead
@ -59,27 +56,16 @@ def gui_main(custom_container, filename):
app_wrapper = ApplicationWrapper() app_wrapper = ApplicationWrapper()
app = app_wrapper.app app = app_wrapper.app
# GlobalCommon object # Common objects
global_common = GlobalCommon(app) global_common = GlobalCommon()
gui_common = GuiCommon(app, global_common)
global_common.display_banner()
if custom_container: if custom_container:
# Do we have this container? success, error_message = global_common.container_exists(custom_container)
with global_common.exec_dangerzone_container( if not success:
["ls", "--container-name", custom_container] click.echo(error_message)
) 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 return
global_common.custom_container = custom_container global_common.custom_container = custom_container
@ -89,10 +75,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 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 platform.system() == "Linux":
if not global_common.ensure_docker_group_preference(): if not gui_common.ensure_docker_group_preference():
return return
try: 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") click.echo("Failed to start docker service")
return return
except AuthorizationFailed: except AuthorizationFailed:
@ -101,10 +87,10 @@ def gui_main(custom_container, filename):
# See if we need to install Docker... # See if we need to install Docker...
if (platform.system() == "Darwin" or platform.system() == "Windows") and ( 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") click.echo("Docker is either not installed or not running")
docker_installer = DockerInstaller(global_common) docker_installer = DockerInstaller(gui_common)
docker_installer.start() docker_installer.start()
return return
@ -124,7 +110,7 @@ def gui_main(custom_container, filename):
window = windows[list(windows.keys())[0]] window = windows[list(windows.keys())[0]]
else: else:
window_id = uuid.uuid4().hex 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) window.delete_window.connect(delete_window)
windows[window_id] = window windows[window_id] = window

303
dangerzone/gui/common.py Normal file
View file

@ -0,0 +1,303 @@
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
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
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")]
)
# %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
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):
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 = "<b>Dangerzone requires Docker</b><br><br>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.<br><br><b>Adding your user to the 'docker' group is more convenient but less secure</b>, 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.global_common):
message = "<b>Dangerzone requires Docker</b><br><br>Docker should be installed, but it looks like it's not running in the background.<br><br>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.global_common):
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, gui_common, global_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_()

View file

@ -8,14 +8,14 @@ import time
import platform import platform
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtGui, QtWidgets
from .container import container_runtime from ..container import container_runtime
class AuthorizationFailed(Exception): class AuthorizationFailed(Exception):
pass pass
def is_docker_installed(global_common): def is_docker_installed():
if platform.system() == "Darwin": if platform.system() == "Darwin":
# Does the docker binary exist? # Does the docker binary exist?
if os.path.isdir("/Applications/Docker.app") and os.path.exists( 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): class DockerInstaller(QtWidgets.QDialog):
def __init__(self, global_common): def __init__(self, gui_common):
super(DockerInstaller, self).__init__() super(DockerInstaller, self).__init__()
self.global_common = global_common
self.setWindowTitle("dangerzone") self.setWindowTitle("dangerzone")
self.setWindowIcon(self.global_common.get_window_icon()) self.setWindowIcon(gui_common.get_window_icon())
# self.setMinimumHeight(170) # self.setMinimumHeight(170)
label = QtWidgets.QLabel() label = QtWidgets.QLabel()

View file

@ -6,20 +6,21 @@ from PySide2 import QtCore, QtGui, QtWidgets
from .doc_selection_widget import DocSelectionWidget from .doc_selection_widget import DocSelectionWidget
from .settings_widget import SettingsWidget from .settings_widget import SettingsWidget
from .tasks_widget import TasksWidget from .tasks_widget import TasksWidget
from .common import Common from ..common import Common
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
delete_window = QtCore.Signal(str) 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__() super(MainWindow, self).__init__()
self.global_common = global_common self.global_common = global_common
self.gui_common = gui_common
self.window_id = window_id self.window_id = window_id
self.common = Common() self.common = Common()
self.setWindowTitle("dangerzone") self.setWindowTitle("dangerzone")
self.setWindowIcon(self.global_common.get_window_icon()) self.setWindowIcon(self.gui_common.get_window_icon())
self.setMinimumWidth(600) self.setMinimumWidth(600)
self.setMinimumHeight(400) self.setMinimumHeight(400)
@ -32,7 +33,7 @@ class MainWindow(QtWidgets.QMainWindow):
) )
) )
header_label = QtWidgets.QLabel("dangerzone") 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_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
header_layout = QtWidgets.QHBoxLayout() header_layout = QtWidgets.QHBoxLayout()
header_layout.addStretch() header_layout.addStretch()
@ -47,7 +48,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.doc_selection_widget.show() self.doc_selection_widget.show()
# Settings # 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.doc_selection_widget.document_selected.connect(
self.settings_widget.document_selected self.settings_widget.document_selected
) )
@ -59,7 +62,9 @@ class MainWindow(QtWidgets.QMainWindow):
) )
# Tasks # 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.tasks_widget.close_window.connect(self.close)
self.doc_selection_widget.document_selected.connect( self.doc_selection_widget.document_selected.connect(
self.tasks_widget.document_selected self.tasks_widget.document_selected
@ -93,4 +98,4 @@ class MainWindow(QtWidgets.QMainWindow):
self.delete_window.emit(self.window_id) self.delete_window.emit(self.window_id)
if platform.system() != "Darwin": if platform.system() != "Darwin":
self.global_common.app.quit() self.gui_common.app.quit()

View file

@ -1,5 +1,4 @@
import os import os
import subprocess
import platform import platform
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtGui, QtWidgets
@ -8,9 +7,10 @@ class SettingsWidget(QtWidgets.QWidget):
start_clicked = QtCore.Signal() start_clicked = QtCore.Signal()
close_window = QtCore.Signal() close_window = QtCore.Signal()
def __init__(self, global_common, common): def __init__(self, global_common, gui_common, common):
super(SettingsWidget, self).__init__() super(SettingsWidget, self).__init__()
self.global_common = global_common self.global_common = global_common
self.gui_common = gui_common
self.common = common self.common = common
# Dangerous document label # Dangerous document label
@ -49,8 +49,8 @@ class SettingsWidget(QtWidgets.QWidget):
) )
self.open_checkbox.clicked.connect(self.update_ui) self.open_checkbox.clicked.connect(self.update_ui)
self.open_combobox = QtWidgets.QComboBox() self.open_combobox = QtWidgets.QComboBox()
for k in self.global_common.pdf_viewers: for k in self.gui_common.pdf_viewers:
self.open_combobox.addItem(k, self.global_common.pdf_viewers[k]) self.open_combobox.addItem(k, self.gui_common.pdf_viewers[k])
open_layout = QtWidgets.QHBoxLayout() open_layout = QtWidgets.QHBoxLayout()
open_layout.addWidget(self.open_checkbox) open_layout.addWidget(self.open_checkbox)
open_layout.addWidget(self.open_combobox) open_layout.addWidget(self.open_combobox)

View file

@ -1,10 +1,5 @@
import subprocess
import time
import os
import pipes
import platform
from PySide2 import QtCore, QtWidgets, QtGui from PySide2 import QtCore, QtWidgets, QtGui
from termcolor import cprint from colorama import Style, Fore
class TaskBase(QtCore.QThread): class TaskBase(QtCore.QThread):
@ -23,11 +18,23 @@ class TaskBase(QtCore.QThread):
with self.global_common.exec_dangerzone_container(args) as p: with self.global_common.exec_dangerzone_container(args) as p:
for line in p.stdout: for line in p.stdout:
output += line.decode() 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) self.update_details.emit(output)
stderr = p.stderr.read().decode() stderr = p.stderr.read().decode()
cprint(stderr, attrs=["dark"]) if len(stderr) > 0:
print("")
for line in stderr.strip().split("\n"):
print(" " + Style.DIM + line)
self.update_details.emit(output) self.update_details.emit(output)
if p.returncode == 126 or p.returncode == 127: if p.returncode == 126 or p.returncode == 127:
@ -65,10 +72,6 @@ class ConvertToPixels(TaskBase):
self.global_common = global_common self.global_common = global_common
self.common = 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): def run(self):
self.update_label.emit("Converting document to pixels") self.update_label.emit("Converting document to pixels")
args = [ args = [
@ -80,75 +83,16 @@ class ConvertToPixels(TaskBase):
"--container-name", "--container-name",
self.global_common.get_container_name(), self.global_common.get_container_name(),
] ]
returncode, output, stderr = self.exec_container(args) returncode, output, _ = self.exec_container(args)
if returncode != 0: if returncode != 0:
return return
# Did we hit an error? success, error_message = self.global_common.validate_convert_to_pixel_output(
for line in output.split("\n"): self.common, output
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")
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 if not success:
self.task_failed.emit(error_message)
# 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 return
self.task_finished.emit() self.task_finished.emit()

View file

@ -11,9 +11,10 @@ from .tasks import PullImageTask, ConvertToPixels, ConvertToPDF
class TasksWidget(QtWidgets.QWidget): class TasksWidget(QtWidgets.QWidget):
close_window = QtCore.Signal() close_window = QtCore.Signal()
def __init__(self, global_common, common): def __init__(self, global_common, gui_common, common):
super(TasksWidget, self).__init__() super(TasksWidget, self).__init__()
self.global_common = global_common self.global_common = global_common
self.gui_common = gui_common
self.common = common self.common = common
# Dangerous document label # Dangerous document label
@ -31,7 +32,7 @@ class TasksWidget(QtWidgets.QWidget):
self.task_details.setStyleSheet( self.task_details.setStyleSheet(
"QLabel { background-color: #ffffff; font-size: 12px; padding: 10px; }" "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.task_details.setAlignment(QtCore.Qt.AlignTop)
self.details_scrollarea = QtWidgets.QScrollArea() self.details_scrollarea = QtWidgets.QScrollArea()
@ -111,7 +112,7 @@ class TasksWidget(QtWidgets.QWidget):
# Open # Open
if self.global_common.settings.get("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 # Clean up
self.common.pixel_dir.cleanup() self.common.pixel_dir.cleanup()
@ -122,7 +123,7 @@ class TasksWidget(QtWidgets.QWidget):
# In macOS, just close the window # In macOS, just close the window
self.close_window.emit() self.close_window.emit()
else: else:
self.global_common.app.quit() self.gui_common.app.quit()
def scroll_to_bottom(self, minimum, maximum): def scroll_to_bottom(self, minimum, maximum):
self.details_scrollarea.verticalScrollBar().setValue(maximum) self.details_scrollarea.verticalScrollBar().setValue(maximum)

View file

@ -7,18 +7,12 @@ class Settings:
def __init__(self, common): def __init__(self, common):
self.common = common self.common = common
self.settings_filename = os.path.join(self.common.appdata_path, "settings.json") 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 = { self.default_settings = {
"save": True, "save": True,
"ocr": True, "ocr": True,
"ocr_language": "English", "ocr_language": "English",
"open": True, "open": True,
"open_app": default_pdf_viewer, "open_app": None,
"update_container": True, "update_container": True,
"linux_prefers_typing_password": None, "linux_prefers_typing_password": None,
} }

1
dev_scripts/dangerzone-cli Symbolic link
View file

@ -0,0 +1 @@
dangerzone

View file

@ -31,7 +31,7 @@ def main():
print("* Building RPM package") print("* Building RPM package")
subprocess.run( 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, shell=True,
cwd=root, cwd=root,
check=True, check=True,

2
poetry.lock generated
View file

@ -374,7 +374,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = ">=3.7,<3.10" python-versions = ">=3.7,<3.10"
content-hash = "26e6acc883dad4194c45e399fadaabc12730fdd3a7b2264737e4e81838183eee" content-hash = "72592722794667cf7cb6bea727b31eb60d710c90d488be9d10a13bbbcaa56688"
[metadata.files] [metadata.files]
altgraph = [ altgraph = [

View file

@ -18,6 +18,7 @@ wmi = {version = "*", platform = "win32"}
pyxdg = {version = "*", platform = "linux"} pyxdg = {version = "*", platform = "linux"}
pyobjc-core = {version = "*", platform = "darwin"} pyobjc-core = {version = "*", platform = "darwin"}
pyobjc-framework-launchservices = {version = "*", platform = "darwin"} pyobjc-framework-launchservices = {version = "*", platform = "darwin"}
colorama = "^0.4.4"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pyinstaller = {version = "*", platform = "darwin"} pyinstaller = {version = "*", platform = "darwin"}
@ -27,6 +28,7 @@ black = "^21.5b2"
[tool.poetry.scripts] [tool.poetry.scripts]
dangerzone = 'dangerzone:main' dangerzone = 'dangerzone:main'
dangerzone-container = 'dangerzone:main' dangerzone-container = 'dangerzone:main'
dangerzone-cli = 'dangerzone:main'
[build-system] [build-system]
requires = ["poetry>=1.1.4"] requires = ["poetry>=1.1.4"]

View file

@ -2,7 +2,9 @@
import setuptools import setuptools
import os import os
import sys import sys
from dangerzone import dangerzone_version
with open("share/version.txt") as f:
version = f.read().strip()
def file_list(path): def file_list(path):
@ -15,7 +17,7 @@ def file_list(path):
setuptools.setup( setuptools.setup(
name="dangerzone", name="dangerzone",
version=dangerzone_version, version=version,
author="Micah Lee", author="Micah Lee",
author_email="micah.lee@theintercept.com", author_email="micah.lee@theintercept.com",
license="MIT", license="MIT",
@ -23,7 +25,10 @@ setuptools.setup(
url="https://github.com/firstlookmedia/dangerzone", url="https://github.com/firstlookmedia/dangerzone",
packages=["dangerzone"], packages=["dangerzone"],
data_files=[ data_files=[
("share/applications", ["install/linux/media.firstlook.dangerzone.desktop"],), (
"share/applications",
["install/linux/media.firstlook.dangerzone.desktop"],
),
( (
"share/icons/hicolor/64x64/apps", "share/icons/hicolor/64x64/apps",
["install/linux/media.firstlook.dangerzone.png"], ["install/linux/media.firstlook.dangerzone.png"],

1
share/version.txt Normal file
View file

@ -0,0 +1 @@
0.2

View file

@ -1,6 +1,6 @@
[DEFAULT] [DEFAULT]
Package3: dangerzone 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 Build-Depends: dh-python, python3, python3-all
Suite: bionic Suite: bionic
X-Python3-Version: >= 3.6 X-Python3-Version: >= 3.6