Merge branch 'release/1.5.0-alpha1'

This commit is contained in:
Sebastian Stenzel
2019-09-19 11:22:25 +02:00
254 changed files with 14621 additions and 7118 deletions

View File

@@ -2,8 +2,8 @@
<configuration default="false" name="Cryptomator macOS" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.mountPointsDir=&quot;/Volumes/&quot; -Xss2m -Xmx512m" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.mountPointsDir=&quot;/Volumes/&quot; -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -14,6 +14,13 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
<include>ffi-version.txt</include>
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>

View File

@@ -14,6 +14,13 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
<include>ffi-version.txt</include>
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>

View File

@@ -14,6 +14,13 @@
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>
<include>ffi-version.txt</include>
</includes>
<outputDirectory>libs</outputDirectory>
</fileSet>
<fileSet>
<directory>target/</directory>
<includes>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.16</version>
<version>1.5.0-alpha1</version>
</parent>
<artifactId>buildkit</artifactId>
<packaging>pom</packaging>
@@ -39,6 +39,7 @@
<directory>${project.basedir}/src/main/resources</directory>
<includes>
<include>version.txt</include>
<include>ffi-version.txt</include>
<include>launcher-mac.sh</include>
<include>launcher-linux.sh</include>
<include>launcher-win.bat</include>

View File

@@ -0,0 +1 @@
${cryptomator.jni.version}

View File

@@ -4,35 +4,64 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.16</version>
<version>1.5.0-alpha1</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator Commons</name>
<description>Shared utilities</description>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>fuse-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>dokany-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>webdav-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<!-- JavaFx -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
</dependency>
<!-- Libs -->
<!-- EasyBind -->
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
</dependency>
<!-- Google -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- DI -->

View File

@@ -5,22 +5,89 @@
*******************************************************************************/
package org.cryptomator.common;
import java.util.Comparator;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.fxmisc.easybind.EasyBind;
import javax.inject.Named;
import javax.inject.Singleton;
import java.net.InetSocketAddress;
import java.util.Comparator;
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;
import dagger.Module;
import dagger.Provides;
@Module(subcomponents = {VaultComponent.class})
public abstract class CommonsModule {
@Module
public class CommonsModule {
private static final int NUM_SCHEDULER_THREADS = 4;
@Provides
@Singleton
@Named("SemVer")
Comparator<String> providesSemVerComparator() {
static Comparator<String> providesSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
static Settings provideSettings(SettingsProvider settingsProvider) {
return settingsProvider.get();
}
@Provides
@Singleton
static ObservableList<Vault> provideVaultList(VaultListManager vaultListManager) {
return vaultListManager.getVaultList();
}
@Provides
@Singleton
static ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
Thread t = new Thread(r);
t.setName("Background Thread " + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
});
shutdownTaskScheduler.accept(executorService::shutdown);
return executorService;
}
@Binds
@Singleton
abstract ExecutorService bindExecutorService(ScheduledExecutorService executor);
@Provides
@Singleton
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
return Bindings.createObjectBinding(() -> {
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
return InetSocketAddress.createUnresolved(host, settings.port().intValue());
}, settings.port());
}
@Provides
@Singleton
static WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
WebDavServer server = WebDavServer.create();
// no need to unsubscribe eventually, because server is a singleton
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
return server;
}
}

View File

@@ -28,6 +28,7 @@ public class Environment {
@Inject
public Environment() {
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
LOG.debug("user.language: {}", System.getProperty("user.language"));
LOG.debug("user.region: {}", System.getProperty("user.region"));
LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));

View File

@@ -0,0 +1,32 @@
/*******************************************************************************
* Copyright (c) 2019 Skymatic GmbH.
* 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 dagger.Module;
import dagger.Provides;
import org.cryptomator.jni.JniFunctions;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.WinFunctions;
import javax.inject.Singleton;
import java.util.Optional;
@Module
public class JniModule {
@Provides
@Singleton
Optional<MacFunctions> provideOptionalMacFunctions() {
return JniFunctions.macFunctions();
}
@Provides
@Singleton
Optional<WinFunctions> provideOptionalWinFunctions() {
return JniFunctions.winFunctions();
}
}

View File

@@ -8,10 +8,14 @@
******************************************************************************/
package org.cryptomator.common.settings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.beans.Observable;
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.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.function.Consumer;
@@ -22,20 +26,24 @@ public class Settings {
public static final int MAX_PORT = 65535;
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
public static final boolean DEFAULT_CHECK_FOR_UDPATES = false;
public static final boolean DEFAULT_START_HIDDEN = false;
public static final int DEFAULT_PORT = 42427;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
public static final String DEFAULT_GVFS_SCHEME = "dav";
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final boolean DEFAULT_DEBUG_MODE = false;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
private final StringProperty preferredGvfsScheme = new SimpleStringProperty(DEFAULT_GVFS_SCHEME);
private final ObjectProperty<WebDavUrlScheme> preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
private Consumer<Settings> saveCmd;
@@ -43,21 +51,23 @@ public class Settings {
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings() {
directories.addListener((ListChangeListener.Change<? extends VaultSettings> change) -> this.save());
directories.addListener(this::somethingChanged);
askedForUpdateCheck.addListener(this::somethingChanged);
checkForUpdates.addListener(this::somethingChanged);
startHidden.addListener(this::somethingChanged);
port.addListener(this::somethingChanged);
numTrayNotifications.addListener(this::somethingChanged);
preferredGvfsScheme.addListener(this::somethingChanged);
debugMode.addListener(this::somethingChanged);
preferredVolumeImpl.addListener(this::somethingChanged);
theme.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer<Settings> saveCmd) {
this.saveCmd = saveCmd;
}
private void somethingChanged(ObservableValue<?> observable, Object oldValue, Object newValue) {
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
this.save();
}
@@ -80,6 +90,10 @@ public class Settings {
public BooleanProperty checkForUpdates() {
return checkForUpdates;
}
public BooleanProperty startHidden() {
return startHidden;
}
public IntegerProperty port() {
return port;
@@ -89,7 +103,7 @@ public class Settings {
return numTrayNotifications;
}
public StringProperty preferredGvfsScheme() {
public ObjectProperty<WebDavUrlScheme> preferredGvfsScheme() {
return preferredGvfsScheme;
}
@@ -101,4 +115,7 @@ public class Settings {
return preferredVolumeImpl;
}
public ObjectProperty<UiTheme> theme() {
return theme;
}
}

View File

@@ -29,11 +29,13 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
writeVaultSettingsArray(out, value.getDirectories());
out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
out.name("startHidden").value(value.startHidden().get());
out.name("port").value(value.port().get());
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get());
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
out.name("debugMode").value(value.debugMode().get());
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
out.name("theme").value(value.theme().get().name());
out.endObject();
}
@@ -62,6 +64,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "checkForUpdatesEnabled":
settings.checkForUpdates().set(in.nextBoolean());
break;
case "startHidden":
settings.startHidden().set(in.nextBoolean());
break;
case "port":
settings.port().set(in.nextInt());
break;
@@ -69,7 +74,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
settings.numTrayNotifications().set(in.nextInt());
break;
case "preferredGvfsScheme":
settings.preferredGvfsScheme().set(in.nextString());
settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
break;
case "debugMode":
settings.debugMode().set(in.nextBoolean());
@@ -77,6 +82,9 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "preferredVolumeImpl":
settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
break;
case "theme":
settings.theme().set(parseUiTheme(in.nextString()));
break;
default:
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
@@ -90,12 +98,31 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
try {
return VolumeImpl.valueOf(nioAdapterName);
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
}
}
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
try {
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
return Settings.DEFAULT_GVFS_SCHEME;
}
}
private UiTheme parseUiTheme(String uiThemeName) {
try {
return UiTheme.valueOf(uiThemeName.toUpperCase());
} catch (IllegalArgumentException e) {
LOG.warn("Invalid volume type {}. Defaulting to {}.", uiThemeName, Settings.DEFAULT_THEME);
return Settings.DEFAULT_THEME;
}
}
private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
List<VaultSettings> result = new ArrayList<>();
in.beginArray();

View File

