improvements to download verifier drag and drop

This commit is contained in:
Craig Raw 2024-03-05 12:23:11 +02:00
parent 3d18477560
commit 81c7bc7ecb
3 changed files with 115 additions and 28 deletions

View file

@ -219,6 +219,8 @@ public class AppController implements Initializable {
private SendToManyDialog sendToManyDialog; private SendToManyDialog sendToManyDialog;
private DownloadVerifierDialog downloadVerifierDialog;
private Tab previouslySelectedTab; private Tab previouslySelectedTab;
private boolean subTabsVisible; private boolean subTabsVisible;
@ -634,7 +636,7 @@ public class AppController implements Initializable {
} catch(TransactionParseException e) { } catch(TransactionParseException e) {
showErrorDialog("Invalid transaction", e.getMessage()); showErrorDialog("Invalid transaction", e.getMessage());
} catch(Exception e) { } 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) { public void openFile(File file) {
if(isWalletFile(file)) { if(isWalletFile(file)) {
openWalletFile(file, true); openWalletFile(file, true);
} else if(isSignatureOrManifestFile(file)) { } else if(isVerifyDownloadFile(file)) {
verifyDownload(new ActionEvent(file, rootStack)); verifyDownload(new ActionEvent(file, rootStack));
} else { } else {
openTransactionFile(file); openTransactionFile(file);
@ -1479,8 +1481,20 @@ public class AppController implements Initializable {
} }
public void verifyDownload(ActionEvent event) { 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.showAndWait();
downloadVerifierDialog = null;
} }
public void minimizeToTray(ActionEvent event) { public void minimizeToTray(ActionEvent event) {

View file

@ -786,7 +786,7 @@ public class AppServices {
} }
public static Window getActiveWindow() { 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) { public static void moveToActiveWindowScreen(Dialog<?> dialog) {
@ -885,7 +885,7 @@ public class AppServices {
for(File file : openFiles) { for(File file : openFiles) {
if(isWalletFile(file)) { if(isWalletFile(file)) {
EventManager.get().post(new RequestWalletOpenEvent(openWindow, file)); EventManager.get().post(new RequestWalletOpenEvent(openWindow, file));
} else if(isSignatureOrManifestFile(file)) { } else if(isVerifyDownloadFile(file)) {
EventManager.get().post(new RequestVerifyDownloadEvent(openWindow, file)); EventManager.get().post(new RequestVerifyDownloadEvent(openWindow, file));
} else { } else {
EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file)); EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file));

View file

@ -18,6 +18,8 @@ import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
@ -35,12 +37,19 @@ import java.security.NoSuchAlgorithmException;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppController.DRAG_OVER_CLASS;
public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> { public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(DownloadVerifierDialog.class); 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 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 long MAX_VALID_MANIFEST_SIZE = 100 * 1024;
private static final String SHA256SUMS_MANIFEST_PREFIX = "sha256sums";
private static final List<String> SIGNATURE_EXTENSIONS = List.of("asc", "sig", "gpg"); private static final List<String> SIGNATURE_EXTENSIONS = List.of("asc", "sig", "gpg");
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt"); private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
@ -48,6 +57,13 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg"); private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip"); private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz"); private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
private static final List<String> 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<File> signature = new SimpleObjectProperty<>(); private final ObjectProperty<File> signature = new SimpleObjectProperty<>();
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>(); private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
@ -69,6 +85,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeader(new Header()); dialogPane.setHeader(new Header());
setupDrag(dialogPane);
VBox vBox = new VBox(); VBox vBox = new VBox();
vBox.setSpacing(20); vBox.setSpacing(20);
@ -179,26 +196,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
boolean verify = true; boolean verify = true;
try { try {
Map<File, String> manifestMap = getManifest(manifestFile); Map<File, String> manifestMap = getManifest(manifestFile);
List<String> releaseExtensions = getReleaseFileExtensions(); File releaseFile = findReleaseFile(manifestFile, manifestMap);
for(File file : manifestMap.keySet()) { if(releaseFile != null && !releaseFile.equals(release.get())) {
if(releaseExtensions.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith(ext))) { release.set(releaseFile);
File releaseFile = new File(manifestFile.getParent(), file.getName()); verify = false;
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;
}
}
} }
} catch(IOException e) { } catch(IOException e) {
log.debug("Error reading manifest file", e); log.debug("Error reading manifest file", e);
@ -227,6 +228,39 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
} }
} }
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() { public void verify() {
boolean signatureVerified = verifySignature(); boolean signatureVerified = verifySignature();
if(signatureVerified) { if(signatureVerified) {
@ -412,6 +446,17 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
} }
} }
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; return null;
} }
@ -427,6 +472,24 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
return null; return null;
} }
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
List<String> releaseExtensions = getReleaseFileExtensions();
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
for(List<String> 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<String> getReleaseFileExtensions() { private List<String> getReleaseFileExtensions() {
Platform platform = Platform.getCurrent(); Platform platform = Platform.getCurrent();
switch(platform) { switch(platform) {
@ -477,13 +540,14 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
return message; return message;
} }
public static boolean isSignatureOrManifestFile(File file) { public static boolean isVerifyDownloadFile(File file) {
if(file != null) { 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; 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 { try {
Map<File, String> manifest = getManifest(file); Map<File, String> manifest = getManifest(file);
return !manifest.isEmpty(); return !manifest.isEmpty();
@ -491,11 +555,20 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
//ignore //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; return false;
} }
public void setSignatureFile(File signatureFile) {
signature.set(signatureFile);
}
private static class Header extends GridPane { private static class Header extends GridPane {
public Header() { public Header() {
setMaxWidth(Double.MAX_VALUE); setMaxWidth(Double.MAX_VALUE);