client/securedrop_client/gui/conversation/export/whistleflow_dialog.py (125 lines of code) (raw):

""" A dialog that allows journalists to export conversations or transcripts to the Whistleflow View VM. This is a clone of FileDialog. """ import datetime import logging from gettext import gettext as _ from PyQt5.QtCore import QSize, pyqtSlot from securedrop_client.export_status import ExportError, ExportStatus from securedrop_client.gui.base import ModalDialog, SecureQLabel from ....export import Export logger = logging.getLogger(__name__) class WhistleflowDialog(ModalDialog): PASSPHRASE_LABEL_SPACING = 0.5 NO_MARGIN = 0 FILENAME_WIDTH_PX = 260 def __init__(self, device: Export, summary: str, file_locations: list[str]) -> None: super().__init__() self.setStyleSheet(self.DIALOG_CSS) self._device = device self._file_locations = file_locations self.file_name = SecureQLabel( summary, wordwrap=False, max_length=self.FILENAME_WIDTH_PX ).text() # Hold onto the error status we receive from the Export VM self.error_status: ExportStatus | None = None # Connect device signals to slots self._device.whistleflow_preflight_check_succeeded.connect( self._on_export_preflight_check_succeeded ) self._device.whistleflow_preflight_check_failed.connect( self._on_export_preflight_check_failed ) self._device.export_completed.connect(self._on_export_succeeded) # # For now, connect both success and error signals to close the print dialog. self._device.whistleflow_call_failure.connect(self._on_export_failed) self._device.whistleflow_call_success.connect(self._on_export_succeeded) # Connect parent signals to slots self.continue_button.setEnabled(False) self.continue_button.clicked.connect(self._run_preflight) # Dialog content self.starting_header = _( "Preparing to export:<br />" '<span style="font-weight:normal">{}</span>' ).format(self.file_name) self.ready_header = _( "Ready to export:<br />" '<span style="font-weight:normal">{}</span>' ).format(self.file_name) self.success_header = _("Export successful") self.error_header = _("Export failed") self.starting_message = _( "<h2>Understand the risks before exporting files</h2>" "<b>Malware</b>" "<br />" "This workstation lets you open files securely. If you open files on another " "computer, any embedded malware may spread to your computer or network. If you are " "unsure how to manage this risk, please print the file, or contact your " "administrator." "<br /><br />" "<b>Anonymity</b>" "<br />" "Files submitted by sources may contain information or hidden metadata that " "identifies who they are. To protect your sources, please consider redacting files " "before working with them on network-connected computers." ) self.generic_error_message = _("See your administrator for help.") self.success_message = _( "Remember to be careful when working with files outside of your Workstation machine." ) self._show_starting_instructions() self.start_animate_header() self._run_preflight() def _show_starting_instructions(self) -> None: self.header.setText(self.starting_header) self.body.setText(self.starting_message) self.adjustSize() def _send_to_whistleflow(self) -> None: timestamp = datetime.datetime.now().isoformat() self._device.send_files_to_whistleflow(f"export-{timestamp}.tar", self._file_locations) def _show_success_message(self) -> None: self.continue_button.clicked.disconnect() self.continue_button.clicked.connect(self.close) self.header.setText(self.success_header) self.continue_button.setText(_("DONE")) self.body.setText(self.success_message) self.cancel_button.hide() self.error_details.hide() self.header_line.show() self.body.show() self.adjustSize() def _show_generic_error_message(self) -> None: self.continue_button.clicked.disconnect() self.continue_button.clicked.connect(self.close) self.continue_button.setText(_("DONE")) self.header.setText(self.error_header) self.body.setText( # nosemgrep: semgrep.untranslated-gui-string f"{self.error_status}: {self.generic_error_message}" ) self.error_details.hide() self.header_line.show() self.body.show() self.adjustSize() @pyqtSlot() def _run_preflight(self) -> None: self._device.run_whistleflow_preflight_checks() @pyqtSlot() def _export_file(self, checked: bool = False) -> None: self.start_animate_activestate() self.cancel_button.setEnabled(False) self._device.send_files_to_whistleflow(self.file_name, self._file_locations) @pyqtSlot() def _on_export_preflight_check_succeeded(self) -> None: # If the continue button is disabled then this is the result of a background preflight check self.stop_animate_header() self.header_icon.update_image("savetodisk.svg", QSize(64, 64)) self.header.setText(self.ready_header) if not self.continue_button.isEnabled(): self.continue_button.clicked.disconnect() self.continue_button.clicked.connect(self._send_to_whistleflow) self.continue_button.setEnabled(True) self.continue_button.setFocus() return self._send_to_whistleflow() @pyqtSlot(object) def _on_export_preflight_check_failed(self, error: ExportError) -> None: self.stop_animate_header() self.header_icon.update_image("savetodisk.svg", QSize(64, 64)) @pyqtSlot() def _on_export_succeeded(self) -> None: self.stop_animate_activestate() self._show_success_message() @pyqtSlot(object) def _on_export_failed(self, error: ExportError) -> None: self.stop_animate_activestate() self.cancel_button.setEnabled(True) logger.error(error.status)