@@ -17,7 +17,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
@@ -38,10 +37,11 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Stream;
@Singleton
public class SettingsProvider implements Provider<Settings> {
public class SettingsProvider implements Supplier<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
private static final long SAVE_DELAY_MS = 1000;

View File

@@ -0,0 +1,18 @@
package org.cryptomator.common.settings;
public enum UiTheme {
LIGHT("Light"),
DARK("Dark");
// CUSTOM("Custom (%s)");
private String displayName;
UiTheme(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -6,6 +6,7 @@
package org.cryptomator.common.settings;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
@@ -23,6 +24,7 @@ import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
/**
@@ -36,6 +38,8 @@ public class VaultSettings {
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = "";
private static final Random RNG = new Random();
private final String id;
private final ObjectProperty<Path> path = new SimpleObjectProperty();
@@ -69,20 +73,9 @@ public class VaultSettings {
}
private static String generateId() {
return asBase64String(nineBytesFrom(UUID.randomUUID()));
}
private static String asBase64String(byte[] bytes) {
byte[] base64Bytes = Base64.getUrlEncoder().encode(bytes);
return new String(base64Bytes, StandardCharsets.US_ASCII);
}
private static byte[] nineBytesFrom(UUID uuid) {
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
uuidBuffer.putLong(uuid.getMostSignificantBits());
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
uuidBuffer.flip();
return uuidBuffer.array();
byte[] randomBytes = new byte[9];
RNG.nextBytes(randomBytes);
return BaseEncoding.base64Url().encode(randomBytes);
}
public static String normalizeMountName(String mountName) {

View File

@@ -0,0 +1,37 @@
package org.cryptomator.common.settings;
import java.util.Arrays;
public enum WebDavUrlScheme {
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
private final String prefix;
private final String displayName;
WebDavUrlScheme(String prefix, String displayName) {this.prefix = prefix;
this.displayName = displayName;
}
public String getPrefix() {
return prefix;
}
public String getDisplayName() {
return displayName;
}
/**
* Finds a WebDavUrlScheme by prefix.
*
* @param prefix Prefix of the WebDavUrlScheme
* @return WebDavUrlScheme with the given <code>prefix</code>.
* @throws IllegalArgumentException if not WebDavUrlScheme with the given <code>prefix</code> was found.
*/
public static WebDavUrlScheme forPrefix(String prefix) throws IllegalArgumentException {
return Arrays.stream(values()) //
.filter(impl -> impl.prefix.equals(prefix)) //
.findAny() //
.orElseThrow(IllegalArgumentException::new);
}
}

View File

@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;

View File

@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import com.google.common.base.Strings;
import org.cryptomator.common.settings.VaultSettings;
@@ -30,6 +30,7 @@ public class DokanyVolume implements Volume {
private final MountFactory mountFactory;
private final WindowsDriveLetters windowsDriveLetters;
private Mount mount;
private Path mountPoint;
@Inject
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, WindowsDriveLetters windowsDriveLetters) {
@@ -45,19 +46,19 @@ public class DokanyVolume implements Volume {
@Override
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException, IOException {
Path mountPath = getMountPoint();
this.mountPoint = determineMountPoint();
String mountName = vaultSettings.mountName().get();
try {
this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME, mountFlags.strip());
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, mountName, FS_TYPE_NAME, mountFlags.strip());
} catch (MountFailedException e) {
if (vaultSettings.getIndividualMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPath);
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
}
throw new VolumeException("Unable to mount Filesystem", e);
}
}
private Path getMountPoint() throws VolumeException, IOException {
private Path determineMountPoint() throws VolumeException, IOException {
Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
if (optionalCustomMountPoint.isPresent()) {
Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
@@ -99,6 +100,11 @@ public class DokanyVolume implements Volume {
mount.close();
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
public static boolean isSupportedStatic() {
return MountFactory.isApplicable();
}

View File

@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import com.google.common.base.Splitter;
import org.apache.commons.lang3.SystemUtils;
@@ -100,8 +100,7 @@ public class FuseVolume implements Volume {
try {
Mounter mounter = FuseMountFactory.getMounter();
EnvironmentVariables envVars = EnvironmentVariables.create() //
.withFlags(splitFlags(mountFlags))
.withMountPoint(mountPoint) //
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
.build();
this.fuseMnt = mounter.mount(root, envVars);
} catch (CommandFailedException e) {
@@ -166,6 +165,11 @@ public class FuseVolume implements Volume {
return FuseVolume.isSupportedStatic();
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
public static boolean isSupportedStatic() {
return (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_LINUX) && FuseMountFactory.isFuseSupported();
}

View File

@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import javax.inject.Scope;
import java.lang.annotation.Documented;

View File

@@ -6,14 +6,16 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import com.google.common.base.Strings;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
@@ -39,36 +41,51 @@ import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.function.Supplier;
@PerVault
public class Vault {
public static final Predicate<Vault> NOT_LOCKED = hasState(State.LOCKED).negate();
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private final VaultSettings vaultSettings;
private final Provider<Volume> volumeProvider;
private final Supplier<String> defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem = new AtomicReference<>();
private final ObjectProperty<State> state = new SimpleObjectProperty<State>(State.LOCKED);
private final StringBinding defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
private final ObjectProperty<VaultState> state;
private final VaultStats stats;
private final StringBinding displayableName;
private final StringBinding displayablePath;
private final BooleanBinding locked;
private final BooleanBinding processing;
private final BooleanBinding unlocked;
private final BooleanBinding needsMigration;
private final ObjectBinding<Path> accessPoint;
private Volume volume;
public enum State {
LOCKED, PROCESSING, UNLOCKED
}
private volatile Volume volume;
@Inject
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags Supplier<String> defaultMountFlags) {
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, VaultStats stats) {
this.vaultSettings = vaultSettings;
this.volumeProvider = volumeProvider;
this.defaultMountFlags = defaultMountFlags;
this.cryptoFileSystem = cryptoFileSystem;
this.state = state;
this.stats = stats;
this.displayableName = Bindings.createStringBinding(this::getDisplayableName, vaultSettings.path());
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
this.processing = Bindings.createBooleanBinding(this::isProcessing, state);
this.unlocked = Bindings.createBooleanBinding(this::isUnlocked, state);
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
this.accessPoint = Bindings.createObjectBinding(this::getAccessPoint, state);
}
// ******************************************************************************
@@ -80,7 +97,7 @@ public class Vault {
}
private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
List<FileSystemFlags> flags = new ArrayList<>();
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
if (vaultSettings.usesReadOnlyMode().get()) {
flags.add(FileSystemFlags.READONLY);
}
@@ -92,40 +109,16 @@ public class Vault {
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
}
public void create(CharSequence passphrase) throws IOException {
if (!isValidVaultDirectory()) {
CryptoFileSystemProvider.initialize(getPath(), MASTERKEY_FILENAME, passphrase);
} else {
throw new FileAlreadyExistsException(getPath().toString());
}
}
public void changePassphrase(CharSequence oldPassphrase, CharSequence newPassphrase) throws IOException, InvalidPassphraseException {
CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase);
}
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
Platform.runLater(() -> state.set(State.PROCESSING));
try {
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
throw new NotDirectoryException("");
}
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
volume = volumeProvider.get();
volume.mount(fs, getMountFlags());
Platform.runLater(() -> {
state.set(State.UNLOCKED);
});
} catch (Exception e) {
Platform.runLater(() -> state.set(State.LOCKED));
throw e;
if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
throw new NotDirectoryException("");
}
CryptoFileSystem fs = getCryptoFileSystem(passphrase);
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags());
}
public synchronized void lock(boolean forced) throws Volume.VolumeException {
Platform.runLater(() -> {
state.set(State.PROCESSING);
});
if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced();
} else {
@@ -139,28 +132,6 @@ public class Vault {
LOG.error("Error closing file system.", e);
}
}
Platform.runLater(() -> {
state.set(State.LOCKED);
});
}
/**
* Ejects any mounted drives and locks this vault. no-op if this vault is currently locked.
*/
public void prepareForShutdown() {
try {
lock(false);
} catch (Volume.VolumeException e) {
if (volume.supportsForcedUnmount()) {
try {
lock(true);
} catch (Volume.VolumeException e1) {
LOG.warn("Failed to force lock vault.", e1);
}
} else {
LOG.warn("Failed to gracefully lock vault.", e);
}
}
}
public void reveal() throws Volume.VolumeException {
@@ -168,21 +139,96 @@ public class Vault {
}
// ******************************************************************************
// Getter/Setter
// *******************************************************************************/
// Observable Properties
// *******************************************************************************
public State getState() {
return state.get();
}
public ReadOnlyObjectProperty<State> stateProperty() {
public ObjectProperty<VaultState> stateProperty() {
return state;
}
public static Predicate<Vault> hasState(State state) {
return vault -> {
return vault.getState() == state;
};
public VaultState getState() {
return state.get();
}
public void setState(VaultState value) {
state.setValue(value);
}
public BooleanBinding lockedProperty() {
return locked;
}
public boolean isLocked() {
return state.get() == VaultState.LOCKED;
}
public BooleanBinding processingProperty() {
return processing;
}
public boolean isProcessing() {
return state.get() == VaultState.PROCESSING;
}
public BooleanBinding unlockedProperty() {
return unlocked;
}
public boolean isUnlocked() {
return state.get() == VaultState.UNLOCKED;
}
public BooleanBinding needsMigrationProperty() {
return needsMigration;
}
public boolean isNeedsMigration() {
return state.get() == VaultState.NEEDS_MIGRATION;
}
public StringBinding displayableNameProperty() {
return displayableName;
}
public String getDisplayableName() {
Path p = vaultSettings.path().get();
return p.getFileName().toString();
}
public ObjectBinding<Path> accessPointProperty() {
return accessPoint;
}
public Path getAccessPoint() {
if (state.get() == VaultState.UNLOCKED) {
assert volume != null;
return volume.getMountPoint().orElse(Path.of(""));
} else {
return Path.of("");
}
}
public StringBinding displayablePathProperty() {
return displayablePath;
}
public String getDisplayablePath() {
Path p = vaultSettings.path().get();
if (p.startsWith(HOME_DIR)) {
Path relativePath = HOME_DIR.relativize(p);
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return p.toString();
}
}
// ******************************************************************************
// Getter/Setter
// *******************************************************************************/
public VaultStats getStats() {
return stats;
}
public Observable[] observables() {
@@ -197,106 +243,31 @@ public class Vault {
return vaultSettings.path().getValue();
}
public Binding<String> displayablePath() {
Path homeDir = Paths.get(SystemUtils.USER_HOME);
return EasyBind.map(vaultSettings.path(), p -> {
if (p.startsWith(homeDir)) {
Path relativePath = homeDir.relativize(p);
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return p.toString();
}
});
}
/**
* @return Directory name without preceeding path components and file extension
*/
public Binding<String> name() {
return EasyBind.map(vaultSettings.path(), Path::getFileName).map(Path::toString);
}
public boolean doesVaultDirectoryExist() {
return Files.isDirectory(getPath());
}
public boolean isValidVaultDirectory() {
return CryptoFileSystemProvider.containsVault(getPath(), MASTERKEY_FILENAME);
}
public long pollBytesRead() {
CryptoFileSystem fs = cryptoFileSystem.get();
if (fs != null) {
return fs.getStats().pollBytesRead();
} else {
return 0l;
}
}
public long pollBytesWritten() {
CryptoFileSystem fs = cryptoFileSystem.get();
if (fs != null) {
return fs.getStats().pollBytesWritten();
} else {
return 0l;
}
}
public String getCustomMountPath() {
return vaultSettings.individualMountPath().getValueSafe();
}
public void setCustomMountPath(String mountPath) {
vaultSettings.individualMountPath().set(mountPath);
}
public String getMountName() {
return vaultSettings.mountName().get();
}
public void setMountName(String mountName) throws IllegalArgumentException {
if (StringUtils.isBlank(mountName)) {
throw new IllegalArgumentException("mount name is empty");
} else {
vaultSettings.mountName().set(VaultSettings.normalizeMountName(mountName));
}
}
public boolean isHavingCustomMountFlags() {
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
}
public String getMountFlags() {
public StringBinding defaultMountFlagsProperty() {
return defaultMountFlags;
}
public String getDefaultMountFlags() {
return defaultMountFlags.get();
}
public String getEffectiveMountFlags() {
String mountFlags = vaultSettings.mountFlags().get();
if (Strings.isNullOrEmpty(mountFlags)) {
return defaultMountFlags.get();
return getDefaultMountFlags();
} else {
return mountFlags;
}
}
public void setMountFlags(String mountFlags) {
public void setCustomMountFlags(String mountFlags) {
vaultSettings.mountFlags().set(mountFlags);
}
public Character getWinDriveLetter() {
if (vaultSettings.winDriveLetter().get() == null) {
return null;
} else {
return vaultSettings.winDriveLetter().get().charAt(0);
}
}
public void setWinDriveLetter(Path root) {
if (root == null) {
vaultSettings.winDriveLetter().set(null);
} else {
char winDriveLetter = root.toString().charAt(0);
vaultSettings.winDriveLetter().set(String.valueOf(winDriveLetter));
}
}
public String getId() {
return vaultSettings.getId();
}

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import dagger.BindsInstance;
import org.cryptomator.common.settings.VaultSettings;
@@ -22,6 +22,9 @@ public interface VaultComponent {
@BindsInstance
Builder vaultSettings(VaultSettings vaultSettings);
@BindsInstance
Builder initialVaultState(VaultState vaultState);
VaultComponent build();
}

View File

@@ -0,0 +1,33 @@
package org.cryptomator.common.vaults;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.VaultSettings;
import java.util.List;
import java.util.stream.Collectors;
/**
* This listener makes sure to reflect any changes to the vault list back to the settings.
*/
class VaultListChangeListener implements ListChangeListener<Vault> {
private final ObservableList<VaultSettings> vaultSettingsList;
public VaultListChangeListener(ObservableList<VaultSettings> vaultSettingsList) {
this.vaultSettingsList = vaultSettingsList;
}
@Override
public void onChanged(Change<? extends Vault> c) {
while(c.next()) {
if (c.wasAdded()) {
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
vaultSettingsList.addAll(c.getFrom(), addedSettings);
} else if (c.wasRemoved()) {
List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).collect(Collectors.toList());
vaultSettingsList.removeAll(removedSettings);
}
}
}
}

View File

@@ -0,0 +1,93 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.common.vaults;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.migration.Migrators;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
@Singleton
public class VaultListManager {
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final VaultComponent.Builder vaultComponentBuilder;
private final ObservableList<Vault> vaultList;
@Inject
public VaultListManager(VaultComponent.Builder vaultComponentBuilder, Settings settings) {
this.vaultComponentBuilder = vaultComponentBuilder;
this.vaultList = FXCollections.observableArrayList(Vault::observables);
addAll(settings.getDirectories());
vaultList.addListener(new VaultListChangeListener(settings.getDirectories()));
}
public ObservableList<Vault> getVaultList() {
return vaultList;
}
public Vault add(Path pathToVault) throws NoSuchFileException {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
throw new NoSuchFileException(pathToVault.toString(), null, "Not a vault directory");
}
Optional<Vault> alreadyExistingVault = get(pathToVault);
if (alreadyExistingVault.isPresent()) {
return alreadyExistingVault.get();
} else {
VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.path().set(pathToVault);
Vault newVault = create(vaultSettings);
vaultList.add(newVault);
return newVault;
}
}
private void addAll(Collection<VaultSettings> vaultSettings) {
Collection<Vault> vaults = vaultSettings.stream().map(this::create).collect(Collectors.toList());
vaultList.addAll(vaults);
}
private Optional<Vault> get(Path vaultPath) {
return vaultList.stream().filter(v -> v.getPath().equals(vaultPath)).findAny();
}
private Vault create(VaultSettings vaultSettings) {
VaultState vaultState = determineVaultState(vaultSettings.path().get());
VaultComponent comp = vaultComponentBuilder.vaultSettings(vaultSettings).initialVaultState(vaultState).build();
return comp.vault();
}
private VaultState determineVaultState(Path pathToVault) {
try {
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.MISSING;
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.NEEDS_MIGRATION;
} else {
return VaultState.LOCKED;
}
} catch (IOException e) {
return VaultState.ERROR;
}
}
}

View File

@@ -3,14 +3,23 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import dagger.Module;
import dagger.Provides;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,13 +27,25 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Supplier;
import java.util.concurrent.atomic.AtomicReference;
@Module
public class VaultModule {
private static final Logger LOG = LoggerFactory.getLogger(VaultModule.class);
@Provides
@PerVault
public AtomicReference<CryptoFileSystem> provideCryptoFileSystemReference() {
return new AtomicReference<>();
}
@Provides
@PerVault
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
return new SimpleObjectProperty<>(initialState);
}
@Provides
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
@@ -44,33 +65,33 @@ public class VaultModule {
@Provides
@PerVault
@DefaultMountFlags
public Supplier<String> provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
return () -> {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
switch (preferredImpl) {
case FUSE:
if (SystemUtils.IS_OS_MAC_OSX) {
return getMacFuseDefaultMountFlags(settings, vaultSettings);
} else if (SystemUtils.IS_OS_LINUX) {
return getLinuxFuseDefaultMountFlags(settings, vaultSettings);
}
case DOKANY:
return getDokanyDefaultMountFlags(settings, vaultSettings);
default:
return "--flags-supported-on-FUSE-or-DOKANY-only";
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
StringProperty mountName = vaultSettings.mountName();
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
return Bindings.createStringBinding(() -> {
VolumeImpl v = preferredVolumeImpl.get();
if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
return getMacFuseDefaultMountFlags(mountName, readOnly);
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
return getLinuxFuseDefaultMountFlags(readOnly);
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
return getDokanyDefaultMountFlags(readOnly);
} else {
return "--flags-supported-on-FUSE-or-DOKANY-only";
}
};
}, mountName, readOnly, preferredVolumeImpl);
}
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
private String getMacFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
private String getMacFuseDefaultMountFlags(ReadOnlyStringProperty mountName, ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_MAC_OSX;
StringBuilder flags = new StringBuilder();
if (vaultSettings.usesReadOnlyMode().get()) {
if (readOnly.get()) {
flags.append(" -ordonly");
}
flags.append(" -ovolname=").append(vaultSettings.mountName().get());
flags.append(" -ovolname=").append(mountName.get());
flags.append(" -oatomic_o_trunc");
flags.append(" -oauto_xattr");
flags.append(" -oauto_cache");
@@ -92,11 +113,10 @@ public class VaultModule {
}
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
private String getLinuxFuseDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_LINUX;
StringBuilder flags = new StringBuilder();
if (vaultSettings.usesReadOnlyMode().get()) {
if (readOnly.get()) {
flags.append(" -oro");
}
flags.append(" -oauto_unmount");
@@ -115,12 +135,11 @@ public class VaultModule {
}
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
private String getDokanyDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
assert SystemUtils.IS_OS_WINDOWS;
StringBuilder flags = new StringBuilder();
flags.append(" --options CURRENT_SESSION");
if (vaultSettings.usesReadOnlyMode().get()) {
if (readOnly.get()) {
flags.append(",WRITE_PROTECTION");
}
flags.append(" --thread-count 5");

View File

@@ -0,0 +1,34 @@
package org.cryptomator.common.vaults;
public enum VaultState {
/**
* No vault found at the provided path
*/
MISSING,
/**
* Vault requires migration to a newer vault format
*/
NEEDS_MIGRATION,
/**
* Vault ready to be unlocked
*/
LOCKED,
/**
* Vault in transition between two other states
*/
PROCESSING,
/**
* Vault is unlocked
*/
UNLOCKED,
/**
* Unknown state due to preceeding unrecoverable exceptions.
*/
ERROR;
}

View File

@@ -0,0 +1,100 @@
package org.cryptomator.common.vaults;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.util.Duration;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@PerVault
public class VaultStats {
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
private final AtomicReference<CryptoFileSystem> fs;
private final ObjectProperty<VaultState> state;
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
@Inject
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
this.fs = fs;
this.state = state;
this.updateService = new UpdateStatsService();
updateService.setExecutor(executor);
updateService.setPeriod(Duration.seconds(1));
state.addListener(this::vaultStateChanged);
}
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
switch (state.get()) {
case UNLOCKED:
assert fs.get() != null;
LOG.debug("start recording stats");
updateService.restart();
break;
default:
LOG.debug("stop recording stats");
updateService.cancel();
break;
}
}
private void updateStats(Optional<CryptoFileSystemStats> stats) {
assert Platform.isFxApplicationThread();
bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0l));
bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0l));
}
private class UpdateStatsService extends ScheduledService<Optional<CryptoFileSystemStats>> {
@Override
protected Task<Optional<CryptoFileSystemStats>> createTask() {
return new Task<>() {
@Override
protected Optional<CryptoFileSystemStats> call() {
return Optional.ofNullable(fs.get()).map(CryptoFileSystem::getStats);
}
};
}
@Override
protected void succeeded() {
assert getValue() != null;
updateStats(getValue());
super.succeeded();
}
}
/* Observables */
public LongProperty bytesPerSecondReadProperty() {
return bytesPerSecondRead;
}
public long getBytesPerSecondRead() {
return bytesPerSecondRead.get();
}
public LongProperty bytesPerSecondWrittenProperty() {
return bytesPerSecondWritten;
}
public long getBytesPerSecondWritten() {
return bytesPerSecondWritten.get();
}
}

View File

@@ -1,9 +1,11 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;
/**
@@ -28,6 +30,8 @@ public interface Volume {
void unmount() throws VolumeException;
Optional<Path> getMountPoint();
// optional forced unmounting:
default boolean supportsForcedUnmount() {

View File

@@ -1,4 +1,4 @@
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import org.cryptomator.common.settings.Settings;
@@ -13,6 +13,8 @@ import javax.inject.Inject;
import javax.inject.Provider;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.Optional;
public class WebDavVolume implements Volume {
@@ -25,6 +27,7 @@ public class WebDavVolume implements Volume {
private WebDavServer server;
private WebDavServletController servlet;
private Mounter.Mount mount;
private Path mountPoint;
@Inject
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings) {
@@ -52,7 +55,7 @@ public class WebDavVolume implements Volume {
}
MountParams mountParams = MountParams.create() //
.withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) //
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get())//
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
.withWebdavHostname(getLocalhostAliasOrNull()) //
.build();
try {
@@ -93,6 +96,11 @@ public class WebDavVolume implements Volume {
cleanup();
}
@Override
public Optional<Path> getMountPoint() {
return Optional.ofNullable(mountPoint);
}
private String getLocalhostAliasOrNull() {
try {
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);

View File

@@ -3,14 +3,14 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.vaults;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Set;
@@ -19,7 +19,7 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
@FxApplicationScoped
@Singleton
public final class WindowsDriveLetters {
private static final Logger LOG = LoggerFactory.getLogger(WindowsDriveLetters.class);

View File

@@ -30,7 +30,7 @@ public class SettingsJsonAdapterTest {
Assertions.assertEquals(2, settings.getDirectories().size());
Assertions.assertEquals(8080, settings.port().get());
Assertions.assertEquals(42, settings.numTrayNotifications().get());
Assertions.assertEquals("dav", settings.preferredGvfsScheme().get());
Assertions.assertEquals(WebDavUrlScheme.DAV, settings.preferredGvfsScheme().get());
Assertions.assertEquals(VolumeImpl.FUSE, settings.preferredVolumeImpl().get());
}

View File

@@ -8,22 +8,20 @@ package org.cryptomator.common.settings;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.util.function.Consumer;
public class SettingsTest {
@Test
public void testAutoSave() throws IOException {
@SuppressWarnings("unchecked")
Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
public void testAutoSave() {
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
Settings settings = new Settings();
settings.setSaveCmd(changeListener);
VaultSettings vaultSettings = VaultSettings.withRandomId();
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
// first change (to property):
settings.preferredGvfsScheme().set("asd");
settings.preferredGvfsScheme().set(WebDavUrlScheme.WEBDAV);
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
// second change (to list):

View File

@@ -0,0 +1,70 @@
package org.cryptomator.common.vaults;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
import java.nio.file.Path;
public class VaultModuleTest {
private final Settings settings = Mockito.mock(Settings.class);
private final VaultSettings vaultSettings = Mockito.mock(VaultSettings.class);
private final VaultModule module = new VaultModule();
@BeforeEach
public void setup(@TempDir Path tmpDir) {
Mockito.when(vaultSettings.mountName()).thenReturn(new SimpleStringProperty("TEST"));
Mockito.when(vaultSettings.usesReadOnlyMode()).thenReturn(new SimpleBooleanProperty(true));
System.setProperty("user.home", tmpDir.toString());
}
@Test
@DisplayName("provideDefaultMountFlags on Mac/FUSE")
@EnabledOnOs(OS.MAC)
public void testMacFuseDefaultMountFlags() {
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE));
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=TEST"));
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly"));
}
@Test
@DisplayName("provideDefaultMountFlags on Linux/FUSE")
@EnabledOnOs(OS.LINUX)
public void testLinuxFuseDefaultMountFlags() {
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.FUSE));
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-oro"));
}
@Test
@DisplayName("provideDefaultMountFlags on Windows/Dokany")
@EnabledOnOs(OS.WINDOWS)
public void testWinDokanyDefaultMountFlags() {
Mockito.when(settings.preferredVolumeImpl()).thenReturn(new SimpleObjectProperty<>(VolumeImpl.DOKANY));
StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings);
MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("--options CURRENT_SESSION,WRITE_PROTECTION"));
}
}

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.16</version>
<version>1.5.0-alpha1</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>
@@ -23,11 +23,7 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<!-- Google -->
<dependency>
<groupId>com.google.guava</groupId>

View File

@@ -5,30 +5,18 @@
*******************************************************************************/
package org.cryptomator.keychain;
import java.util.Optional;
import java.util.Set;
import com.google.common.collect.Sets;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
import org.cryptomator.jni.JniFunctions;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.WinFunctions;
import org.cryptomator.common.JniModule;
@Module
import java.util.Optional;
import java.util.Set;
@Module(includes = {JniModule.class})
public class KeychainModule {
@Provides
Optional<MacFunctions> provideOptionalMacFunctions() {
return JniFunctions.macFunctions();
}
@Provides
Optional<WinFunctions> provideOptionalWinFunctions() {
return JniFunctions.winFunctions();
}
@Provides
@ElementsIntoSet
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain, LinuxSecretServiceKeychainAccess linKeychain) {

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.16</version>
<version>1.5.0-alpha1</version>
</parent>
<artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name>

View File

@@ -11,30 +11,38 @@ 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);
static final ConcurrentMap<Runnable, Boolean> SHUTDOWN_TASKS = new ConcurrentHashMap<>();
private final ConcurrentMap<Runnable, Boolean> tasks = new ConcurrentHashMap<>();
@Inject
CleanShutdownPerformer() {
super(null, null, "ShutdownTasks", 0);
}
@Override
public void run() {
LOG.debug("Running graceful shutdown tasks...");
SHUTDOWN_TASKS.keySet().forEach(r -> {
tasks.keySet().forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("Exception while shutting down.", e);
}
});
SHUTDOWN_TASKS.clear();
LOG.info("Goodbye.");
tasks.clear();
}
static void scheduleShutdownTask(Runnable task) {
SHUTDOWN_TASKS.put(task, Boolean.TRUE);
void scheduleShutdownTask(Runnable task) {
tasks.put(task, Boolean.TRUE);
}
static void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new CleanShutdownPerformer());
void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(this);
}
}

View File

@@ -5,12 +5,10 @@
*******************************************************************************/
package org.cryptomator.launcher;
import javafx.application.Application;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.logging.LoggerConfiguration;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.launcher.UiLauncher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,6 +17,7 @@ import javax.inject.Named;
import javax.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
@Singleton
public class Cryptomator {
@@ -32,13 +31,19 @@ public class Cryptomator {
private final DebugMode debugMode;
private final IpcFactory ipcFactory;
private final Optional<String> applicationVersion;
private final CountDownLatch shutdownLatch;
private final CleanShutdownPerformer shutdownPerformer;
private final UiLauncher uiLauncher;
@Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion) {
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, CleanShutdownPerformer shutdownPerformer, UiLauncher uiLauncher) {
this.logConfig = logConfig;
this.debugMode = debugMode;
this.ipcFactory = ipcFactory;
this.applicationVersion = applicationVersion;
this.shutdownLatch = shutdownLatch;
this.shutdownPerformer = shutdownPerformer;
this.uiLauncher = uiLauncher;
}
public static void main(String[] args) {
@@ -48,6 +53,7 @@ public class Cryptomator {
/**
* Main entry point of the application launcher.
*
* @param args The arguments passed to this program via {@link #main(String[])}.
* @return Nonzero exit code in case of an error.
*/
@@ -63,6 +69,7 @@ public class Cryptomator {
try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
if (endpoint.isConnectedToRemote()) {
endpoint.getRemote().revealRunningApp();
LOG.info("Found running application instance. Shutting down...");
return 2;
} else {
@@ -77,45 +84,22 @@ public class Cryptomator {
/**
* Launches the JavaFX application and waits until shutdown is requested.
*
* @return Nonzero exit code in case of an error.
* @implNote This method blocks until {@link #shutdownLatch} reached zero.
*/
private int runGuiApplication() {
try {
CleanShutdownPerformer.registerShutdownHook();
Application.launch(MainApp.class);
LOG.info("Shutting down...");
shutdownPerformer.registerShutdownHook();
uiLauncher.launch();
shutdownLatch.await();
LOG.info("UI shut down");
return 0;
} catch (Throwable e) {
LOG.error("Terminating due to error", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return 1;
}
}
// We need a separate FX Application class, until we can use the module system. See https://stackoverflow.com/q/54756176/4014509
public static class MainApp extends Application {
@Override
public void start(Stage primaryStage) {
LOG.info("JavaFX application started.");
primaryStage.setMinWidth(652.0);
primaryStage.setMinHeight(440.0);
FxApplicationComponent fxApplicationComponent = CRYPTOMATOR_COMPONENT.fxApplicationComponent() //
.fxApplication(this) //
.mainWindow(primaryStage) //
.build();
MainController mainCtrl = fxApplicationComponent.fxmlLoader().load("/fxml/main.fxml");
mainCtrl.initStage(primaryStage);
primaryStage.show();
}
@Override
public void stop() {
LOG.info("JavaFX application stopped.");
}
}
}

View File

@@ -3,15 +3,14 @@ package org.cryptomator.launcher;
import dagger.Component;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.logging.LoggerModule;
import org.cryptomator.ui.launcher.UiLauncherModule;
import javax.inject.Singleton;
@Singleton
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class, UiLauncherModule.class})
public interface CryptomatorComponent {
Cryptomator application();
FxApplicationComponent.Builder fxApplicationComponent();
}

View File

@@ -2,31 +2,28 @@ package org.cryptomator.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.ui.model.AppLaunchEvent;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
@Module
class CryptomatorModule {
@Provides
@Singleton
static Settings provideSettings(SettingsProvider settingsProvider) {
return settingsProvider.get();
@Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler(CleanShutdownPerformer shutdownPerformer) {
return shutdownPerformer::scheduleShutdownTask;
}
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10);
@Named("shutdownLatch")
static CountDownLatch provideShutdownLatch() {
return new CountDownLatch(1);
}
@Provides

View File

@@ -6,7 +6,7 @@
*******************************************************************************/
package org.cryptomator.launcher;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,16 +34,14 @@ class FileOpenRequestHandler {
@Inject
public FileOpenRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.launchEventQueue = launchEventQueue;
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) {
Desktop.getDesktop().setOpenFileHandler(this::openFiles);
} catch (UnsupportedOperationException e) {
LOG.info("Unable to setOpenFileHandler, probably not supported on this OS.");
}
}
private void openFiles(final OpenFilesEvent evt) {
private void openFiles(OpenFilesEvent evt) {
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath);
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
@@ -61,7 +59,7 @@ class FileOpenRequestHandler {
return null;
}
}).filter(Objects::nonNull);
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}

View File

@@ -1,26 +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 dagger.Module;
import dagger.Provides;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.ui.UiModule;
import javax.inject.Named;
import java.util.function.Consumer;
@Module(includes = {UiModule.class})
class FxApplicationModule {
@Provides
@FxApplicationScoped
@Named("shutdownTaskScheduler")
Consumer<Runnable> provideShutdownTaskScheduler() {
return CleanShutdownPerformer::scheduleShutdownTask;
}
}

View File

@@ -10,6 +10,8 @@ import java.rmi.RemoteException;
interface IpcProtocol extends Remote {
void handleLaunchArgs(String[] args) throws RemoteException;
void revealRunningApp() throws RemoteException;
void handleLaunchArgs(String... args) throws RemoteException;
}

View File

@@ -1,11 +1,15 @@
package org.cryptomator.launcher;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream;
@Singleton
class IpcProtocolImpl implements IpcProtocol {
@@ -13,15 +17,22 @@ class IpcProtocolImpl implements IpcProtocol {
private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
private final FileOpenRequestHandler fileOpenRequestHandler;
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler) {
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
this.launchEventQueue = launchEventQueue;
}
@Override
public void handleLaunchArgs(String[] args) {
LOG.info("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
public void revealRunningApp() {
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Stream.empty()));
}
@Override
public void handleLaunchArgs(String... args) {
LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
fileOpenRequestHandler.handleLaunchArgs(args);
}

View File

@@ -5,7 +5,7 @@
*******************************************************************************/
package org.cryptomator.launcher;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
@@ -64,7 +64,7 @@ public class FileOpenRequestHandlerTest {
@Test
@DisplayName("./cryptomator.exe foo (with full event queue)")
public void testOpenArgsWithFullQueue() throws IOException {
queue.add(new AppLaunchEvent(Stream.empty()));
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Stream.empty()));
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
inTest.handleLaunchArgs(new String[]{"foo"});

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.16</version>
<version>1.5.0-alpha1</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -24,9 +24,9 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependency versions -->
<cryptomator.cryptolib.version>1.2.1</cryptomator.cryptolib.version>
<cryptomator.cryptofs.version>1.8.8</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.0.0</cryptomator.jni.version>
<cryptomator.cryptolib.version>1.2.2</cryptomator.cryptolib.version>
<cryptomator.cryptofs.version>1.9.0-beta1</cryptomator.cryptofs.version>
<cryptomator.jni.version>2.2.1</cryptomator.jni.version>
<cryptomator.fuse.version>1.2.0</cryptomator.fuse.version>
<cryptomator.dokany.version>1.1.11</cryptomator.dokany.version>
<cryptomator.webdav.version>1.0.10</cryptomator.webdav.version>
@@ -51,16 +51,6 @@
</properties>
<repositories>
<repository>
<id>ossrh-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>jcenter</id>
<url>http://jcenter.bintray.com</url>
@@ -129,6 +119,11 @@
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.4.16</version>
<version>1.5.0-alpha1</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -18,29 +18,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>commons</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>fuse-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>dokany-nio-adapter</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>webdav-nio-adapter</artifactId>
</dependency>
<!-- CryptoLib -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
@@ -72,7 +53,7 @@
<artifactId>gson</artifactId>
</dependency>
<!-- apache commons -->
<!-- Apache Commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
@@ -92,7 +73,7 @@
<dependency>
<groupId>com.nulab-inc</groupId>
<artifactId>zxcvbn</artifactId>
<version>1.2.2</version>
<version>1.2.7</version>
</dependency>
<!-- Logging -->

View File

@@ -1,216 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - implementation of github issue #56
*******************************************************************************/
package org.cryptomator.ui;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.l10n.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.swing.SwingUtilities;
import java.awt.AWTException;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@FxApplicationScoped
public class ExitUtil {
private static final Logger LOG = LoggerFactory.getLogger(ExitUtil.class);
private final Stage mainWindow;
private final Localization localization;
private final Settings settings;
private final Optional<MacFunctions> macFunctions;
private TrayIcon trayIcon;
@Inject
public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, Optional<MacFunctions> macFunctions) {
this.mainWindow = mainWindow;
this.localization = localization;
this.settings = settings;
this.macFunctions = macFunctions;
}
public void initExitHandler(Runnable exitCommand) {
if (SystemUtils.IS_OS_LINUX) {
initMinimizeExitHandler(exitCommand);
} else {
initTrayIconExitHandler(exitCommand);
}
}
private void initMinimizeExitHandler(Runnable exitCommand) {
mainWindow.setOnCloseRequest(e -> {
if (Platform.isImplicitExit()) {
exitCommand.run();
} else {
mainWindow.setIconified(true);
e.consume();
}
});
}
private void initTrayIconExitHandler(Runnable exitCommand) {
trayIcon = createTrayIcon(exitCommand);
try {
// double clicking tray icon should open Cryptomator
if (SystemUtils.IS_OS_WINDOWS) {
trayIcon.addMouseListener(new TrayIconMouseListener());
}
SystemTray.getSystemTray().add(trayIcon);
mainWindow.setOnCloseRequest((e) -> {
if (Platform.isImplicitExit()) {
exitCommand.run();
} else {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication));
mainWindow.close();
this.showTrayNotification(trayIcon);
}
});
} catch (SecurityException | AWTException ex) {
// not working? then just go ahead and close the app
mainWindow.setOnCloseRequest((ev) -> {
exitCommand.run();
});
}
}
private TrayIcon createTrayIcon(Runnable exitCommand) {
final PopupMenu popup = new PopupMenu();
final MenuItem showItem = new MenuItem(localization.getString("tray.menu.open"));
showItem.addActionListener(this::restoreFromTray);
popup.add(showItem);
final MenuItem exitItem = new MenuItem(localization.getString("tray.menu.quit"));
exitItem.addActionListener(e -> exitCommand.run());
popup.add(exitItem);
final Image image = getAppropriateTrayIconImage(true);
return new TrayIcon(image, localization.getString("app.name"), popup);
}
/**
* @return true if <code>defaults read -g AppleInterfaceStyle</code> has an exit status of <code>0</code> (i.e. _not_ returning "key not found").
*/
private boolean isMacMenuBarDarkMode() {
try {
// check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents..
final Process proc = Runtime.getRuntime().exec(new String[] {"defaults", "read", "-g", "AppleInterfaceStyle"});
proc.waitFor(100, TimeUnit.MILLISECONDS);
return proc.exitValue() == 0;
} catch (IOException | InterruptedException | IllegalThreadStateException ex) {
// IllegalThreadStateException thrown by proc.exitValue(), if process didn't terminate
LOG.warn("Determining MAC OS X dark mode settings failed. Assuming default (light) mode.");
return false;
}
}
private void showTrayNotification(TrayIcon trayIcon) {
int remainingTrayNotification = settings.numTrayNotifications().get();
if (remainingTrayNotification <= 0) {
return;
} else {
settings.numTrayNotifications().set(remainingTrayNotification - 1);
}
final Runnable notificationCmd;
if (SystemUtils.IS_OS_MAC_OSX) {
final String title = localization.getString("tray.infoMsg.title");
final String msg = localization.getString("tray.infoMsg.msg.osx");
final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title);
notificationCmd = () -> {
try {
final ScriptEngineManager mgr = new ScriptEngineManager();
final ScriptEngine engine = mgr.getEngineByName("AppleScriptEngine");
if (engine != null) {
engine.eval(notificationCenterAppleScript);
} else {
Runtime.getRuntime().exec(new String[] {"/usr/bin/osascript", "-e", notificationCenterAppleScript});
}
} catch (ScriptException | IOException e) {
// ignore, user will notice the tray icon anyway.
}
};
} else {
final String title = localization.getString("tray.infoMsg.title");
final String msg = localization.getString("tray.infoMsg.msg");
notificationCmd = () -> {
trayIcon.displayMessage(title, msg, MessageType.INFO);
};
}
SwingUtilities.invokeLater(() -> {
notificationCmd.run();
});
}
private class TrayIconMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
restoreFromTray(new ActionEvent(e.getSource(), e.getID(), e.paramString()));
}
}
}
private void restoreFromTray(ActionEvent event) {
Platform.runLater(() -> {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication));
mainWindow.show();
mainWindow.requestFocus();
});
}
public void updateTrayIcon(boolean areAllVaultsLocked) {
if (trayIcon != null) {
Image image = getAppropriateTrayIconImage(areAllVaultsLocked);
trayIcon.setImage(image);
}
}
private Image getAppropriateTrayIconImage(boolean areAllVaultsLocked) {
String resourceName;
if (SystemUtils.IS_OS_MAC_OSX && isMacMenuBarDarkMode()) {
resourceName = areAllVaultsLocked ? "/tray_icon_mac_white.png" : "/tray_icon_unlocked_mac_white.png";
} else if (SystemUtils.IS_OS_MAC_OSX) {
resourceName = areAllVaultsLocked ? "/tray_icon_mac_black.png" : "/tray_icon_unlocked_mac_black.png";
} else {
resourceName = areAllVaultsLocked ? "/tray_icon.png" : "/tray_icon_unlocked.png";
}
return Toolkit.getDefaultToolkit().getImage(getClass().getResource(resourceName));
}
}

