diff --git a/.github/stale.yml b/.github/stale.yml index 58ad1e2ad..61494684a 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,12 +1,14 @@ # Number of days of inactivity before an issue becomes stale -daysUntilStale: 90 +daysUntilStale: 180 # Number of days of inactivity before a stale issue is closed daysUntilClose: 30 # Issues with these labels will never be considered stale exemptLabels: - type:security-issue # never close automatically + - type:feature-request # never close automatically - state:awaiting-response # handled by different bot - state:blocked + - state:confirmed # Set to true to ignore issues in a milestone (defaults to false) exemptMilestones: true # Label to use when marking an issue as stale diff --git a/.idea/compiler.xml b/.idea/compiler.xml index cfda7c67b..1119f53ee 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -31,10 +31,10 @@ - - + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 0e7df1ed1..c3807468d 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,11 +1,13 @@ - + - - - - - + + + + + + + \ No newline at end of file diff --git a/main/buildkit/pom.xml b/main/buildkit/pom.xml index 11ad987bd..c08e82d9f 100644 --- a/main/buildkit/pom.xml +++ b/main/buildkit/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.0-beta1 + 1.5.0-beta2 buildkit pom diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 1f9b121ec..5a00af424 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.0-beta1 + 1.5.0-beta2 commons Cryptomator Commons @@ -48,6 +48,12 @@ easybind + + + com.auth0 + java-jwt + + com.google.guava diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java index 5d86ca6ce..795dc6dcd 100644 --- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java @@ -28,13 +28,23 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; @Module(subcomponents = {VaultComponent.class}) public abstract class CommonsModule { private static final int NUM_SCHEDULER_THREADS = 4; + @Provides + @Singleton + @Named("licensePublicKey") + static String provideLicensePublicKey() { + // in PEM format without the dash-escaped begin/end lines + return "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW" // + + "fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV" // + + "gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE" // + + "r0DzRyj4ixPIt38CQB8="; + } + @Provides @Singleton @Named("SemVer") @@ -56,7 +66,7 @@ public abstract class CommonsModule { @Provides @Singleton - static ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer shutdownTaskScheduler) { + static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) { final AtomicInteger threadNumber = new AtomicInteger(1); ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> { Thread t = new Thread(r); @@ -64,7 +74,7 @@ public abstract class CommonsModule { t.setDaemon(true); return t; }); - shutdownTaskScheduler.accept(executorService::shutdown); + shutdownHook.runOnShutdown(executorService::shutdown); return executorService; } diff --git a/main/commons/src/main/java/org/cryptomator/common/Environment.java b/main/commons/src/main/java/org/cryptomator/common/Environment.java index 52e23ff07..8dfb56ef2 100644 --- a/main/commons/src/main/java/org/cryptomator/common/Environment.java +++ b/main/commons/src/main/java/org/cryptomator/common/Environment.java @@ -25,6 +25,7 @@ public class Environment { private static final Path RELATIVE_HOME_DIR = Paths.get("~"); private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME); private static final char PATH_LIST_SEP = ':'; + private static final int DEFAULT_MIN_PW_LENGTH = 8; @Inject public Environment() { @@ -37,6 +38,7 @@ public class Environment { LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath")); LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir")); LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir")); + LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength")); } public boolean useCustomLogbackConfig() { @@ -63,6 +65,19 @@ public class Environment { return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir); } + public int getMinPwLength() { + return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH); + } + + private int getInt(String propertyName, int defaultValue) { + String value = System.getProperty(propertyName); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { // includes "null" values + return defaultValue; + } + } + private Optional getPath(String propertyName) { String value = System.getProperty(propertyName); return Optional.ofNullable(value).map(Paths::get); diff --git a/main/commons/src/main/java/org/cryptomator/common/LicenseChecker.java b/main/commons/src/main/java/org/cryptomator/common/LicenseChecker.java new file mode 100644 index 000000000..93f16f755 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/LicenseChecker.java @@ -0,0 +1,56 @@ +package org.cryptomator.common; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import com.google.common.io.BaseEncoding; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Optional; + +@Singleton +class LicenseChecker { + + private final JWTVerifier verifier; + + @Inject + public LicenseChecker(@Named("licensePublicKey") String pemEncodedPublicKey) { + Algorithm algorithm = Algorithm.ECDSA512(decodePublicKey(pemEncodedPublicKey), null); + this.verifier = JWT.require(algorithm).build(); + } + + private static ECPublicKey decodePublicKey(String pemEncodedPublicKey) { + try { + byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey); + PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes)); + if (key instanceof ECPublicKey) { + return (ECPublicKey) key; + } else { + throw new IllegalStateException("Key not an EC public key."); + } + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException("Invalid license public key", e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + public Optional check(String licenseKey) { + try { + return Optional.of(verifier.verify(licenseKey)); + } catch (JWTVerificationException exception) { + return Optional.empty(); + } + } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java b/main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java new file mode 100644 index 000000000..70d439afe --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/LicenseHolder.java @@ -0,0 +1,79 @@ +package org.cryptomator.common; + +import com.auth0.jwt.interfaces.DecodedJWT; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import org.cryptomator.common.settings.Settings; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Optional; + +@Singleton +public class LicenseHolder { + + private final Settings settings; + private final LicenseChecker licenseChecker; + private final ObjectProperty validJwtClaims; + private final StringBinding licenseSubject; + private final BooleanBinding validLicenseProperty; + + @Inject + public LicenseHolder(LicenseChecker licenseChecker, Settings settings) { + this.settings = settings; + this.licenseChecker = licenseChecker; + this.validJwtClaims = new SimpleObjectProperty<>(); + this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims); + this.validLicenseProperty = validJwtClaims.isNotNull(); + + Optional claims = licenseChecker.check(settings.licenseKey().get()); + validJwtClaims.set(claims.orElse(null)); + } + + public boolean validateAndStoreLicense(String licenseKey) { + Optional claims = licenseChecker.check(licenseKey); + validJwtClaims.set(claims.orElse(null)); + if (claims.isPresent()) { + settings.licenseKey().set(licenseKey); + return true; + } else { + return false; + } + } + + /* Observable Properties */ + + public Optional getLicenseKey() { + DecodedJWT claims = validJwtClaims.get(); + if (claims != null) { + return Optional.of(claims.getToken()); + } else { + return Optional.empty(); + } + } + + public StringBinding licenseSubjectProperty() { + return licenseSubject; + } + + public String getLicenseSubject() { + DecodedJWT claims = validJwtClaims.get(); + if (claims != null) { + return claims.getSubject(); + } else { + return null; + } + } + + public BooleanBinding validLicenseProperty() { + return validLicenseProperty; + } + + public boolean isValidLicense() { + return validLicenseProperty.get(); + } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/Optionals.java b/main/commons/src/main/java/org/cryptomator/common/Optionals.java deleted file mode 100644 index 4ccea7d8b..000000000 --- a/main/commons/src/main/java/org/cryptomator/common/Optionals.java +++ /dev/null @@ -1,28 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -package org.cryptomator.common; - -import java.util.Optional; -import java.util.function.Function; - -public final class Optionals { - - private Optionals() { - } - - /** - * Returns a function that is equivalent to the input function but immediately gets the value of the returned optional when invoked. - * - * @param the type of the input to the function - * @param the type of the result of the function - * @param function An {@code Optional}-bearing input function {@code Function>} - * @return A {@code Function}, that may throw a NoSuchElementException, if the original function returns an empty optional. - */ - public static Function unwrap(Function> function) { - return t -> function.apply(t).get(); - } - -} diff --git a/main/commons/src/main/java/org/cryptomator/common/ShutdownHook.java b/main/commons/src/main/java/org/cryptomator/common/ShutdownHook.java new file mode 100644 index 000000000..0fcb9d676 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/ShutdownHook.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the accompanying LICENSE file. + *******************************************************************************/ +package org.cryptomator.common; + +import com.google.common.util.concurrent.Runnables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Queue; +import java.util.concurrent.PriorityBlockingQueue; + +@Singleton +public class ShutdownHook extends Thread { + + private static final int PRIO_VERY_LAST = Integer.MIN_VALUE; + public static final int PRIO_LAST = PRIO_VERY_LAST + 1; + public static final int PRIO_DEFAULT = 0; + public static final int PRIO_FIRST = Integer.MAX_VALUE; + private static final Logger LOG = LoggerFactory.getLogger(ShutdownHook.class); + private static final OrderedTask POISON = new OrderedTask(PRIO_VERY_LAST, Runnables.doNothing()); + private final Queue tasks = new PriorityBlockingQueue<>(); + + @Inject + ShutdownHook() { + super(null, null, "ShutdownTasks", 0); + Runtime.getRuntime().addShutdownHook(this); + LOG.debug("Registered shutdown hook."); + } + + @Override + public void run() { + LOG.debug("Running graceful shutdown tasks..."); + tasks.add(POISON); + Runnable task; + while ((task = tasks.remove()) != POISON) { + try { + task.run(); + } catch (RuntimeException e) { + LOG.error("Exception while shutting down.", e); + } + } + } + + /** + * Schedules a task to be run during shutdown with default order + * + * @param task The task to be scheduled + */ + public void runOnShutdown(Runnable task) { + runOnShutdown(PRIO_DEFAULT, task); + } + + /** + * Schedules a task to be run with the given priority + * + * @param priority Tasks with high priority will be run before task with lower priority + * @param task The task to be scheduled + */ + public void runOnShutdown(int priority, Runnable task) { + tasks.add(new OrderedTask(priority, task)); + } + + private static class OrderedTask implements Comparable, Runnable { + + private final int priority; + private final Runnable task; + + public OrderedTask(int priority, Runnable task) { + this.priority = priority; + this.task = task; + } + + @Override + public int compareTo(OrderedTask other) { + // overflow-safe signum impl: + if (this.priority > other.priority) { + return -1; // higher prio -> this before other + } else if (this.priority < other.priority) { + return +1; // lower prio -> other before this + } else { + return 0; // same prio + } + } + + @Override + public void run() { + task.run(); + } + } + +} \ No newline at end of file diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java index ff5613f1e..16d16b01d 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java @@ -15,6 +15,8 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.NodeOrientation; @@ -35,6 +37,7 @@ public class Settings { public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE; public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT; public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT; + private static final String DEFAULT_LICENSE_KEY = ""; private final ObservableList directories = FXCollections.observableArrayList(VaultSettings::observables); private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK); @@ -47,6 +50,7 @@ public class Settings { private final ObjectProperty preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL); private final ObjectProperty theme = new SimpleObjectProperty<>(DEFAULT_THEME); private final ObjectProperty userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION); + private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY); private Consumer saveCmd; @@ -65,6 +69,7 @@ public class Settings { preferredVolumeImpl.addListener(this::somethingChanged); theme.addListener(this::somethingChanged); userInterfaceOrientation.addListener(this::somethingChanged); + licenseKey.addListener(this::somethingChanged); } void setSaveCmd(Consumer saveCmd) { @@ -126,4 +131,8 @@ public class Settings { public ObjectProperty userInterfaceOrientation() { return userInterfaceOrientation; } + + public StringProperty licenseKey() { + return licenseKey; + } } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index 874994cf8..33afb7306 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -38,6 +38,7 @@ public class SettingsJsonAdapter extends TypeAdapter { out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name()); out.name("theme").value(value.theme().get().name()); out.name("uiOrientation").value(value.userInterfaceOrientation().get().name()); + out.name("licenseKey").value(value.licenseKey().get()); out.endObject(); } @@ -90,6 +91,9 @@ public class SettingsJsonAdapter extends TypeAdapter { case "uiOrientation": settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString())); break; + case "licenseKey": + settings.licenseKey().set(in.nextString()); + break; default: LOG.warn("Unsupported vault setting found in JSON: " + name); in.skipValue(); diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index 3bb3496b8..e950fd4e2 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -18,6 +18,7 @@ import org.cryptomator.cryptofs.migration.Migrators; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Collection; @@ -67,7 +68,13 @@ public class VaultListManager { } private Optional get(Path vaultPath) { - return vaultList.stream().filter(v -> v.getPath().equals(vaultPath)).findAny(); + return vaultList.stream().filter(v -> { + try { + return Files.isSameFile(vaultPath, v.getPath()); + } catch (IOException e) { + return false; + } + }).findAny(); } private Vault create(VaultSettings vaultSettings) { @@ -76,7 +83,7 @@ public class VaultListManager { return comp.vault(); } - private VaultState determineVaultState(Path pathToVault) { + public static VaultState determineVaultState(Path pathToVault) { try { if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) { return VaultState.MISSING; diff --git a/main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java b/main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java new file mode 100644 index 000000000..5ae8f0fb9 --- /dev/null +++ b/main/commons/src/test/java/org/cryptomator/common/LicenseCheckerTest.java @@ -0,0 +1,62 @@ +package org.cryptomator.common; + +import com.auth0.jwt.interfaces.DecodedJWT; +import org.cryptomator.common.LicenseChecker; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +class LicenseCheckerTest { + + private static final String PUBLIC_KEY = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ" // + + "PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47" // + + "6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM" // + + "Al8G7CqwoJOsW7Kddns="; + + private LicenseChecker licenseChecker; + + @BeforeEach + public void setup() { + licenseChecker = new LicenseChecker(PUBLIC_KEY); + } + + @Test + public void testCheckValidLicense() { + String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj"; + + Optional decoded = licenseChecker.check(license); + + Assertions.assertTrue(decoded.isPresent()); + Assertions.assertEquals("cryptobot@example.com", decoded.get().getSubject()); + } + + @Test + public void testCheckInvalidLicenseHeader() { + String license = "EyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj"; + + Optional decoded = licenseChecker.check(license); + + Assertions.assertFalse(decoded.isPresent()); + } + + @Test + public void testCheckInvalidLicensePayload() { + String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.EyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.AQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj"; + + Optional decoded = licenseChecker.check(license); + + Assertions.assertFalse(decoded.isPresent()); + } + + @Test + public void testCheckInvalidLicenseSignature() { + String license = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InhaRGZacHJ5NFA5dlpQWnlHMmZOQlJqLTdMejVvbVZkbTd0SG9DZ1NOZlkifQ.eyJzdWIiOiJjcnlwdG9ib3RAZXhhbXBsZS5jb20iLCJpYXQiOjE1MTYyMzkwMjJ9.aQaBIKQdNCxmRJi2wLOcbagTgi39WhdWwgdpKTYSPicg-aPr_tst_RjmnqMemx3cBe0Blr4nEbj_lAtSKHz_i61fAUyI1xCIAZYbK9Q3ICHIHQl3AiuCpBwFl-k81OB4QDYiKpEc9gLN5dhW_VymJMsgOvyiC0UjC91f2AM7s46byDNj"; + + Optional decoded = licenseChecker.check(license); + + Assertions.assertFalse(decoded.isPresent()); + } + +} \ No newline at end of file diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml index 441e75af8..568dfca7f 100644 --- a/main/keychain/pom.xml +++ b/main/keychain/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.0-beta1 + 1.5.0-beta2 keychain System Keychain Access diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml index a56cd33c6..9338d9943 100644 --- a/main/launcher/pom.xml +++ b/main/launcher/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.0-beta1 + 1.5.0-beta2 launcher Cryptomator Launcher diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/CleanShutdownPerformer.java b/main/launcher/src/main/java/org/cryptomator/launcher/CleanShutdownPerformer.java deleted file mode 100644 index 717fb86a8..000000000 --- a/main/launcher/src/main/java/org/cryptomator/launcher/CleanShutdownPerformer.java +++ /dev/null @@ -1,48 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -package org.cryptomator.launcher; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -class CleanShutdownPerformer extends Thread { - - private static final Logger LOG = LoggerFactory.getLogger(CleanShutdownPerformer.class); - private final ConcurrentMap tasks = new ConcurrentHashMap<>(); - - @Inject - CleanShutdownPerformer() { - super(null, null, "ShutdownTasks", 0); - } - - @Override - public void run() { - LOG.debug("Running graceful shutdown tasks..."); - tasks.keySet().forEach(r -> { - try { - r.run(); - } catch (RuntimeException e) { - LOG.error("Exception while shutting down.", e); - } - }); - tasks.clear(); - } - - void scheduleShutdownTask(Runnable task) { - tasks.put(task, Boolean.TRUE); - } - - void registerShutdownHook() { - Runtime.getRuntime().addShutdownHook(this); - } -} \ No newline at end of file diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java index a4d861c3e..04eb9448d 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/Cryptomator.java @@ -32,17 +32,15 @@ public class Cryptomator { private final IpcFactory ipcFactory; private final Optional applicationVersion; private final CountDownLatch shutdownLatch; - private final CleanShutdownPerformer shutdownPerformer; private final UiLauncher uiLauncher; @Inject - Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, CleanShutdownPerformer shutdownPerformer, UiLauncher uiLauncher) { + Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) { this.logConfig = logConfig; this.debugMode = debugMode; this.ipcFactory = ipcFactory; this.applicationVersion = applicationVersion; this.shutdownLatch = shutdownLatch; - this.shutdownPerformer = shutdownPerformer; this.uiLauncher = uiLauncher; } @@ -90,7 +88,6 @@ public class Cryptomator { */ private int runGuiApplication() { try { - shutdownPerformer.registerShutdownHook(); uiLauncher.launch(); shutdownLatch.await(); LOG.info("UI shut down"); diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java index 268d69002..6b37e29b6 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java @@ -7,18 +7,10 @@ import javax.inject.Named; import javax.inject.Singleton; import java.util.Optional; import java.util.concurrent.CountDownLatch; -import java.util.function.Consumer; @Module class CryptomatorModule { - - @Provides - @Singleton - @Named("shutdownTaskScheduler") - Consumer provideShutdownTaskScheduler(CleanShutdownPerformer shutdownPerformer) { - return shutdownPerformer::scheduleShutdownTask; - } - + @Provides @Singleton @Named("shutdownLatch") diff --git a/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java b/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java index d1916abee..cb3bf6ff1 100644 --- a/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java +++ b/main/launcher/src/main/java/org/cryptomator/logging/LoggerConfiguration.java @@ -5,9 +5,8 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; -import ch.qos.logback.core.hook.DelayingShutdownHook; -import ch.qos.logback.core.util.Duration; import org.cryptomator.common.Environment; +import org.cryptomator.common.ShutdownHook; import javax.inject.Inject; import javax.inject.Named; @@ -16,26 +15,27 @@ import java.util.Map; @Singleton public class LoggerConfiguration { - - private static final double SHUTDOWN_DELAY_MS = 100; - + private final LoggerContext context; private final Environment environment; private final Appender stdout; private final Appender upgrade; private final Appender file; + private final ShutdownHook shutdownHook; @Inject LoggerConfiguration(LoggerContext context, // Environment environment, // @Named("stdoutAppender") Appender stdout, // @Named("upgradeAppender") Appender upgrade, // - @Named("fileAppender") Appender file) { + @Named("fileAppender") Appender file, // + ShutdownHook shutdownHook) { this.context = context; this.environment = environment; this.stdout = stdout; this.upgrade = upgrade; this.file = file; + this.shutdownHook = shutdownHook; } public void init() { @@ -62,9 +62,7 @@ public class LoggerConfiguration { upgrades.setAdditive(false); // add shutdown hook - DelayingShutdownHook shutdownHook = new DelayingShutdownHook(); - shutdownHook.setContext(context); - shutdownHook.setDelay(Duration.buildByMilliseconds(SHUTDOWN_DELAY_MS)); + shutdownHook.runOnShutdown(ShutdownHook.PRIO_LAST, context::stop); } } diff --git a/main/pom.xml b/main/pom.xml index 026092229..386bf7b7b 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator main - 1.5.0-beta1 + 1.5.0-beta2 pom Cryptomator @@ -23,26 +23,25 @@ UTF-8 - + 1.9.0-rc2 - 2.2.1 - 1.2.1 - 1.1.11 + 2.2.2 + 1.2.2 + 1.1.12 1.0.10 + 13.0.1 - 3.9 - + 3.8.3 1.0.3 - 28.1-jre 2.25.2 2.8.6 - 1.7.29 1.2.3 + 5.5.2 3.1.0 2.2 @@ -156,6 +155,13 @@ commons-lang3 ${commons-lang3.version} + + + + com.auth0 + java-jwt + ${jwt.version} + diff --git a/main/ui/pom.xml b/main/ui/pom.xml index e2c17e375..afe40303b 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.0-beta1 + 1.5.0-beta2 ui Cryptomator GUI @@ -37,7 +37,7 @@ org.fxmisc.easybind easybind - + @@ -65,7 +65,7 @@ com.nulab-inc zxcvbn - 1.2.7 + 1.3.0 diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultFailureExisitingController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultFailureExistingController.java similarity index 80% rename from main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultFailureExisitingController.java rename to main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultFailureExistingController.java index 201f53345..1bb9eef40 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultFailureExisitingController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultFailureExistingController.java @@ -15,14 +15,14 @@ import javax.inject.Inject; import java.nio.file.Path; @AddVaultWizardScoped -public class AddVaultFailureExisitingController implements FxController { +public class AddVaultFailureExistingController implements FxController { private final Stage window; private final Lazy previousScene; private final StringBinding vaultName; @Inject - AddVaultFailureExisitingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy previousScene, ObjectProperty pathOfFailedVault){ + AddVaultFailureExistingController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy previousScene, ObjectProperty pathOfFailedVault){ this.window = window; this.previousScene = previousScene; this.vaultName = Bindings.createStringBinding(() -> pathOfFailedVault.get().getFileName().toString(),pathOfFailedVault); diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java index 4eaa1c207..aafa97f5e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java @@ -19,7 +19,10 @@ import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.NewPasswordController; +import org.cryptomator.ui.common.PasswordStrengthUtil; import org.cryptomator.ui.mainwindow.MainWindow; +import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController; import javax.inject.Named; import javax.inject.Provider; @@ -31,6 +34,13 @@ import java.util.ResourceBundle; @Module public abstract class AddVaultModule { + @Provides + @AddVaultWizardScoped + @Named("newPassword") + static ObjectProperty provideNewPasswordProperty() { + return new SimpleObjectProperty<>(""); + } + @Provides @AddVaultWizardWindow @AddVaultWizardScoped @@ -150,8 +160,8 @@ public abstract class AddVaultModule { @Binds @IntoMap - @FxControllerKey(AddVaultFailureExisitingController.class) - abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExisitingController controller); + @FxControllerKey(AddVaultFailureExistingController.class) + abstract FxController bindAddVaultFailureExistingController(AddVaultFailureExistingController controller); @Binds @IntoMap @@ -168,13 +178,27 @@ public abstract class AddVaultModule { @FxControllerKey(CreateNewVaultPasswordController.class) abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller); - @Binds + @Provides @IntoMap - @FxControllerKey(AddVaultSuccessController.class) - abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller); + @FxControllerKey(NewPasswordController.class) + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty password) { + return new NewPasswordController(resourceBundle, strengthRater, password); + } @Binds @IntoMap @FxControllerKey(CreateNewVaultRecoveryKeyController.class) abstract FxController bindCreateNewVaultRecoveryKeyController(CreateNewVaultRecoveryKeyController controller); + + @Provides + @IntoMap + @FxControllerKey(RecoveryKeyDisplayController.class) + static FxController provideRecoveryKeyDisplayController(@AddVaultWizardWindow Stage window, @Named("vaultName") StringProperty vaultName, @Named("recoveryKey") StringProperty recoveryKey) { + return new RecoveryKeyDisplayController(window, vaultName.get(), recoveryKey.get()); + } + + @Binds + @IntoMap + @FxControllerKey(AddVaultSuccessController.class) + abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index e15d195bf..e0c5f9bec 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -52,8 +52,10 @@ public class CreateNewVaultLocationController implements FxController { private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH; public ToggleGroup predefinedLocationToggler; + public RadioButton iclouddriveRadioButton; public RadioButton dropboxRadioButton; public RadioButton gdriveRadioButton; + public RadioButton onedriveRadioButton; public RadioButton customRadioButton; @Inject @@ -91,10 +93,14 @@ public class CreateNewVaultLocationController implements FxController { } private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { - if (dropboxRadioButton.equals(newValue)) { + if (iclouddriveRadioButton.equals(newValue)) { + vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get())); + } else if (dropboxRadioButton.equals(newValue)) { vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get())); } else if (gdriveRadioButton.equals(newValue)) { vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get())); + } else if (onedriveRadioButton.equals(newValue)) { + vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get())); } else if (customRadioButton.equals(newValue)) { vaultPath.set(customVaultPath.resolve(vaultName.get())); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index ab2fe27a0..4ddf54c6a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -5,16 +5,14 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.BooleanProperty; -import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.Scene; import javafx.scene.control.ContentDisplay; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; @@ -24,11 +22,7 @@ import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.Tasks; -import org.cryptomator.ui.controls.FontAwesome5IconView; -import org.cryptomator.ui.controls.NiceSecurePasswordField; -import org.cryptomator.ui.common.PasswordStrengthUtil; import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; -import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +52,7 @@ public class CreateNewVaultPasswordController implements FxController { private final Stage window; private final Lazy chooseLocationScene; private final Lazy recoveryKeyScene; + private final Lazy successScene; private final ExecutorService executor; private final RecoveryKeyFactory recoveryKeyFactory; private final StringProperty vaultNameProperty; @@ -66,26 +61,22 @@ public class CreateNewVaultPasswordController implements FxController { private final StringProperty recoveryKeyProperty; private final VaultListManager vaultListManager; private final ResourceBundle resourceBundle; - private final PasswordStrengthUtil strengthRater; + private final ObjectProperty password; private final ReadmeGenerator readmeGenerator; - private final IntegerProperty passwordStrength; private final BooleanProperty processing; private final BooleanProperty readyToCreateVault; private final ObjectBinding createVaultButtonState; - public NiceSecurePasswordField passwordField; - public NiceSecurePasswordField reenterField; - public Label passwordStrengthLabel; - public HBox passwordMatchBox; - public FontAwesome5IconView checkmark; - public FontAwesome5IconView cross; - public Label passwordMatchLabel; + public ToggleGroup recoveryKeyChoice; + public Toggle showRecoveryKey; + public Toggle skipRecoveryKey; @Inject - CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ReadmeGenerator readmeGenerator) { + CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty password, ReadmeGenerator readmeGenerator) { this.window = window; this.chooseLocationScene = chooseLocationScene; this.recoveryKeyScene = recoveryKeyScene; + this.successScene = successScene; this.executor = executor; this.recoveryKeyFactory = recoveryKeyFactory; this.vaultNameProperty = vaultName; @@ -94,9 +85,8 @@ public class CreateNewVaultPasswordController implements FxController { this.recoveryKeyProperty = recoveryKey; this.vaultListManager = vaultListManager; this.resourceBundle = resourceBundle; - this.strengthRater = strengthRater; + this.password = password; this.readmeGenerator = readmeGenerator; - this.passwordStrength = new SimpleIntegerProperty(-1); this.processing = new SimpleBooleanProperty(); this.readyToCreateVault = new SimpleBooleanProperty(); this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing); @@ -104,22 +94,8 @@ public class CreateNewVaultPasswordController implements FxController { @FXML public void initialize() { - //binds the actual strength value to the rating of the password util - passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters().toString()), passwordField.textProperty())); - //binding indicating if the passwords not match - BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0, passwordField.textProperty(), reenterField.textProperty()); - BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty(); - readyToCreateVault.bind(reenterFieldNotEmpty.and(passwordsMatch).and(processing.not())); - //make match indicator invisible when passwords do not match or one is empty - passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty); - checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty)); - checkmark.managedProperty().bind(checkmark.visibleProperty()); - cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty)); - cross.managedProperty().bind(cross.visibleProperty()); - passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("addvaultwizard.new.passwordsMatch")).otherwise(resourceBundle.getString("addvaultwizard.new.passwordsDoNotMatch"))); - - //bindsings for the password strength indicator - passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); + BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password); + readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not())); } @FXML @@ -130,7 +106,7 @@ public class CreateNewVaultPasswordController implements FxController { @FXML public void next() { Path pathToVault = vaultPathProperty.get(); - + try { Files.createDirectory(pathToVault); } catch (FileAlreadyExistsException e) { @@ -141,12 +117,41 @@ public class CreateNewVaultPasswordController implements FxController { LOG.error("", e); } + if (showRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) { + showRecoveryKeyScene(); + } else if (skipRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) { + showSuccessScene(); + } else { + throw new IllegalStateException("Unexpected toggle state"); + } + } + + private void showRecoveryKeyScene() { + Path pathToVault = vaultPathProperty.get(); processing.set(true); Tasks.create(() -> { - initializeVault(pathToVault, passwordField.getCharacters()); - return recoveryKeyFactory.createRecoveryKey(pathToVault, passwordField.getCharacters()); + initializeVault(pathToVault, password.get()); + return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get()); }).onSuccess(recoveryKey -> { - initializationSucceeded(pathToVault, recoveryKey); + initializationSucceeded(pathToVault); + recoveryKeyProperty.set(recoveryKey); + window.setScene(recoveryKeyScene.get()); + }).onError(IOException.class, e -> { + // TODO show generic error screen + LOG.error("", e); + }).andFinally(() -> { + processing.set(false); + }).runOnce(executor); + } + + private void showSuccessScene() { + Path pathToVault = vaultPathProperty.get(); + processing.set(true); + Tasks.create(() -> { + initializeVault(pathToVault, password.get()); + }).onSuccess(() -> { + initializationSucceeded(pathToVault); + window.setScene(successScene.get()); }).onError(IOException.class, e -> { // TODO show generic error screen LOG.error("", e); @@ -175,13 +180,11 @@ public class CreateNewVaultPasswordController implements FxController { } LOG.info("Created vault at {}", path); } - - private void initializationSucceeded(Path pathToVault, String recoveryKey) { + + private void initializationSucceeded(Path pathToVault) { try { Vault newVault = vaultListManager.add(pathToVault); vaultProperty.set(newVault); - recoveryKeyProperty.set(recoveryKey); - window.setScene(recoveryKeyScene.get()); } catch (NoSuchFileException e) { throw new UncheckedIOException(e); } @@ -197,14 +200,6 @@ public class CreateNewVaultPasswordController implements FxController { return vaultNameProperty; } - public IntegerProperty passwordStrengthProperty() { - return passwordStrength; - } - - public int getPasswordStrength() { - return passwordStrength.get(); - } - public BooleanProperty readyToCreateVaultProperty() { return readyToCreateVault; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultRecoveryKeyController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultRecoveryKeyController.java index 4c38f1250..44f186774 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultRecoveryKeyController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultRecoveryKeyController.java @@ -1,7 +1,6 @@ package org.cryptomator.ui.addvaultwizard; import dagger.Lazy; -import javafx.beans.property.StringProperty; import javafx.fxml.FXML; import javafx.scene.Scene; import javafx.stage.Stage; @@ -10,33 +9,20 @@ import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import javax.inject.Inject; -import javax.inject.Named; public class CreateNewVaultRecoveryKeyController implements FxController { private final Stage window; private final Lazy successScene; - private final StringProperty recoveryKeyProperty; @Inject - CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, @Named("recoveryKey")StringProperty recoveryKey) { + CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene) { this.window = window; this.successScene = successScene; - this.recoveryKeyProperty = recoveryKey; } @FXML public void next() { window.setScene(successScene.get()); } - - /* Getter/Setter */ - - public String getRecoveryKey() { - return recoveryKeyProperty.get(); - } - - public StringProperty recoveryKeyProperty() { - return recoveryKeyProperty; - } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java index a8a889acf..b3b6a4b25 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java @@ -13,20 +13,30 @@ import java.nio.file.Paths; public class LocationPresets { private static final String USER_HOME = System.getProperty("user.home"); + private static final String[] ICLOUDDRIVE_LOCATIONS = {"~/Library/Mobile Documents/iCloud~com~setolabs~Cryptomator/Documents"}; private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"}; private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"}; + private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"}; + private final ReadOnlyObjectProperty iclouddriveLocation; private final ReadOnlyObjectProperty dropboxLocation; private final ReadOnlyObjectProperty gdriveLocation; + private final ReadOnlyObjectProperty onedriveLocation; + private final BooleanBinding foundIclouddrive; private final BooleanBinding foundDropbox; private final BooleanBinding foundGdrive; + private final BooleanBinding foundOnedrive; @Inject public LocationPresets() { + this.iclouddriveLocation = new SimpleObjectProperty<>(existingWritablePath(ICLOUDDRIVE_LOCATIONS)); this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS)); this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS)); + this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS)); + this.foundIclouddrive = iclouddriveLocation.isNotNull(); this.foundDropbox = dropboxLocation.isNotNull(); this.foundGdrive = gdriveLocation.isNotNull(); + this.foundOnedrive = onedriveLocation.isNotNull(); } private static Path existingWritablePath(String... candidates) { @@ -49,6 +59,22 @@ public class LocationPresets { /* Observables */ + public ReadOnlyObjectProperty iclouddriveLocationProperty() { + return iclouddriveLocation; + } + + public Path getIclouddriveLocation() { + return iclouddriveLocation.get(); + } + + public BooleanBinding foundIclouddriveProperty() { + return foundIclouddrive; + } + + public boolean isFoundIclouddrive() { + return foundIclouddrive.get(); + } + public ReadOnlyObjectProperty dropboxLocationProperty() { return dropboxLocation; } @@ -73,7 +99,7 @@ public class LocationPresets { return gdriveLocation.get(); } - public BooleanBinding froundGdriveProperty() { + public BooleanBinding foundGdriveProperty() { return foundGdrive; } @@ -81,4 +107,20 @@ public class LocationPresets { return foundGdrive.get(); } + public ReadOnlyObjectProperty onedriveLocationProperty() { + return onedriveLocation; + } + + public Path getOnedriveLocation() { + return onedriveLocation.get(); + } + + public BooleanBinding foundOnedriveProperty() { + return foundOnedrive; + } + + public boolean isFoundOnedrive() { + return foundOnedrive.get(); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java index 30fcaa5d2..d711f9069 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java @@ -3,6 +3,7 @@ package org.cryptomator.ui.changepassword; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -22,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; import java.io.IOException; import java.util.ResourceBundle; @@ -33,47 +35,24 @@ public class ChangePasswordController implements FxController { private final Stage window; private final Vault vault; - private final ResourceBundle resourceBundle; - private final PasswordStrengthUtil strengthRater; - private final IntegerProperty passwordStrength; + private final ObjectProperty newPassword; public NiceSecurePasswordField oldPasswordField; - public NiceSecurePasswordField newPasswordField; - public NiceSecurePasswordField reenterPasswordField; - public Label passwordStrengthLabel; - public HBox passwordMatchBox; - public FontAwesome5IconView checkmark; - public FontAwesome5IconView cross; - public Label passwordMatchLabel; public CheckBox finalConfirmationCheckbox; public Button finishButton; @Inject - public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { + public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword) { this.window = window; this.vault = vault; - this.resourceBundle = resourceBundle; - this.strengthRater = strengthRater; - this.passwordStrength = new SimpleIntegerProperty(-1); + this.newPassword = newPassword; } @FXML public void initialize() { - //binds the actual strength value to the rating of the password util - passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(newPasswordField.getCharacters().toString()), newPasswordField.textProperty())); - //binding indicating if the passwords not match - BooleanBinding passwordsMatch = Bindings.createBooleanBinding(() -> CharSequence.compare(newPasswordField.getCharacters(), reenterPasswordField.getCharacters()) == 0, newPasswordField.textProperty(), reenterPasswordField.textProperty()); - BooleanBinding reenterFieldNotEmpty = reenterPasswordField.textProperty().isNotEmpty(); - //disable the finish button when passwords do not match or one is empty - finishButton.disableProperty().bind(reenterFieldNotEmpty.not().or(passwordsMatch.not()).or(finalConfirmationCheckbox.selectedProperty().not())); - //make match indicator invisible when passwords do not match or one is empty - passwordMatchBox.visibleProperty().bind(reenterFieldNotEmpty); - checkmark.visibleProperty().bind(passwordsMatch.and(reenterFieldNotEmpty)); - checkmark.managedProperty().bind(checkmark.visibleProperty()); - cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty)); - cross.managedProperty().bind(cross.visibleProperty()); - passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("changepassword.passwordsMatch")).otherwise(resourceBundle.getString("changepassword.passwordsDoNotMatch"))); - passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); + BooleanBinding hasNotConfirmedCheckbox = finalConfirmationCheckbox.selectedProperty().not(); + BooleanBinding isInvalidNewPassword = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword); + finishButton.disableProperty().bind(hasNotConfirmedCheckbox.or(isInvalidNewPassword)); } @FXML @@ -84,15 +63,15 @@ public class ChangePasswordController implements FxController { @FXML public void finish() { try { - CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPasswordField.getCharacters()); + CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get()); LOG.info("Successful changed password for {}", vault.getDisplayableName()); window.close(); } catch (IOException e) { - //TODO + // TODO show generic error screen LOG.error("IO error occured during password change. Unable to perform operation.", e); e.printStackTrace(); } catch (InvalidPassphraseException e) { - //TODO + // TODO shake LOG.info("Wrong old password."); } } @@ -102,12 +81,5 @@ public class ChangePasswordController implements FxController { public Vault getVault() { return vault; } - - public IntegerProperty passwordStrengthProperty() { - return passwordStrength; - } - - public int getPasswordStrength() { - return passwordStrength.get(); - } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java index 7a2262aee..fcef4a7ec 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java @@ -4,6 +4,8 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Modality; @@ -14,15 +16,25 @@ import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.NewPasswordController; +import org.cryptomator.ui.common.PasswordStrengthUtil; import javax.inject.Named; import javax.inject.Provider; +import java.nio.CharBuffer; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; @Module abstract class ChangePasswordModule { + + @Provides + @ChangePasswordScoped + @Named("newPassword") + static ObjectProperty provideNewPasswordProperty() { + return new SimpleObjectProperty<>(""); + } @Provides @ChangePasswordWindow @@ -58,5 +70,12 @@ abstract class ChangePasswordModule { @IntoMap @FxControllerKey(ChangePasswordController.class) abstract FxController bindUnlockController(ChangePasswordController controller); + + @Provides + @IntoMap + @FxControllerKey(NewPasswordController.class) + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty password) { + return new NewPasswordController(resourceBundle, strengthRater, password); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 553e70bef..e6f449ccb 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -18,9 +18,11 @@ public enum FxmlFile { PREFERENCES("/fxml/preferences.fxml"), // QUIT("/fxml/quit.fxml"), // RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), // - RECOVERYKEY_DISPLAY("/fxml/recoverykey_display.fxml"), // + RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), // REMOVE_VAULT("/fxml/remove_vault.fxml"), // UNLOCK("/fxml/unlock.fxml"), + UNLOCK_GENERIC_ERROR("/fxml/unlock_generic_error.fxml"), // + UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), // UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), // VAULT_OPTIONS("/fxml/vault_options.fxml"), // WRONGFILEALERT("/fxml/wrongfilealert.fxml"); diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java new file mode 100644 index 000000000..2d0c424eb --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java @@ -0,0 +1,74 @@ +package org.cryptomator.ui.common; + +import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import org.cryptomator.ui.controls.FontAwesome5IconView; +import org.cryptomator.ui.controls.NiceSecurePasswordField; +import org.fxmisc.easybind.EasyBind; + +import java.util.ResourceBundle; + +public class NewPasswordController implements FxController { + + private final ResourceBundle resourceBundle; + private final PasswordStrengthUtil strengthRater; + private final ObjectProperty password; + private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); + + public NiceSecurePasswordField passwordField; + public NiceSecurePasswordField reenterField; + public Label passwordStrengthLabel; + public Label passwordMatchLabel; + public FontAwesome5IconView checkmark; + public FontAwesome5IconView cross; + + public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty password) { + this.resourceBundle = resourceBundle; + this.strengthRater = strengthRater; + this.password = password; + } + + @FXML + public void initialize() { + BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty()); + BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty(); + passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty())); + passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); + + passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty); + passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(checkmark).otherwise(cross)); + passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch"))); + + passwordField.textProperty().addListener(this::passwordsDidChange); + reenterField.textProperty().addListener(this::passwordsDidChange); + } + + private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) { + if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) { + password.set(passwordField.getCharacters()); + } else { + password.set(""); + } + } + + private boolean hasSamePasswordInBothFields() { + return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0; + } + + /* Getter/Setter */ + + public IntegerProperty passwordStrengthProperty() { + return passwordStrength; + } + + public int getPasswordStrength() { + return passwordStrength.get(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/PasswordStrengthUtil.java b/main/ui/src/main/java/org/cryptomator/ui/common/PasswordStrengthUtil.java index 76385c372..0224118cd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/PasswordStrengthUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/PasswordStrengthUtil.java @@ -8,12 +8,11 @@ *******************************************************************************/ package org.cryptomator.ui.common; -import com.google.common.base.Strings; import com.nulabinc.zxcvbn.Zxcvbn; +import org.cryptomator.common.Environment; import org.cryptomator.ui.fxapp.FxApplicationScoped; import javax.inject.Inject; -import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; @@ -22,30 +21,37 @@ public class PasswordStrengthUtil { private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel."; + private static final List SANITIZED_INPUTS = List.of("cryptomator"); - private final Zxcvbn zxcvbn; - private final List sanitizedInputs; private final ResourceBundle resourceBundle; + private final int minPwLength; + private final Zxcvbn zxcvbn; @Inject - public PasswordStrengthUtil(ResourceBundle resourceBundle) { + public PasswordStrengthUtil(ResourceBundle resourceBundle, Environment environment) { this.resourceBundle = resourceBundle; + this.minPwLength = environment.getMinPwLength(); this.zxcvbn = new Zxcvbn(); - this.sanitizedInputs = List.of("cryptomator"); } - public int computeRate(String password) { - if (Strings.isNullOrEmpty(password)) { + public boolean fulfillsMinimumRequirements(CharSequence password) { + return password.length() >= minPwLength; + } + + public int computeRate(CharSequence password) { + if (password == null || password.length() < minPwLength) { return -1; } else { int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length()); - return zxcvbn.measure(password.substring(0, numCharsToRate), sanitizedInputs).getScore(); + return zxcvbn.measure(password.subSequence(0, numCharsToRate), SANITIZED_INPUTS).getScore(); } } public String getStrengthDescription(Number score) { - if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) { - return resourceBundle.getString("passwordStrength.messageLabel." + score.intValue()); + if (score.intValue() == -1) { + return String.format(resourceBundle.getString(RESSOURCE_PREFIX + "tooShort"), minPwLength); + } else if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) { + return resourceBundle.getString(RESSOURCE_PREFIX + score.intValue()); } else { return ""; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java b/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java new file mode 100644 index 000000000..accab1b89 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java @@ -0,0 +1,28 @@ +package org.cryptomator.ui.common; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +public class StackTraceController implements FxController { + + private final String stackTrace; + + public StackTraceController(Exception cause) { + this.stackTrace = provideStackTrace(cause); + } + + static String provideStackTrace(Exception cause) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + cause.printStackTrace(new PrintStream(baos)); + return baos.toString(StandardCharsets.UTF_8); + } + + /* Getter/Setter */ + + public String getStackTrace() { + return stackTrace; + } + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java new file mode 100644 index 000000000..ec2030fd2 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java @@ -0,0 +1,190 @@ +package org.cryptomator.ui.common; + +import javafx.concurrent.Task; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.common.vaults.Volume; +import org.cryptomator.ui.fxapp.FxApplicationScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + +@FxApplicationScoped +public class VaultService { + + private static final Logger LOG = LoggerFactory.getLogger(VaultService.class); + + private final ExecutorService executorService; + + @Inject + public VaultService(ExecutorService executorService) { + this.executorService = executorService; + } + + public void reveal(Vault vault) { + executorService.execute(createRevealTask(vault)); + } + + /** + * Creates but doesn't start a reveal task. + * + * @param vault The vault to reveal + */ + public Task createRevealTask(Vault vault) { + Task task = new RevealVaultTask(vault); + task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayableName())); + task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayableName(), evt.getSource().getException())); + return task; + } + + /** + * Locks a vault in a background thread. + * + * @param vault The vault to lock + * @param forced Whether to attempt a forced lock + */ + public void lock(Vault vault, boolean forced) { + executorService.execute(createLockTask(vault, forced)); + } + + /** + * Creates but doesn't start a lock task. + * + * @param vault The vault to lock + * @param forced Whether to attempt a forced lock + */ + public Task createLockTask(Vault vault, boolean forced) { + Task task = new LockVaultTask(vault, forced); + task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayableName())); + task.setOnFailed(evt -> LOG.error("Failed to lock " + vault.getDisplayableName(), evt.getSource().getException())); + return task; + } + + /** + * Locks all given vaults in a background thread. + * + * @param vaults The vaults to lock + * @param forced Whether to attempt a forced lock + */ + public void lockAll(Collection vaults, boolean forced) { + executorService.execute(createLockAllTask(vaults, forced)); + } + + /** + * Creates but doesn't start a lock-all task. + * + * @param vaults The list of vaults to be locked + * @param forced Whether to attempt a forced lock + * @return Meta-Task that waits until all vaults are locked or fails after the first failure of a subtask + */ + public Task> createLockAllTask(Collection vaults, boolean forced) { + List> lockTasks = vaults.stream().map(v -> new LockVaultTask(v, forced)).collect(Collectors.toUnmodifiableList()); + lockTasks.forEach(executorService::execute); + Task> task = new WaitForTasksTask(lockTasks); + String vaultNames = vaults.stream().map(Vault::getDisplayableName).collect(Collectors.joining(", ")); + task.setOnSucceeded(evt -> LOG.info("Locked {}", vaultNames)); + task.setOnFailed(evt -> LOG.error("Failed to lock vaults " + vaultNames, evt.getSource().getException())); + return task; + } + + private static class RevealVaultTask extends Task { + + private final Vault vault; + + /** + * @param vault The vault to lock + */ + public RevealVaultTask(Vault vault) { + this.vault = vault; + } + + @Override + protected Vault call() throws Volume.VolumeException { + vault.reveal(); + return vault; + } + } + + /** + * A task that waits for completion of multiple other tasks + */ + private static class WaitForTasksTask extends Task> { + + private final Collection> startedTasks; + + public WaitForTasksTask(Collection> tasks) { + this.startedTasks = List.copyOf(tasks); + } + + @Override + protected Collection call() throws Exception { + Iterator> remainingTasks = startedTasks.iterator(); + Collection completed = new ArrayList<>(); + try { + // wait for all tasks: + while (remainingTasks.hasNext()) { + Vault lockedVault = remainingTasks.next().get(); + completed.add(lockedVault); + } + } catch (ExecutionException e) { + // cancel all remaining: + while (remainingTasks.hasNext()) { + remainingTasks.next().cancel(true); + } + throw e; + } + return List.copyOf(completed); + } + } + + /** + * A task that locks a vault + */ + private static class LockVaultTask extends Task { + + private final Vault vault; + private final boolean forced; + + /** + * @param vault The vault to lock + * @param forced Whether to attempt a forced lock + */ + public LockVaultTask(Vault vault, boolean forced) { + this.vault = vault; + this.forced = forced; + } + + @Override + protected Vault call() throws Volume.VolumeException { + vault.lock(forced); + return vault; + } + + @Override + protected void scheduled() { + vault.setState(VaultState.PROCESSING); + } + + @Override + protected void succeeded() { + vault.setState(VaultState.LOCKED); + } + + @Override + protected void failed() { + vault.setState(VaultState.UNLOCKED); + } + + } + + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index f5b62ae2b..742d29438 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -9,23 +9,29 @@ public enum FontAwesome5Icon { CHECK("\uF00C"), // COG("\uF013"), // COGS("\uF085"), // + COPY("\uF0C5"), // EXCLAMATION("\uF12A"), + EXCLAMATION_CIRCLE("\uF06A"), // EXCLAMATION_TRIANGLE("\uF071"), // EYE("\uF06E"), // EYE_SLASH("\uF070"), // FILE_IMPORT("\uF56F"), // FOLDER_OPEN("\uF07C"), // + HAND_HOLDING_HEART("\uF4BE"), // + HEART("\uF004"), // HDD("\uF0A0"), // KEY("\uF084"), // + LINK("\uF0C1"), // LOCK_ALT("\uF30D"), // LOCK_OPEN_ALT("\uF3C2"), // - MINUS("\uF068"), // PLUS("\uF067"), // + PRINT("\uF02F"), // QUESTION("\uF128"), // SPARKLES("\uF890"), // SPINNER("\uF110"), // SYNC("\uF021"), // TIMES("\uF00D"), // + USER_CROWN("\uF6A4"), // WRENCH("\uF0AD"), // ; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java b/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java index 3a85fbad6..0a50f5322 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java @@ -89,7 +89,7 @@ public class NiceSecurePasswordField extends StackPane { } public void swipe() { - passwordField.swipe();; + passwordField.swipe(); } public void selectAll() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index f52a9b2ed..aca5650ea 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -9,6 +9,7 @@ import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import javafx.stage.Stage; +import org.cryptomator.common.LicenseHolder; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; import org.cryptomator.common.vaults.Vault; @@ -16,8 +17,10 @@ import org.cryptomator.jni.JniException; import org.cryptomator.jni.MacApplicationUiAppearance; import org.cryptomator.jni.MacApplicationUiState; import org.cryptomator.jni.MacFunctions; +import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; +import org.cryptomator.ui.preferences.SelectedPreferencesTab; import org.cryptomator.ui.quit.QuitComponent; import org.cryptomator.ui.unlock.UnlockComponent; import org.slf4j.Logger; @@ -38,17 +41,21 @@ public class FxApplication extends Application { private final UnlockComponent.Builder unlockWindowBuilder; private final QuitComponent.Builder quitWindowBuilder; private final Optional macFunctions; + private final VaultService vaultService; + private final LicenseHolder licenseHolder; private final ObservableSet visibleStages = FXCollections.observableSet(); private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages); @Inject - FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional macFunctions) { + FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional macFunctions, VaultService vaultService, LicenseHolder licenseHolder) { this.settings = settings; this.mainWindow = mainWindow; this.preferencesWindow = preferencesWindow; this.unlockWindowBuilder = unlockWindowBuilder; this.quitWindowBuilder = quitWindowBuilder; this.macFunctions = macFunctions; + this.vaultService = vaultService; + this.licenseHolder = licenseHolder; } public void start() { @@ -79,9 +86,9 @@ public class FxApplication extends Application { } } - public void showPreferencesWindow() { + public void showPreferencesWindow(SelectedPreferencesTab selectedTab) { Platform.runLater(() -> { - Stage stage = preferencesWindow.get().showPreferencesWindow(); + Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab); addVisibleStage(stage); LOG.debug("Showing Preferences"); }); @@ -111,11 +118,16 @@ public class FxApplication extends Application { }); } + public VaultService getVaultService() { + return vaultService; + } + private void themeChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) { loadSelectedStyleSheet(newValue); } - private void loadSelectedStyleSheet(UiTheme theme) { + private void loadSelectedStyleSheet(UiTheme desiredTheme) { + UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT; switch (theme) { // case CUSTOM: // // TODO diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 43fa9e749..a51470489 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -5,8 +5,10 @@ *******************************************************************************/ package org.cryptomator.ui.fxapp; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import javafx.application.Application; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.image.Image; @@ -44,6 +46,9 @@ abstract class FxApplicationModule { return Optional.empty(); } } + + @Binds + abstract Application bindApplication(FxApplication application); @Provides static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index f0acc0992..14d9350da 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -1,30 +1,24 @@ package org.cryptomator.ui.mainwindow; -import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; +import javafx.scene.input.DragEvent; import javafx.scene.input.TransferMode; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Region; -import javafx.scene.layout.VBox; -import javafx.stage.Stage; -import org.cryptomator.common.vaults.Vault; +import javafx.scene.layout.StackPane; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.fxapp.FxApplication; -import org.cryptomator.ui.fxapp.UpdateChecker; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javax.inject.Named; import java.io.File; +import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.util.Collection; +import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; @MainWindowScoped public class MainWindowController implements FxController { @@ -32,27 +26,14 @@ public class MainWindowController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class); private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes - private final Stage window; - private final FxApplication application; - private final boolean minimizeToSysTray; - private final UpdateChecker updateChecker; - private final BooleanBinding updateAvailable; private final VaultListManager vaultListManager; private final WrongFileAlertComponent.Builder wrongFileAlert; - public HBox titleBar; - public VBox root; - public Pane dragAndDropIndicator; - public Region resizer; - private double xOffset; - private double yOffset; + private final BooleanProperty draggingOver = new SimpleBooleanProperty(); + private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty(); + public StackPane root; @Inject - public MainWindowController(@MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) { - this.window = window; - this.application = application; - this.minimizeToSysTray = minimizeToSysTray; - this.updateChecker = updateChecker; - this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); + public MainWindowController(VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) { this.vaultListManager = vaultListManager; this.wrongFileAlert = wrongFileAlert; } @@ -60,77 +41,70 @@ public class MainWindowController implements FxController { @FXML public void initialize() { LOG.debug("init MainWindowController"); - titleBar.setOnMousePressed(event -> { - xOffset = event.getSceneX(); - yOffset = event.getSceneY(); - }); - titleBar.setOnMouseDragged(event -> { - window.setX(event.getScreenX() - xOffset); - window.setY(event.getScreenY() - yOffset); - }); - resizer.setOnMouseDragged(event -> { - // we know for a fact that window is borderless. i.e. the scene starts at 0/0 of the window. - window.setWidth(event.getSceneX()); - window.setHeight(event.getSceneY()); - }); - updateChecker.automaticallyCheckForUpdatesIfEnabled(); - dragAndDropIndicator.setVisible(false); - root.setOnDragOver(event -> { - if (event.getGestureSource() != root && event.getDragboard().hasFiles()) { - /* allow for both copying and moving, whatever user chooses */ - event.acceptTransferModes(TransferMode.COPY_OR_MOVE); - dragAndDropIndicator.setVisible(true); - } - event.consume(); - }); - root.setOnDragExited(event -> dragAndDropIndicator.setVisible(false)); - root.setOnDragDropped(event -> { - if (event.getGestureSource() != root && event.getDragboard().hasFiles()) { - /* allow for both copying and moving, whatever user chooses */ - event.acceptTransferModes(TransferMode.COPY_OR_MOVE); - Collection vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).flatMap(this::addVault).collect(Collectors.toSet()); - if (vaultPaths.isEmpty()) { - wrongFileAlert.build().showWrongFileAlertWindow(); - } - } - event.consume(); - }); + root.setOnDragEntered(this::handleDragEvent); + root.setOnDragOver(this::handleDragEvent); + root.setOnDragDropped(this::handleDragEvent); + root.setOnDragExited(this::handleDragEvent); } - private Stream addVault(Path pathToVault) { + private void handleDragEvent(DragEvent event) { + if (DragEvent.DRAG_ENTERED.equals(event.getEventType()) && event.getGestureSource() == null) { + draggingOver.set(true); + } else if (DragEvent.DRAG_OVER.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { + event.acceptTransferModes(TransferMode.ANY); + draggingVaultOver.set(event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(this::containsVault)); + } else if (DragEvent.DRAG_DROPPED.equals(event.getEventType()) && event.getGestureSource() == null && event.getDragboard().hasFiles()) { + Set vaultPaths = event.getDragboard().getFiles().stream().map(File::toPath).filter(this::containsVault).collect(Collectors.toSet()); + if (vaultPaths.isEmpty()) { + wrongFileAlert.build().showWrongFileAlertWindow(); + } else { + vaultPaths.forEach(this::addVault); + } + event.setDropCompleted(!vaultPaths.isEmpty()); + event.consume(); + } else if (DragEvent.DRAG_EXITED.equals(event.getEventType())) { + draggingOver.set(false); + draggingVaultOver.set(false); + } + } + + private boolean containsVault(Path path) { + if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) { + return true; + } else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) { + return true; + } else { + return false; + } + } + + private void addVault(Path pathToVault) { try { if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) { - return Stream.of(vaultListManager.add(pathToVault.getParent())); + vaultListManager.add(pathToVault.getParent()); } else { - return Stream.of(vaultListManager.add(pathToVault)); + vaultListManager.add(pathToVault); } } catch (NoSuchFileException e) { LOG.debug("Not a vault: {}", pathToVault); } - return Stream.empty(); - } - - @FXML - public void close() { - if (minimizeToSysTray) { - window.close(); - } else { - window.setIconified(true); - } - } - - @FXML - public void showPreferences() { - application.showPreferencesWindow(); } /* Getter/Setter */ - public BooleanBinding updateAvailableProperty() { - return updateAvailable; + public BooleanProperty draggingOverProperty() { + return draggingOver; } - public boolean isUpdateAvailable() { - return updateAvailable.get(); + public boolean isDraggingOver() { + return draggingOver.get(); + } + + public BooleanProperty draggingVaultOverProperty() { + return draggingVaultOver; + } + + public boolean isDraggingVaultOver() { + return draggingVaultOver.get(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java index 5ef76c5e0..8fb9dd1a4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java @@ -68,6 +68,16 @@ abstract class MainWindowModule { @FxControllerKey(MainWindowController.class) abstract FxController bindMainWindowController(MainWindowController controller); + @Binds + @IntoMap + @FxControllerKey(MainWindowTitleController.class) + abstract FxController bindMainWindowTitleController(MainWindowTitleController controller); + + @Binds + @IntoMap + @FxControllerKey(ResizeController.class) + abstract FxController bindResizeController(ResizeController controller); + @Binds @IntoMap @FxControllerKey(VaultListController.class) diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowSceneFactory.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowSceneFactory.java index b86a56e5f..dbb9b4cbf 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowSceneFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowSceneFactory.java @@ -17,22 +17,22 @@ public class MainWindowSceneFactory extends DefaultSceneFactory { protected static final KeyCodeCombination SHORTCUT_N = new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN); - private final Lazy mainWindowController; + private final Lazy mainWindowTitleController; private final Lazy vaultListController; @Inject - public MainWindowSceneFactory(Settings settings, Lazy mainWindowController, Lazy vaultListController) { + public MainWindowSceneFactory(Settings settings, Lazy mainWindowTitleController, Lazy vaultListController) { super(settings); - this.mainWindowController = mainWindowController; + this.mainWindowTitleController = mainWindowTitleController; this.vaultListController = vaultListController; } @Override protected void setupDefaultAccelerators(Scene scene, Stage stage) { if (SystemUtils.IS_OS_WINDOWS) { - scene.getAccelerators().put(ALT_F4, mainWindowController.get()::close); + scene.getAccelerators().put(ALT_F4, mainWindowTitleController.get()::close); } else { - scene.getAccelerators().put(SHORTCUT_W, mainWindowController.get()::close); + scene.getAccelerators().put(SHORTCUT_W, mainWindowTitleController.get()::close); } scene.getAccelerators().put(SHORTCUT_N, vaultListController.get()::didClickAddVault); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java new file mode 100644 index 000000000..49bd905f5 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java @@ -0,0 +1,95 @@ +package org.cryptomator.ui.mainwindow; + +import javafx.beans.binding.BooleanBinding; +import javafx.fxml.FXML; +import javafx.scene.layout.HBox; +import javafx.stage.Stage; +import org.cryptomator.common.LicenseHolder; +import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.FxApplication; +import org.cryptomator.ui.fxapp.UpdateChecker; +import org.cryptomator.ui.preferences.SelectedPreferencesTab; +import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; + +@MainWindowScoped +public class MainWindowTitleController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(MainWindowTitleController.class); + + public HBox titleBar; + + private final Stage window; + private final FxApplication application; + private final boolean minimizeToSysTray; + private final UpdateChecker updateChecker; + private final BooleanBinding updateAvailable; + private final LicenseHolder licenseHolder; + + private double xOffset; + private double yOffset; + + @Inject + MainWindowTitleController(@MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, LicenseHolder licenseHolder) { + this.window = window; + this.application = application; + this.minimizeToSysTray = minimizeToSysTray; + this.updateChecker = updateChecker; + this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); + this.licenseHolder = licenseHolder; + } + + @FXML + public void initialize() { + LOG.debug("init MainWindowTitleController"); + updateChecker.automaticallyCheckForUpdatesIfEnabled(); + titleBar.setOnMousePressed(event -> { + xOffset = event.getSceneX(); + yOffset = event.getSceneY(); + }); + titleBar.setOnMouseDragged(event -> { + window.setX(event.getScreenX() - xOffset); + window.setY(event.getScreenY() - yOffset); + }); + } + + @FXML + public void close() { + if (minimizeToSysTray) { + window.close(); + } else { + window.setIconified(true); + } + } + + @FXML + public void showPreferences() { + application.showPreferencesWindow(SelectedPreferencesTab.ANY); + } + + @FXML + public void showDonationKeyPreferences() { + application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY); + } + + /* Getter/Setter */ + + public LicenseHolder getLicenseHolder() { + return licenseHolder; + } + + public BooleanBinding updateAvailableProperty() { + return updateAvailable; + } + + public boolean isUpdateAvailable() { + return updateAvailable.get(); + } + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java new file mode 100644 index 000000000..fbf67aeb2 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java @@ -0,0 +1,102 @@ +package org.cryptomator.ui.mainwindow; + +import javafx.fxml.FXML; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; +import javafx.stage.Stage; +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; + +@MainWindow +public class ResizeController implements FxController { + + private final Stage window; + + public Region tlResizer; + public Region trResizer; + public Region blResizer; + public Region brResizer; + + private double origX, origY, origW, origH; + + @Inject + ResizeController(@MainWindow Stage window) { + this.window = window; + // TODO inject settings and save current position and size + } + + @FXML + public void initialize() { + tlResizer.setOnMousePressed(this::startResize); + trResizer.setOnMousePressed(this::startResize); + blResizer.setOnMousePressed(this::startResize); + brResizer.setOnMousePressed(this::startResize); + tlResizer.setOnMouseDragged(this::resizeTopLeft); + trResizer.setOnMouseDragged(this::resizeTopRight); + blResizer.setOnMouseDragged(this::resizeBottomLeft); + brResizer.setOnMouseDragged(this::resizeBottomRight); + } + + private void startResize(MouseEvent evt) { + origX = window.getX(); + origY = window.getY(); + origW = window.getWidth(); + origH = window.getHeight(); + } + + private void resizeTopLeft(MouseEvent evt) { + resizeTop(evt); + resizeLeft(evt); + } + + private void resizeTopRight(MouseEvent evt) { + resizeTop(evt); + resizeRight(evt); + } + + private void resizeBottomLeft(MouseEvent evt) { + resizeBottom(evt); + resizeLeft(evt); + } + + private void resizeBottomRight(MouseEvent evt) { + resizeBottom(evt); + resizeRight(evt); + } + + private void resizeTop(MouseEvent evt) { + double newY = evt.getScreenY(); + double dy = newY - origY; + double newH = origH - dy; + if (newH < window.getMaxHeight() && newH > window.getMinHeight()) { + window.setY(newY); + window.setHeight(newH); + } + } + + private void resizeLeft(MouseEvent evt) { + double newX = evt.getScreenX(); + double dx = newX - origX; + double newW = origW - dx; + if (newW < window.getMaxWidth() && newW > window.getMinWidth()) { + window.setX(newX); + window.setWidth(newW); + } + } + + private void resizeBottom(MouseEvent evt) { + double newH = evt.getSceneY(); + if (newH < window.getMaxHeight() && newH > window.getMinHeight()) { + window.setHeight(newH); + } + } + + private void resizeRight(MouseEvent evt) { + double newW = evt.getSceneX(); + if (newW < window.getMaxWidth() && newW > window.getMinWidth()) { + window.setWidth(newW); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index f2d801679..32f9bc9b0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -4,53 +4,32 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.fxml.FXML; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.common.vaults.VaultState; -import org.cryptomator.common.vaults.Volume; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.Tasks; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.cryptomator.ui.common.VaultService; import javax.inject.Inject; -import java.util.concurrent.ExecutorService; @MainWindowScoped public class VaultDetailUnlockedController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(VaultDetailUnlockedController.class); - private final ReadOnlyObjectProperty vault; - private final ExecutorService executor; + private final VaultService vaultService; @Inject - public VaultDetailUnlockedController(ObjectProperty vault, ExecutorService executor) { + public VaultDetailUnlockedController(ObjectProperty vault, VaultService vaultService) { this.vault = vault; - this.executor = executor; + this.vaultService = vaultService; } @FXML public void revealAccessLocation() { - try { - vault.get().reveal(); - } catch (Volume.VolumeException e) { - LOG.error("Failed to reveal vault.", e); - } + vaultService.reveal(vault.get()); } @FXML public void lock() { - Vault v = vault.get(); - v.setState(VaultState.PROCESSING); - Tasks.create(() -> { - v.lock(false); - }).onSuccess(() -> { - LOG.trace("Regular unmount succeeded."); - v.setState(VaultState.LOCKED); - }).onError(Exception.class, e -> { - v.setState(VaultState.UNLOCKED); - LOG.error("Regular unmount failed.", e); - // TODO - }).runOnce(executor); + vaultService.lock(vault.get(), false); + // TODO count lock attempts, and allow forced lock } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index 82fe7c7c7..0da7063ce 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -1,14 +1,18 @@ package org.cryptomator.ui.mainwindow; +import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ListView; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.common.vaults.VaultState; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.removevault.RemoveVaultComponent; @@ -23,7 +27,6 @@ public class VaultListController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class); - private final Stage window; private final ObservableList vaults; private final ObjectProperty selectedVault; private final VaultListCellFactory cellFactory; @@ -34,8 +37,7 @@ public class VaultListController implements FxController { public ListView vaultList; @Inject - VaultListController(@MainWindow Stage window, ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) { - this.window = window; + VaultListController(ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) { this.vaults = vaults; this.selectedVault = selectedVault; this.cellFactory = cellFactory; @@ -43,6 +45,7 @@ public class VaultListController implements FxController { this.removeVault = removeVault; this.noVaultSelected = selectedVault.isNull(); this.emptyVaultList = Bindings.isEmpty(vaults); + selectedVault.addListener(this::selectedVaultDidChange); } public void initialize() { @@ -59,6 +62,23 @@ public class VaultListController implements FxController { }); } + private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) { + VaultState reportedState = newValue.getState(); + switch (reportedState) { + case LOCKED: + case NEEDS_MIGRATION: + case MISSING: + VaultState determinedState = VaultListManager.determineVaultState(newValue.getPath()); + newValue.setState(determinedState); + break; + case ERROR: + case UNLOCKED: + case PROCESSING: + default: + // no-op + } + } + @FXML public void didClickAddVault() { addVaultWizard.build().showAddVaultWizard(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java new file mode 100644 index 000000000..0bc469a77 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java @@ -0,0 +1,43 @@ +package org.cryptomator.ui.preferences; + +import org.cryptomator.jni.MacFunctions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +class AutoStartMacStrategy implements AutoStartStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(AutoStartMacStrategy.class); + + private final MacFunctions macFunctions; + + public AutoStartMacStrategy(MacFunctions macFunctions) { + this.macFunctions = macFunctions; + } + + @Override + public CompletionStage isAutoStartEnabled() { + boolean enabled = macFunctions.launchServices().isLoginItemEnabled(); + return CompletableFuture.completedFuture(enabled); + } + + @Override + public void enableAutoStart() throws TogglingAutoStartFailedException { + if (macFunctions.launchServices().enableLoginItem()) { + LOG.debug("Added login item."); + } else { + throw new TogglingAutoStartFailedException("Failed to add login item."); + } + } + + @Override + public void disableAutoStart() throws TogglingAutoStartFailedException { + if (macFunctions.launchServices().disableLoginItem()) { + LOG.debug("Removed login item."); + } else { + throw new TogglingAutoStartFailedException("Failed to remove login item."); + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java new file mode 100644 index 000000000..d006d8681 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java @@ -0,0 +1,26 @@ +package org.cryptomator.ui.preferences; + +import dagger.Module; +import dagger.Provides; +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.MacFunctions; + +import java.util.Optional; + +@Module +abstract class AutoStartModule { + + @Provides + @PreferencesScoped + public static Optional provideAutoStartStrategy(Optional macFunctions) { + if (SystemUtils.IS_OS_MAC_OSX && macFunctions.isPresent()) { + return Optional.of(new AutoStartMacStrategy(macFunctions.get())); + } else if (SystemUtils.IS_OS_WINDOWS) { + Optional exeName = ProcessHandle.current().info().command(); + return exeName.map(AutoStartWinStrategy::new); + } else { + return Optional.empty(); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java new file mode 100644 index 000000000..99b21b4cd --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java @@ -0,0 +1,24 @@ +package org.cryptomator.ui.preferences; + +import java.util.concurrent.CompletionStage; + +public interface AutoStartStrategy { + + CompletionStage isAutoStartEnabled(); + + void enableAutoStart() throws TogglingAutoStartFailedException; + + void disableAutoStart() throws TogglingAutoStartFailedException; + + class TogglingAutoStartFailedException extends Exception { + + public TogglingAutoStartFailedException(String message) { + super(message); + } + + public TogglingAutoStartFailedException(String message, Throwable cause) { + super(message, cause); + } + + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java new file mode 100644 index 000000000..f4a8b0578 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java @@ -0,0 +1,91 @@ +package org.cryptomator.ui.preferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +class AutoStartWinStrategy implements AutoStartStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(AutoStartWinStrategy.class); + private static final String HKCU_AUTOSTART_KEY = "\"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\""; + private static final String AUTOSTART_VALUE = "Cryptomator"; + private final String exePath; + + public AutoStartWinStrategy(String exePath) { + this.exePath = exePath; + } + + @Override + public CompletionStage isAutoStartEnabled() { + ProcessBuilder regQuery = new ProcessBuilder("reg", "query", HKCU_AUTOSTART_KEY, // + "/v", AUTOSTART_VALUE); + try { + Process proc = regQuery.start(); + return proc.onExit().thenApply(p -> p.exitValue() == 0); + } catch (IOException e) { + LOG.warn("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); + return CompletableFuture.completedFuture(false); + } + } + + @Override + public void enableAutoStart() throws TogglingAutoStartFailedException { + ProcessBuilder regAdd = new ProcessBuilder("reg", "add", HKCU_AUTOSTART_KEY, // + "/v", AUTOSTART_VALUE, // + "/t", "REG_SZ", // + "/d", "\"" + exePath + "\"", // + "/f"); + String command = regAdd.command().stream().collect(Collectors.joining(" ")); + try { + Process proc = regAdd.start(); + boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS); + if (finishedInTime) { + LOG.debug("Added {} to registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); + } else { + throw new TogglingAutoStartFailedException("Adding registry value failed."); + } + } catch (IOException e) { + throw new TogglingAutoStartFailedException("Adding registry value failed. " + command, e); + } + } + + @Override + public void disableAutoStart() throws TogglingAutoStartFailedException { + ProcessBuilder regRemove = new ProcessBuilder("reg", "delete", HKCU_AUTOSTART_KEY, // + "/v", AUTOSTART_VALUE, // + "/f"); + String command = regRemove.command().stream().collect(Collectors.joining(" ")); + try { + Process proc = regRemove.start(); + boolean finishedInTime = waitForProcess(proc, 5, TimeUnit.SECONDS); + if (finishedInTime) { + LOG.debug("Removed {} from registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); + } else { + throw new TogglingAutoStartFailedException("Removing registry value failed."); + } + } catch (IOException e) { + throw new TogglingAutoStartFailedException("Removing registry value failed. " + command, e); + } + } + + private static boolean waitForProcess(Process proc, int timeout, TimeUnit timeUnit) { + boolean finishedInTime = false; + try { + finishedInTime = proc.waitFor(timeout, timeUnit); + } catch (InterruptedException e) { + LOG.error("Timeout while reading registry", e); + Thread.currentThread().interrupt(); + } finally { + if (!finishedInTime) { + proc.destroyForcibly(); + } + } + return finishedInTime; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java new file mode 100644 index 000000000..476887b26 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java @@ -0,0 +1,45 @@ +package org.cryptomator.ui.preferences; + +import javafx.application.Application; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; +import javafx.scene.control.TextArea; +import org.cryptomator.common.LicenseHolder; +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; + +@PreferencesScoped +public class DonationKeyPreferencesController implements FxController { + + private static final String DONATION_URI = "https://store.cryptomator.org/desktop"; + + private final Application application; + private final LicenseHolder licenseHolder; + public TextArea donationKeyField; + + @Inject + DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder) { + this.application = application; + this.licenseHolder = licenseHolder; + } + + @FXML + public void initialize() { + donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null)); + donationKeyField.textProperty().addListener(this::registrationKeyChanged); + } + + private void registrationKeyChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") String oldValue, String newValue) { + licenseHolder.validateAndStoreLicense(newValue); + } + + @FXML + public void getDonationKey() { + application.getHostServices().showDocument(DONATION_URI); + } + + public LicenseHolder getLicenseHolder() { + return licenseHolder; + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index bbccb0ad7..ae18dbd3f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -1,6 +1,9 @@ package org.cryptomator.ui.preferences; +import javafx.application.Platform; import javafx.beans.value.ObservableValue; +import javafx.concurrent.Task; +import javafx.fxml.FXML; import javafx.geometry.NodeOrientation; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; @@ -8,6 +11,7 @@ import javafx.scene.control.RadioButton; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.util.StringConverter; +import org.cryptomator.common.LicenseHolder; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; import org.cryptomator.ui.common.FxController; @@ -15,25 +19,38 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; +import java.util.Optional; +import java.util.concurrent.ExecutorService; @PreferencesScoped public class GeneralPreferencesController implements FxController { - + private static final Logger LOG = LoggerFactory.getLogger(GeneralPreferencesController.class); private final Settings settings; + private final boolean trayMenuSupported; + private final Optional autoStartStrategy; + private final LicenseHolder licenseHolder; + private final ExecutorService executor; public ChoiceBox themeChoiceBox; public CheckBox startHiddenCheckbox; public CheckBox debugModeCheckbox; + public CheckBox autoStartCheckbox; public ToggleGroup nodeOrientation; public RadioButton nodeOrientationLtr; public RadioButton nodeOrientationRtl; @Inject - GeneralPreferencesController(Settings settings) { + GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional autoStartStrategy, LicenseHolder licenseHolder, ExecutorService executor) { this.settings = settings; + this.trayMenuSupported = trayMenuSupported; + this.autoStartStrategy = autoStartStrategy; + this.licenseHolder = licenseHolder; + this.executor = executor; } + @FXML public void initialize() { themeChoiceBox.getItems().addAll(UiTheme.values()); themeChoiceBox.valueProperty().bindBidirectional(settings.theme()); @@ -43,9 +60,23 @@ public class GeneralPreferencesController implements FxController { debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode()); - nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation); + autoStartStrategy.ifPresent(autoStart -> { + autoStart.isAutoStartEnabled().thenAccept(enabled -> { + Platform.runLater(() -> autoStartCheckbox.setSelected(enabled)); + }); + }); + nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT); nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT); + nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation); + } + + public boolean isTrayMenuSupported() { + return this.trayMenuSupported; + } + + public boolean isAutoStartSupported() { + return autoStartStrategy.isPresent(); } private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { @@ -58,6 +89,20 @@ public class GeneralPreferencesController implements FxController { } } + @FXML + public void toggleAutoStart() { + autoStartStrategy.ifPresent(autoStart -> { + boolean enableAutoStart = autoStartCheckbox.isSelected(); + Task toggleTask = new ToggleAutoStartTask(autoStart, enableAutoStart); + toggleTask.setOnFailed(evt -> autoStartCheckbox.setSelected(!enableAutoStart)); // restore previous state + executor.execute(toggleTask); + }); + } + + public LicenseHolder getLicenseHolder() { + return licenseHolder; + } + /* Helper classes */ private static class UiThemeConverter extends StringConverter { @@ -73,4 +118,25 @@ public class GeneralPreferencesController implements FxController { } } + private static class ToggleAutoStartTask extends Task { + + private final AutoStartStrategy autoStart; + private final boolean enable; + + public ToggleAutoStartTask(AutoStartStrategy autoStart, boolean enable) { + this.autoStart = autoStart; + this.enable = enable; + } + + @Override + protected Void call() throws Exception { + if (enable) { + autoStart.enableAutoStart(); + } else { + autoStart.disableAutoStart(); + } + return null; + } + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesComponent.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesComponent.java index 49408d639..320b8c63f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesComponent.java @@ -7,9 +7,9 @@ package org.cryptomator.ui.preferences; import dagger.Lazy; import dagger.Subcomponent; +import javafx.beans.property.ObjectProperty; import javafx.scene.Scene; import javafx.stage.Stage; -import org.cryptomator.ui.common.FXMLLoaderFactory; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; @@ -23,7 +23,10 @@ public interface PreferencesComponent { @FxmlScene(FxmlFile.PREFERENCES) Lazy scene(); - default Stage showPreferencesWindow() { + ObjectProperty selectedTabProperty(); + + default Stage showPreferencesWindow(SelectedPreferencesTab selectedTab) { + selectedTabProperty().set(selectedTab); Stage stage = window(); stage.setScene(scene().get()); stage.show(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java index e63505e4b..718f9d443 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java @@ -1,6 +1,9 @@ package org.cryptomator.ui.preferences; +import javafx.beans.Observable; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; @@ -15,26 +18,50 @@ import javax.inject.Inject; public class PreferencesController implements FxController { private final Stage window; + private final ObjectProperty selectedTabProperty; private final BooleanBinding updateAvailable; public TabPane tabPane; public Tab generalTab; + public Tab volumeTab; public Tab updatesTab; + public Tab donationKeyTab; @Inject - public PreferencesController(@PreferencesWindow Stage window, UpdateChecker updateChecker) { + public PreferencesController(@PreferencesWindow Stage window, ObjectProperty selectedTabProperty, UpdateChecker updateChecker) { this.window = window; + this.selectedTabProperty = selectedTabProperty; this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); } @FXML public void initialize() { window.setOnShowing(this::windowWillAppear); + selectedTabProperty.addListener(observable -> this.selectChosenTab()); } - private void windowWillAppear(@SuppressWarnings("unused") WindowEvent windowEvent) { - if (updateAvailable.get()) { - tabPane.getSelectionModel().select(updatesTab); + private void selectChosenTab() { + Tab toBeSelected = getTabToSelect(selectedTabProperty.get()); + tabPane.getSelectionModel().select(toBeSelected); + } + + private Tab getTabToSelect(SelectedPreferencesTab selectedTab) { + switch (selectedTab) { + case UPDATES: + return updatesTab; + case VOLUME: + return volumeTab; + case DONATION_KEY: + return donationKeyTab; + case GENERAL: + return generalTab; + case ANY: + default: + return updateAvailable.get() ? updatesTab : generalTab; } } + private void windowWillAppear(@SuppressWarnings("unused") WindowEvent windowEvent) { + selectChosenTab(); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java index ae4233cb8..8b76364ef 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java @@ -4,6 +4,8 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; @@ -20,9 +22,15 @@ import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; -@Module +@Module(includes = {AutoStartModule.class}) abstract class PreferencesModule { + @Provides + @PreferencesScoped + static ObjectProperty provideSelectedTabProperty() { + return new SimpleObjectProperty<>(SelectedPreferencesTab.ANY); + } + @Provides @PreferencesWindow @PreferencesScoped @@ -70,4 +78,9 @@ abstract class PreferencesModule { @FxControllerKey(VolumePreferencesController.class) abstract FxController bindVolumePreferencesController(VolumePreferencesController controller); + @Binds + @IntoMap + @FxControllerKey(DonationKeyPreferencesController.class) + abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller); + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java new file mode 100644 index 000000000..74f305c6e --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java @@ -0,0 +1,28 @@ +package org.cryptomator.ui.preferences; + +public enum SelectedPreferencesTab { + /** + * Let the controller decide which tab to show. + */ + ANY, + + /** + * Show general tab + */ + GENERAL, + + /** + * Show volume tab + */ + VOLUME, + + /** + * Show updates tab + */ + UPDATES, + + /** + * Show donation key tab + */ + DONATION_KEY, +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java index 897bce32c..66dd13ade 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java @@ -1,26 +1,22 @@ package org.cryptomator.ui.quit; -import javafx.application.Platform; import javafx.collections.ObservableList; -import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ContentDisplay; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.common.vaults.VaultState; -import org.cryptomator.common.vaults.Volume; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.VaultService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.awt.desktop.QuitResponse; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.Executor; +import java.util.Collection; import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; @QuitScoped public class QuitController implements FxController { @@ -30,15 +26,17 @@ public class QuitController implements FxController { private final Stage window; private final QuitResponse response; private final ObservableList unlockedVaults; - private final ExecutorService executor; + private final ExecutorService executorService; + private final VaultService vaultService; public Button lockAndQuitButton; @Inject - QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList vaults, ExecutorService executor) { + QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList vaults, ExecutorService executorService, VaultService vaultService) { this.window = window; this.response = response; this.unlockedVaults = vaults.filtered(Vault::isUnlocked); - this.executor = executor; + this.executorService = executorService; + this.vaultService = vaultService; } @FXML @@ -52,84 +50,24 @@ public class QuitController implements FxController { public void lockAndQuit() { lockAndQuitButton.setDisable(true); lockAndQuitButton.setContentDisplay(ContentDisplay.LEFT); - - Iterator toBeLocked = List.copyOf(unlockedVaults).iterator(); - ScheduledService lockAllService = new LockAllVaultsService(executor, toBeLocked); - lockAllService.setOnSucceeded(evt -> { - if (!toBeLocked.hasNext()) { + + Task> lockAllTask = vaultService.createLockAllTask(unlockedVaults, false); + lockAllTask.setOnSucceeded(evt -> { + LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayableName).collect(Collectors.joining(", "))); + if (unlockedVaults.isEmpty()) { window.close(); response.performQuit(); } }); - lockAllService.setOnFailed(evt -> { + lockAllTask.setOnFailed(evt -> { + LOG.warn("Locking failed", lockAllTask.getException()); lockAndQuitButton.setDisable(false); lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY); // TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!) // see https://github.com/cryptomator/cryptomator/blob/1.4.16/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java#L151-L163 response.cancelQuit(); }); - lockAllService.start(); + executorService.execute(lockAllTask); } - /** - * @param vault The vault to lock - * @return Task that tries to lock the given vault gracefully. - */ - private Task createGracefulLockTask(Vault vault) { - Task task = new Task() { - @Override - protected Void call() throws Volume.VolumeException { - vault.lock(false); - LOG.info("Locked {}", vault.getDisplayableName()); - return null; - } - }; - task.setOnScheduled(evt -> { - vault.setState(VaultState.PROCESSING); - }); - task.setOnSucceeded(evt -> { - vault.setState(VaultState.LOCKED); - }); - task.setOnFailed(evt -> { - LOG.warn("Failed to lock vault", vault); - }); - return task; - } - - /** - * @return Task that succeeds immediately - */ - private Task createNoopTask() { - return new Task<>() { - @Override - protected Void call() { - return null; - } - }; - } - - private class LockAllVaultsService extends ScheduledService { - - private final Iterator vaultsToLock; - - public LockAllVaultsService(Executor executor, Iterator vaultsToLock) { - this.vaultsToLock = vaultsToLock; - setExecutor(executor); - setRestartOnFailure(false); - } - - @Override - protected Task createTask() { - assert Platform.isFxApplicationThread(); - if (vaultsToLock.hasNext()) { - return createGracefulLockTask(vaultsToLock.next()); - } else { - // This should be unreachable code, since vaultsToLock is only accessed on the FX App Thread. - // But if quitting the application takes longer for any reason, this service should shut down properly - reset(); - return createNoopTask(); - } - } - } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 2ca6ebb69..945256f9e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -40,7 +40,7 @@ public class RecoveryKeyCreationController implements FxController { public NiceSecurePasswordField passwordField; @Inject - public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_DISPLAY) Lazy successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) { + public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) { this.window = window; this.successScene = successScene; this.vault = vault; diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java index 2d96d786f..2b61ddf85 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyDisplayController.java @@ -1,26 +1,73 @@ package org.cryptomator.ui.recoverykey; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.StringProperty; import javafx.fxml.FXML; +import javafx.print.PageLayout; +import javafx.print.Printer; +import javafx.print.PrinterJob; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.text.Font; +import javafx.scene.text.FontSmoothingType; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import javafx.stage.Stage; -import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.inject.Inject; - -@RecoveryKeyScoped public class RecoveryKeyDisplayController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyDisplayController.class); private final Stage window; - private final Vault vault; - private final StringProperty recoveryKeyProperty; - - @Inject - public RecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey) { + private final String vaultName; + private final String recoveryKey; + + public RecoveryKeyDisplayController(Stage window, String vaultName, String recoveryKey) { this.window = window; - this.vault = vault; - this.recoveryKeyProperty = recoveryKey; + this.vaultName = vaultName; + this.recoveryKey = recoveryKey; + } + + @FXML + public void printRecoveryKey() { + // TODO localize + + PrinterJob job = PrinterJob.createPrinterJob(); + if (job != null && job.showPrintDialog(window)) { + PageLayout pageLayout = job.getJobSettings().getPageLayout(); + + Text heading = new Text("Cryptomator Recovery Key\n" + vaultName + "\n"); + heading.setFont(Font.font("serif", FontWeight.BOLD, 20)); + heading.setFontSmoothingType(FontSmoothingType.LCD); + + Text key = new Text(recoveryKey); + key.setFont(Font.font("serif", FontWeight.NORMAL, 16)); + key.setFontSmoothingType(FontSmoothingType.GRAY); + + TextFlow textFlow = new TextFlow(); + textFlow.setPrefSize(pageLayout.getPrintableWidth(), pageLayout.getPrintableHeight()); + textFlow.getChildren().addAll(heading, key); + textFlow.setLineSpacing(6); + + if (job.printPage(textFlow)) { + LOG.info("Recovery key printed."); + job.endJob(); + } else { + LOG.warn("Printing recovery key failed."); + } + } else { + LOG.info("Printing recovery key canceled by user."); + } + } + + @FXML + public void copyRecoveryKey() { + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(recoveryKey); + Clipboard.getSystemClipboard().setContent(clipboardContent); + LOG.info("Recovery key copied to clipboard."); } @FXML @@ -30,15 +77,15 @@ public class RecoveryKeyDisplayController implements FxController { /* Getter/Setter */ - public Vault getVault() { - return vault; - } - - public ReadOnlyStringProperty recoveryKeyProperty() { - return recoveryKeyProperty; + public boolean isPrinterSupported() { + return Printer.getDefaultPrinter() != null; } public String getRecoveryKey() { - return recoveryKeyProperty.get(); + return recoveryKey; + } + + public String getVaultName() { + return vaultName; } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java index 261b50682..80f89c749 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java @@ -10,6 +10,7 @@ import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Modality; import javafx.stage.Stage; +import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FXMLLoaderFactory; import org.cryptomator.ui.common.FxController; @@ -63,10 +64,10 @@ abstract class RecoveryKeyModule { } @Provides - @FxmlScene(FxmlFile.RECOVERYKEY_DISPLAY) + @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) @RecoveryKeyScoped - static Scene provideRecoveryKeyDisplayScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene("/fxml/recoverykey_display.fxml"); + static Scene provideRecoveryKeySuccessScene(@RecoveryKeyWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/recoverykey_success.fxml"); } // ------------------ @@ -76,9 +77,16 @@ abstract class RecoveryKeyModule { @FxControllerKey(RecoveryKeyCreationController.class) abstract FxController bindRecoveryKeyCreationController(RecoveryKeyCreationController controller); - @Binds + @Provides @IntoMap @FxControllerKey(RecoveryKeyDisplayController.class) - abstract FxController bindRecoveryKeyDisplayController(RecoveryKeyDisplayController controller); + static FxController provideRecoveryKeyDisplayController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey) { + return new RecoveryKeyDisplayController(window, vault.getDisplayableName(), recoveryKey.get()); + } + + @Binds + @IntoMap + @FxControllerKey(RecoveryKeySuccessController.class) + abstract FxController bindRecoveryKeySuccessController(RecoveryKeySuccessController controller); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeySuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeySuccessController.java new file mode 100644 index 000000000..ae9e2e70f --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeySuccessController.java @@ -0,0 +1,24 @@ +package org.cryptomator.ui.recoverykey; + +import javafx.fxml.FXML; +import javafx.stage.Stage; +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; + +@RecoveryKeyScoped +public class RecoveryKeySuccessController implements FxController { + + private final Stage window; + + @Inject + public RecoveryKeySuccessController(@RecoveryKeyWindow Stage window) { + this.window = window; + } + + @FXML + public void close() { + window.close(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index c95a02905..bef3c441c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -3,11 +3,14 @@ package org.cryptomator.ui.traymenu; import javafx.application.Platform; import javafx.beans.Observable; import javafx.collections.ObservableList; +import org.cryptomator.common.ShutdownHook; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.common.vaults.Volume; import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.launcher.FxApplicationStarter; +import org.cryptomator.ui.preferences.SelectedPreferencesTab; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,17 +40,17 @@ class TrayMenuController { private final ResourceBundle resourceBundle; private final FxApplicationStarter fxApplicationStarter; private final CountDownLatch shutdownLatch; - private final Settings settings; + private final ShutdownHook shutdownHook; private final ObservableList vaults; private final PopupMenu menu; private final AtomicBoolean allowSuddenTermination; @Inject - TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, Settings settings, ObservableList vaults) { + TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList vaults) { this.resourceBundle = resourceBundle; this.fxApplicationStarter = fxApplicationStarter; this.shutdownLatch = shutdownLatch; - this.settings = settings; + this.shutdownHook = shutdownHook; this.vaults = vaults; this.menu = new PopupMenu(); this.allowSuddenTermination = new AtomicBoolean(true); @@ -71,6 +74,7 @@ class TrayMenuController { if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) { Desktop.getDesktop().setQuitHandler(this::handleQuitRequest); } + shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults); // allow sudden termination if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) { @@ -112,6 +116,11 @@ class TrayMenuController { } menu.addSeparator(); + MenuItem lockAllItem = new MenuItem(resourceBundle.getString("traymenu.lockAllVaults")); + lockAllItem.addActionListener(this::lockAllVaults); + lockAllItem.setEnabled(!vaults.filtered(Vault::isUnlocked).isEmpty()); + menu.add(lockAllItem); + MenuItem quitApplicationItem = new MenuItem(resourceBundle.getString("traymenu.quitApplication")); quitApplicationItem.addActionListener(this::quitApplication); menu.add(quitApplicationItem); @@ -126,11 +135,11 @@ class TrayMenuController { submenu.add(unlockItem); } else if (vault.isUnlocked()) { MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock")); - lockItem.setEnabled(false); // TODO add action listener + lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault)); submenu.add(lockItem); MenuItem revealItem = new MenuItem(resourceBundle.getString("traymenu.vault.reveal")); - revealItem.setEnabled(false); // TODO add action listener + revealItem.addActionListener(createActionListenerForVault(vault, this::revealVault)); submenu.add(revealItem); } @@ -145,12 +154,24 @@ class TrayMenuController { fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault)); } + private void lockVault(Vault vault) { + fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false)); + } + + private void lockAllVaults(ActionEvent actionEvent) { + fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lockAll(vaults.filtered(Vault::isUnlocked), false)); + } + + private void revealVault(Vault vault) { + fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().reveal(vault)); + } + void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) { fxApplicationStarter.get(true).thenAccept(app -> app.showMainWindow()); } private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) { - fxApplicationStarter.get(true).thenAccept(FxApplication::showPreferencesWindow); + fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY)); } private void handleQuitRequest(EventObject e, QuitResponse response) { @@ -174,4 +195,16 @@ class TrayMenuController { } }); } + + private void forceUnmountRemainingVaults() { + for (Vault vault : vaults) { + if (vault.isUnlocked()) { + try { + vault.lock(true); + } catch (Volume.VolumeException e) { + LOG.error("Failed to unmount vault " + vault.getPath(), e); + } + } + } + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java index 7c1221aaf..837dc827f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java @@ -7,6 +7,7 @@ import javafx.animation.Timeline; import javafx.beans.binding.Bindings; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.WritableValue; @@ -32,6 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.NotDirectoryException; import java.util.Arrays; @@ -49,19 +51,25 @@ public class UnlockController implements FxController { private final ObjectBinding unlockButtonState; private final Optional keychainAccess; private final Lazy successScene; + private final Lazy invalidMountPointScene; + private final Lazy genericErrorScene; + private final ObjectProperty genericErrorCause; private final ForgetPasswordComponent.Builder forgetPassword; private final BooleanProperty unlockButtonDisabled; public NiceSecurePasswordField passwordField; public CheckBox savePassword; @Inject - public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, ForgetPasswordComponent.Builder forgetPassword) { + public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy genericErrorScene, @Named("genericErrorCause") ObjectProperty genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) { this.window = window; this.vault = vault; this.executor = executor; this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty()); this.keychainAccess = keychainAccess; this.successScene = successScene; + this.invalidMountPointScene = invalidMountPointScene; + this.genericErrorScene = genericErrorScene; + this.genericErrorCause = genericErrorCause; this.forgetPassword = forgetPassword; this.unlockButtonDisabled = new SimpleBooleanProperty(); } @@ -100,17 +108,16 @@ public class UnlockController implements FxController { shakeWindow(); passwordField.selectAll(); passwordField.requestFocus(); - }).onError(UnsupportedVaultFormatException.class, e -> { - // TODO }).onError(NotDirectoryException.class, e -> { LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage()); - // TODO + window.setScene(invalidMountPointScene.get()); }).onError(DirectoryNotEmptyException.class, e -> { LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage()); - // TODO + window.setScene(invalidMountPointScene.get()); }).onError(Exception.class, e -> { // including RuntimeExceptions LOG.error("Unlock failed for technical reasons.", e); - // TODO + genericErrorCause.set(e); + window.setScene(genericErrorScene.get()); }).andFinally(() -> { if (!vault.isUnlocked()) { vault.setState(VaultState.LOCKED); diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockGenericErrorController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockGenericErrorController.java new file mode 100644 index 000000000..faa357969 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockGenericErrorController.java @@ -0,0 +1,29 @@ +package org.cryptomator.ui.unlock; + +import dagger.Lazy; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; + +import javax.inject.Inject; + +@UnlockScoped +public class UnlockGenericErrorController implements FxController { + + private final Stage window; + private final Lazy unlockScene; + + @Inject + UnlockGenericErrorController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene) { + this.window = window; + this.unlockScene = unlockScene; + } + + @FXML + public void back() { + window.setScene(unlockScene.get()); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java new file mode 100644 index 000000000..d73c96c07 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java @@ -0,0 +1,39 @@ +package org.cryptomator.ui.unlock; + +import dagger.Lazy; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; + +import javax.inject.Inject; + +@UnlockScoped +public class UnlockInvalidMountPointController implements FxController { + + private final Stage window; + private final Lazy unlockScene; + private final Vault vault; + + @Inject + UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @UnlockWindow Vault vault) { + this.window = window; + this.unlockScene = unlockScene; + this.vault = vault; + } + + @FXML + public void back() { + window.setScene(unlockScene.get()); + } + + /* Getter/Setter */ + + public String getMountPoint() { + return vault.getVaultSettings().getIndividualMountPath().orElse("AUTO"); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 488f7cfa8..69488010f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -4,6 +4,8 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Modality; @@ -14,6 +16,7 @@ import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.StackTraceController; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import javax.inject.Named; @@ -44,6 +47,13 @@ abstract class UnlockModule { return stage; } + @Provides + @Named("genericErrorCause") + @UnlockScoped + static ObjectProperty provideGenericErrorCause() { + return new SimpleObjectProperty<>(); + } + @Provides @FxmlScene(FxmlFile.UNLOCK) @UnlockScoped @@ -58,6 +68,21 @@ abstract class UnlockModule { return fxmlLoaders.createScene("/fxml/unlock_success.fxml"); } + @Provides + @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) + @UnlockScoped + static Scene provideInvalidMountPointScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/unlock_invalid_mount_point.fxml"); + } + + + @Provides + @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) + @UnlockScoped + static Scene provideGenericErrorScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/unlock_generic_error.fxml"); + } + // ------------------ @@ -71,5 +96,22 @@ abstract class UnlockModule { @FxControllerKey(UnlockSuccessController.class) abstract FxController bindUnlockSuccessController(UnlockSuccessController controller); + @Binds + @IntoMap + @FxControllerKey(UnlockInvalidMountPointController.class) + abstract FxController bindUnlockInvalidMountPointController(UnlockInvalidMountPointController controller); + + @Binds + @IntoMap + @FxControllerKey(UnlockGenericErrorController.class) + abstract FxController bindUnlockGenericErrorController(UnlockGenericErrorController controller); + + @Provides + @IntoMap + @FxControllerKey(StackTraceController.class) + static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty errorCause) { + return new StackTraceController(errorCause.get()); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java index 44e1c0010..cf61ab2e7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java @@ -5,13 +5,13 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.ContentDisplay; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.Tasks; +import org.cryptomator.ui.common.VaultService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,14 +26,16 @@ public class UnlockSuccessController implements FxController { private final Stage window; private final Vault vault; private final ExecutorService executor; + private final VaultService vaultService; private final ObjectProperty revealButtonState; private final BooleanProperty revealButtonDisabled; @Inject - public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor) { + public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, VaultService vaultService) { this.window = window; this.vault = vault; this.executor = executor; + this.vaultService = vaultService; this.revealButtonState = new SimpleObjectProperty<>(ContentDisplay.TEXT_ONLY); this.revealButtonDisabled = new SimpleBooleanProperty(); } @@ -49,17 +51,19 @@ public class UnlockSuccessController implements FxController { LOG.trace("UnlockSuccessController.revealAndClose()"); revealButtonState.set(ContentDisplay.LEFT); revealButtonDisabled.set(true); - Tasks.create(() -> { - vault.reveal(); - }).onSuccess(() -> { - window.close(); - }).onError(InvalidPassphraseException.class, e -> { - // TODO - LOG.warn("Reveal failed.", e); - }).andFinally(() -> { + + Task revealTask = vaultService.createRevealTask(vault); + revealTask.setOnSucceeded(evt -> { revealButtonState.set(ContentDisplay.TEXT_ONLY); revealButtonDisabled.set(false); - }).runOnce(executor); + window.close(); + }); + revealTask.setOnFailed(evt -> { + LOG.warn("Reveal failed.", revealTask.getException()); + revealButtonState.set(ContentDisplay.TEXT_ONLY); + revealButtonDisabled.set(false); + }); + executor.execute(revealTask); } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index 6efa3e407..9ed5ba2ea 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -84,12 +84,7 @@ public class MountOptionsController implements FxController { driveLetterSelection.getItems().addAll(windowsDriveLetters.getAllDriveLetters()); driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters)); driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get()); - vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir)); - vault.getVaultSettings().winDriveLetter().bind( // - Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) // - .then(driveLetterSelection.getSelectionModel().selectedItemProperty()) // - .otherwise((String) null) // - ); + if (vault.getVaultSettings().usesIndividualMountPath().get()) { mountPoint.selectToggle(mountPointCustomDir); } else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) { @@ -97,6 +92,13 @@ public class MountOptionsController implements FxController { } else { mountPoint.selectToggle(mountPointAuto); } + + vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir)); + vault.getVaultSettings().winDriveLetter().bind( // + Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) // + .then(driveLetterSelection.getSelectionModel().selectedItemProperty()) // + .otherwise((String) null) // + ); } @FXML @@ -116,7 +118,7 @@ public class MountOptionsController implements FxController { @FXML private void chooseCustomMountPoint() { DirectoryChooser directoryChooser = new DirectoryChooser(); - directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.winDirChooser")); + directoryChooser.setTitle(resourceBundle.getString("vaultOptions.mount.mountPoint.directoryPickerTitle")); try { directoryChooser.setInitialDirectory(Path.of(System.getProperty("user.home")).toFile()); } catch (Exception e) { diff --git a/main/ui/src/main/resources/css/dark_theme.css b/main/ui/src/main/resources/css/dark_theme.css index c277fc09f..7ad3a1da5 100644 --- a/main/ui/src/main/resources/css/dark_theme.css +++ b/main/ui/src/main/resources/css/dark_theme.css @@ -78,6 +78,7 @@ SCROLL_BAR_THUMB_NORMAL: GRAY_3; SCROLL_BAR_THUMB_HOVER: GRAY_4; INDICATOR_BG: RED_5; + DRAG_N_DROP_INDICATOR_BG: GRAY_3; PROGRESS_INDICATOR_BEGIN: GRAY_7; PROGRESS_INDICATOR_END: GRAY_5; PROGRESS_BAR_BG: GRAY_2; @@ -116,6 +117,10 @@ -fx-font-size: 0.8em; } +.label-extra-small { + -fx-font-size: 0.64em; +} + .text-flow > * { -fx-fill: TEXT_FILL; } @@ -179,20 +184,6 @@ -fx-fill: CONTROL_WHITE_BG_ARMED; } -.main-window .resizer { - -fx-background-color: linear-gradient(to bottom right, - transparent 50%, - CONTROL_BORDER_NORMAL 51%, - CONTROL_BORDER_NORMAL 60%, - transparent 61%, - transparent 70%, - CONTROL_BORDER_NORMAL 71%, - CONTROL_BORDER_NORMAL 80%, - transparent 81% - ); - -fx-cursor: nw_resize; -} - .main-window .update-indicator { -fx-background-color: PRIMARY_BG, white, INDICATOR_BG; -fx-background-insets: 0, 1px, 2px; @@ -201,6 +192,16 @@ -fx-translate-y: 1px; } +.main-window .drag-n-drop-indicator { + -fx-border-color: DRAG_N_DROP_INDICATOR_BG; + -fx-border-width: 3px; +} + +.main-window .drag-n-drop-indicator .drag-n-drop-header { + -fx-background-color: DRAG_N_DROP_INDICATOR_BG; + -fx-padding: 3px; +} + /******************************************************************************* * * * TabPane * @@ -212,34 +213,36 @@ } .tab-pane > .tab-header-area { - -fx-padding: 6px 12px 0 12px; - -fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG; - -fx-background-insets: 0, 0 0 1px 0; + -fx-padding: 0 12px; + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 0 0 3px 0; } -.tab-pane > .tab-header-area > .headers-region > .tab { - -fx-background-color: CONTROL_BORDER_NORMAL, MAIN_BG; - -fx-background-insets: 0 0 1px 0, 1px; - -fx-background-radius: 4px 4px 0 0; - -fx-padding: 0.2em 1em 0.2em 1em; +.tab-pane .tab { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 0 0 3px 0; + -fx-padding: 6px 12px; } -.tab-pane > .tab-header-area > .headers-region > .tab:selected { - -fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG; - -fx-background-insets: 0, 1px 1px 0 1px; +.tab-pane .tab:selected { + -fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL; } -.tab-pane > .tab-header-area > .headers-region > .tab > .tab-container > .tab-label { - -fx-text-fill: TEXT_FILL; +.tab-pane .tab .tab-label { + -fx-text-fill: SECONDARY_BG; -fx-alignment: CENTER; } -.tab-pane > .tab-header-area > .headers-region > .tab .glyph-icon { +.tab-pane .tab .glyph-icon { -fx-fill: SECONDARY_BG; } -.tab-pane > .tab-header-area > .headers-region > .tab:selected .glyph-icon { - -fx-fill: TEXT_FILL; +.tab-pane .tab:selected .glyph-icon { + -fx-fill: PRIMARY_BG; +} + +.tab-pane .tab:selected .tab-label { + -fx-text-fill: TEXT_FILL_PRIMARY; } /******************************************************************************* @@ -672,7 +675,6 @@ ******************************************************************************/ .choice-box { - -fx-text-fill: TEXT_FILL; -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; -fx-background-insets: 0, 1px; -fx-background-radius: 4px; @@ -683,6 +685,18 @@ -fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL; } +.choice-box:disabled { + -fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED; +} + +.choice-box > .label { + -fx-text-fill: TEXT_FILL; +} + +.choice-box:disabled > .label { + -fx-text-fill: TEXT_FILL_SECONDARY; +} + .choice-box > .open-button { -fx-padding: 0 0 0 0.3em; } @@ -694,6 +708,10 @@ -fx-shape: "M 0 0 h 7 l -3.5 4 z"; } +.choice-box:disabled > .open-button > .arrow { + -fx-background-color: transparent, TEXT_FILL_SECONDARY; +} + .choice-box .context-menu { -fx-translate-x: -1.4em; } diff --git a/main/ui/src/main/resources/css/light_theme.css b/main/ui/src/main/resources/css/light_theme.css index a4ad762ed..a09d3a908 100644 --- a/main/ui/src/main/resources/css/light_theme.css +++ b/main/ui/src/main/resources/css/light_theme.css @@ -78,6 +78,7 @@ SCROLL_BAR_THUMB_NORMAL: GRAY_7; SCROLL_BAR_THUMB_HOVER: GRAY_6; INDICATOR_BG: RED_5; + DRAG_N_DROP_INDICATOR_BG: GRAY_5; PROGRESS_INDICATOR_BEGIN: GRAY_2; PROGRESS_INDICATOR_END: GRAY_4; PROGRESS_BAR_BG: GRAY_8; @@ -116,6 +117,10 @@ -fx-font-size: 0.8em; } +.label-extra-small { + -fx-font-size: 0.64em; +} + .text-flow > * { -fx-fill: TEXT_FILL; } @@ -179,20 +184,6 @@ -fx-fill: CONTROL_WHITE_BG_ARMED; } -.main-window .resizer { - -fx-background-color: linear-gradient(to bottom right, - transparent 50%, - CONTROL_BORDER_NORMAL 51%, - CONTROL_BORDER_NORMAL 60%, - transparent 61%, - transparent 70%, - CONTROL_BORDER_NORMAL 71%, - CONTROL_BORDER_NORMAL 80%, - transparent 81% - ); - -fx-cursor: nw_resize; -} - .main-window .update-indicator { -fx-background-color: PRIMARY_BG, white, INDICATOR_BG; -fx-background-insets: 0, 1px, 2px; @@ -201,6 +192,16 @@ -fx-translate-y: 1px; } +.main-window .drag-n-drop-indicator { + -fx-border-color: DRAG_N_DROP_INDICATOR_BG; + -fx-border-width: 3px; +} + +.main-window .drag-n-drop-indicator .drag-n-drop-header { + -fx-background-color: DRAG_N_DROP_INDICATOR_BG; + -fx-padding: 3px; +} + /******************************************************************************* * * * TabPane * @@ -212,34 +213,36 @@ } .tab-pane > .tab-header-area { - -fx-padding: 6px 12px 0 12px; - -fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG; - -fx-background-insets: 0, 0 0 1px 0; + -fx-padding: 0 12px; + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 0 0 3px 0; } -.tab-pane > .tab-header-area > .headers-region > .tab { - -fx-background-color: CONTROL_BORDER_NORMAL, MAIN_BG; - -fx-background-insets: 0 0 1px 0, 1px; - -fx-background-radius: 4px 4px 0 0; - -fx-padding: 0.2em 1em 0.2em 1em; +.tab-pane .tab { + -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; + -fx-background-insets: 0, 0 0 3px 0; + -fx-padding: 6px 12px; } -.tab-pane > .tab-header-area > .headers-region > .tab:selected { - -fx-background-color: CONTROL_BORDER_FOCUSED, MAIN_BG; - -fx-background-insets: 0, 1px 1px 0 1px; +.tab-pane .tab:selected { + -fx-background-color: PRIMARY_BG, CONTROL_PRIMARY_LIGHT_BG_NORMAL; } -.tab-pane > .tab-header-area > .headers-region > .tab > .tab-container > .tab-label { - -fx-text-fill: TEXT_FILL; +.tab-pane .tab .tab-label { + -fx-text-fill: SECONDARY_BG; -fx-alignment: CENTER; } -.tab-pane > .tab-header-area > .headers-region > .tab .glyph-icon { +.tab-pane .tab .glyph-icon { -fx-fill: SECONDARY_BG; } -.tab-pane > .tab-header-area > .headers-region > .tab:selected .glyph-icon { - -fx-fill: TEXT_FILL; +.tab-pane .tab:selected .glyph-icon { + -fx-fill: PRIMARY_BG; +} + +.tab-pane .tab:selected .tab-label { + -fx-text-fill: TEXT_FILL_PRIMARY; } /******************************************************************************* @@ -672,7 +675,6 @@ ******************************************************************************/ .choice-box { - -fx-text-fill: TEXT_FILL; -fx-background-color: CONTROL_BORDER_NORMAL, CONTROL_BG_NORMAL; -fx-background-insets: 0, 1px; -fx-background-radius: 4px; @@ -683,6 +685,18 @@ -fx-background-color: CONTROL_BORDER_FOCUSED, CONTROL_BG_NORMAL; } +.choice-box:disabled { + -fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED; +} + +.choice-box > .label { + -fx-text-fill: TEXT_FILL; +} + +.choice-box:disabled > .label { + -fx-text-fill: TEXT_FILL_SECONDARY; +} + .choice-box > .open-button { -fx-padding: 0 0 0 0.3em; } @@ -694,6 +708,10 @@ -fx-shape: "M 0 0 h 7 l -3.5 4 z"; } +.choice-box:disabled > .open-button > .arrow { + -fx-background-color: transparent, TEXT_FILL_SECONDARY; +} + .choice-box .context-menu { -fx-translate-x: -1.4em; } diff --git a/main/ui/src/main/resources/fxml/addvault_existing.fxml b/main/ui/src/main/resources/fxml/addvault_existing.fxml index 4b989e291..e9c64f88a 100644 --- a/main/ui/src/main/resources/fxml/addvault_existing.fxml +++ b/main/ui/src/main/resources/fxml/addvault_existing.fxml @@ -11,8 +11,8 @@ diff --git a/main/ui/src/main/resources/fxml/addvault_existing_error.fxml b/main/ui/src/main/resources/fxml/addvault_existing_error.fxml index c145b6283..244f57602 100644 --- a/main/ui/src/main/resources/fxml/addvault_existing_error.fxml +++ b/main/ui/src/main/resources/fxml/addvault_existing_error.fxml @@ -11,9 +11,9 @@ diff --git a/main/ui/src/main/resources/fxml/addvault_new_location.fxml b/main/ui/src/main/resources/fxml/addvault_new_location.fxml index dfb1addbf..420a0f960 100644 --- a/main/ui/src/main/resources/fxml/addvault_new_location.fxml +++ b/main/ui/src/main/resources/fxml/addvault_new_location.fxml @@ -14,8 +14,8 @@ @@ -29,8 +29,10 @@