diff --git a/.gitignore b/.gitignore index 246e7ffff..5ddd9fc95 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,9 @@ .classpath target/ test-output/ + +# IntelliJ Settings Files # +.idea/ +out/ +.idea_modules/ +*.iws diff --git a/.travis.yml b/.travis.yml index ff30b781b..611045f47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' ht script: mvn -fmain/pom.xml clean test -after_success: mvn -fmain/pom.xml clean test jacoco:report coveralls:report +after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report notifications: webhooks: diff --git a/main/ant-kit/src/main/resources/package/linux/Cryptomator.png b/main/ant-kit/src/main/resources/package/linux/Cryptomator.png index 99d9fb876..1e0832c42 100644 Binary files a/main/ant-kit/src/main/resources/package/linux/Cryptomator.png and b/main/ant-kit/src/main/resources/package/linux/Cryptomator.png differ diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 81ef3d6ef..5603f36b5 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -17,11 +17,28 @@ Shared utilities + com.google.guava guava - + + org.apache.commons + commons-lang3 + + + + + com.google.dagger + dagger + + + com.google.dagger + dagger-compiler + provided + + + junit junit @@ -43,4 +60,13 @@ test + + + + + org.jacoco + jacoco-maven-plugin + + + diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java new file mode 100644 index 000000000..d56d22f80 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java @@ -0,0 +1,21 @@ +package org.cryptomator.common; + +import java.util.Comparator; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class CommonsModule { + + @Provides + @Singleton + @Named("SemVer") + Comparator providesSemVerComparator() { + return new SemVerComparator(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/SemVerComparator.java b/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java similarity index 97% rename from main/ui/src/main/java/org/cryptomator/ui/util/SemVerComparator.java rename to main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java index b9031b471..930e2e93d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/SemVerComparator.java +++ b/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.ui.util; +package org.cryptomator.common; import java.util.Comparator; diff --git a/main/ui/src/test/java/org/cryptomator/ui/util/SemVerComparatorTest.java b/main/commons/src/test/java/org/cryptomator/common/SemVerComparatorTest.java similarity index 95% rename from main/ui/src/test/java/org/cryptomator/ui/util/SemVerComparatorTest.java rename to main/commons/src/test/java/org/cryptomator/common/SemVerComparatorTest.java index a505d0af4..859eb9471 100644 --- a/main/ui/src/test/java/org/cryptomator/ui/util/SemVerComparatorTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/SemVerComparatorTest.java @@ -6,10 +6,11 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.ui.util; +package org.cryptomator.common; import java.util.Comparator; +import org.cryptomator.common.SemVerComparator; import org.junit.Assert; import org.junit.Test; diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java index 9f6c5dbff..68f1a43e7 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java @@ -10,10 +10,12 @@ package org.cryptomator.frontend.webdav; import javax.inject.Singleton; +import org.cryptomator.common.CommonsModule; + import dagger.Component; @Singleton -@Component +@Component(modules = {CommonsModule.class}) public interface WebDavComponent { WebDavServer server(); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java similarity index 91% rename from main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java rename to main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java index 0acf2ddee..f381e2e6f 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java @@ -12,11 +12,13 @@ package org.cryptomator.frontend.webdav.mount; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Comparator; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; import org.apache.commons.io.IOUtils; @@ -26,15 +28,18 @@ import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.frontend.Frontend.MountParam; @Singleton -final class MacOsXWebDavMounter implements WebDavMounterStrategy { +final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy { + + private final Comparator semVerComparator; @Inject - MacOsXWebDavMounter() { + MacOsXAppleScriptWebDavMounter(@Named("SemVer") Comparator semVerComparator) { + this.semVerComparator = semVerComparator; } @Override public boolean shouldWork() { - return SystemUtils.IS_OS_MAC_OSX; + return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0; } @Override diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java new file mode 100644 index 000000000..89e8a86a2 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2014, 2016 Sebastian Stenzel, Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation, strategy fine tuning + * Markus Kreusch - Refactored WebDavMounter to use strategy pattern + ******************************************************************************/ +package org.cryptomator.frontend.webdav.mount; + +import java.net.URI; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.frontend.CommandFailedException; +import org.cryptomator.frontend.Frontend.MountParam; +import org.cryptomator.frontend.webdav.mount.command.Script; + +@Singleton +final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy { + + private final Comparator semVerComparator; + + @Inject + MacOsXShellScriptWebDavMounter(@Named("SemVer") Comparator semVerComparator) { + this.semVerComparator = semVerComparator; + } + + @Override + public boolean shouldWork() { + return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0; + } + + @Override + public void warmUp(int serverPort) { + // no-op + } + + @Override + public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { + final String mountName = mountParams.getOrDefault(MountParam.MOUNT_NAME, Optional.empty()).orElseThrow(() -> { + return new IllegalArgumentException("Missing mount parameter MOUNT_NAME."); + }); + + // we don't use the uri to derive a path, as it *could* be longer than 255 chars. + final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString(); + final Script mountScript = Script.fromLines("mkdir \"$MOUNT_PATH\"", "mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"").addEnv("DAV_AUTHORITY", uri.getRawAuthority()) + .addEnv("DAV_PATH", uri.getRawPath()).addEnv("MOUNT_PATH", path).addEnv("MOUNT_NAME", mountName); + mountScript.execute(); + return new MacWebDavMount(path); + } + + private static class MacWebDavMount extends AbstractWebDavMount { + private final String mountPath; + private final Script revealScript; + private final Script unmountScript; + + private MacWebDavMount(String mountPath) { + this.mountPath = mountPath; + this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath); + this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath); + } + + @Override + public void unmount() throws CommandFailedException { + // only attempt unmount if user didn't unmount manually: + if (Files.exists(FileSystems.getDefault().getPath(mountPath))) { + unmountScript.execute(); + } + } + + @Override + public void reveal() throws CommandFailedException { + revealScript.execute(); + } + + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java index edb5645fa..3cd1b2a08 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java @@ -19,74 +19,87 @@ import javax.inject.Singleton; @Singleton class MountStrategies implements Collection { - + private final Collection delegate; - + @Inject - MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) { - delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter)); + MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) { + delegate = unmodifiableList(asList(linuxMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter)); } + @Override public int size() { return delegate.size(); } + @Override public boolean isEmpty() { return delegate.isEmpty(); } + @Override public boolean contains(Object o) { return delegate.contains(o); } + @Override public Iterator iterator() { return delegate.iterator(); } + @Override public Object[] toArray() { return delegate.toArray(); } + @Override public T[] toArray(T[] a) { return delegate.toArray(a); } + @Override public boolean add(WebDavMounterStrategy e) { return delegate.add(e); } + @Override public boolean remove(Object o) { return delegate.remove(o); } + @Override public boolean containsAll(Collection c) { return delegate.containsAll(c); } + @Override public boolean addAll(Collection c) { return delegate.addAll(c); } + @Override public boolean removeAll(Collection c) { return delegate.removeAll(c); } + @Override public boolean retainAll(Collection c) { return delegate.retainAll(c); } + @Override public void clear() { delegate.clear(); } + @Override public boolean equals(Object o) { return delegate.equals(o); } + @Override public int hashCode() { return delegate.hashCode(); } - - } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java index a2a257a61..ee8fd5b96 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java @@ -11,10 +11,16 @@ package org.cryptomator.frontend.webdav.mount; import static org.cryptomator.frontend.webdav.mount.command.Script.fromLines; +import java.io.IOException; +import java.io.InterruptedIOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -22,12 +28,16 @@ import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.frontend.Frontend.MountParam; import org.cryptomator.frontend.webdav.mount.command.CommandResult; import org.cryptomator.frontend.webdav.mount.command.Script; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A {@link WebDavMounterStrategy} utilizing the "net use" command. @@ -37,7 +47,9 @@ import org.cryptomator.frontend.webdav.mount.command.Script; @Singleton final class WindowsWebDavMounter implements WebDavMounterStrategy { + private static final Logger LOG = LoggerFactory.getLogger(WindowsWebDavMounter.class); private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*"); + private static final Pattern REG_QUERY_PROXY_OVERRIDES_PATTERN = Pattern.compile("\\s*ProxyOverride\\s+REG_SZ\\s+(.*)\\s*"); private static final String AUTO_ASSIGN_DRIVE_LETTER = "*"; private static final String LOCALHOST = "localhost"; private static final int MOUNT_TIMEOUT_SECONDS = 60; @@ -60,12 +72,12 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { @Override public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { - final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.of(AUTO_ASSIGN_DRIVE_LETTER)).orElse(AUTO_ASSIGN_DRIVE_LETTER); + final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.empty()).orElse(AUTO_ASSIGN_DRIVE_LETTER); if (driveLetters.getOccupiedDriveLetters().contains(CharUtils.toChar(driveLetter))) { throw new CommandFailedException("Drive letter occupied."); } - - final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.of(LOCALHOST)).orElse(LOCALHOST); + + final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.empty()).orElse(LOCALHOST); try { final URI adjustedUri = new URI(uri.getScheme(), uri.getUserInfo(), hostname, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); CommandResult mountResult = mount(adjustedUri, driveLetter); @@ -74,14 +86,14 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { throw new IllegalArgumentException("Invalid host: " + hostname); } } - + private CommandResult mount(URI uri, String driveLetter) throws CommandFailedException { - final Script proxyBypassScript = fromLines( - "reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \";%DAV_HOST%;%DAV_HOST%:%DAV_PORT%\" /f"); - proxyBypassScript.addEnv("DAV_HOST", uri.getHost()); - proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())); - proxyBypassScript.execute(); - + try { + addProxyOverrides(uri); + } catch (IOException e) { + throw new CommandFailedException(e); + } + final String driveLetterStr = AUTO_ASSIGN_DRIVE_LETTER.equals(driveLetter) ? AUTO_ASSIGN_DRIVE_LETTER : driveLetter + ":"; final Script mountScript = fromLines("net use %DRIVE_LETTER% \\\\%DAV_HOST%@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); mountScript.addEnv("DRIVE_LETTER", driveLetterStr); @@ -90,6 +102,44 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } + + private void addProxyOverrides(URI uri) throws IOException, CommandFailedException { + try { + // get existing value for ProxyOverride key from reqistry: + ProcessBuilder query = new ProcessBuilder("reg", "query", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride"); + Process queryCmd = query.start(); + String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8); + int queryResult = queryCmd.waitFor(); + + // determine new value for ProxyOverride key: + Set overrides = new HashSet<>(); + Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut); + if (queryResult == 0 && matcher.find()) { + String[] existingOverrides = StringUtils.split(matcher.group(1), ';'); + overrides.addAll(Arrays.asList(existingOverrides)); + } + overrides.removeIf(s -> s.startsWith(uri.getHost() + ":")); + overrides.add(""); + overrides.add(uri.getHost()); + overrides.add(uri.getHost() + ":" + uri.getPort()); + + // set new value: + String overridesStr = StringUtils.join(overrides, ';'); + ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f"); + LOG.debug("Invoking command: " + StringUtils.join(add.command(), ' ')); + Process addCmd = add.start(); + int addResult = addCmd.waitFor(); + if (addResult != 0) { + String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8); + throw new CommandFailedException(addStdErr); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + InterruptedIOException ioException = new InterruptedIOException(); + ioException.initCause(e); + throw ioException; + } + } private String getDriveLetter(String result) throws CommandFailedException { final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result); diff --git a/main/jacoco-report/.gitignore b/main/jacoco-report/.gitignore new file mode 100644 index 000000000..b83d22266 --- /dev/null +++ b/main/jacoco-report/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/main/jacoco-report/pom.xml b/main/jacoco-report/pom.xml new file mode 100644 index 000000000..97a4411aa --- /dev/null +++ b/main/jacoco-report/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + org.cryptomator + main + 1.1.0-SNAPSHOT + + jacoco-report + Cryptomator Code Coverage Report + + + + + org.cryptomator + commons + + + org.cryptomator + commons-test + + + + + org.cryptomator + filesystem-api + + + org.cryptomator + filesystem-crypto + + + org.cryptomator + filesystem-crypto-integration-tests + + + org.cryptomator + filesystem-inmemory + + + org.cryptomator + filesystem-nameshortening + + + org.cryptomator + filesystem-nio + + + org.cryptomator + filesystem-stats + + + + + org.cryptomator + frontend-api + + + org.cryptomator + frontend-webdav + + + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + + + + + \ No newline at end of file diff --git a/main/pom.xml b/main/pom.xml index abcdf6abc..39715d6b6 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -35,12 +35,12 @@ 1.3 2.4 4.0 - 3.3.2 + 3.4 1.10 3.1 2.4.4 1.10.19 - 2.0.2 + 2.4 @@ -49,6 +49,16 @@ https://jitpack.io + + + + jacoco-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + true + + + @@ -81,6 +91,12 @@ ${project.version} test + + org.cryptomator + filesystem-invariants-tests + ${project.version} + test + org.cryptomator filesystem-nameshortening @@ -238,6 +254,7 @@ hamcrest-all ${hamcrest.version} + @@ -280,6 +297,12 @@ ant-kit + + test-coverage + + jacoco-report + + @@ -304,7 +327,7 @@ org.jacoco jacoco-maven-plugin - 0.7.5.201505241946 + 0.7.7-SNAPSHOT prepare-agent @@ -313,6 +336,12 @@ + + + **/*_* + **/Dagger* + + @@ -331,6 +360,9 @@ coveralls-maven-plugin 4.0.0 + + jacoco-report/target/site/jacoco-aggregate/jacoco.xml + ${env.COVERALLS_REPO_TOKEN} diff --git a/main/ui/pom.xml b/main/ui/pom.xml index c1dc24234..6808f3887 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -99,5 +99,12 @@ org.cryptomator commons-test + + + + com.nulab-inc + zxcvbn + 1.1.1 + diff --git a/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java b/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java index f4fbdb1a0..3150ddc4d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java +++ b/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java @@ -36,6 +36,8 @@ public class Cryptomator { private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer(); public static void main(String[] args) { + String cryptomatorVersion = Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion()).orElse("SNAPSHOT"); + LOG.info("Starting Cryptomator {} on {} {} ({})", cryptomatorVersion, SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); if (SystemUtils.IS_OS_MAC_OSX) { /* * On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java index fa432cc9d..3740b566d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -8,13 +8,13 @@ *******************************************************************************/ package org.cryptomator.ui; -import java.util.Comparator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.inject.Named; import javax.inject.Singleton; +import org.cryptomator.common.CommonsModule; import org.cryptomator.crypto.engine.impl.CryptoEngineModule; import org.cryptomator.frontend.FrontendFactory; import org.cryptomator.frontend.webdav.WebDavServer; @@ -24,7 +24,6 @@ import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.settings.SettingsProvider; import org.cryptomator.ui.util.DeferredCloser; -import org.cryptomator.ui.util.SemVerComparator; import com.fasterxml.jackson.databind.ObjectMapper; @@ -33,7 +32,7 @@ import dagger.Provides; import javafx.application.Application; import javafx.stage.Stage; -@Module(includes = CryptoEngineModule.class) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class}) class CryptomatorModule { private final Application application; @@ -65,13 +64,6 @@ class CryptomatorModule { return closer; } - @Provides - @Singleton - @Named("SemVer") - Comparator provideSemVerComparator() { - return new SemVerComparator(); - } - @Provides @Singleton @Named("VaultJsonMapper") diff --git a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java index 39cb090e3..3c4976cf4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java @@ -136,6 +136,7 @@ class ExitUtil { return; } else { settings.setNumTrayNotifications(settings.getNumTrayNotifications() - 1); + settings.save(); } final Runnable notificationCmd; if (SystemUtils.IS_OS_MAC_OSX) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index ed01184b3..86c16261b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -38,6 +38,7 @@ public class MainApplication extends Application { @Override public void start(Stage primaryStage) throws IOException { + LOG.info("JavaFX application started"); final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this, primaryStage)).build(); final MainController mainCtrl = comp.mainController(); closer = comp.deferredCloser(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 33f7ae11f..2b5e644c9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -5,22 +5,35 @@ * * Contributors: * Sebastian Stenzel - initial API and implementation + * Jean-Noël Charon - password strength meter *******************************************************************************/ package org.cryptomator.ui.controllers; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; +import com.nulabinc.zxcvbn.Strength; +import com.nulabinc.zxcvbn.Zxcvbn; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.scene.control.Label; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.engine.InvalidPassphraseException; import org.cryptomator.crypto.engine.UnsupportedVaultFormatException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.util.PasswordStrengthUtil; +import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +56,7 @@ public class ChangePasswordController extends LocalizedFXMLViewController { private final Application app; final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); + final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 @Inject public ChangePasswordController(Application app, Localization localization) { @@ -50,6 +64,9 @@ public class ChangePasswordController extends LocalizedFXMLViewController { this.app = app; } + @Inject + PasswordStrengthUtil strengthRater; + @FXML private SecPasswordField oldPasswordField; @@ -68,12 +85,24 @@ public class ChangePasswordController extends LocalizedFXMLViewController { @FXML private Hyperlink downloadsPageLink; + @FXML + private Label passwordStrengthLabel; + + @FXML + private Rectangle passwordStrengthShape; + @Override public void initialize() { BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty(); BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty(); BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer))); + passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate)); + + passwordStrengthShape.widthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getWidth)); + passwordStrengthShape.fillProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthColor)); + passwordStrengthShape.strokeWidthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrokeWidth)); + passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); } @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index 19a1187df..0a87c4fd6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -2,35 +2,42 @@ * Copyright (c) 2014, 2016 Sebastian Stenzel * This file is licensed under the terms of the MIT license. * See the LICENSE.txt file for more info. - * + * * Contributors: * Sebastian Stenzel - initial API and implementation + * Jean-Noël Charon - password strength meter ******************************************************************************/ package org.cryptomator.ui.controllers; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URL; -import java.nio.file.FileAlreadyExistsException; -import java.util.Optional; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import org.cryptomator.ui.controls.SecPasswordField; -import org.cryptomator.ui.model.Vault; -import org.cryptomator.ui.settings.Localization; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.*; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.model.Vault; +import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.util.PasswordStrengthUtil; +import org.fxmisc.easybind.EasyBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.FileAlreadyExistsException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.nulabinc.zxcvbn.*; @Singleton public class InitializeController extends LocalizedFXMLViewController { @@ -39,12 +46,16 @@ public class InitializeController extends LocalizedFXMLViewController { final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); + final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 @Inject public InitializeController(Localization localization) { super(localization); } + @Inject + PasswordStrengthUtil strengthRater; + @FXML private SecPasswordField passwordField; @@ -57,11 +68,23 @@ public class InitializeController extends LocalizedFXMLViewController { @FXML private Label messageLabel; + @FXML + private Label passwordStrengthLabel; + + @FXML + private Rectangle passwordStrengthShape; + @Override public void initialize() { BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty(); BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer)); + passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate)); + + passwordStrengthShape.widthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getWidth)); + passwordStrengthShape.fillProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthColor)); + passwordStrengthShape.strokeWidthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrokeWidth)); + passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); } @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index 5c262f6de..06808e265 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -5,33 +5,10 @@ * * Contributors: * Sebastian Stenzel - initial API and implementation + * Jean-Noël Charon - confirmation dialog on vault removal ******************************************************************************/ package org.cryptomator.ui.controllers; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; -import javax.inject.Singleton; - -import org.cryptomator.ui.controls.DirectoryListCell; -import org.cryptomator.ui.model.Vault; -import org.cryptomator.ui.model.VaultFactory; -import org.cryptomator.ui.settings.Localization; -import org.cryptomator.ui.settings.Settings; -import org.fxmisc.easybind.EasyBind; -import org.fxmisc.easybind.monadic.MonadicBinding; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import dagger.Lazy; import javafx.application.Platform; import javafx.beans.binding.Binding; @@ -40,20 +17,41 @@ import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Side; import javafx.scene.Parent; -import javafx.scene.control.Button; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.control.ToggleButton; +import javafx.scene.control.*; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.stage.FileChooser; import javafx.stage.Stage; +import org.cryptomator.ui.controls.DirectoryListCell; +import org.cryptomator.ui.model.Vault; +import org.cryptomator.ui.model.VaultFactory; +import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.settings.Settings; +import org.cryptomator.ui.util.DialogBuilderUtil; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; +import java.io.File; +import java.io.IOException; +import java.net.URL; +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; @Singleton public class MainController extends LocalizedFXMLViewController { @@ -94,6 +92,9 @@ public class MainController extends LocalizedFXMLViewController { this.changePasswordController = changePasswordController; this.settingsController = settingsController; this.vaults = FXCollections.observableList(settings.getDirectories()); + this.vaults.addListener((Change c) -> { + settings.save(); + }); // derived bindings: this.isShowingSettings = activeController.isEqualTo(settingsController.get()); @@ -224,9 +225,17 @@ public class MainController extends LocalizedFXMLViewController { @FXML private void didClickRemoveSelectedEntry(ActionEvent e) { - vaults.remove(selectedVault.get()); - if (vaults.isEmpty()) { - activeController.set(welcomeController.get()); + Dialog confirmDialog = DialogBuilderUtil.buildConfirmationDialog( + localization.getString("main.directoryList.remove.confirmation.title"), + localization.getString("main.directoryList.remove.confirmation.header"), + localization.getString("main.directoryList.remove.confirmation.content") + ); + Optional choice = confirmDialog.showAndWait(); + if (choice.get() == ButtonType.OK){ + vaults.remove(selectedVault.get()); + if (vaults.isEmpty()) { + activeController.set(welcomeController.get()); + } } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index 7af0e8269..80cd60cab 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -59,9 +59,9 @@ public class SettingsController extends LocalizedFXMLViewController { useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6()); versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT"))); - EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), settings::setCheckForUpdatesEnabled); + EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), this::checkForUpdateDidChange); EasyBind.subscribe(portField.textProperty(), this::portDidChange); - EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), settings::setUseIpv6); + EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), this::useIpv6DidChange); } @Override @@ -73,21 +73,30 @@ public class SettingsController extends LocalizedFXMLViewController { return Optional.ofNullable(getClass().getPackage().getImplementationVersion()); } + private void checkForUpdateDidChange(Boolean newValue) { + settings.setCheckForUpdatesEnabled(newValue); + settings.save(); + } + private void portDidChange(String newValue) { try { int port = Integer.parseInt(newValue); - if (port < Settings.MIN_PORT) { + if (port < Settings.MIN_PORT || port > Settings.MAX_PORT) { settings.setPort(Settings.DEFAULT_PORT); - } else if (port < Settings.MAX_PORT) { - settings.setPort(port); } else { - portField.setText(String.valueOf(Settings.MAX_PORT)); + settings.setPort(port); + settings.save(); } } catch (NumberFormatException e) { portField.setText(String.valueOf(Settings.DEFAULT_PORT)); } } + private void useIpv6DidChange(Boolean newValue) { + settings.setUseIpv6(newValue); + settings.save(); + } + private void filterNumericKeyEvents(KeyEvent t) { if (t.getCharacter() == null || t.getCharacter().length() == 0) { return; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java index 5c976e317..0ac30e0b2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java @@ -154,6 +154,7 @@ public class WelcomeController extends LocalizedFXMLViewController { Platform.runLater(() -> { this.updateLink.setText(msg); this.updateLink.setVisible(true); + this.updateLink.setDisable(false); }); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java index d12082ef5..1c2fbf8d5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java @@ -1,27 +1,70 @@ package org.cryptomator.ui.settings; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; import java.util.Enumeration; +import java.util.Locale; +import java.util.Objects; +import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @Singleton public class Localization extends ResourceBundle { + private static final Logger LOG = LoggerFactory.getLogger(Localization.class); + + private static final String LOCALIZATION_DEFAULT_FILE = "/localization/en.txt"; + private static final String LOCALIZATION_FILENAME_FMT = "/localization/%s.txt"; + private static final String LOCALIZATION_FILE = String.format(LOCALIZATION_FILENAME_FMT, Locale.getDefault().getLanguage()); + + private final ResourceBundle fallback; + private final ResourceBundle localized; + @Inject public Localization() { - this.parent = ResourceBundle.getBundle("localization"); + try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_DEFAULT_FILE)) { + Objects.requireNonNull(in); + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + this.fallback = new PropertyResourceBundle(reader); + LOG.info("Loaded localization from {}", LOCALIZATION_FILE); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_FILE)) { + if (in != null) { + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + this.localized = new PropertyResourceBundle(reader); + } else { + this.localized = this.fallback; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override protected Object handleGetObject(String key) { - return parent.getObject(key); + return localized.containsKey(key) ? localized.getObject(key) : fallback.getObject(key); } @Override public Enumeration getKeys() { - return parent.getKeys(); + Collection keys = CollectionUtils.union(localized.keySet(), fallback.keySet()); + return Collections.enumeration(keys); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 85c8fefdc..17f7c5b25 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -11,6 +11,7 @@ package org.cryptomator.ui.settings; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.cryptomator.ui.model.Vault; @@ -23,10 +24,12 @@ public class Settings implements Serializable { private static final long serialVersionUID = 7609959894417878744L; public static final int MIN_PORT = 1024; public static final int MAX_PORT = 65535; - public static final int DEFAULT_PORT = 0; + public static final int DEFAULT_PORT = 42427; public static final boolean DEFAULT_USE_IPV6 = false; public static final Integer DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; + private final Consumer saveCmd; + @JsonProperty("directories") private List directories; @@ -35,7 +38,7 @@ public class Settings implements Serializable { @JsonProperty("port") private Integer port; - + @JsonProperty("useIpv6") private Boolean useIpv6; @@ -45,8 +48,12 @@ public class Settings implements Serializable { /** * Package-private constructor; use {@link SettingsProvider}. */ - Settings() { + Settings(Consumer saveCmd) { + this.saveCmd = saveCmd; + } + public void save() { + saveCmd.accept(this); } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java b/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java index 9d40ac09d..417b6c369 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java @@ -16,6 +16,12 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; import javax.inject.Named; @@ -23,7 +29,6 @@ import javax.inject.Provider; import javax.inject.Singleton; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.ui.util.DeferredCloser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,9 +37,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; @Singleton public class SettingsProvider implements Provider { - private static final Logger LOG = LoggerFactory.getLogger(Settings.class); + private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class); private static final Path SETTINGS_DIR; private static final String SETTINGS_FILE = "settings.json"; + private static final long SAVE_DELAY_MS = 1000; static { final String appdata = System.getenv("APPDATA"); @@ -52,12 +58,12 @@ public class SettingsProvider implements Provider { } } - private final DeferredCloser deferredCloser; private final ObjectMapper objectMapper; + private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicReference> scheduledSaveCmd = new AtomicReference<>(); @Inject - public SettingsProvider(DeferredCloser deferredCloser, @Named("VaultJsonMapper") ObjectMapper objectMapper) { - this.deferredCloser = deferredCloser; + public SettingsProvider(@Named("VaultJsonMapper") ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @@ -72,28 +78,39 @@ public class SettingsProvider implements Provider { @Override public Settings get() { - Settings settings = null; + final Settings settings = new Settings(this::scheduleSave); try { final Path settingsPath = getSettingsPath(); final InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ); - settings = objectMapper.readValue(in, Settings.class); + objectMapper.readerForUpdating(settings).readValue(in); + LOG.info("Settings loaded from " + settingsPath); } catch (IOException e) { - LOG.warn("Failed to load settings, creating new one."); - settings = new Settings(); + LOG.info("Failed to load settings, creating new one."); } - deferredCloser.closeLater(settings, this::save); return settings; } - private void save(Settings settings) { + private void scheduleSave(Settings settings) { if (settings == null) { return; } + ScheduledFuture saveCmd = saveScheduler.schedule(() -> { + this.save(settings); + } , SAVE_DELAY_MS, TimeUnit.MILLISECONDS); + ScheduledFuture previousSaveCmd = scheduledSaveCmd.getAndSet(saveCmd); + if (previousSaveCmd != null) { + previousSaveCmd.cancel(false); + } + } + + private void save(Settings settings) { + Objects.requireNonNull(settings); try { final Path settingsPath = getSettingsPath(); Files.createDirectories(settingsPath.getParent()); final OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); objectMapper.writeValue(out, settings); + LOG.info("Settings saved to " + settingsPath); } catch (IOException e) { LOG.error("Failed to save settings.", e); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/DialogBuilderUtil.java b/main/ui/src/main/java/org/cryptomator/ui/util/DialogBuilderUtil.java new file mode 100644 index 000000000..a5b6dd64b --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/DialogBuilderUtil.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Jean-Noël Charon - initial API and implementation + *******************************************************************************/ +package org.cryptomator.ui.util; + +import javafx.scene.control.Alert; + +public class DialogBuilderUtil { + + public DialogBuilderUtil() {} + + public static Alert buildInformationDialog(String title, String header, String content) { + return buildDialog(title, header, content,Alert.AlertType.INFORMATION); + } + + public static Alert buildWarningDialog(String title, String header, String content) { + return buildDialog(title, header, content,Alert.AlertType.WARNING); + } + + public static Alert buildErrorDialog(String title, String header, String content) { + return buildDialog(title, header, content,Alert.AlertType.ERROR); + } + + public static Alert buildConfirmationDialog(String title, String header, String content) { + return buildDialog(title, header, content,Alert.AlertType.CONFIRMATION); + } + + private static Alert buildDialog(String title, String header, String content, Alert.AlertType type) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(header); + alert.setContentText(content); + return alert; + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java b/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java new file mode 100644 index 000000000..fe6af2f61 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Jean-Noël Charon - initial API and implementation + *******************************************************************************/ +package org.cryptomator.ui.util; + +import com.nulabinc.zxcvbn.Zxcvbn; +import javafx.beans.property.IntegerProperty; +import javafx.scene.paint.Color; +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.ui.settings.Localization; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.List; + +@Singleton +public class PasswordStrengthUtil { + + private final Zxcvbn zxcvbn; + private final List sanitizedInputs; + private final Localization localization; + + @Inject + public PasswordStrengthUtil(Localization localization){ + this.localization = localization; + this.zxcvbn = new Zxcvbn(); + this.sanitizedInputs = new ArrayList<>(); + this.sanitizedInputs.add("cryptomator"); + } + + public int computeRate(String password) { + if (StringUtils.isEmpty(password)) { + return -1; + } else { + return zxcvbn.measure(password, sanitizedInputs).getScore(); + } + } + + public Color getStrengthColor(Number score) { + Color strengthColor = Color.web("#FF0000"); + switch (score.intValue()) { + case 0: + strengthColor = Color.web("#FF0000"); + break; + case 1: + strengthColor = Color.web("#FF8000"); + break; + case 2: + strengthColor = Color.web("#FFBF00"); + break; + case 3: + strengthColor = Color.web("#FFFF00"); + break; + case 4: + strengthColor = Color.web("#BFFF00"); + break; + default: + strengthColor = Color.web("#FF0000"); + break; + } + return strengthColor; + } + + public int getWidth(Number score) { + int width = 0; + switch (score.intValue()) { + case 0: + width += 5; + break; + case 1: + width += 25; + break; + case 2: + width += 50; + break; + case 3: + width += 75; + break; + case 4: + width = 100; + break; + default: + width = 0; + break; + } + return Math.round(width*2.23f); + } + + public float getStrokeWidth(Number score) { + if (score.intValue() >= 0) { + return 0.5f; + } else { + return 0; + } + } + + public String getStrengthDescription(Number score) { + if (score.intValue() >= 0) { + return String.format(localization.getString("initialize.messageLabel.passwordStrength"), + localization.getString("initialize.messageLabel.passwordStrength." + score.intValue())); + } else { + return ""; + } + } + +} diff --git a/main/ui/src/main/resources/bot_welcome.png b/main/ui/src/main/resources/bot_welcome.png index 5daf92fd6..a429621d9 100644 Binary files a/main/ui/src/main/resources/bot_welcome.png and b/main/ui/src/main/resources/bot_welcome.png differ diff --git a/main/ui/src/main/resources/bot_welcome@2x.png b/main/ui/src/main/resources/bot_welcome@2x.png index 8c25a5e21..d6e201c20 100644 Binary files a/main/ui/src/main/resources/bot_welcome@2x.png and b/main/ui/src/main/resources/bot_welcome@2x.png differ diff --git a/main/ui/src/main/resources/css/dialog-confirm.png b/main/ui/src/main/resources/css/dialog-confirm.png new file mode 100644 index 000000000..3c86ba7d4 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-confirm.png differ diff --git a/main/ui/src/main/resources/css/dialog-confirm@2x.png b/main/ui/src/main/resources/css/dialog-confirm@2x.png new file mode 100644 index 000000000..189be79e4 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-confirm@2x.png differ diff --git a/main/ui/src/main/resources/css/dialog-error.png b/main/ui/src/main/resources/css/dialog-error.png new file mode 100644 index 000000000..002e0563e Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-error.png differ diff --git a/main/ui/src/main/resources/css/dialog-error@2x.png b/main/ui/src/main/resources/css/dialog-error@2x.png new file mode 100644 index 000000000..ae098b9df Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-error@2x.png differ diff --git a/main/ui/src/main/resources/css/dialog-fewer-details.png b/main/ui/src/main/resources/css/dialog-fewer-details.png new file mode 100644 index 000000000..9ee1c3fb2 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-fewer-details.png differ diff --git a/main/ui/src/main/resources/css/dialog-fewer-details@2x.png b/main/ui/src/main/resources/css/dialog-fewer-details@2x.png new file mode 100644 index 000000000..e00e1f982 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-fewer-details@2x.png differ diff --git a/main/ui/src/main/resources/css/dialog-information.png b/main/ui/src/main/resources/css/dialog-information.png new file mode 100644 index 000000000..7d972640a Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-information.png differ diff --git a/main/ui/src/main/resources/css/dialog-information@2x.png b/main/ui/src/main/resources/css/dialog-information@2x.png new file mode 100644 index 000000000..110d63b22 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-information@2x.png differ diff --git a/main/ui/src/main/resources/css/dialog-more-details.png b/main/ui/src/main/resources/css/dialog-more-details.png new file mode 100644 index 000000000..ec2a7f367 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-more-details.png differ diff --git a/main/ui/src/main/resources/css/dialog-more-details@2x.png b/main/ui/src/main/resources/css/dialog-more-details@2x.png new file mode 100644 index 000000000..5348a8606 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-more-details@2x.png differ diff --git a/main/ui/src/main/resources/css/dialog-warning.png b/main/ui/src/main/resources/css/dialog-warning.png new file mode 100644 index 000000000..4ac083c51 Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-warning.png differ diff --git a/main/ui/src/main/resources/css/dialog-warning@2x.png b/main/ui/src/main/resources/css/dialog-warning@2x.png new file mode 100644 index 000000000..9abe4465b Binary files /dev/null and b/main/ui/src/main/resources/css/dialog-warning@2x.png differ diff --git a/main/ui/src/main/resources/css/linux_theme.css b/main/ui/src/main/resources/css/linux_theme.css index 4beabf9e8..e9f40d05a 100644 --- a/main/ui/src/main/resources/css/linux_theme.css +++ b/main/ui/src/main/resources/css/linux_theme.css @@ -5,6 +5,7 @@ * * Contributors: * Sebastian Stenzel - initial API and implementation + * Jean-Noël Charon - implementation of the dialog css * */ @@ -440,4 +441,89 @@ -fx-stroke-width: 2px; } .default-color0.chart-series-line { -fx-stroke: COLOR_CHART_GREEN; } -.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; } \ No newline at end of file +.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; } + +/******************************************************************************* + * * + * Dialog * + * * + ******************************************************************************/ + +.dialog-pane { + -fx-background-color: COLOR_BACKGROUND; + -fx-padding: 0; +} + +.dialog-pane > .expandable-content { + -fx-padding: 0.666em; /* 8px */ +} + +.dialog-pane > .button-bar > .container { + -fx-padding: 0.833em; /* 10px */ +} + +.dialog-pane > .content.label { + -fx-alignment: top-left; + -fx-padding: 1.333em 0.833em 0 0.833em; /* 16px 10px 0px 10px */ +} + +.dialog-pane > .content { + -fx-padding: 0.833em; /* 10 */ +} + +.dialog-pane:no-header .graphic-container { + -fx-padding: 0.833em 0 0 0.833em; /* 10px 0px 0px 10px */ +} + +.dialog-pane:header .header-panel { + /*-fx-padding: 0.833em 1.166em 0.833em 1.166em; *//* 10px 14px 10px 14px */ + -fx-padding: 0.833em; /* 10px */ + -fx-background-color: COLOR_BORDER, linear-gradient(COLOR_BACKGROUND, derive(COLOR_BACKGROUND, 30%)); + -fx-background-insets: 0, 0 0 1 0; +} + +.dialog-pane:header .header-panel .label { + -fx-font-size: 1.167em; /* 14px */ + -fx-wrap-text: true; +} + +.dialog-pane:header .header-panel .graphic-container { + /* This prevents the text in the header running directly into the graphic */ + -fx-padding: 0 0 0 0.833em; /* 0px 0px 0px 10px */ +} + +.dialog-pane > .button-bar > .container > .details-button { + -fx-alignment: baseline-left; + -fx-focus-traversable: false; + -fx-padding: 0.416em; /* 5px */ +} + +.dialog-pane > .button-bar > .container > .details-button.more { + -fx-graphic: url("dialog-more-details.png"); +} + +.dialog-pane > .button-bar > .container > .details-button.less { + -fx-graphic: url("dialog-fewer-details.png"); +} + +.dialog-pane > .button-bar > .container > .details-button:hover { + -fx-underline: true; +} + +.alert.confirmation.dialog-pane, +.text-input-dialog.dialog-pane, +.choice-dialog.dialog-pane { + -fx-graphic: url("dialog-confirm.png"); +} + +.alert.information.dialog-pane { + -fx-graphic: url("dialog-information.png"); +} + +.alert.error.dialog-pane { + -fx-graphic: url("dialog-error.png"); +} + +.alert.warning.dialog-pane { + -fx-graphic: url("dialog-warning.png"); +} \ No newline at end of file diff --git a/main/ui/src/main/resources/css/mac_theme.css b/main/ui/src/main/resources/css/mac_theme.css index bac4a985d..a34917ecb 100644 --- a/main/ui/src/main/resources/css/mac_theme.css +++ b/main/ui/src/main/resources/css/mac_theme.css @@ -5,6 +5,7 @@ * * Contributors: * Sebastian Stenzel - initial API and implementation + * Jean-Noël Charon - implementation of the dialog css * */ @@ -530,4 +531,89 @@ -fx-stroke-width: 2px; } .default-color0.chart-series-line { -fx-stroke: COLOR_CHART_GREEN; } -.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; } \ No newline at end of file +.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; } + +/******************************************************************************* + * * + * Dialog * + * * + ******************************************************************************/ + +.dialog-pane { + -fx-background-color: COLOR_BACKGROUND; + -fx-padding: 0; +} + +.dialog-pane > .expandable-content { + -fx-padding: 0.666em; /* 8px */ +} + +.dialog-pane > .button-bar > .container { + -fx-padding: 0.833em; /* 10px */ +} + +.dialog-pane > .content.label { + -fx-alignment: top-left; + -fx-padding: 1.333em 0.833em 0 0.833em; /* 16px 10px 0px 10px */ +} + +.dialog-pane > .content { + -fx-padding: 0.833em; /* 10 */ +} + +.dialog-pane:no-header .graphic-container { + -fx-padding: 0.833em 0 0 0.833em; /* 10px 0px 0px 10px */ +} + +.dialog-pane:header .header-panel { + /*-fx-padding: 0.833em 1.166em 0.833em 1.166em; *//* 10px 14px 10px 14px */ + -fx-padding: 0.833em; /* 10px */ + -fx-background-color: COLOR_BORDER, linear-gradient(COLOR_BACKGROUND, derive(COLOR_BACKGROUND, 30%)); + -fx-background-insets: 0, 0 0 1 0; +} + +.dialog-pane:header .header-panel .label { + -fx-font-size: 1.167em; /* 14px */ + -fx-wrap-text: true; +} + +.dialog-pane:header .header-panel .graphic-container { + /* This prevents the text in the header running directly into the graphic */ + -fx-padding: 0 0 0 0.833em; /* 0px 0px 0px 10px */ +} + +.dialog-pane > .button-bar > .container > .details-button { + -fx-alignment: baseline-left; + -fx-focus-traversable: false; + -fx-padding: 0.416em; /* 5px */ +} + +.dialog-pane > .button-bar > .container > .details-button.more { + -fx-graphic: url("dialog-more-details.png"); +} + +.dialog-pane > .button-bar > .container > .details-button.less { + -fx-graphic: url("dialog-fewer-details.png"); +} + +.dialog-pane > .button-bar > .container > .details-button:hover { + -fx-underline: true; +} + +.alert.confirmation.dialog-pane, +.text-input-dialog.dialog-pane, +.choice-dialog.dialog-pane { + -fx-graphic: url("dialog-confirm.png"); +} + +.alert.information.dialog-pane { + -fx-graphic: url("dialog-information.png"); +} + +.alert.error.dialog-pane { + -fx-graphic: url("dialog-error.png"); +} + +.alert.warning.dialog-pane { + -fx-graphic: url("dialog-warning.png"); +} \ No newline at end of file diff --git a/main/ui/src/main/resources/css/win_theme.css b/main/ui/src/main/resources/css/win_theme.css index f0109b004..254615e41 100644 --- a/main/ui/src/main/resources/css/win_theme.css +++ b/main/ui/src/main/resources/css/win_theme.css @@ -5,6 +5,7 @@ * * Contributors: * Sebastian Stenzel - initial API and implementation + * Jean-Noël Charon - implementation of the dialog css * */ @@ -512,4 +513,89 @@ -fx-stroke-width: 2px; } .default-color0.chart-series-line { -fx-stroke: COLOR_CHART_GREEN; } -.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; } \ No newline at end of file +.default-color1.chart-series-line { -fx-stroke: COLOR_CHART_RED; } + +/******************************************************************************* + * * + * Dialog * + * * + ******************************************************************************/ + +.dialog-pane { + -fx-background-color: COLOR_BACKGROUND; + -fx-padding: 0; +} + +.dialog-pane > .expandable-content { + -fx-padding: 0.666em; /* 8px */ +} + +.dialog-pane > .button-bar > .container { + -fx-padding: 0.833em; /* 10px */ +} + +.dialog-pane > .content.label { + -fx-alignment: top-left; + -fx-padding: 1.333em 0.833em 0 0.833em; /* 16px 10px 0px 10px */ +} + +.dialog-pane > .content { + -fx-padding: 0.833em; /* 10 */ +} + +.dialog-pane:no-header .graphic-container { + -fx-padding: 0.833em 0 0 0.833em; /* 10px 0px 0px 10px */ +} + +.dialog-pane:header .header-panel { + /*-fx-padding: 0.833em 1.166em 0.833em 1.166em; *//* 10px 14px 10px 14px */ + -fx-padding: 0.833em; /* 10px */ + -fx-background-color: COLOR_BORDER, linear-gradient(COLOR_BACKGROUND, derive(COLOR_BACKGROUND, 30%)); + -fx-background-insets: 0, 0 0 1 0; +} + +.dialog-pane:header .header-panel .label { + -fx-font-size: 1.167em; /* 14px */ + -fx-wrap-text: true; +} + +.dialog-pane:header .header-panel .graphic-container { + /* This prevents the text in the header running directly into the graphic */ + -fx-padding: 0 0 0 0.833em; /* 0px 0px 0px 10px */ +} + +.dialog-pane > .button-bar > .container > .details-button { + -fx-alignment: baseline-left; + -fx-focus-traversable: false; + -fx-padding: 0.416em; /* 5px */ +} + +.dialog-pane > .button-bar > .container > .details-button.more { + -fx-graphic: url("dialog-more-details.png"); +} + +.dialog-pane > .button-bar > .container > .details-button.less { + -fx-graphic: url("dialog-fewer-details.png"); +} + +.dialog-pane > .button-bar > .container > .details-button:hover { + -fx-underline: true; +} + +.alert.confirmation.dialog-pane, +.text-input-dialog.dialog-pane, +.choice-dialog.dialog-pane { + -fx-graphic: url("dialog-confirm.png"); +} + +.alert.information.dialog-pane { + -fx-graphic: url("dialog-information.png"); +} + +.alert.error.dialog-pane { + -fx-graphic: url("dialog-error.png"); +} + +.alert.warning.dialog-pane { + -fx-graphic: url("dialog-warning.png"); +} \ No newline at end of file diff --git a/main/ui/src/main/resources/fxml/change_password.fxml b/main/ui/src/main/resources/fxml/change_password.fxml index 93b7f18b0..22c17ce9b 100644 --- a/main/ui/src/main/resources/fxml/change_password.fxml +++ b/main/ui/src/main/resources/fxml/change_password.fxml @@ -6,6 +6,7 @@ Contributors: Sebastian Stenzel - initial API and implementation + Jean-Noël Charon - password strength meter --> @@ -22,6 +23,7 @@ + @@ -44,12 +46,18 @@