View File

@@ -1,82 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui;
import dagger.Module;
import dagger.Provides;
import javafx.beans.binding.Binding;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.controllers.ViewControllerModule;
import org.cryptomator.ui.model.VaultComponent;
import org.fxmisc.easybind.EasyBind;
import javax.inject.Named;
import java.net.InetSocketAddress;
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(includes = {ViewControllerModule.class, KeychainModule.class}, subcomponents = {VaultComponent.class})
public class UiModule {
private static final int NUM_SCHEDULER_THREADS = 4;
@Provides
@FxApplicationScoped
ScheduledExecutorService provideScheduledExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
Thread t = new Thread(r);
t.setName("Scheduler Thread " + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
});
shutdownTaskScheduler.accept(executorService::shutdown);
return executorService;
}
@Provides
@FxApplicationScoped
ExecutorService provideExecutorService(@Named("shutdownTaskScheduler") Consumer<Runnable> shutdownTaskScheduler) {
final AtomicInteger threadNumber = new AtomicInteger(1);
ExecutorService executorService = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setName("Background Thread " + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
});
shutdownTaskScheduler.accept(executorService::shutdown);
return executorService;
}
@Provides
@FxApplicationScoped
Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
return EasyBind.map(settings.port(), (Number port) -> {
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
return InetSocketAddress.createUnresolved(host, port.intValue());
});
}
@Provides
@FxApplicationScoped
WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
WebDavServer server = WebDavServer.create();
// no need to unsubscribe eventually, because server is a singleton
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
return server;
}
}

View File

@@ -0,0 +1,182 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoSet;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FXMLLoaderFactory;
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.mainwindow.MainWindow;
import javax.inject.Named;
import javax.inject.Provider;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
@Module
public abstract class AddVaultModule {
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @AddVaultWizardWindow Lazy<Map<KeyCodeCombination, Runnable>> accelerators) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.sceneProperty().addListener(observable -> {
stage.getScene().getAccelerators().putAll(accelerators.get());
});
windowIcon.ifPresent(stage.getIcons()::add);
return stage;
}
@Provides
@AddVaultWizardScoped
static ObjectProperty<Path> provideVaultPath() {
return new SimpleObjectProperty<>();
}
@Provides
@AddVaultWizardScoped
static StringProperty provideVaultName() {
return new SimpleStringProperty("");
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static ObjectProperty<Vault> provideVault() {
return new SimpleObjectProperty<>();
}
// ------------------
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
static Map<KeyCodeCombination, Runnable> provideDefaultAccellerators(@AddVaultWizardWindow Set<Map.Entry<KeyCombination, Runnable>> accelerators) {
return Map.ofEntries(accelerators.toArray(Map.Entry[]::new));
}
@Provides
@IntoSet
@AddVaultWizardWindow
static Map.Entry<KeyCombination, Runnable> provideCloseWindowShortcut(@AddVaultWizardWindow Stage window) {
if (SystemUtils.IS_OS_WINDOWS) {
return Map.entry(new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN), window::close);
} else {
return Map.entry(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN), window::close);
}
}
// ------------------
@Provides
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
@AddVaultWizardScoped
static Scene provideWelcomeScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
Scene scene = fxmlLoaders.createScene("/fxml/addvault_welcome.fxml");
KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
scene.getAccelerators().put(cmdW, window::close);
return scene;
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_EXISTING)
@AddVaultWizardScoped
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_existing.fxml");
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
@AddVaultWizardScoped
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_new_name.fxml");
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
@AddVaultWizardScoped
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_new_location.fxml");
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
@AddVaultWizardScoped
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_new_password.fxml");
}
@Provides
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
@AddVaultWizardScoped
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FXMLLoaderFactory fxmlLoaders, @AddVaultWizardWindow Stage window) {
return fxmlLoaders.createScene("/fxml/addvault_success.fxml");
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(AddVaultWelcomeController.class)
abstract FxController bindWelcomeController(AddVaultWelcomeController controller);
@Binds
@IntoMap
@FxControllerKey(ChooseExistingVaultController.class)
abstract FxController bindChooseExistingVaultController(ChooseExistingVaultController controller);
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultNameController.class)
abstract FxController bindCreateNewVaultNameController(CreateNewVaultNameController controller);
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultLocationController.class)
abstract FxController bindCreateNewVaultLocationController(CreateNewVaultLocationController controller);
@Binds
@IntoMap
@FxControllerKey(CreateNewVaultPasswordController.class)
abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller);
@Binds
@IntoMap
@FxControllerKey(AddVaultSuccessController.class)
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller);
}

View File

@@ -0,0 +1,47 @@
package org.cryptomator.ui.addvaultwizard;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import javax.inject.Inject;
@AddVaultWizardScoped
public class AddVaultSuccessController implements FxController {
private final FxApplication fxApplication;
private final Stage window;
private final ReadOnlyObjectProperty<Vault> vault;
@Inject
AddVaultSuccessController(FxApplication fxApplication, @AddVaultWizardWindow Stage window, @AddVaultWizardWindow ObjectProperty<Vault> vault) {
this.fxApplication = fxApplication;
this.window = window;
this.vault = vault;
}
@FXML
public void unlockAndClose() {
close();
fxApplication.showUnlockWindow(vault.get());
}
@FXML
public void close() {
window.close();
}
/* Observables */
public ReadOnlyObjectProperty<Vault> vaultProperty() {
return vault;
}
public Vault getVault() {
return vault.get();
}
}

View File

@@ -0,0 +1,38 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
@AddVaultWizardScoped
public class AddVaultWelcomeController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(AddVaultWelcomeController.class);
private final Stage window;
private final Lazy<Scene> chooseExistingVaultScene;
private final Lazy<Scene> createNewVaultScene;
@Inject
AddVaultWelcomeController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> chooseExistingVaultScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> createNewVaultScene) {
this.window = window;
this.chooseExistingVaultScene = chooseExistingVaultScene;
this.createNewVaultScene = createNewVaultScene;
}
public void createNewVault() {
LOG.debug("AddVaultWelcomeController.createNewVault()");
window.setScene(createNewVaultScene.get());
}
public void chooseExistingVault() {
LOG.debug("AddVaultWelcomeController.chooseExistingVault()");
window.setScene(chooseExistingVaultScene.get());
}
}

View File

@@ -0,0 +1,38 @@
/*******************************************************************************
* 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.ui.addvaultwizard;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
@AddVaultWizardScoped
@Subcomponent(modules = {AddVaultModule.class})
public interface AddVaultWizardComponent {
@AddVaultWizardWindow
Stage window();
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
Lazy<Scene> scene();
default void showAddVaultWizard() {
Stage stage = window();
stage.setScene(scene().get());
stage.sizeToScene();
stage.show();
}
@Subcomponent.Builder
interface Builder {
AddVaultWizardComponent build();
}
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.addvaultwizard;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface AddVaultWizardScoped {
}

View File

@@ -0,0 +1,14 @@
package org.cryptomator.ui.addvaultwizard;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface AddVaultWizardWindow {
}

View File

@@ -0,0 +1,72 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.File;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ResourceBundle;
@AddVaultWizardScoped
public class ChooseExistingVaultController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ChooseExistingVaultController.class);
private final Stage window;
private final Lazy<Scene> welcomeScene;
private final Lazy<Scene> successScene;
private final ObjectProperty<Path> vaultPath;
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
@Inject
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.successScene = successScene;
this.vaultPath = vaultPath;
this.vault = vault;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
}
@FXML
public void back() {
window.setScene(welcomeScene.get());
}
@FXML
public void chooseFileAndNext() {
//TODO: error handling & cannot unlock added vault
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File file = fileChooser.showOpenDialog(window);
if (file != null) {
vaultPath.setValue(file.toPath().toAbsolutePath().getParent());
try {
Vault newVault = vaultListManager.add(vaultPath.get());
vault.set(newVault);
window.setScene(successScene.get());
} catch (NoSuchFileException e) {
LOG.error("Nope", e);
// TODO
}
}
}
}

View File

@@ -0,0 +1,186 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;
@AddVaultWizardScoped
public class CreateNewVaultLocationController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class);
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home"));
private final Stage window;
private final Lazy<Scene> chooseNameScene;
private final Lazy<Scene> choosePasswordScene;
private final LocationPresets locationPresets;
private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName;
private final ResourceBundle resourceBundle;
private final BooleanBinding validVaultPath;
private final BooleanBinding invalidVaultPath;
private final BooleanProperty usePresetPath;
private final StringProperty warningText;
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
public ToggleGroup predefinedLocationToggler;
public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton;
public RadioButton customRadioButton;
@Inject
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window;
this.chooseNameScene = chooseNameScene;
this.choosePasswordScene = choosePasswordScene;
this.locationPresets = locationPresets;
this.vaultPath = vaultPath;
this.vaultName = vaultName;
this.resourceBundle = resourceBundle;
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
this.invalidVaultPath = validVaultPath.not();
this.usePresetPath = new SimpleBooleanProperty();
this.warningText = new SimpleStringProperty();
}
private boolean isValidVaultPath() {
return vaultPath.get() != null && Files.notExists(vaultPath.get());
}
@FXML
public void initialize() {
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
vaultPath.addListener(this::vaultPathDidChange);
}
private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
if (!Files.notExists(newValue)) {
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} else {
warningText.set(null);
}
}
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
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 (customRadioButton.equals(newValue)) {
vaultPath.set(customVaultPath.resolve(vaultName.get()));
}
}
@FXML
public void back() {
window.setScene(chooseNameScene.get());
}
@FXML
public void next() {
try {
// check if we have write access AND the vaultPath doesn't already exist:
assert Files.isDirectory(vaultPath.get().getParent());
Path createdDir = Files.createDirectory(vaultPath.get());
Files.delete(createdDir); // assert: dir exists and is empty
window.setScene(choosePasswordScene.get());
} catch (FileAlreadyExistsException e) {
LOG.warn("Can not use already existing vault path: {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} catch (NoSuchFileException | DirectoryNotEmptyException e) {
LOG.error("Failed to delete recently created directory.", e);
// TODO show generic error text for unexpected exception
} catch (IOException e) {
LOG.warn("Can not create vault at path: {}", vaultPath.get());
// TODO show generic error text for unexpected exception
}
}
@FXML
public void chooseCustomVaultPath() {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle"));
directoryChooser.setInitialDirectory(customVaultPath.toFile());
final File file = directoryChooser.showDialog(window);
if (file != null) {
customVaultPath = file.toPath().toAbsolutePath();
vaultPath.set(customVaultPath.resolve(vaultName.get()));
}
}
/* Getter/Setter */
public Path getVaultPath() {
return vaultPath.get();
}
public ObjectProperty<Path> vaultPathProperty() {
return vaultPath;
}
public BooleanBinding invalidVaultPathProperty() {
return invalidVaultPath;
}
public Boolean getInvalidVaultPath() {
return invalidVaultPath.get();
}
public LocationPresets getLocationPresets() {
return locationPresets;
}
public BooleanProperty usePresetPathProperty() {
return usePresetPath;
}
public boolean getUsePresetPath() {
return usePresetPath.get();
}
public StringProperty warningTextProperty() {
return warningText;
}
public String getWarningText() {
return warningText.get();
}
public BooleanBinding showWarningProperty() {
return warningText.isNotEmpty();
}
public boolean isShowWarning() {
return showWarningProperty().get();
}
}

View File

