From 81c7bc7ecba3f607baf0fdeaf7e145d79f341984 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 5 Mar 2024 12:23:11 +0200 Subject: [PATCH] improvements to download verifier drag and drop --- .../sparrowwallet/sparrow/AppController.java | 20 ++- .../sparrowwallet/sparrow/AppServices.java | 4 +- .../control/DownloadVerifierDialog.java | 119 ++++++++++++++---- 3 files changed, 115 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 296703c4..4c2a4f14 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -219,6 +219,8 @@ public class AppController implements Initializable { private SendToManyDialog sendToManyDialog; + private DownloadVerifierDialog downloadVerifierDialog; + private Tab previouslySelectedTab; private boolean subTabsVisible; @@ -634,7 +636,7 @@ public class AppController implements Initializable { } catch(TransactionParseException e) { showErrorDialog("Invalid transaction", e.getMessage()); } catch(Exception e) { - showErrorDialog("Invalid file", e.getMessage()); + showErrorDialog("Invalid file", "Cannot recognise the format of this file."); } } } @@ -993,7 +995,7 @@ public class AppController implements Initializable { public void openFile(File file) { if(isWalletFile(file)) { openWalletFile(file, true); - } else if(isSignatureOrManifestFile(file)) { + } else if(isVerifyDownloadFile(file)) { verifyDownload(new ActionEvent(file, rootStack)); } else { openTransactionFile(file); @@ -1479,8 +1481,20 @@ public class AppController implements Initializable { } public void verifyDownload(ActionEvent event) { - DownloadVerifierDialog downloadVerifierDialog = new DownloadVerifierDialog(event.getSource() instanceof File file ? file : null); + if(downloadVerifierDialog != null) { + Stage stage = (Stage)downloadVerifierDialog.getDialogPane().getScene().getWindow(); + stage.setAlwaysOnTop(true); + stage.setAlwaysOnTop(false); + if(event.getSource() instanceof File file) { + downloadVerifierDialog.setSignatureFile(file); + } + return; + } + + downloadVerifierDialog = new DownloadVerifierDialog(event.getSource() instanceof File file ? file : null); + downloadVerifierDialog.initOwner(rootStack.getScene().getWindow()); downloadVerifierDialog.showAndWait(); + downloadVerifierDialog = null; } public void minimizeToTray(ActionEvent event) { diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index ef71dbfc..8b9cfb01 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -786,7 +786,7 @@ public class AppServices { } public static Window getActiveWindow() { - return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : null); + return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : (Stage.getWindows().iterator().hasNext() ? Stage.getWindows().iterator().next() : null)); } public static void moveToActiveWindowScreen(Dialog dialog) { @@ -885,7 +885,7 @@ public class AppServices { for(File file : openFiles) { if(isWalletFile(file)) { EventManager.get().post(new RequestWalletOpenEvent(openWindow, file)); - } else if(isSignatureOrManifestFile(file)) { + } else if(isVerifyDownloadFile(file)) { EventManager.get().post(new RequestVerifyDownloadEvent(openWindow, file)); } else { EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file)); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java index dd553060..8aac67c2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java @@ -18,6 +18,8 @@ import javafx.geometry.Pos; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.input.Dragboard; +import javafx.scene.input.TransferMode; import javafx.scene.layout.*; import javafx.stage.FileChooser; import javafx.stage.Stage; @@ -35,12 +37,19 @@ import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import static com.sparrowwallet.sparrow.AppController.DRAG_OVER_CLASS; + public class DownloadVerifierDialog extends Dialog { private static final Logger log = LoggerFactory.getLogger(DownloadVerifierDialog.class); + private static final DateFormat signatureDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy z"); + private static final long MAX_VALID_MANIFEST_SIZE = 100 * 1024; + private static final String SHA256SUMS_MANIFEST_PREFIX = "sha256sums"; private static final List SIGNATURE_EXTENSIONS = List.of("asc", "sig", "gpg"); private static final List MANIFEST_EXTENSIONS = List.of("txt"); @@ -48,6 +57,13 @@ public class DownloadVerifierDialog extends Dialog { private static final List MACOS_RELEASE_EXTENSIONS = List.of("dmg"); private static final List WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip"); private static final List LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz"); + private static final List DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu"); + private static final List ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z"); + + private static final String SPARROW_RELEASE_PREFIX = "sparrow-"; + private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc"; + private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*"); + private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024; private final ObjectProperty signature = new SimpleObjectProperty<>(); private final ObjectProperty manifest = new SimpleObjectProperty<>(); @@ -69,6 +85,7 @@ public class DownloadVerifierDialog extends Dialog { dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); AppServices.setStageIcon(dialogPane.getScene().getWindow()); dialogPane.setHeader(new Header()); + setupDrag(dialogPane); VBox vBox = new VBox(); vBox.setSpacing(20); @@ -179,26 +196,10 @@ public class DownloadVerifierDialog extends Dialog { boolean verify = true; try { Map manifestMap = getManifest(manifestFile); - List releaseExtensions = getReleaseFileExtensions(); - for(File file : manifestMap.keySet()) { - if(releaseExtensions.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith(ext))) { - File releaseFile = new File(manifestFile.getParent(), file.getName()); - if(releaseFile.exists() && !releaseFile.equals(release.get())) { - release.set(releaseFile); - verify = false; - break; - } - } - } - if(release.get() == null) { - for(File file : manifestMap.keySet()) { - File releaseFile = new File(manifestFile.getParent(), file.getName()); - if(releaseFile.exists() && !releaseFile.equals(release.get())) { - release.set(releaseFile); - verify = false; - break; - } - } + File releaseFile = findReleaseFile(manifestFile, manifestMap); + if(releaseFile != null && !releaseFile.equals(release.get())) { + release.set(releaseFile); + verify = false; } } catch(IOException e) { log.debug("Error reading manifest file", e); @@ -227,6 +228,39 @@ public class DownloadVerifierDialog extends Dialog { } } + private void setupDrag(DialogPane dialogPane) { + dialogPane.setOnDragOver(event -> { + if(event.getGestureSource() != dialogPane && event.getDragboard().hasFiles()) { + event.acceptTransferModes(TransferMode.COPY_OR_MOVE); + } + event.consume(); + }); + + dialogPane.setOnDragDropped(event -> { + Dragboard db = event.getDragboard(); + boolean success = false; + if(db.hasFiles()) { + for(File file : db.getFiles()) { + if(isVerifyDownloadFile(file)) { + signature.set(file); + break; + } + } + success = true; + } + event.setDropCompleted(success); + event.consume(); + }); + + dialogPane.setOnDragEntered(event -> { + dialogPane.getStyleClass().add(DRAG_OVER_CLASS); + }); + + dialogPane.setOnDragExited(event -> { + dialogPane.getStyleClass().removeAll(DRAG_OVER_CLASS); + }); + } + public void verify() { boolean signatureVerified = verifySignature(); if(signatureVerified) { @@ -412,6 +446,17 @@ public class DownloadVerifierDialog extends Dialog { } } + if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) { + Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName()); + if(matcher.find()) { + String version = matcher.group(); + File signatureFile = new File(providedFile.getParentFile(), SPARROW_RELEASE_PREFIX + version + SPARROW_SIGNATURE_SUFFIX); + if(signatureFile.exists()) { + return signatureFile; + } + } + } + return null; } @@ -427,6 +472,24 @@ public class DownloadVerifierDialog extends Dialog { return null; } + private File findReleaseFile(File manifestFile, Map manifestMap) { + List releaseExtensions = getReleaseFileExtensions(); + List> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of("")); + + for(List extensions : extensionLists) { + for(File file : manifestMap.keySet()) { + if(extensions.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith(ext))) { + File releaseFile = new File(manifestFile.getParent(), file.getName()); + if(releaseFile.exists()) { + return releaseFile; + } + } + } + } + + return null; + } + private List getReleaseFileExtensions() { Platform platform = Platform.getCurrent(); switch(platform) { @@ -477,13 +540,14 @@ public class DownloadVerifierDialog extends Dialog { return message; } - public static boolean isSignatureOrManifestFile(File file) { + public static boolean isVerifyDownloadFile(File file) { if(file != null) { - if(file.getName().length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith("." + ext))) { + String name = file.getName().toLowerCase(Locale.ROOT); + if(name.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext))) { return true; } - if(file.getName().toLowerCase(Locale.ROOT).endsWith(".txt")) { + if(MANIFEST_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext)) || name.startsWith(SHA256SUMS_MANIFEST_PREFIX)) { try { Map manifest = getManifest(file); return !manifest.isEmpty(); @@ -491,11 +555,20 @@ public class DownloadVerifierDialog extends Dialog { //ignore } } + + if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) { + Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name); + return matcher.find(); + } } return false; } + public void setSignatureFile(File signatureFile) { + signature.set(signatureFile); + } + private static class Header extends GridPane { public Header() { setMaxWidth(Double.MAX_VALUE);