@@ -0,0 +1,105 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
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;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.regex.Pattern;
@AddVaultWizardScoped
public class CreateNewVaultNameController implements FxController {
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[\\w -]+", Pattern.UNICODE_CHARACTER_CLASS);
public TextField textField;
private final Stage window;
private final Lazy<Scene> welcomeScene;
private final Lazy<Scene> chooseLocationScene;
private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName;
private final BooleanBinding validVaultName;
private final BooleanBinding invalidVaultName;
private final StringBinding warningText;
@Inject
CreateNewVaultNameController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, ObjectProperty<Path> vaultPath, StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.chooseLocationScene = chooseLocationScene;
this.vaultPath = vaultPath;
this.vaultName = vaultName;
this.validVaultName = Bindings.createBooleanBinding(this::isValidVaultName, vaultName);
this.invalidVaultName = validVaultName.not();
this.warningText = Bindings.when(vaultName.isNotEmpty().and(invalidVaultName)).then(resourceBundle.getString("addvaultwizard.new.invalidName")).otherwise((String) null);
}
@FXML
public void initialize() {
vaultName.bind(textField.textProperty());
vaultName.addListener(this::vaultNameChanged);
}
public boolean isValidVaultName() {
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
}
private void vaultNameChanged(@SuppressWarnings("unused") Observable observable) {
if (isValidVaultName()) {
if (vaultPath.get() != null) {
// update vaultPath if it is already set but the user went back to change its name:
vaultPath.set(vaultPath.get().resolveSibling(vaultName.get()));
}
}
}
@FXML
public void back() {
window.setScene(welcomeScene.get());
}
@FXML
public void next() {
window.setScene(chooseLocationScene.get());
}
/* Getter/Setter */
public BooleanBinding invalidVaultNameProperty() {
return invalidVaultName;
}
public boolean isInvalidVaultName() {
return invalidVaultName.get();
}
public StringBinding warningTextProperty() {
return warningText;
}
public String getWarningText() {
return warningText.get();
}
public BooleanBinding showWarningProperty() {
return warningText.isNotEmpty();
}
public boolean isShowWarning() {
return showWarningProperty().get();
}
}

View File

@@ -0,0 +1,217 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
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.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
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.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.channels.WritableByteChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import static java.nio.charset.StandardCharsets.US_ASCII;
@AddVaultWizardScoped
public class CreateNewVaultPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final Stage window;
private final Lazy<Scene> chooseLocationScene;
private final Lazy<Scene> successScene;
private final ExecutorService executor;
private final StringProperty vaultName;
private final ObjectProperty<Path> vaultPath;
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final ReadmeGenerator readmeGenerator;
private final IntegerProperty passwordStrength;
private final BooleanProperty processing;
private final BooleanProperty readyToCreateVault;
private final ObjectBinding<ContentDisplay> createVaultButtonState;
public NiceSecurePasswordField passwordField;
public NiceSecurePasswordField reenterField;
public Label passwordStrengthLabel;
public HBox passwordMatchBox;
public FontAwesome5IconView checkmark;
public FontAwesome5IconView cross;
public Label passwordMatchLabel;
public CheckBox finalConfirmationCheckbox;
@Inject
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ExecutorService executor, StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ReadmeGenerator readmeGenerator) {
this.window = window;
this.chooseLocationScene = chooseLocationScene;
this.successScene = successScene;
this.executor = executor;
this.vaultName = vaultName;
this.vaultPath = vaultPath;
this.vault = vault;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.readmeGenerator = readmeGenerator;
this.passwordStrength = new SimpleIntegerProperty(-1);
this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
}
@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(finalConfirmationCheckbox.selectedProperty()).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));
}
@FXML
public void back() {
window.setScene(chooseLocationScene.get());
}
@FXML
public void next() {
Path pathToVault = vaultPath.get();
try {
Files.createDirectory(pathToVault);
} catch (FileAlreadyExistsException e) {
LOG.error("Vault dir already exists.", e);
window.setScene(chooseLocationScene.get());
} catch (IOException e) {
// TODO show generic error screen
LOG.error("", e);
}
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, passwordField.getCharacters());
}).onSuccess(() -> {
initializationSucceeded(pathToVault);
}).onError(IOException.class, e -> {
// TODO show generic error screen
LOG.error("", e);
}).andFinally(() -> {
processing.set(false);
}).runOnce(executor);
}
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withPassphrase(passphrase) //
.withFlags(Collections.emptySet()) //
.withMasterkeyFilename(MASTERKEY_FILENAME) //
.build();
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
}
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
}
LOG.info("Created vault at {}", path);
}
private void initializationSucceeded(Path pathToVault) {
try {
Vault newVault = vaultListManager.add(pathToVault);
vault.set(newVault);
window.setScene(successScene.get());
} catch (NoSuchFileException e) {
throw new UncheckedIOException(e);
}
}
/* Getter/Setter */
public String getVaultName() {
return vaultName.get();
}
public StringProperty vaultNameProperty() {
return vaultName;
}
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
public BooleanProperty readyToCreateVaultProperty() {
return readyToCreateVault;
}
public boolean isReadyToCreateVault() {
return readyToCreateVault.get();
}
public ObjectBinding<ContentDisplay> createVaultButtonStateProperty() {
return createVaultButtonState;
}
public ContentDisplay getCreateVaultButtonState() {
return processing.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
}
}

View File

@@ -0,0 +1,84 @@
package org.cryptomator.ui.addvaultwizard;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javax.inject.Inject;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@AddVaultWizardScoped
public class LocationPresets {
private static final String USER_HOME = System.getProperty("user.home");
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
private final ReadOnlyObjectProperty<Path> dropboxLocation;
private final ReadOnlyObjectProperty<Path> gdriveLocation;
private final BooleanBinding foundDropbox;
private final BooleanBinding foundGdrive;
@Inject
public LocationPresets() {
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
this.foundDropbox = dropboxLocation.isNotNull();
this.foundGdrive = gdriveLocation.isNotNull();
}
private static Path existingWritablePath(String... candidates) {
for (String candidate : candidates) {
Path path = Paths.get(resolveHomePath(candidate));
if (Files.isDirectory(path)) {
return path;
}
}
return null;
}
private static String resolveHomePath(String path) {
if (path.startsWith("~/")) {
return USER_HOME + path.substring(1);
} else {
return path;
}
}
/* Observables */
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
return dropboxLocation;
}
public Path getDropboxLocation() {
return dropboxLocation.get();
}
public BooleanBinding foundDropboxProperty() {
return foundDropbox;
}
public boolean isFoundDropbox() {
return foundDropbox.get();
}
public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
return gdriveLocation;
}
public Path getGdriveLocation() {
return gdriveLocation.get();
}
public BooleanBinding froundGdriveProperty() {
return foundGdrive;
}
public boolean isFoundGdrive() {
return foundGdrive.get();
}
}

View File

@@ -0,0 +1,70 @@
package org.cryptomator.ui.addvaultwizard;
import javax.inject.Inject;
import java.util.List;
import java.util.ResourceBundle;
@AddVaultWizardScoped
public class ReadmeGenerator {
// specs: https://web.archive.org/web/20190708132914/http://www.kleinlercher.at/tools/Windows_Protocols/Word2007RTFSpec9.pdf
private static final String RTF_HEADER = "{\\rtf1\\fbidis\\ansi\\uc0\\fs32\n";
private static final String RTF_FOOTER = "}";
private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://www.google.com/\"}{\\fldrslt google.com}}";
private final ResourceBundle resourceBundle;
@Inject
public ReadmeGenerator(ResourceBundle resourceBundle){
this.resourceBundle = resourceBundle;
}
public String createVaultStorageLocationReadmeRtf() {
return createDocument(List.of( //
resourceBundle.getString("addvault.new.readme.storageLocation.1"), //
resourceBundle.getString("addvault.new.readme.storageLocation.2"), //
resourceBundle.getString("addvault.new.readme.storageLocation.3"), //
String.format(resourceBundle.getString("addvault.new.readme.storageLocation.4"), HELP_URL) //
));
}
public String createVaultAccessLocationReadmeRtf() {
return createDocument(List.of( //
resourceBundle.getString("addvault.new.readme.accessLocation.1"), //
resourceBundle.getString("addvault.new.readme.accessLocation.2"), //
resourceBundle.getString("addvault.new.readme.accessLocation.3") //
));
}
// visible for testing
String createDocument(Iterable<String> paragraphs) {
StringBuilder sb = new StringBuilder(RTF_HEADER);
for (String p : paragraphs) {
sb.append("\\par {\\sa80 ");
appendEscaped(sb, p);
sb.append("}\n");
}
sb.append(RTF_FOOTER);
return sb.toString();
}
// visible for testing
String escapeNonAsciiChars(CharSequence input) {
StringBuilder sb = new StringBuilder();
appendEscaped(sb, input);
return sb.toString();
}
private void appendEscaped(StringBuilder sb, CharSequence input) {
input.chars().forEachOrdered(c -> {
if (c < 128) {
sb.append((char) c);
} else if (c < 0xFFFF) {
sb.append("\\u").append(c);
}
});
}
}

View File

@@ -0,0 +1,42 @@
package org.cryptomator.ui.changepassword;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named;
@ChangePasswordScoped
@Subcomponent(modules = {ChangePasswordModule.class})
public interface ChangePasswordComponent {
@ChangePasswordWindow
Stage window();
@FxmlScene(FxmlFile.CHANGEPASSWORD)
Lazy<Scene> scene();
default void showChangePasswordWindow() {
Stage stage = window();
stage.setScene(scene().get());
stage.show();
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@ChangePasswordWindow Vault vault);
@BindsInstance
Builder owner(@Named("changePasswordOwner") Stage owner);
ChangePasswordComponent build();
}
}

View File

@@ -0,0 +1,113 @@
package org.cryptomator.ui.changepassword;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ResourceBundle;
@ChangePasswordScoped
public class ChangePasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes
private final Stage window;
private final Vault vault;
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final IntegerProperty passwordStrength;
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) {
this.window = window;
this.vault = vault;
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.passwordStrength = new SimpleIntegerProperty(-1);
}
@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));
}
@FXML
public void cancel() {
window.close();
}
@FXML
public void finish() {
try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPasswordField.getCharacters());
LOG.info("Successful changed password for {}", vault.getDisplayableName());
window.close();
} catch (IOException e) {
//TODO
LOG.error("IO error occured during password change. Unable to perform operation.", e);
e.printStackTrace();
} catch (InvalidPassphraseException e) {
//TODO
LOG.info("Wrong old password.");
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public IntegerProperty passwordStrengthProperty() {
return passwordStrength;
}
public int getPasswordStrength() {
return passwordStrength.get();
}
}

View File

@@ -0,0 +1,61 @@
package org.cryptomator.ui.changepassword;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FXMLLoaderFactory;
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 javax.inject.Named;
import javax.inject.Provider;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
abstract class ChangePasswordModule {
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
}
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("changepassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add);
return stage;
}
@Provides
@FxmlScene(FxmlFile.CHANGEPASSWORD)
@ChangePasswordScoped
static Scene provideUnlockScene(@ChangePasswordWindow FXMLLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene("/fxml/changepassword.fxml");
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(ChangePasswordController.class)
abstract FxController bindUnlockController(ChangePasswordController controller);
}

View File

@@ -0,0 +1,13 @@
package org.cryptomator.ui.changepassword;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface ChangePasswordScoped {
}

View File

@@ -0,0 +1,14 @@
package org.cryptomator.ui.changepassword;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface ChangePasswordWindow {
}

View File

@@ -0,0 +1,72 @@
package org.cryptomator.ui.common;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javax.inject.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.ResourceBundle;
public class FXMLLoaderFactory {
private final Map<Class<? extends FxController>, Provider<FxController>> factories;
private final ResourceBundle resourceBundle;
public FXMLLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
this.factories = factories;
this.resourceBundle = resourceBundle;
}
/**
* @return A new FXMLLoader instance
*/
public FXMLLoader construct() {
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(this::constructController);
loader.setResources(resourceBundle);
return loader;
}
/**
* Loads the FXML given fxml resource in a new FXMLLoader instance.
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
* @return The FXMLLoader used to load the file
* @throws IOException if an error occurs while loading the FXML file
*/
public FXMLLoader load(String fxmlResourceName) throws IOException {
FXMLLoader loader = construct();
try (InputStream in = getClass().getResourceAsStream(fxmlResourceName)) {
loader.load(in);
}
return loader;
}
/**
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
*/
public Scene createScene(String fxmlResourceName) {
final FXMLLoader loader;
try {
loader = load(fxmlResourceName);
} catch (IOException e) {
throw new UncheckedIOException("Failed to load " + fxmlResourceName, e);
}
Parent root = loader.getRoot();
return new Scene(root);
}
private FxController constructController(Class<?> aClass) {
if (!factories.containsKey(aClass)) {
throw new IllegalArgumentException("ViewController not registered: " + aClass);
} else {
return factories.get(aClass).get();
}
}
}

View File

@@ -0,0 +1,49 @@
package org.cryptomator.ui.common;
import javafx.scene.text.Font;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
public class FontLoader {
private static final Logger LOG = LoggerFactory.getLogger(FontLoader.class);
private static final double DEFAULT_FONT_SIZE = 12;
public static Font load(String resourcePath) throws FontLoaderException {
try (InputStream in = FontLoader.class.getResourceAsStream(resourcePath)) {
if (in == null) {
throw new FontLoaderException(resourcePath);
} else {
return load(resourcePath, in);
}
} catch (IOException e) {
throw new FontLoaderException(resourcePath, e);
}
}
private static Font load(String resourcePath, InputStream in) throws FontLoaderException {
Font font = Font.loadFont(in, DEFAULT_FONT_SIZE);
if (font != null) {
LOG.debug("Loaded family: {}", font.getFamily());
return font;
} else {
throw new FontLoaderException(resourcePath);
}
}
public static class FontLoaderException extends IOException {
private FontLoaderException(String resourceName) {
super("Failed to load font: " + resourceName);
}
private FontLoaderException(String resourceName, Throwable cause) {
super("Failed to load font: " + resourceName, cause);
}
}
}

View File

@@ -0,0 +1,9 @@
/*******************************************************************************
* 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.ui.common;
public interface FxController {
}

View File

@@ -3,21 +3,22 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.controllers;
package org.cryptomator.ui.common;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import dagger.MapKey;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import dagger.MapKey;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
// TODO rename after refactoring
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@MapKey
public @interface ViewControllerKey {
Class<? extends ViewController> value();
public @interface FxControllerKey {
Class<? extends FxController> value();
}

View File

@@ -0,0 +1,29 @@
package org.cryptomator.ui.common;
public enum FxmlFile {
ADDVAULT_WELCOME("/fxml/addvault_welcome.fxml"), //
ADDVAULT_EXISTING("/fxml/addvault_existing.fxml"), //
ADDVAULT_NEW_NAME("/fxml/addvault_new_name.fxml"), //
ADDVAULT_NEW_LOCATION("/fxml/addvault_new_location.fxml"), //
ADDVAULT_NEW_PASSWORD("/fxml/addvault_new_password.fxml"), //
ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), //
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock2.fxml"), // TODO rename
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
private final String filename;
FxmlFile(String filename) {
this.filename = filename;
}
}

View File

@@ -0,0 +1,16 @@
package org.cryptomator.ui.common;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface FxmlScene {
FxmlFile value();
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Jean-Noël Charon - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.common;
import com.google.common.base.Strings;
import com.nulabinc.zxcvbn.Zxcvbn;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
@FxApplicationScoped
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 final Zxcvbn zxcvbn;
private final List<String> sanitizedInputs;
private final ResourceBundle resourceBundle;
@Inject
public PasswordStrengthUtil(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
this.zxcvbn = new Zxcvbn();
this.sanitizedInputs = new ArrayList<>();
this.sanitizedInputs.add("cryptomator");
}
public int computeRate(String password) {
if (Strings.isNullOrEmpty(password)) {
return -1;
} else {
int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length());
return zxcvbn.measure(password.substring(0, numCharsToRate), sanitizedInputs).getScore();
}
}
public String getStrengthDescription(Number score) {
if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) {
return resourceBundle.getString("passwordStrength.messageLabel." + score.intValue());
} else {
return "";
}
}
}

View File

@@ -3,7 +3,7 @@
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.ui.util;
package org.cryptomator.ui.common;
import java.util.ArrayList;
import java.util.List;

View File

@@ -1,203 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - password strength meter
*******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
import java.util.Optional;
public class ChangePasswordController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private final Application app;
private final PasswordStrengthUtil strengthRater;
private final Localization localization;
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // 0-4
private Optional<ChangePasswordListener> listener = Optional.empty();
private Vault vault;
@Inject
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
this.app = app;
this.strengthRater = strengthRater;
this.localization = localization;
}
@FXML
private SecPasswordField oldPasswordField;
@FXML
private SecPasswordField newPasswordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button changePasswordButton;
@FXML
private Text messageText;
@FXML
private Hyperlink downloadsPageLink;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@FXML
private GridPane root;
@Override
public void initialize() {
oldPasswordField.textProperty().addListener(this::passwordsChanged);
newPasswordField.textProperty().addListener(this::passwordsChanged);
retypePasswordField.textProperty().addListener(this::passwordsChanged);
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
private void passwordsChanged(@SuppressWarnings("unused") Observable observable) {
boolean oldPasswordEmpty = oldPasswordField.getCharacters().length() == 0;
boolean newPasswordEmpty = newPasswordField.getCharacters().length() == 0;
boolean passwordsEqual = newPasswordField.getCharacters().equals(retypePasswordField.getCharacters());
changePasswordButton.setDisable(oldPasswordEmpty || newPasswordEmpty || !passwordsEqual);
passwordStrength.set(strengthRater.computeRate(newPasswordField.getCharacters().toString()));
}
@Override
public Parent getRoot() {
return root;
}
@Override
public void focus() {
oldPasswordField.requestFocus();
}
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
// trigger "default" change to refresh key bindings:
changePasswordButton.setDefaultButton(false);
changePasswordButton.setDefaultButton(true);
messageText.setText(null);
}
// ****************************************
// Downloads link
// ****************************************
@FXML
public void didClickDownloadsLink(ActionEvent event) {
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
}
// ****************************************
// Change password button
// ****************************************
@FXML
private void didClickChangePasswordButton(ActionEvent event) {
downloadsPageLink.setVisible(false);
try {
vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
messageText.setText(null);
listener.ifPresent(this::invokeListenerLater);
} catch (InvalidPassphraseException e) {
messageText.setText(localization.getString("changePassword.errorMessage.wrongPassword"));
Platform.runLater(oldPasswordField::requestFocus);
} catch (UncheckedIOException | IOException ex) {
messageText.setText(localization.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (UnsupportedVaultFormatException e) {
downloadsPageLink.setVisible(true);
LOG.warn("Unable to unlock vault: " + e.getMessage());
if (e.isVaultOlderThanSoftware()) {
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
}
} finally {
oldPasswordField.swipe();
newPasswordField.swipe();
retypePasswordField.swipe();
}
}
/* Getter/Setter */
public ChangePasswordListener getListener() {
return listener.orElse(null);
}
public void setListener(ChangePasswordListener listener) {
this.listener = Optional.ofNullable(listener);
}
/* callback */
private void invokeListenerLater(ChangePasswordListener listener) {
Platform.runLater(() -> {
listener.didChangePassword();
});
}
@FunctionalInterface
interface ChangePasswordListener {
void didChangePassword();
}
}

View File

@@ -1,167 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - password strength meter
******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.util.Objects;
import java.util.Optional;
public class InitializeController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private final Localization localization;
private final PasswordStrengthUtil strengthRater;
private IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // strengths: 0-4
private Optional<InitializationListener> listener = Optional.empty();
private Vault vault;
@Inject
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
this.localization = localization;
this.strengthRater = strengthRater;
}
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button okButton;
@FXML
private Label messageLabel;
@FXML
private Label passwordStrengthLabel;
@FXML
private Region passwordStrengthLevel0;
@FXML
private Region passwordStrengthLevel1;
@FXML
private Region passwordStrengthLevel2;
@FXML
private Region passwordStrengthLevel3;
@FXML
private Region passwordStrengthLevel4;
@FXML
private GridPane root;
@Override
public void initialize() {
passwordField.textProperty().addListener(this::passwordsChanged);
retypePasswordField.textProperty().addListener(this::passwordsChanged);
passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel2.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(2), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel3.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(3), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLevel4.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(4), strengthRater::getBackgroundWithStrengthColor));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
}
private void passwordsChanged(@SuppressWarnings("unused") Observable observable) {
boolean passwordsEmpty = passwordField.getCharacters().length() == 0;
boolean passwordsEqual = passwordField.getCharacters().equals(retypePasswordField.getCharacters());
okButton.setDisable(passwordsEmpty || !passwordsEqual);
passwordStrength.set(strengthRater.computeRate(passwordField.getCharacters().toString()));
}
@Override
public Parent getRoot() {
return root;
}
@Override
public void focus() {
passwordField.requestFocus();
}
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
// trigger "default" change to refresh key bindings:
okButton.setDefaultButton(false);
okButton.setDefaultButton(true);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void initializeVault(ActionEvent event) {
final CharSequence passphrase = passwordField.getCharacters();
try {
vault.create(passphrase);
listener.ifPresent(this::invokeListenerLater);
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed"));
} finally {
passwordField.swipe();
retypePasswordField.swipe();
}
}
/* Getter/Setter */
public InitializationListener getListener() {
return listener.orElse(null);
}
public void setListener(InitializationListener listener) {
this.listener = Optional.ofNullable(listener);
}
/* callback */
private void invokeListenerLater(InitializationListener listener) {
Platform.runLater(() -> {
listener.didInitialize();
});
}
@FunctionalInterface
interface InitializationListener {
void didInitialize();
}
}

View File

@@ -1,554 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - confirmation dialog on vault removal
******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Cell;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.ui.ExitUtil;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.model.AutoUnlocker;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.VaultList;
import org.cryptomator.ui.model.upgrade.UpgradeStrategies;
import org.cryptomator.ui.model.upgrade.UpgradeStrategy;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.cryptomator.ui.util.Tasks;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.monadic.MonadicBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.stream.Stream;
import static org.cryptomator.ui.util.DialogBuilderUtil.buildErrorDialog;
@FxApplicationScoped
public class MainController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window";
private static final String INACTIVE_WINDOW_STYLE_CLASS = "inactive-window";
private final Stage mainWindow;
private final ExitUtil exitUtil;
private final Localization localization;
private final ExecutorService executorService;
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
private final VaultFactory vaultFactoy;
private final ViewControllerLoader viewControllerLoader;
private final ObjectProperty<ViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults;
private final BooleanBinding areAllVaultsLocked;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final ObjectExpression<Vault.State> selectedVaultState = ObjectExpression.objectExpression(EasyBind.select(selectedVault).selectObject(Vault::stateProperty));
private final BooleanExpression isSelectedVaultValid = BooleanExpression.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false));
private final BooleanExpression canEditSelectedVault = selectedVaultState.isEqualTo(Vault.State.LOCKED);
private final MonadicBinding<UpgradeStrategy> upgradeStrategyForSelectedVault;
private final BooleanBinding isShowingSettings;
private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
private Subscription subs = Subscription.EMPTY;
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExitUtil exitUtil, Localization localization,
VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) {
this.mainWindow = mainWindow;
this.executorService = executorService;
this.launchEventQueue = launchEventQueue;
this.exitUtil = exitUtil;
this.localization = localization;
this.vaultFactoy = vaultFactoy;
this.viewControllerLoader = viewControllerLoader;
this.vaults = vaults;
// derived bindings:
this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass));
this.upgradeStrategyForSelectedVault = EasyBind.monadic(selectedVault).map(upgradeStrategies::getUpgradeStrategy);
this.areAllVaultsLocked = Bindings.isEmpty(FXCollections.observableList(vaults, Vault::observables).filtered(Vault.NOT_LOCKED));
EasyBind.subscribe(areAllVaultsLocked, exitUtil::updateTrayIcon);
EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit);
autoUnlocker.unlockAllSilently();
try {
Desktop.getDesktop().setPreferencesHandler(e -> {
Platform.runLater(this::toggleShowSettings);
});
} catch (UnsupportedOperationException e) {
LOG.info("Unable to setPreferencesHandler, probably not supported on this OS.");
}
}
@FXML
private ContextMenu vaultListCellContextMenu;
@FXML
private MenuItem changePasswordMenuItem;
@FXML
private ContextMenu addVaultContextMenu;
@FXML
private HBox root;
@FXML
private ListView<Vault> vaultList;
@FXML
private ToggleButton addVaultButton;
@FXML
private Button removeVaultButton;
@FXML
private ToggleButton settingsButton;
@FXML
private Pane contentPane;
@FXML
private Pane emptyListInstructions;
@Override
public void initialize() {
vaultList.setItems(vaults);
vaultList.getSelectionModel().clearSelection();
vaultList.setOnKeyReleased(this::didPressKeyOnList);
vaultList.setCellFactory(this::createDirecoryListCell);
root.setOnKeyReleased(this::didPressKeyOnRoot);
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
removeVaultButton.disableProperty().bind(canEditSelectedVault.not());
emptyListInstructions.visibleProperty().bind(Bindings.isEmpty(vaults));
changePasswordMenuItem.visibleProperty().bind(isSelectedVaultValid.and(Bindings.isNull(upgradeStrategyForSelectedVault)));
subs = subs.and(EasyBind.subscribe(selectedVault, this::selectedVaultDidChange));
subs = subs.and(EasyBind.subscribe(activeController, this::activeControllerDidChange));
subs = subs.and(EasyBind.subscribe(isShowingSettings, settingsButton::setSelected));
subs = subs.and(EasyBind.subscribe(addVaultContextMenu.showingProperty(), addVaultButton::setSelected));
}
@Override
public Parent getRoot() {
return root;
}
public void initStage(Stage stage) {
stage.setScene(new Scene(getRoot()));
stage.sizeToScene();
stage.setTitle(localization.getString("app.name")); // set once before bind to avoid display bugs with Linux window managers
stage.titleProperty().bind(windowTitle());
stage.setResizable(false);
loadFont("/css/ionicons.ttf");
loadFont("/css/fontawesome-webfont.ttf");
if (SystemUtils.IS_OS_MAC_OSX) {
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), ACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty()));
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), INACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty().not()));
Application.setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon_512.png")));
Application.setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon_32.png")));
Application.setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
exitUtil.initExitHandler(() -> Platform.runLater(this::gracefulShutdown));
listenToFileOpenRequests(stage);
}
private void gracefulShutdown() {
vaults.filtered(Vault.NOT_LOCKED).forEach(Vault::prepareForShutdown);
if (!vaults.filtered(Vault.NOT_LOCKED).isEmpty()) {
mainWindow.show(); // to keep the application open
ButtonType tryAgainButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.tryAgain"));
ButtonType forceShutdownButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.forceShutdown"));
Alert gracefulShutdownDialog = DialogBuilderUtil.buildGracefulShutdownDialog(
localization.getString("main.gracefulShutdown.dialog.title"), localization.getString("main.gracefulShutdown.dialog.header"), localization.getString("main.gracefulShutdown.dialog.content"),
forceShutdownButtonType, ButtonType.CANCEL, forceShutdownButtonType, tryAgainButtonType);
Optional<ButtonType> choice = gracefulShutdownDialog.showAndWait();
choice.ifPresent(btnType -> {
if (tryAgainButtonType.equals(btnType)) {
gracefulShutdown();
} else if (forceShutdownButtonType.equals(btnType)) {
Platform.runLater(Platform::exit);
} else {
if (!vaults.filtered(Vault.NOT_LOCKED).isEmpty()) {
showUnlockedView(vaults.get(0), false); //if there are still unlocked vaults, show one of them
} else {
showUnlockView(UnlockController.State.UNLOCKING); //otherwise show any vault
}
}
});
} else {
Platform.runLater(Platform::exit);
}
}
private void loadFont(String resourcePath) {
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
Font.loadFont(in, 12.0);
} catch (IOException e) {
LOG.warn("Error loading font from path: " + resourcePath, e);
}
}
private void listenToFileOpenRequests(Stage stage) {
Tasks.create(launchEventQueue::take).onSuccess(event -> {
stage.setIconified(false);
stage.show();
stage.toFront();
stage.requestFocus();
event.getPathsToOpen().forEach(path -> addVault(path, true));
}).schedulePeriodically(executorService, Duration.ZERO, Duration.ZERO);
}
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setVaultContextMenu(vaultListCellContextMenu);
cell.setOnMouseClicked(this::didClickOnListCell);
return cell;
}
// ****************************************
// UI Events
// ****************************************
@FXML
private void didClickAddVault() {
if (addVaultContextMenu.isShowing()) {
addVaultContextMenu.hide();
} else {
addVaultContextMenu.show(addVaultButton, Side.BOTTOM, 0.0, 0.0);
}
}
@FXML
private void didClickCreateNewVault() {
final FileChooser fileChooser = new FileChooser();
final File file = fileChooser.showSaveDialog(mainWindow);
if (file == null) {
return;
}
try {
final Path vaultDir = file.toPath();
if (Files.exists(vaultDir)) {
try (Stream<Path> stream = Files.list(vaultDir)) {
if (stream.filter(this::isNotHidden).findAny().isPresent()) {
buildErrorDialog( //
localization.getString("main.createVault.nonEmptyDir.title"), //
localization.getString("main.createVault.nonEmptyDir.header"), //
localization.getString("main.createVault.nonEmptyDir.content"), //
ButtonType.OK).show();
return;
}
}
} else {
Files.createDirectory(vaultDir);
}
addVault(vaultDir, true);
} catch (IOException e) {
LOG.error("Unable to create vault", e);
}
}
private boolean isNotHidden(Path file) {
return !file.getFileName().toString().startsWith(".");
}
@FXML
private void didClickAddExistingVaults() {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
final List<File> files = fileChooser.showOpenMultipleDialog(mainWindow);
if (files != null) {
for (final File file : files) {
addVault(file.toPath(), true);
}
}
}
/**
* adds the given directory or selects it if it is already in the list of directories.
*
* @param path to a vault directory or masterkey file
*/
public void addVault(final Path path, boolean select) {
final Path vaultPath;
if (path != null && Files.isDirectory(path)) {
vaultPath = path;
} else if (path != null && Files.isReadable(path)) {
vaultPath = path.getParent();
} else {
LOG.warn("Ignoring attempt to add vault with invalid path: {}", path);
return;
}
final Vault vault = vaults.stream().filter(v -> v.getPath().equals(vaultPath)).findAny().orElseGet(() -> {
VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.path().set(vaultPath);
return vaultFactoy.get(vaultSettings);
});
if (!vaults.contains(vault)) {
vaults.add(vault);
}
if (select) {
vaultList.getSelectionModel().select(vault);
activeController.get().focus();
}
}
@FXML
private void didClickRemoveSelectedEntry() {
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("main.directoryList.remove.confirmation.title"), //
localization.getString("main.directoryList.remove.confirmation.header"), //
localization.getString("main.directoryList.remove.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
vaults.remove(selectedVault.get());
if (vaults.isEmpty()) {
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
} else {
activeController.get().focus();
}
}
}
@FXML
private void didClickChangePassword() {
showChangePasswordView();
}
@FXML
private void didClickShowSettings() {
toggleShowSettings();
}
private void toggleShowSettings() {
if (isShowingSettings.get()) {
showWelcomeView();
} else {
showPreferencesView();
}
vaultList.getSelectionModel().clearSelection();
}
// ****************************************
// Binding Listeners
// ****************************************
private void activeControllerDidChange(ViewController newValue) {
final Parent root = newValue.getRoot();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
}
private void selectedVaultDidChange(Vault newValue) {
if (newValue == null) {
return;
}
if (newValue.getState() != Vault.State.LOCKED) {
this.showUnlockedView(newValue, false);
} else if (!newValue.doesVaultDirectoryExist()) {
this.showNotFoundView();
} else if (newValue.isValidVaultDirectory() && upgradeStrategyForSelectedVault.isPresent()) {
this.showUpgradeView();
} else if (newValue.isValidVaultDirectory()) {
this.showUnlockView(UnlockController.State.UNLOCKING);
} else {
this.showInitializeView();
}
}
private void didPressKeyOnList(KeyEvent e) {
if (e.getCode() == KeyCode.ENTER || e.getCode() == KeyCode.SPACE) {
activeController.get().focus();
}
}
private void didPressKeyOnRoot(KeyEvent event) {
boolean triggered;
if (SystemUtils.IS_OS_MAC) {
triggered = event.isMetaDown();
} else {
triggered = event.isControlDown() && !event.isAltDown();
}
if (triggered && event.getCode().isDigitKey()) {
int digit = Integer.valueOf(event.getText());
switch (digit) {
case 0: {
vaultList.getSelectionModel().clearSelection();
showWelcomeView();
return;
}
default: {
vaultList.getSelectionModel().select(digit - 1);
activeController.get().focus();
return;
}
}
}
}
private void didClickOnListCell(MouseEvent e) {
if (MouseEvent.MOUSE_CLICKED.equals(e.getEventType()) && e.getSource() instanceof Cell && ((Cell<?>) e.getSource()).isSelected()) {
activeController.get().focus();
}
}
// ****************************************
// Public Bindings
// ****************************************
public Binding<String> windowTitle() {
return EasyBind.monadic(selectedVault).flatMap(Vault::name).orElse(localization.getString("app.name"));
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showWelcomeView() {
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
}
private void showPreferencesView() {
activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
}
private void showNotFoundView() {
activeController.set(viewControllerLoader.load("/fxml/notfound.fxml"));
}
private void showInitializeView() {
final InitializeController ctrl = viewControllerLoader.load("/fxml/initialize.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didInitialize);
activeController.set(ctrl);
}
public void didInitialize() {
showUnlockView(UnlockController.State.INITIALIZED);
activeController.get().focus();
}
private void showUpgradeView() {
final UpgradeController ctrl = viewControllerLoader.load("/fxml/upgrade.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUpgrade);
activeController.set(ctrl);
}
public void didUpgrade() {
showUnlockView(UnlockController.State.UPGRADED);
activeController.get().focus();
}
private void showUnlockView(UnlockController.State state) {
final UnlockController ctrl = viewControllerLoader.load("/fxml/unlock.fxml");
ctrl.setVault(selectedVault.get(), state);
ctrl.setListener(this::didUnlock);
activeController.set(ctrl);
}
public void didUnlock(Vault vault) {
if (vault.equals(selectedVault.getValue())) {
this.showUnlockedView(vault, vault.getVaultSettings().revealAfterMount().getValue());
}
}
private void showUnlockedView(Vault vault, boolean reveal) {
final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> viewControllerLoader.load("/fxml/unlocked.fxml"));
ctrl.setVault(vault);
ctrl.setListener(this::didLock);
if (reveal) {
ctrl.revealVault(vault);
}
activeController.set(ctrl);
}
public void didLock(UnlockedController ctrl) {
unlockedVaults.remove(ctrl.getVault());
if (ctrl.getVault().getId() == selectedVault.get().getId()) {
showUnlockView(UnlockController.State.UNLOCKING);
}
activeController.get().focus();
}
private void showChangePasswordView() {
final ChangePasswordController ctrl = viewControllerLoader.load("/fxml/change_password.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didChangePassword);
activeController.set(ctrl);
Platform.runLater(ctrl::focus);
}
public void didChangePassword() {
showUnlockView(UnlockController.State.PASSWORD_CHANGED);
activeController.get().focus();
}
}

View File

@@ -1,31 +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.ui.controllers;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.layout.VBox;
import org.cryptomator.common.FxApplicationScoped;
import javax.inject.Inject;
@FxApplicationScoped
public class NotFoundController implements ViewController {
@Inject
public NotFoundController() {
// no-op
}
@FXML
VBox root;
@Override
public Parent getRoot() {
return root;
}
}

View File

@@ -1,181 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Volume;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Optional;
@FxApplicationScoped
public class SettingsController implements ViewController {
private static final CharMatcher DIGITS_MATCHER = CharMatcher.inRange('0', '9');
private final Localization localization;
private final Settings settings;
private final Optional<String> applicationVersion;
@Inject
public SettingsController(Localization localization, Settings settings, @Named("applicationVersion") Optional<String> applicationVersion) {
this.localization = localization;
this.settings = settings;
this.applicationVersion = applicationVersion;
this.webdavSettings = new Group();
}
@FXML
private CheckBox checkForUpdatesCheckbox;
private Group webdavSettings;
@FXML
private Label portFieldLabel;
@FXML
private TextField portField;
@FXML
private Button changePortButton;
@FXML
private Label versionLabel;
@FXML
private Label prefGvfsSchemeLabel;
@FXML
private ChoiceBox<String> prefGvfsScheme;
@FXML
private ChoiceBox<VolumeImpl> volume;
@FXML
private CheckBox debugModeCheckbox;
@FXML
private VBox root;
@Override
public void initialize() {
versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion.orElse("SNAPSHOT")));
checkForUpdatesCheckbox.setDisable(areUpdatesManagedExternally());
checkForUpdatesCheckbox.setSelected(settings.checkForUpdates().get() && !areUpdatesManagedExternally());
//NIOADAPTER
volume.getItems().addAll(Volume.getCurrentSupportedAdapters());
volume.setValue(settings.preferredVolumeImpl().get());
volume.setConverter(new NioAdapterImplStringConverter());
volume.valueProperty().addListener(this::setVisibilityGvfsElements);
//WEBDAV
webdavSettings.visibleProperty().bind(volume.valueProperty().isEqualTo(VolumeImpl.WEBDAV));
webdavSettings.managedProperty().bind(webdavSettings.visibleProperty());
prefGvfsScheme.managedProperty().bind(webdavSettings.visibleProperty());
prefGvfsSchemeLabel.managedProperty().bind(webdavSettings.visibleProperty());
portFieldLabel.managedProperty().bind(webdavSettings.visibleProperty());
portFieldLabel.visibleProperty().bind(webdavSettings.visibleProperty());
changePortButton.managedProperty().bind(webdavSettings.visibleProperty());
portField.managedProperty().bind(webdavSettings.visibleProperty());
portField.visibleProperty().bind(webdavSettings.visibleProperty());
portField.setText(String.valueOf(settings.port().intValue()));
portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents);
changePortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(portField.textProperty()));
changePortButton.disableProperty().bind(Bindings.createBooleanBinding(this::isPortValid, portField.textProperty()).not());
prefGvfsScheme.getItems().add("dav");
prefGvfsScheme.getItems().add("webdav");
prefGvfsScheme.setValue(settings.preferredGvfsScheme().get());
prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX);
prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX);
debugModeCheckbox.setSelected(settings.debugMode().get());
settings.checkForUpdates().bind(checkForUpdatesCheckbox.selectedProperty());
settings.preferredGvfsScheme().bind(prefGvfsScheme.valueProperty());
settings.preferredVolumeImpl().bind(volume.valueProperty());
settings.debugMode().bind(debugModeCheckbox.selectedProperty());
}
@Override
public Parent getRoot() {
return root;
}
@FXML
private void changePort() {
assert isPortValid() : "Button must be disabled, if port is invalid.";
try {
int port = Integer.parseInt(portField.getText());
settings.port().set(port);
} catch (NumberFormatException e) {
throw new IllegalStateException("Button must be disabled, if port is invalid.", e);
}
}
private boolean isPortValid() {
try {
int port = Integer.parseInt(portField.getText());
return port == 0 // choose port automatically
|| port >= Settings.MIN_PORT && port <= Settings.MAX_PORT; // port within range
} catch (NumberFormatException e) {
return false;
}
}
private void filterNumericKeyEvents(KeyEvent t) {
if (!Strings.isNullOrEmpty(t.getCharacter()) && !DIGITS_MATCHER.matchesAllOf(t.getCharacter())) {
t.consume();
}
}
private void setVisibilityGvfsElements(@SuppressWarnings("unused") Observable obs, @SuppressWarnings("unused")Object oldValue, Object newValue) {
prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX && ((VolumeImpl) newValue).getDisplayName().equals("WebDAV"));
prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX && ((VolumeImpl) newValue).getDisplayName().equals("WebDAV"));
}
private boolean areUpdatesManagedExternally() {
return Boolean.parseBoolean(System.getProperty("cryptomator.updatesManagedExternally", "false"));
}
private static class NioAdapterImplStringConverter extends StringConverter<VolumeImpl> {
@Override
public String toString(VolumeImpl object) {
return object.getDisplayName();
}
@Override
public VolumeImpl fromString(String string) {
return VolumeImpl.forDisplayName(string);
}
}
}

View File

@@ -1,581 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.WindowsDriveLetters;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.cryptomator.ui.util.Tasks;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
public class UnlockController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private static final CharMatcher ALPHA_NUMERIC_MATCHER = CharMatcher.inRange('a', 'z') //
.or(CharMatcher.inRange('A', 'Z')) //
.or(CharMatcher.inRange('0', '9')) //
.or(CharMatcher.is('_')) //
.precomputed();
private final Application app;
private final Stage mainWindow;
private final Localization localization;
private final WindowsDriveLetters driveLetters;
private final ChangeListener<Path> driveLetterChangeListener = this::winDriveLetterDidChange;
private final Optional<KeychainAccess> keychainAccess;
private final Settings settings;
private final ExecutorService executor;
private Vault vault;
private Optional<UnlockListener> listener = Optional.empty();
private Subscription vaultSubs = Subscription.EMPTY;
private BooleanProperty unlocking = new SimpleBooleanProperty();
@Inject
public UnlockController(Application app, @Named("mainWindow") Stage mainWindow, Localization localization, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess, Settings settings, ExecutorService executor) {
this.app = app;
this.mainWindow = mainWindow;
this.localization = localization;
this.driveLetters = driveLetters;
this.keychainAccess = keychainAccess;
this.settings = settings;
this.executor = executor;
}
@FXML
private SecPasswordField passwordField;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private Text messageText;
@FXML
private CheckBox savePassword;
@FXML
private TextField mountName;
@FXML
private CheckBox useCustomMountFlags;
@FXML
private TextField mountFlags;
@FXML
private CheckBox revealAfterMount;
@FXML
private CheckBox useCustomWinDriveLetter;
@FXML
private ChoiceBox<Path> winDriveLetter;
@FXML
private CheckBox useCustomMountPoint;
@FXML
private HBox customMountPoint;
@FXML
private Label customMountPointLabel;
@FXML
private Hyperlink downloadsPageLink;
@FXML
private VBox advancedOptions;
@FXML
private VBox root;
@FXML
private CheckBox unlockAfterStartup;
@FXML
private CheckBox useReadOnlyMode;
@Override
public void initialize() {
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
advancedOptions.disableProperty().bind(unlocking);
unlockButton.disableProperty().bind(unlocking.or(passwordField.textProperty().isEmpty()));
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
useReadOnlyMode.selectedProperty().addListener(this::useReadOnlyDidChange);
useCustomMountFlags.selectedProperty().addListener(this::useCustomMountFlagsDidChange);
mountFlags.disableProperty().bind(useCustomMountFlags.selectedProperty().not());
mountFlags.textProperty().addListener(this::mountFlagsDidChange);
savePassword.setDisable(!keychainAccess.isPresent());
unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not()));
downloadsPageLink.visibleProperty().bind(downloadsPageLink.managedProperty());
customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty());
customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty());
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
winDriveLetter.disableProperty().bind(useCustomWinDriveLetter.selectedProperty().not());
if (!SystemUtils.IS_OS_WINDOWS) {
useCustomWinDriveLetter.setVisible(false);
useCustomWinDriveLetter.setManaged(false);
winDriveLetter.setVisible(false);
winDriveLetter.setManaged(false);
}
}
@Override
public Parent getRoot() {
return root;
}
@Override
public void focus() {
passwordField.requestFocus();
}
void setVault(Vault vault, State state) {
vaultSubs.unsubscribe();
vaultSubs = Subscription.EMPTY;
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
if (Objects.equals(this.vault, Objects.requireNonNull(vault))) {
return;
}
assert vault != null;
this.vault = vault;
advancedOptions.setVisible(false);
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
state.successMessage().map(localization::getString).ifPresent(messageText::setText);
downloadsPageLink.setManaged(false);
mountName.setText(vault.getMountName());
useCustomMountFlags.setSelected(vault.isHavingCustomMountFlags());
mountFlags.setText(vault.getMountFlags());
savePassword.setSelected(false);
// auto-fill pw from keychain:
if (keychainAccess.isPresent()) {
try {
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
savePassword.setSelected(true);
passwordField.setPassword(storedPw);
passwordField.selectRange(storedPw.length, storedPw.length);
Arrays.fill(storedPw, ' ');
}
} catch (KeychainAccessException e) {
LOG.error("Failed to load stored password from system keychain.", e);
}
}
VaultSettings vaultSettings = vault.getVaultSettings();
unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get());
revealAfterMount.setSelected(vaultSettings.revealAfterMount().get());
useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get());
// WEBDAV-dependent controls:
if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) {
useCustomMountPoint.setVisible(false);
useCustomMountPoint.setManaged(false);
useCustomMountFlags.setVisible(false);
useCustomMountFlags.setManaged(false);
mountFlags.setVisible(false);
mountFlags.setManaged(false);
} else {
useCustomMountPoint.setVisible(true);
useCustomMountPoint.setSelected(vaultSettings.usesIndividualMountPath().get());
if (Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
customMountPointLabel.setText(localization.getString("unlock.label.chooseMountPath"));
} else {
customMountPointLabel.setText(displayablePath(vaultSettings.individualMountPath().getValueSafe()));
}
}
// OS-dependent controls:
if (SystemUtils.IS_OS_WINDOWS) {
winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
winDriveLetter.getItems().clear();
winDriveLetter.getItems().add(null);
winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
winDriveLetter.getItems().sort(new WinDriveLetterComparator());
winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
chooseSelectedDriveLetter();
winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
useCustomWinDriveLetter.setSelected(!vaultSettings.usesIndividualMountPath().get());
useCustomWinDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not());
useCustomWinDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not());
}
vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set));
vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), vaultSettings.revealAfterMount()::set));
vaultSubs = vaultSubs.and(EasyBind.subscribe(useCustomMountPoint.selectedProperty(), vaultSettings.usesIndividualMountPath()::set));
vaultSubs = vaultSubs.and(EasyBind.subscribe(useReadOnlyMode.selectedProperty(), vaultSettings.usesReadOnlyMode()::set));
}
private String displayablePath(String path) {
Path homeDir = Paths.get(SystemUtils.USER_HOME);
Path p = Paths.get(path);
if (p.startsWith(homeDir)) {
Path relativePath = homeDir.relativize(p);
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
return homePrefix + relativePath.toString();
} else {
return p.toString();
}
}
// ****************************************
// Downloads link
// ****************************************
@FXML
public void didClickDownloadsLink() {
app.getHostServices().showDocument("https://cryptomator.org/downloads/#allVersions");
}
// ****************************************
// Advanced options button
// ****************************************
@FXML
private void didClickAdvancedOptionsButton() {
messageText.setText(null);
downloadsPageLink.setManaged(false);
advancedOptions.setVisible(!advancedOptions.isVisible());
if (advancedOptions.isVisible()) {
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.hide"));
} else {
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
}
}
private void filterAlphanumericKeyEvents(KeyEvent t) {
if (!Strings.isNullOrEmpty(t.getCharacter()) && !ALPHA_NUMERIC_MATCHER.matchesAllOf(t.getCharacter())) {
t.consume();
}
}
private void mountNameDidChange(@SuppressWarnings("unused") ObservableValue<? extends String> property, @SuppressWarnings("unused") String oldValue, String newValue) {
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.getMountName());
} else {
vault.setMountName(newValue);
}
if (!useCustomMountFlags.isSelected()) {
mountFlags.setText(vault.getMountFlags()); // update default flags
}
}
private void useReadOnlyDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused") Boolean oldValue, Boolean newValue) {
vault.getVaultSettings().usesReadOnlyMode().setValue(newValue);
if (!useCustomMountFlags.isSelected()) {
mountFlags.setText(vault.getMountFlags()); // update default flags
}
}
private void useCustomMountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends Boolean> property, @SuppressWarnings("unused") Boolean oldValue, Boolean newValue) {
if (!newValue) {
vault.setMountFlags(VaultSettings.DEFAULT_MOUNT_FLAGS);
mountFlags.setText(vault.getMountFlags());
}
}
private void mountFlagsDidChange(@SuppressWarnings("unused") ObservableValue<? extends String> property, @SuppressWarnings("unused") String oldValue, String newValue) {
if (useCustomMountFlags.isSelected()) {
vault.setMountFlags(newValue);
}
}
@FXML
public void didClickChooseCustomMountPoint() {
DirectoryChooser dirChooser = new DirectoryChooser();
File file = dirChooser.showDialog(mainWindow);
if (file != null) {
customMountPointLabel.setText(displayablePath(file.toString()));
vault.setCustomMountPath(file.toString());
}
}
@FXML
public void didClickCustomWinDriveLetterCheckbox() {
if (!useCustomWinDriveLetter.isSelected()) {
winDriveLetter.setValue(null);
}
}
@FXML
public void didClickCustomMountPointCheckbox() {
useCustomWinDriveLetter.setSelected(vault.getWinDriveLetter() != null);
}
/**
* Converts 'C' to "C:" to translate between model and GUI.
*/
private class WinDriveLetterLabelConverter extends StringConverter<Path> {
@Override
public String toString(Path root) {
if (root == null) {
return localization.getString("unlock.choicebox.winDriveLetter.auto");
} else if (root.endsWith("occupied")) {
return root.getRoot().toString().substring(0, 1) + " (" + localization.getString("unlock.choicebox.winDriveLetter.occupied") + ")";
} else {
return root.toString().substring(0, 1);
}
}
@Override
public Path fromString(String string) {
if (localization.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) {
return null;
} else {
return Path.of(string);
}
}
}
/**
* Natural sorting of ASCII letters, but <code>null</code> always on first, as this is "auto-assign".
*/
private static class WinDriveLetterComparator implements Comparator<Path> {
@Override
public int compare(Path c1, Path c2) {
if (c1 == null) {
return -1;
} else if (c2 == null) {
return 1;
} else {
return c1.compareTo(c2);
}
}
}
private void winDriveLetterDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> property, @SuppressWarnings("unused") Path oldValue, Path newValue) {
vault.setWinDriveLetter(newValue);
}
private void chooseSelectedDriveLetter() {
assert SystemUtils.IS_OS_WINDOWS;
// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
if (vault.getWinDriveLetter() != null) {
final Path pickedRoot = Path.of(vault.getWinDriveLetter() + ":\\");
if (driveLetters.getOccupiedDriveLetters().contains(pickedRoot)) {
Path alteredPath = pickedRoot.resolve("occupied");
this.winDriveLetter.getItems().add(alteredPath);
this.winDriveLetter.getSelectionModel().select(alteredPath);
} else {
this.winDriveLetter.getSelectionModel().select(pickedRoot);
}
} else {
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
this.winDriveLetter.getSelectionModel().selectFirst();
}
}
// ****************************************
// Save password checkbox
// ****************************************
@FXML
private void didClickSavePasswordCheckbox() {
if (!savePassword.isSelected() && hasStoredPassword()) {
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("unlock.savePassword.delete.confirmation.title"), //
localization.getString("unlock.savePassword.delete.confirmation.header"), //
localization.getString("unlock.savePassword.delete.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
try {
keychainAccess.get().deletePassphrase(vault.getId());
} catch (KeychainAccessException e) {
LOG.error("Failed to remove entry from system keychain.", e);
}
} else if (ButtonType.CANCEL.equals(choice.get())) {
savePassword.setSelected(true);
}
}
}
private boolean hasStoredPassword() {
try {
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
boolean hasPw = (storedPw != null);
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
return hasPw;
} catch (KeychainAccessException e) {
return false;
}
}
// ****************************************
// Unlock button
// ****************************************
@FXML
private void didClickUnlockButton() {
unlocking.set(true);
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
unlockButton.setContentDisplay(ContentDisplay.LEFT);
CharSequence password = passwordField.getCharacters();
Tasks.create(() -> {
vault.unlock(password);
if (keychainAccess.isPresent() && savePassword.isSelected()) {
keychainAccess.get().storePassphrase(vault.getId(), password);
}
}).onSuccess(() -> {
messageText.setText(null);
downloadsPageLink.setManaged(false);
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
passwordField.swipe();
}).onError(InvalidPassphraseException.class, e -> {
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
downloadsPageLink.setManaged(false);
passwordField.selectAll();
passwordField.requestFocus();
}).onError(UnsupportedVaultFormatException.class, e -> {
if (e.isVaultOlderThanSoftware()) {
// whitespace after localized text used as separator between text and hyperlink
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
downloadsPageLink.setManaged(true);
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
downloadsPageLink.setManaged(true);
} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
}
}).onError(NotDirectoryException.class, e -> {
LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
advancedOptions.setVisible(true);
messageText.setText(null);
downloadsPageLink.setManaged(false);
showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNonExisting");
}).onError(DirectoryNotEmptyException.class, e -> {
LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage());
advancedOptions.setVisible(true);
messageText.setText(null);
downloadsPageLink.setManaged(false);
showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNotEmpty");
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Unlock failed for technical reasons.", e);
messageText.setText(localization.getString("unlock.errorMessage.unlockFailed"));
downloadsPageLink.setManaged(false);
}).andFinally(() -> {
unlocking.set(false);
unlockButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
}).runOnce(executor);
}
private void showUnlockFailedErrorDialog(String localizableContentKey) {
String title = localization.getString("unlock.failedDialog.title");
String header = localization.getString("unlock.failedDialog.header");
String content = localization.getString(localizableContentKey);
Alert alert = DialogBuilderUtil.buildErrorDialog(title, header, content, ButtonType.OK);
alert.show();
}
/* callback */
public void setListener(UnlockListener listener) {
this.listener = Optional.ofNullable(listener);
}
@FunctionalInterface
interface UnlockListener {
void didUnlock(Vault vault);
}
/* state */
public enum State {
UNLOCKING(null), //
INITIALIZED("unlock.successLabel.vaultCreated"), //
PASSWORD_CHANGED("unlock.successLabel.passwordChanged"), //
UPGRADED("unlock.successLabel.upgraded");
private Optional<String> successMessage;
State(String successMessage) {
this.successMessage = Optional.ofNullable(successMessage);
}
public Optional<String> successMessage() {
return successMessage;
}
}
}

View File

@@ -1,275 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.VBox;
import javafx.stage.PopupWindow.AnchorLocation;
import javafx.util.Duration;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.cryptomator.ui.util.Tasks;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import static java.lang.String.format;
public class UnlockedController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockedController.class);
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.5;
private final Localization localization;
private final ExecutorService executor;
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<LockListener> listener = Optional.empty();
private Timeline ioAnimation;
@FXML
private Label messageLabel;
@FXML
private LineChart<Number, Number> ioGraph;
@FXML
private NumberAxis xAxis;
@FXML
private ToggleButton moreOptionsButton;
@FXML
private ContextMenu moreOptionsMenu;
@FXML
private VBox root;
@Inject
public UnlockedController(Localization localization, ExecutorService executor) {
this.localization = localization;
this.executor = executor;
}
@Override
public void initialize() {
EasyBind.subscribe(vault, this::vaultChanged);
EasyBind.subscribe(moreOptionsMenu.showingProperty(), moreOptionsButton::setSelected);
}
@Override
public Parent getRoot() {
return root;
}
private void vaultChanged(Vault newVault) {
if (newVault == null) {
return;
}
// (re)start throughput statistics:
stopIoSampling();
startIoSampling();
}
@FXML
private void didClickLockVault() {
regularLockVault(this::lockVaultSucceeded);
}
private void lockVaultSucceeded() {
listener.ifPresent(listener -> listener.didLock(this));
}
private void regularLockVault(Runnable onSuccess) {
Tasks.create(() -> {
vault.get().lock(false);
}).onSuccess(() -> {
LOG.trace("Regular unmount succeeded.");
onSuccess.run();
}).onError(Exception.class, e -> {
onRegularUnmountVaultFailed(e, onSuccess);
}).runOnce(executor);
}
private void onRegularUnmountVaultFailed(Exception e, Runnable onSuccess) {
if (vault.get().supportsForcedUnmount()) {
LOG.trace("Regular unmount failed.", e);
Alert confirmDialog = DialogBuilderUtil.buildYesNoDialog( //
format(localization.getString("unlocked.lock.force.confirmation.title"), vault.get().name().getValue()), //
localization.getString("unlocked.lock.force.confirmation.header"), //
localization.getString("unlocked.lock.force.confirmation.content"), //
ButtonType.NO);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.YES.equals(choice.get())) {
forcedLockVault(onSuccess);
} else {
LOG.trace("Unmount cancelled.", e);
}
} else {
LOG.error("Regular unmount failed.", e);
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
}
}
private void forcedLockVault(Runnable onSuccess) {
Tasks.create(() -> {
vault.get().lock(true);
}).onSuccess(() -> {
LOG.trace("Forced unmount succeeded.");
onSuccess.run();
}).onError(Exception.class, e -> {
LOG.error("Forced unmount failed.", e);
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
}).runOnce(executor);
}
@FXML
private void didClickMoreOptions() {
if (moreOptionsMenu.isShowing()) {
moreOptionsMenu.hide();
} else {
moreOptionsMenu.setAnchorLocation(AnchorLocation.CONTENT_TOP_RIGHT);
moreOptionsMenu.show(moreOptionsButton, Side.BOTTOM, moreOptionsButton.getWidth(), 0.0);
}
}
@FXML
private void didClickRevealVault() {
revealVault(vault.get());
}
void revealVault(Vault vault) {
Tasks.create(() -> {
vault.reveal();
}).onSuccess(() -> {
LOG.trace("Reveal succeeded.");
messageLabel.setText(null);
}).onError(Exception.class, e -> {
LOG.error("Reveal failed.", e);
messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
}).runOnce(executor);
}
// ****************************************
// IO Graph
// ****************************************
private void startIoSampling() {
final Series<Number, Number> decryptedBytes = new Series<>();
decryptedBytes.setName(localization.getString("unlocked.label.statsDecrypted"));
final Series<Number, Number> encryptedBytes = new Series<>();
encryptedBytes.setName(localization.getString("unlocked.label.statsEncrypted"));
ioGraph.getData().add(decryptedBytes);
ioGraph.getData().add(encryptedBytes);
ioAnimation = new Timeline();
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(decryptedBytes, encryptedBytes)));
ioAnimation.setCycleCount(Animation.INDEFINITE);
ioAnimation.play();
}
private void stopIoSampling() {
if (ioAnimation != null) {
ioGraph.getData().clear();
ioAnimation.stop();
}
}
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
private final Series<Number, Number> decryptedBytes;
private final Series<Number, Number> encryptedBytes;
public IoSamplingAnimationHandler(Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
this.decryptedBytes = decryptedBytes;
this.encryptedBytes = encryptedBytes;
// initialize data once and change value of datapoints later:
for (int i = 0; i < IO_SAMPLING_STEPS; i++) {
decryptedBytes.getData().add(new Data<>(i, 0));
encryptedBytes.getData().add(new Data<>(i, 0));
}
xAxis.setLowerBound(0);
xAxis.setUpperBound(IO_SAMPLING_STEPS);
}
@Override
public void handle(ActionEvent event) {
// move all values one step:
for (int i = 0; i < IO_SAMPLING_STEPS - 1; i++) {
int j = i + 1;
Number tmp = decryptedBytes.getData().get(j).getYValue();
decryptedBytes.getData().get(i).setYValue(tmp);
tmp = encryptedBytes.getData().get(j).getYValue();
encryptedBytes.getData().get(i).setYValue(tmp);
}
// add latest value:
final long decBytes = vault.get().pollBytesRead();
final double decMb = decBytes * BYTES_TO_MEGABYTES_FACTOR;
final long encBytes = vault.get().pollBytesWritten();
final double encMb = encBytes * BYTES_TO_MEGABYTES_FACTOR;
decryptedBytes.getData().get(IO_SAMPLING_STEPS - 1).setYValue(decMb);
encryptedBytes.getData().get(IO_SAMPLING_STEPS - 1).setYValue(encMb);
}
}
/* Getter/Setter */
public Vault getVault() {
return this.vault.get();
}
public void setVault(Vault vault) {
this.vault.set(vault);
}
/* callback */
public void setListener(LockListener listener) {
this.listener = Optional.ofNullable(listener);
}
@FunctionalInterface
interface LockListener {
void didLock(UnlockedController ctrl);
}
}

View File

@@ -1,162 +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.ui.controllers;
import javax.inject.Inject;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.GridPane;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.upgrade.UpgradeStrategies;
import org.cryptomator.ui.model.upgrade.UpgradeStrategy;
import org.cryptomator.ui.model.upgrade.UpgradeStrategy.UpgradeFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.Tasks;
import org.fxmisc.easybind.EasyBind;
public class UpgradeController implements ViewController {
private final ObjectProperty<UpgradeStrategy> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final ExecutorService executor;
private Optional<UpgradeListener> listener = Optional.empty();
private Vault vault;
@Inject
public UpgradeController(UpgradeStrategies strategies, ExecutorService executor) {
this.strategies = strategies;
this.executor = executor;
}
@FXML
private Label upgradeTitleLabel;
@FXML
private Label upgradeMsgLabel;
@FXML
private SecPasswordField passwordField;
@FXML
private CheckBox confirmationCheckbox;
@FXML
private Button upgradeButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label errorLabel;
@FXML
private GridPane root;
@Override
public void initialize() {
upgradeTitleLabel.textProperty().bind(EasyBind.monadic(strategy).map(this::upgradeTitle).orElse(""));
upgradeMsgLabel.textProperty().bind(EasyBind.monadic(strategy).map(this::upgradeMessage).orElse(""));
BooleanExpression passwordProvided = passwordField.textProperty().isNotEmpty().and(passwordField.disabledProperty().not());
BooleanExpression syncFinished = confirmationCheckbox.selectedProperty();
upgradeButton.disableProperty().bind(passwordProvided.not().or(syncFinished.not()));
}
@Override
public Parent getRoot() {
return root;
}
@Override
public void focus() {
passwordField.requestFocus();
}
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
errorLabel.setText(null);
strategy.set(strategies.getUpgradeStrategy(vault));
// trigger "default" change to refresh key bindings:
upgradeButton.setDefaultButton(false);
upgradeButton.setDefaultButton(true);
}
// ****************************************
// Upgrade label
// ****************************************
private String upgradeTitle(UpgradeStrategy instruction) {
return instruction.getTitle(vault);
}
private String upgradeMessage(UpgradeStrategy instruction) {
return instruction.getMessage(vault);
}
// ****************************************
// Upgrade button
// ****************************************
@FXML
private void didClickUpgradeButton(ActionEvent event) {
EasyBind.monadic(strategy).ifPresent(this::upgrade);
}
private void upgrade(UpgradeStrategy instruction) {
passwordField.setDisable(true);
progressIndicator.setVisible(true);
Tasks //
.create(() -> {
if (!instruction.isApplicable(vault)) {
throw new IllegalStateException("No ugprade needed for " + vault.getPath());
}
instruction.upgrade(vault, passwordField.getCharacters());
}) //
.onSuccess(this::showNextUpgrade) //
.onError(UpgradeFailedException.class, e -> {
errorLabel.setText(e.getLocalizedMessage());
}) //
.andFinally(() -> {
progressIndicator.setVisible(false);
passwordField.setDisable(false);
passwordField.swipe();
}).runOnce(executor);
}
private void showNextUpgrade() {
errorLabel.setText(null);
UpgradeStrategy nextStrategy = strategies.getUpgradeStrategy(vault);
if (nextStrategy != null) {
strategy.set(nextStrategy);
} else {
listener.ifPresent(UpgradeListener::didUpgrade);
}
}
/* callback */
public void setListener(UpgradeListener listener) {
this.listener = Optional.ofNullable(listener);
}
@FunctionalInterface
interface UpgradeListener {
void didUpgrade();
}
}

View File

@@ -1,31 +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.ui.controllers;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
public interface ViewController extends Initializable {
Parent getRoot();
@Override
default void initialize(URL location, ResourceBundle resources) {
initialize();
}
default void initialize() {
// no-op
}
default void focus() {
// no-op
}
}

View File

@@ -1,51 +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.ui.controllers;
import javafx.fxml.FXMLLoader;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.ui.l10n.Localization;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Map;
@FxApplicationScoped
public class ViewControllerLoader {
private final Map<Class<? extends ViewController>, Provider<ViewController>> controllerProviders;
private final Localization localization;
@Inject
public ViewControllerLoader(Map<Class<? extends ViewController>, Provider<ViewController>> controllerProviders, Localization localization) {
this.controllerProviders = controllerProviders;
this.localization = localization;
}
public <T extends ViewController> T load(String resourceName) {
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(this::constructController);
loader.setResources(localization);
try (InputStream in = getClass().getResourceAsStream(resourceName)) {
loader.load(in);
} catch (IOException e) {
throw new UncheckedIOException("Error loading FXML: " + resourceName, e);
}
return loader.getController();
}
private ViewController constructController(Class<?> clazz) {
Provider<ViewController> ctrlProvider = controllerProviders.get(clazz);
if (ctrlProvider == null) {
throw new IllegalStateException("No provider for type " + clazz.getName() + " registered.");
}
return ctrlProvider.get();
}
}

View File

@@ -1,78 +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.ui.controllers;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
@Module
public class ViewControllerModule {
@Provides
@IntoMap
@ViewControllerKey(ChangePasswordController.class)
ViewController provideChangePasswordController(ChangePasswordController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(InitializeController.class)
ViewController provideInitializeController(InitializeController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(MainController.class)
ViewController provideMainController(MainController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(NotFoundController.class)
ViewController provideNotFoundController(NotFoundController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(SettingsController.class)
ViewController provideSettingsController(SettingsController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(UnlockController.class)
ViewController provideUnlockController(UnlockController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(UnlockedController.class)
ViewController provideUnlockedController(UnlockedController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(UpgradeController.class)
ViewController provideUpgradeController(UpgradeController controller) {
return controller;
}
@Provides
@IntoMap
@ViewControllerKey(WelcomeController.class)
ViewController provideWelcomeController(WelcomeController controller) {
return controller;
}
}

View File

@@ -1,195 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.VBox;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.FxApplicationScoped;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.util.Tasks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.cryptomator.ui.util.DialogBuilderUtil.buildYesNoDialog;
@FxApplicationScoped
public class WelcomeController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
private final Application app;
private final Optional<String> applicationVersion;
private final Localization localization;
private final Settings settings;
private final Comparator<String> semVerComparator;
private final ScheduledExecutorService executor;
@Inject
public WelcomeController(Application app, @Named("applicationVersion") Optional<String> applicationVersion, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator,
ScheduledExecutorService executor) {
this.app = app;
this.applicationVersion = applicationVersion;
this.localization = localization;
this.settings = settings;
this.semVerComparator = semVerComparator;
this.executor = executor;
}
@FXML
private Node checkForUpdatesContainer;
@FXML
private Label checkForUpdatesStatus;
@FXML
private ProgressIndicator checkForUpdatesIndicator;
@FXML
private Hyperlink updateLink;
@FXML
private VBox root;
@Override
public void initialize(URL location, ResourceBundle resources) {
if (areUpdatesManagedExternally()) {
checkForUpdatesContainer.setVisible(false);
} else if (!settings.askedForUpdateCheck().get()) {
this.askForUpdateCheck();
} else if (settings.checkForUpdates().get()) {
this.checkForUpdates();
}
}
@Override
public Parent getRoot() {
return root;
}
// ****************************************
// Check for updates
// ****************************************
private boolean areUpdatesManagedExternally() {
return Boolean.parseBoolean(System.getProperty("cryptomator.updatesManagedExternally", "false"));
}
private void askForUpdateCheck() {
Tasks.create(() -> {}).onSuccess(() -> {
Optional<ButtonType> result = buildYesNoDialog(
localization.getString("welcome.askForUpdateCheck.dialog.title"),
localization.getString("welcome.askForUpdateCheck.dialog.header"),
localization.getString("welcome.askForUpdateCheck.dialog.content"),
ButtonType.YES).showAndWait();
if (result.isPresent()) {
settings.askedForUpdateCheck().set(true);
settings.checkForUpdates().set(result.get().equals(ButtonType.YES));
}
if (settings.checkForUpdates().get()) {
this.checkForUpdates();
}
}).scheduleOnce(executor, 1, TimeUnit.SECONDS);
}
private void checkForUpdates() {
checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking"));
checkForUpdatesIndicator.setVisible(true);
Tasks.create(() -> {
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", applicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
URL url = URI.create("https://api.cryptomator.org/updates/latestVersion.json").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.addRequestProperty("User-Agent", userAgent);
conn.connect();
try {
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
return Optional.<byte[]>empty();
}
try (InputStream in = conn.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
in.transferTo(out);
return Optional.of(out.toByteArray());
}
} finally {
conn.disconnect();
}
}).onSuccess(response -> {
response.ifPresent(bytes -> {
Gson gson = new GsonBuilder().setLenient().create();
String json = new String(bytes, StandardCharsets.UTF_8);
Map<String, String> map = gson.fromJson(json, new TypeToken<Map<String, String>>() {
}.getType());
if (map != null) {
this.compareVersions(map);
}
});
}).onError(Exception.class, e -> {
LOG.warn("Error checking for updates", e);
}).andFinally(() -> {
checkForUpdatesStatus.setText("");
checkForUpdatesIndicator.setVisible(false);
}).runOnce(executor);
}
private void compareVersions(final Map<String, String> latestVersions) {
assert Platform.isFxApplicationThread();
final String latestVersion;
if (SystemUtils.IS_OS_MAC_OSX) {
latestVersion = latestVersions.get("mac");
} else if (SystemUtils.IS_OS_WINDOWS) {
latestVersion = latestVersions.get("win");
} else if (SystemUtils.IS_OS_LINUX) {
latestVersion = latestVersions.get("linux");
} else {
// no version check possible on unsupported OS
return;
}
final String currentVersion = applicationVersion.orElse(null);
LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion);
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
final String msg = String.format(localization.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
this.updateLink.setText(msg);
this.updateLink.setVisible(true);
this.updateLink.setDisable(false);
}
}
@FXML
public void didClickUpdateLink(ActionEvent event) {
app.getHostServices().showDocument("https://cryptomator.org/");
}
}

View File

@@ -0,0 +1,20 @@
package org.cryptomator.ui.controls;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import java.util.regex.Pattern;
public class AlphanumericTextField extends TextField {
private final static Pattern DIGIT_PATTERN = Pattern.compile("\\w*");
public AlphanumericTextField() {
this.setTextFormatter(new TextFormatter<>(this::filterNumericTextChange));
}
private TextFormatter.Change filterNumericTextChange(TextFormatter.Change change) {
return DIGIT_PATTERN.matcher(change.getText()).matches() ? change : null;
}
}

View File

@@ -1,105 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
* All rights reserved.
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.controls;
import org.cryptomator.ui.model.Vault;
import org.fxmisc.easybind.EasyBind;
import javafx.beans.binding.ObjectExpression;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
public class DirectoryListCell extends DraggableListCell<Vault> {
private static final Color UNLOCKED_ICON_COLOR = new Color(0.901, 0.494, 0.133, 1.0);
private final Label statusText = new Label();
private final Label nameText = new Label();
private final Label pathText = new Label();
private final VBox vbox = new VBox(4.0, nameText, pathText);
private final HBox hbox = new HBox(6.0, statusText, vbox);
private ContextMenu vaultContextMenu;
public DirectoryListCell() {
ObjectExpression<Vault.State> vaultState = ObjectExpression.objectExpression(EasyBind.select(itemProperty()).selectObject(Vault::stateProperty));
hbox.setAlignment(Pos.CENTER_LEFT);
hbox.setPrefWidth(1);
vbox.setFillWidth(true);
nameText.textProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::name));
nameText.textFillProperty().bind(this.textFillProperty());
nameText.fontProperty().bind(this.fontProperty());
pathText.textProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::displayablePath));
pathText.setTextOverrun(OverrunStyle.ELLIPSIS);
pathText.getStyleClass().add("detail-label");
statusText.textProperty().bind(EasyBind.map(vaultState, this::getStatusIconText));
statusText.textFillProperty().bind(EasyBind.combine(vaultState, textFillProperty(), this::getStatusIconColor));
statusText.setMinSize(16.0, 16.0);
statusText.setAlignment(Pos.CENTER);
statusText.getStyleClass().add("fontawesome");
tooltipProperty().bind(EasyBind.monadic(itemProperty()).flatMap(Vault::displayablePath).map(p -> new Tooltip(p.toString())));
contextMenuProperty().bind(EasyBind.map(vaultState, this::getContextMenu));
setGraphic(hbox);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
private String getStatusIconText(Vault.State state) {
if (state == null) {
return "";
}
switch (state) {
case UNLOCKED:
case PROCESSING:
return "\uf09c";
case LOCKED:
default:
return "\uf023";
}
}
private Paint getStatusIconColor(Vault.State state, Paint lockedValue) {
if (state == null) {
return lockedValue;
}
switch (state) {
case UNLOCKED:
case PROCESSING:
return UNLOCKED_ICON_COLOR;
case LOCKED:
default:
return lockedValue;
}
}
private ContextMenu getContextMenu(Vault.State state) {
if (state == Vault.State.LOCKED) {
return vaultContextMenu;
} else {
return null;
}
}
public void setVaultContextMenu(ContextMenu contextMenu) {
this.vaultContextMenu = contextMenu;
}
}

View File

@@ -8,11 +8,8 @@
*******************************************************************************/
package org.cryptomator.ui.controls;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.geometry.Insets;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
@@ -21,22 +18,17 @@ import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderImage;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import org.fxmisc.easybind.EasyBind;
class DraggableListCell<T> extends ListCell<T> {
import java.util.List;
private static final double DROP_LINE_WIDTH = 4.0;
private static final Paint DROP_LINE_COLOR = Color.gray(0.0, 0.6);
private final List<BorderStroke> defaultBorderStrokes;
private final List<BorderImage> defaultBorderImages;
public class DraggableListCell<T> extends ListCell<T> {
private static final String DROP_ABOVE_CLASS = "drop-above";
private static final String DROP_BELOW_CLASS = "drop-below";
private final BooleanProperty dropAbove = new SimpleBooleanProperty();
private final BooleanProperty dropBelow = new SimpleBooleanProperty();
public DraggableListCell() {
setOnDragDetected(this::onDragDetected);
@@ -45,19 +37,25 @@ class DraggableListCell<T> extends ListCell<T> {
setOnDragExited(this::onDragExited);
setOnDragDropped(this::onDragDropped);
setOnDragDone(DragEvent::consume);
this.defaultBorderStrokes = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getStrokes();
this.defaultBorderImages = this.getBorder() == null ? Collections.emptyList() : this.getBorder().getImages();
}
private Border createDropPositionBorder(double verticalCursorPosition) {
EasyBind.includeWhen(getStyleClass(), DROP_ABOVE_CLASS, dropAbove);
EasyBind.includeWhen(getStyleClass(), DROP_BELOW_CLASS, dropBelow);
}
private void setDropPositionStyleClass(double verticalCursorPosition) {
boolean isUpperHalf = verticalCursorPosition < this.getHeight() / 2.0;
final double topBorder = isUpperHalf ? DROP_LINE_WIDTH : 0.0;
final double bottomBorder = !isUpperHalf ? DROP_LINE_WIDTH : 0.0;
final BorderWidths borderWidths = new BorderWidths(topBorder, 0.0, bottomBorder, 0.0);
final BorderStroke dragStroke = new BorderStroke(DROP_LINE_COLOR, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, borderWidths, Insets.EMPTY);
final List<BorderStroke> strokes = new ArrayList<BorderStroke>(defaultBorderStrokes);
strokes.add(0, dragStroke);
return new Border(strokes, defaultBorderImages);
if (isUpperHalf) {
this.dropAbove.set(true);
this.dropBelow.set(false);
} else {
this.dropAbove.set(false);
this.dropBelow.set(true);
}
}
private void resetDropPositionStyleClasses() {
this.dropAbove.set(false);
this.dropBelow.set(false);
}
private void onDragDetected(MouseEvent event) {
@@ -82,7 +80,7 @@ class DraggableListCell<T> extends ListCell<T> {
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
event.acceptTransferModes(TransferMode.MOVE);
setBorder(createDropPositionBorder(event.getY()));
setDropPositionStyleClass(event.getY());
}
event.consume();
@@ -94,7 +92,7 @@ class DraggableListCell<T> extends ListCell<T> {
}
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
setBorder(createDropPositionBorder(event.getY()));
setDropPositionStyleClass(event.getY());
}
}
@@ -104,7 +102,7 @@ class DraggableListCell<T> extends ListCell<T> {
}
if (event.getGestureSource() instanceof DraggableListCell<?> && event.getGestureSource() != this && event.getDragboard().hasString()) {
setBorder(new Border(defaultBorderStrokes, defaultBorderImages));
resetDropPositionStyleClasses();
}
}

View File

@@ -0,0 +1,40 @@
package org.cryptomator.ui.controls;
/**
* Inspired by de.jensd:fontawesomefx-fontawesome
*/
public enum FontAwesome5Icon {
ANCHOR("\uF13D"), //
ARROW_ALT_UP("\uF357"), //
CHECK("\uF00C"), //
COG("\uF013"), //
COGS("\uF085"), //
EXCLAMATION_TRIANGLE("\uF071"), //
EYE("\uF06E"), //
EYE_SLASH("\uF070"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
HDD("\uF0A0"), //
KEY("\uF084"), //
LOCK_ALT("\uF30D"), //
LOCK_OPEN_ALT("\uF3C2"), //
MINUS("\uF068"), //
PLUS("\uF067"), //
QUESTION("\uF128"), //
SPARKLES("\uF890"), //
SPINNER("\uF110"), //
SYNC("\uF021"), //
TIMES("\uF00D"), //
WRENCH("\uF0AD"), //
;
private final String unicode;
FontAwesome5Icon(String unicode) {
this.unicode = unicode;
}
public String unicode() {
return unicode;
}
}

View File

@@ -0,0 +1,77 @@
package org.cryptomator.ui.controls;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import org.cryptomator.ui.common.FontLoader;
import java.io.UncheckedIOException;
/**
* Inspired by de.jensd:fontawesomefx-fontawesome
*/
public class FontAwesome5IconView extends Text {
private static final FontAwesome5Icon DEFAULT_GLYPH = FontAwesome5Icon.ANCHOR;
private static final double DEFAULT_GLYPH_SIZE = 12.0;
private static final String FONT_PATH = "/css/fontawesome5-pro-solid.otf";
private static final Font FONT;
private ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
private DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
static {
try {
FONT = FontLoader.load(FONT_PATH);
} catch (FontLoader.FontLoaderException e) {
throw new UncheckedIOException(e);
}
}
public FontAwesome5IconView() {
getStyleClass().addAll("glyph-icon");
glyphProperty().addListener(this::glyphChanged);
glyphSizeProperty().addListener(this::glyphSizeChanged);
setFont(FONT);
setGlyph(DEFAULT_GLYPH);
setGlyphSize(DEFAULT_GLYPH_SIZE);
}
private void glyphChanged(@SuppressWarnings("unused") ObservableValue<? extends FontAwesome5Icon> observable, @SuppressWarnings("unused") FontAwesome5Icon oldValue, FontAwesome5Icon newValue) {
setText(newValue.unicode());
}
private void glyphSizeChanged(@SuppressWarnings("unused") ObservableValue<? extends Number> observable, @SuppressWarnings("unused") Number oldValue, Number newValue) {
setFont(new Font(FONT.getFamily(), newValue.doubleValue()));
}
/* Getter/Setter */
public ObjectProperty<FontAwesome5Icon> glyphProperty() {
return glyph;
}
public void setGlyph(FontAwesome5Icon glyph) {
this.glyph.set(glyph == null ? DEFAULT_GLYPH : glyph);
}
public FontAwesome5Icon getGlyph() {
return glyph.get();
}
public DoubleProperty glyphSizeProperty() {
return glyphSize;
}
public void setGlyphSize(double glyphSize) {
this.glyphSize.set(glyphSize);
}
public double getGlyphSize() {
return glyphSize.get();
}
}

View File

@@ -0,0 +1,54 @@
package org.cryptomator.ui.controls;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
public class FormattedLabel extends Label {
private final StringProperty format = new SimpleStringProperty("");
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
// TODO: add arg2, arg3, ... on demand
public FormattedLabel() {
textProperty().bind(createStringBinding());
}
protected StringBinding createStringBinding() {
return Bindings.createStringBinding(this::updateText, format, arg1);
}
private String updateText() {
return String.format(format.get(), arg1.get());
}
/* Observables */
public StringProperty formatProperty() {
return format;
}
public String getFormat() {
return format.get();
}
public void setFormat(String format) {
this.format.set(format);
}
public ObjectProperty<Object> arg1Property() {
return arg1;
}
public Object getArg1() {
return arg1.get();
}
public void setArg1(Object arg1) {
this.arg1.set(arg1);
}
}

View File

@@ -0,0 +1,94 @@
package org.cryptomator.ui.controls;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
public class NiceSecurePasswordField extends StackPane {
private static final String STYLE_CLASS = "nice-secure-password-field";
private static final String ICONS_STLYE_CLASS = "icons";
private static final String REVEAL_BUTTON_STLYE_CLASS = "reveal-button";
private static final int ICON_SPACING = 6;
private static final double ICON_SIZE = 14.0;
private final SecurePasswordField passwordField = new SecurePasswordField();
private final FontAwesome5IconView capsLockedIcon = new FontAwesome5IconView();
private final FontAwesome5IconView nonPrintableCharsIcon = new FontAwesome5IconView();
private final FontAwesome5IconView revealPasswordIcon = new FontAwesome5IconView();
private final ToggleButton revealPasswordButton = new ToggleButton(null, revealPasswordIcon);
private final HBox iconContainer = new HBox(ICON_SPACING, nonPrintableCharsIcon, capsLockedIcon, revealPasswordButton);
public NiceSecurePasswordField() {
getStyleClass().add(STYLE_CLASS);
iconContainer.setAlignment(Pos.CENTER_RIGHT);
iconContainer.setMaxWidth(Double.NEGATIVE_INFINITY);
iconContainer.setPrefWidth(42); // TODO
iconContainer.getStyleClass().add(ICONS_STLYE_CLASS);
StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT);
capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_ALT_UP);
capsLockedIcon.setGlyphSize(ICON_SIZE);
capsLockedIcon.visibleProperty().bind(passwordField.capsLockedProperty());
capsLockedIcon.managedProperty().bind(passwordField.capsLockedProperty());
nonPrintableCharsIcon.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
nonPrintableCharsIcon.setGlyphSize(ICON_SIZE);
nonPrintableCharsIcon.visibleProperty().bind(passwordField.containingNonPrintableCharsProperty());
nonPrintableCharsIcon.managedProperty().bind(passwordField.containingNonPrintableCharsProperty());
revealPasswordIcon.setGlyph(FontAwesome5Icon.EYE);
revealPasswordIcon.glyphProperty().bind(Bindings.createObjectBinding(this::getRevealPasswordGlyph, revealPasswordButton.selectedProperty()));
revealPasswordIcon.setGlyphSize(ICON_SIZE);
revealPasswordButton.setContentDisplay(ContentDisplay.LEFT);
revealPasswordButton.setFocusTraversable(false);
revealPasswordButton.visibleProperty().bind(passwordField.focusedProperty());
revealPasswordButton.managedProperty().bind(passwordField.focusedProperty());
revealPasswordButton.getStyleClass().add(REVEAL_BUTTON_STLYE_CLASS);
passwordField.revealPasswordProperty().bind(revealPasswordButton.selectedProperty());
getChildren().addAll(passwordField, iconContainer);
}
private FontAwesome5Icon getRevealPasswordGlyph() {
return revealPasswordButton.isSelected() ? FontAwesome5Icon.EYE_SLASH : FontAwesome5Icon.EYE;
}
/* Passthrough */
@Override
public void requestFocus() {
passwordField.requestFocus();
}
public StringProperty textProperty() {
return passwordField.textProperty();
}
public CharSequence getCharacters() {
return passwordField.getCharacters();
}
public void setPassword(char[] password) {
passwordField.setPassword(password);
}
public void swipe() {
passwordField.swipe();;
}
public void selectAll() {
passwordField.selectAll();
}
public void selectRange(int anchor, int caretPosition) {
passwordField.selectRange(anchor, caretPosition);
}
}

View File

@@ -0,0 +1,20 @@
package org.cryptomator.ui.controls;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import java.util.regex.Pattern;
public class NumericTextField extends TextField {
private final static Pattern DIGIT_PATTERN = Pattern.compile("\\d*");
public NumericTextField() {
this.setTextFormatter(new TextFormatter<>(this::filterNumericTextChange));
}
private TextFormatter.Change filterNumericTextChange(TextFormatter.Change change) {
return DIGIT_PATTERN.matcher(change.getText()).matches() ? change : null;
}
}

View File

@@ -0,0 +1,85 @@
package org.cryptomator.ui.controls;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import org.fxmisc.easybind.EasyBind;
public class PasswordStrengthIndicator extends HBox {
private static final String STYLECLASS = "password-strength-indicator";
private static final String SEGMENT_CLASS = "segment";
private static final String ACTIVE_SEGMENT_CLASS = "active";
private static final String STRENGTH_0_CLASS = "strength-0";
private static final String STRENGTH_1_CLASS = "strength-1";
private static final String STRENGTH_2_CLASS = "strength-2";
private static final String STRENGTH_3_CLASS = "strength-3";
private static final String STRENGTH_4_CLASS = "strength-4";
private final Region s0;
private final Region s1;
private final Region s2;
private final Region s3;
private final Region s4;
private final IntegerProperty strength = new SimpleIntegerProperty();
private final BooleanBinding isStrength0 = strength.isEqualTo(0);
private final BooleanBinding isStrength1 = strength.isEqualTo(1);
private final BooleanBinding isStrength2 = strength.isEqualTo(2);
private final BooleanBinding isStrength3 = strength.isEqualTo(3);
private final BooleanBinding isStrength4 = strength.isEqualTo(4);
private final BooleanBinding isMinimumStrength0 = strength.greaterThanOrEqualTo(0);
private final BooleanBinding isMinimumStrength1 = strength.greaterThanOrEqualTo(1);
private final BooleanBinding isMinimumStrength2 = strength.greaterThanOrEqualTo(2);
private final BooleanBinding isMinimumStrength3 = strength.greaterThanOrEqualTo(3);
private final BooleanBinding isMinimumStrength4 = strength.greaterThanOrEqualTo(4);
public PasswordStrengthIndicator() {
this.s0 = new Region();
this.s1 = new Region();
this.s2 = new Region();
this.s3 = new Region();
this.s4 = new Region();
getChildren().addAll(s0, s1, s2, s3, s4);
setHgrow(s0, Priority.ALWAYS);
setHgrow(s1, Priority.ALWAYS);
setHgrow(s2, Priority.ALWAYS);
setHgrow(s3, Priority.ALWAYS);
setHgrow(s4, Priority.ALWAYS);
getStyleClass().add(STYLECLASS);
s0.getStyleClass().add(SEGMENT_CLASS);
s1.getStyleClass().add(SEGMENT_CLASS);
s2.getStyleClass().add(SEGMENT_CLASS);
s3.getStyleClass().add(SEGMENT_CLASS);
s4.getStyleClass().add(SEGMENT_CLASS);
EasyBind.includeWhen(s0.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength0);
EasyBind.includeWhen(s1.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength1);
EasyBind.includeWhen(s2.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength2);
EasyBind.includeWhen(s3.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength3);
EasyBind.includeWhen(s4.getStyleClass(), ACTIVE_SEGMENT_CLASS, isMinimumStrength4);
EasyBind.includeWhen(getStyleClass(), STRENGTH_0_CLASS, isStrength0);
EasyBind.includeWhen(getStyleClass(), STRENGTH_1_CLASS, isStrength1);
EasyBind.includeWhen(getStyleClass(), STRENGTH_2_CLASS, isStrength2);
EasyBind.includeWhen(getStyleClass(), STRENGTH_3_CLASS, isStrength3);
EasyBind.includeWhen(getStyleClass(), STRENGTH_4_CLASS, isStrength4);
}
/* Observables */
public IntegerProperty strengthProperty() {
return strength;
}
public void setStrength(int strength) {
this.strength.set(strength);
}
public int getStrength() {
return strength.get();
}
}

View File

@@ -11,20 +11,19 @@ package org.cryptomator.ui.controls;
import com.google.common.base.Strings;
import javafx.beans.NamedArg;
import javafx.beans.Observable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.control.IndexRange;
import javafx.scene.control.PasswordField;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TextField;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import java.awt.Toolkit;
import java.nio.CharBuffer;
@@ -33,54 +32,54 @@ import java.text.Normalizer.Form;
import java.util.Arrays;
/**
* Patched PasswordField that doesn't create String copies of the password in memory. Instead the password is stored in a char[] that can be swiped.
* Patched PasswordField that doesn't create String copies of the password in memory (unless explicitly revealed). Instead the password is stored in a char[] that can be swiped.
*
* @implNote Since {@link #setText(String)} is final, we can not override its behaviour. For that reason you should not use the {@link #textProperty()} for anything else than display purposes.
*/
public class SecPasswordField extends PasswordField {
public class SecurePasswordField extends TextField {
private static final char SWIPE_CHAR = ' ';
private static final int INITIAL_BUFFER_SIZE = 50;
private static final int GROW_BUFFER_SIZE = 50;
private static final String PLACEHOLDER = "*";
private static final double PADDING = 2.0;
private static final double INDICATOR_PADDING = 4.0;
private static final Color INDICATOR_COLOR = new Color(0.901, 0.494, 0.133, 1.0);
private static final String DEFAULT_PLACEHOLDER = "";
private static final String STYLE_CLASS = "secure-password-field";
private final Tooltip tooltip = new Tooltip();
private final Label indicator = new Label();
private final String nonPrintableCharsWarning;
private final String capslockWarning;
private final String placeholderChar;
private final BooleanProperty capsLocked = new SimpleBooleanProperty();
private final BooleanProperty containingNonPrintableChars = new SimpleBooleanProperty();
private final BooleanProperty revealPassword = new SimpleBooleanProperty();
private char[] content = new char[INITIAL_BUFFER_SIZE];
private int length = 0;
public SecPasswordField() {
this("", "");
public SecurePasswordField() {
this(DEFAULT_PLACEHOLDER);
}
public SecPasswordField(@NamedArg("nonPrintableCharsWarning") String nonPrintableCharsWarning, @NamedArg("capslockWarning") String capslockWarning) {
this.nonPrintableCharsWarning = nonPrintableCharsWarning;
this.capslockWarning = capslockWarning;
indicator.setPadding(new Insets(PADDING, INDICATOR_PADDING, PADDING, INDICATOR_PADDING));
indicator.setAlignment(Pos.CENTER_RIGHT);
indicator.setMouseTransparent(true);
indicator.setTextOverrun(OverrunStyle.CLIP);
indicator.setTextFill(INDICATOR_COLOR);
indicator.setFont(Font.font(indicator.getFont().getFamily(), 15.0));
this.getChildren().add(indicator);
this.setTooltip(tooltip);
public SecurePasswordField(@NamedArg("placeholderChar") String placeholderChar) {
this.getStyleClass().add(STYLE_CLASS);
this.placeholderChar = placeholderChar;
this.setAccessibleRole(AccessibleRole.PASSWORD_FIELD);
this.addEventHandler(DragEvent.DRAG_OVER, this::handleDragOver);
this.addEventHandler(DragEvent.DRAG_DROPPED, this::handleDragDropped);
this.addEventHandler(KeyEvent.ANY, this::handleKeyEvent);
this.revealPasswordProperty().addListener(this::revealPasswordChanged);
this.focusedProperty().addListener(this::focusedChanged);
}
@Override
protected void layoutChildren() {
super.layoutChildren();
indicator.relocate(0.0, 0.0);
indicator.resize(getWidth(), getHeight());
public void cut() {
}
public void copy() {
}
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch(attribute) {
case TEXT:
return null;
default:
return super.queryAccessibleAttribute(attribute, parameters);
}
}
private void handleDragOver(DragEvent event) {
@@ -101,42 +100,32 @@ public class SecPasswordField extends PasswordField {
private void handleKeyEvent(KeyEvent e) {
if (e.getCode() == KeyCode.CAPS) {
updateVisualHints(true);
updateCapsLocked();
}
}
private void revealPasswordChanged(@SuppressWarnings("unused") Observable observable) {
IndexRange selection = getSelection();
if (isRevealPassword()) {
super.setText(this.getCharacters().toString());
} else {
String placeholderText = Strings.repeat(placeholderChar, length);
super.setText(placeholderText);
}
selectRange(selection.getStart(), selection.getEnd());
}
private void focusedChanged(@SuppressWarnings("unused") Observable observable) {
updateVisualHints(isFocused());
updateCapsLocked();
}
private void updateVisualHints(boolean focused) {
StringBuilder tooltipSb = new StringBuilder();
StringBuilder indicatorSb = new StringBuilder();
if (containsNonPrintableCharacters()) {
indicatorSb.append('⚠');
tooltipSb.append("- ").append(nonPrintableCharsWarning).append('\n');
}
private void updateCapsLocked() {
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
if (focused && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK)) {
indicatorSb.append('⇪');
tooltipSb.append("- ").append(capslockWarning).append('\n');
}
indicator.setText(indicatorSb.toString());
if (!indicator.getText().isEmpty()) {
setPadding(new Insets(PADDING, getIndicatorWidth(), PADDING, PADDING));
} else {
setPadding(new Insets(PADDING));
}
tooltip.setText(tooltipSb.toString());
if (tooltip.getText().isEmpty()) {
setTooltip(null);
} else {
setTooltip(tooltip);
}
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
}
private double getIndicatorWidth() {
return new Text(indicator.getText()).getLayoutBounds().getWidth() + INDICATOR_PADDING * 2.0;
private void updateContainingNonPrintableChars() {
containingNonPrintableChars.set(containsNonPrintableCharacters());
}
/**
@@ -184,9 +173,13 @@ public class SecPasswordField extends PasswordField {
normalizedText.getChars(0, normalizedText.length(), content, start);
// trigger visual hints
updateVisualHints(true);
String placeholderString = Strings.repeat(PLACEHOLDER, normalizedText.length());
super.replaceText(start, end, placeholderString);
updateContainingNonPrintableChars();
if (isRevealPassword()) {
super.replaceText(start, end, text);
} else {
String placeholderString = Strings.repeat(placeholderChar, normalizedText.length());
super.replaceText(start, end, placeholderString);
}
}
private void growContentIfNeeded() {
@@ -201,9 +194,9 @@ public class SecPasswordField extends PasswordField {
/**
* Creates a CharSequence by wrapping the password characters.
*
* @return A character sequence backed by the SecPasswordField's buffer (not a copy).
* @return A character sequence backed by the SecurePasswordField's buffer (not a copy).
* @implNote The CharSequence will not copy the backing char[].
* Therefore any mutation to the SecPasswordField's content will mutate or eventually swipe the returned CharSequence.
* Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
* @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
* @see #swipe()
*/
@@ -239,7 +232,7 @@ public class SecPasswordField extends PasswordField {
content = Arrays.copyOf(password, password.length);
length = password.length;
String placeholderString = Strings.repeat(PLACEHOLDER, password.length);
String placeholderString = Strings.repeat(placeholderChar, password.length);
setText(placeholderString);
}
@@ -256,4 +249,33 @@ public class SecPasswordField extends PasswordField {
Arrays.fill(buffer, SWIPE_CHAR);
}
/* Observable Properties */
public ReadOnlyBooleanProperty capsLockedProperty() {
return capsLocked;
}
public boolean isCapsLocked() {
return capsLocked.get();
}
public ReadOnlyBooleanProperty containingNonPrintableCharsProperty() {
return containingNonPrintableChars;
}
public boolean isContainingNonPrintableChars() {
return containingNonPrintableChars.get();
}
public BooleanProperty revealPasswordProperty() {
return revealPassword;
}
public boolean isRevealPassword() {
return revealPassword.get();
}
public void setRevealPassword(boolean revealPassword) {
this.revealPassword.set(revealPassword);
}
}

View File

@@ -0,0 +1,92 @@
package org.cryptomator.ui.controls;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
public class ThrougputLabel extends Label {
private static final long kibsThreshold = 1l << 7; // 0.128 kiB/s
private static final long mibsThreshold = 1l << 19; // 0.512 MiB/s
private final StringProperty idleFormat = new SimpleStringProperty("-");
private final StringProperty kibsFormat = new SimpleStringProperty("%.3f");
private final StringProperty mibsFormat = new SimpleStringProperty("%.3f");
private final LongProperty bytesPerSecond = new SimpleLongProperty();
public ThrougputLabel() {
textProperty().bind(createStringBinding());
}
protected StringBinding createStringBinding() {
return Bindings.createStringBinding(this::updateText, kibsFormat, mibsFormat, bytesPerSecond);
}
private String updateText() {
long bps = bytesPerSecond.get();
if (bps > mibsThreshold) {
double mibs = ((double) bytesPerSecond.get()) / 1024.0 / 1024.0;
return String.format(mibsFormat.get(), mibs);
} else if (bps > kibsThreshold) {
double kibs = ((double) bytesPerSecond.get()) / 1024.0;
return String.format(kibsFormat.get(), kibs);
} else {
return String.format(idleFormat.get(), bps);
}
}
/* Observables */
public StringProperty idleFormatProperty() {
return idleFormat;
}
public String getIdleFormat() {
return idleFormat.get();
}
public void setIdleFormat(String idleFormat) {
this.idleFormat.set(idleFormat);
}
public StringProperty kibsFormatProperty() {
return kibsFormat;
}
public String getKibsFormat() {
return kibsFormat.get();
}
public void setKibsFormat(String kibsFormat) {
this.kibsFormat.set(kibsFormat);
}
public StringProperty mibsFormatProperty() {
return mibsFormat;
}
public String getMibsFormat() {
return mibsFormat.get();
}
public void setMibsFormat(String mibsFormat) {
this.mibsFormat.set(mibsFormat);
}
public LongProperty bytesPerSecondProperty() {
return bytesPerSecond;
}
public long getBytesPerSecond() {
return bytesPerSecond.get();
}
public void setBytesPerSecond(long bytesPerSecond) {
this.bytesPerSecond.set(bytesPerSecond);
}
}

View File

@@ -0,0 +1,51 @@
package org.cryptomator.ui.forgetPassword;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Named;
import java.util.concurrent.CompletableFuture;
@ForgetPasswordScoped
@Subcomponent(modules = {ForgetPasswordModule.class})
public interface ForgetPasswordComponent {
@ForgetPasswordWindow
ReadOnlyBooleanProperty confirmedProperty();
@ForgetPasswordWindow
Stage window();
@FxmlScene(FxmlFile.FORGET_PASSWORD)
Lazy<Scene> scene();
default CompletableFuture<Boolean> showForgetPassword() {
CompletableFuture<Boolean> result = new CompletableFuture<>();
Stage stage = window();
stage.setScene(scene().get());
stage.sizeToScene();
stage.show();
stage.setOnHidden(evt -> result.complete(confirmedProperty().get()));
return result;
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@ForgetPasswordWindow Vault vault);
@BindsInstance
Builder owner(@Named("forgetPasswordOwner") Stage owner);
ForgetPasswordComponent build();
}
}

View File

@@ -0,0 +1,53 @@
package org.cryptomator.ui.forgetPassword;
import javafx.beans.property.BooleanProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.Optional;
@ForgetPasswordScoped
public class ForgetPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ForgetPasswordController.class);
private final Stage window;
private final Vault vault;
private final Optional<KeychainAccess> keychainAccess;
private final BooleanProperty confirmedResult;
@Inject
public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional<KeychainAccess> keychainAccess, @ForgetPasswordWindow BooleanProperty confirmedResult) {
this.window = window;
this.vault = vault;
this.keychainAccess = keychainAccess;
this.confirmedResult = confirmedResult;
}
@FXML
public void close() {
window.close();
}
@FXML
public void finish() {
if (keychainAccess.isPresent()) {
try {
keychainAccess.get().deletePassphrase(vault.getId());
LOG.debug("Forgot password for vault {}.", vault.getDisplayableName());
confirmedResult.setValue(true);
} catch (KeychainAccessException e) {
LOG.error("Failed to remove entry from system keychain.", e);
confirmedResult.setValue(false);
}
}
window.close();
}
}

View File

@@ -0,0 +1,74 @@
package org.cryptomator.ui.forgetPassword;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.ui.common.FXMLLoaderFactory;
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 javax.inject.Named;
import javax.inject.Provider;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module
abstract class ForgetPasswordModule {
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, ResourceBundle resourceBundle) {
return new FXMLLoaderFactory(factories, resourceBundle);
}
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcon") Optional<Image> windowIcon, @Named("forgetPasswordOwner") Stage owner) {
Stage stage = new Stage();
stage.setTitle(resourceBundle.getString("forgetPassword.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
windowIcon.ifPresent(stage.getIcons()::add);
return stage;
}
@Provides
@FxmlScene(FxmlFile.FORGET_PASSWORD)
@ForgetPasswordScoped
static Scene provideForgetPasswordScene(@ForgetPasswordWindow FXMLLoaderFactory fxmlLoaders, @ForgetPasswordWindow Stage window) {
return fxmlLoaders.createScene("/fxml/forget_password.fxml");
}
@Provides
@ForgetPasswordWindow
@ForgetPasswordScoped
static BooleanProperty provideConfirmedProperty() {
return new SimpleBooleanProperty(false);
}
@Binds
@ForgetPasswordWindow
@ForgetPasswordScoped
abstract ReadOnlyBooleanProperty bindReadOnlyConfirmedProperty(@ForgetPasswordWindow BooleanProperty confirmedProperty);
// ------------------
@Binds
@IntoMap
@FxControllerKey(ForgetPasswordController.class)
abstract FxController bindForgetPasswordController(ForgetPasswordController controller);
}

Some files were not shown because too many files have changed in this diff Show More