Merge branch 'develop' into feature/restore-vaultconfig

This commit is contained in:
Jan-Peter Klein
2025-02-19 19:49:35 +01:00
155 changed files with 1942 additions and 1351 deletions

View File

@@ -1,4 +1,5 @@
import ch.qos.logback.classic.spi.Configurator;
import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
import org.cryptomator.common.locationpresets.DropboxWindowsLocationPresetsProvider;
@@ -13,6 +14,9 @@ import org.cryptomator.common.locationpresets.OneDriveLinuxLocationPresetsProvid
import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider;
import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider;
import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider;
import org.cryptomator.networking.SSLContextWithMacKeychain;
import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.networking.SSLContextWithWindowsCertStore;
import org.cryptomator.integrations.tray.TrayMenuController;
import org.cryptomator.logging.LogbackConfiguratorFactory;
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
@@ -51,11 +55,14 @@ open module org.cryptomator.desktop {
requires jakarta.inject;
requires static javax.inject;
requires java.compiler;
requires com.github.benmanes.caffeine;
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
uses SSLContextProvider;
provides TrayMenuController with AwtTrayMenuController;
provides Configurator with LogbackConfiguratorFactory;
provides SSLContextProvider with SSLContextWithWindowsCertStore, SSLContextWithMacKeychain, SSLContextWithPKCS12TrustStore;
provides LocationPresetsProvider with //
DropboxWindowsLocationPresetsProvider, DropboxMacLocationPresetsProvider, DropboxLinuxLocationPresetsProvider, //
GoogleDriveMacLocationPresetsProvider, GoogleDriveWindowsLocationPresetsProvider, //

View File

@@ -1,8 +1,7 @@
package org.cryptomator.common.keychain;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
@@ -24,9 +23,9 @@ public class KeychainManager implements KeychainAccessProvider {
@Inject
KeychainManager(ObjectExpression<KeychainAccessProvider> selectedKeychain) {
this.keychain = selectedKeychain;
this.passphraseStoredProperties = CacheBuilder.newBuilder() //
this.passphraseStoredProperties = Caffeine.newBuilder() //
.weakValues() //
.build(CacheLoader.from(this::createStoredPassphraseProperty));
.build(this::createStoredPassphraseProperty);
keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll());
}
@@ -124,7 +123,7 @@ public class KeychainManager implements KeychainAccessProvider {
* @see #isPassphraseStored(String)
*/
public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) {
return passphraseStoredProperties.getUnchecked(key);
return passphraseStoredProperties.get(key);
}
private BooleanProperty createStoredPassphraseProperty(String key) {

View File

@@ -67,7 +67,7 @@ public final class GoogleDriveMacLocationPresetsProvider implements LocationPres
*/
private String getDriveLocationString(Path accountPath) {
String accountName = accountPath.getFileName().toString().replace("GoogleDrive-", "");
return STR."Google Drive - \{accountName}";
return "Google Drive - " + accountName;
}
/**

View File

@@ -160,7 +160,7 @@ public class Mounter {
var mountService = mountProviders.stream().filter(s -> s.getClass().getName().equals(vaultSettings.mountService.getValue())).findFirst().orElse(defaultMountService.getValue());
if (isConflictingMountService(mountService)) {
var msg = STR."\{mountService.getClass()} unavailable due to conflict with either of \{CONFLICTING_MOUNT_SERVICES.get(mountService.getClass().getName())}";
var msg = mountService.getClass() + " unavailable due to conflict with either of " + CONFLICTING_MOUNT_SERVICES.get(mountService.getClass().getName());
throw new ConflictingMountServiceException(msg);
}

View File

@@ -58,6 +58,7 @@ public class VaultSettings {
public final StringExpression mountName;
public final StringProperty mountService;
public final IntegerProperty port;
public final StringProperty lastKnownKeyLoader;
VaultSettings(VaultSettingsJson json) {
this.id = json.id;
@@ -74,6 +75,7 @@ public class VaultSettings {
this.mountPoint = new SimpleObjectProperty<>(this, "mountPoint", json.mountPoint == null ? null : Path.of(json.mountPoint));
this.mountService = new SimpleStringProperty(this, "mountService", json.mountService);
this.port = new SimpleIntegerProperty(this, "port", json.port);
this.lastKnownKeyLoader = new SimpleStringProperty(this, "lastKnownKeyLoader", json.lastKnownKeyLoader);
// mount name is no longer an explicit setting, see https://github.com/cryptomator/cryptomator/pull/1318
this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
final String name;
@@ -99,7 +101,7 @@ public class VaultSettings {
}
Observable[] observables() {
return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode, port, mountService};
return new Observable[]{actionAfterUnlock, autoLockIdleSeconds, autoLockWhenIdle, displayName, maxCleartextFilenameLength, mountFlags, mountPoint, path, revealAfterMount, unlockAfterStartup, usesReadOnlyMode, port, mountService, lastKnownKeyLoader};
}
public static VaultSettings withRandomId() {
@@ -130,6 +132,7 @@ public class VaultSettings {
json.mountPoint = mountPoint.map(Path::toString).getValue();
json.mountService = mountService.get();
json.port = port.get();
json.lastKnownKeyLoader = lastKnownKeyLoader.get();
return json;
}

View File

@@ -48,6 +48,9 @@ class VaultSettingsJson {
@JsonProperty("mountService")
String mountService;
@JsonProperty("lastKnownKeyLoader")
String lastKnownKeyLoader;
@JsonProperty("port")
int port = VaultSettings.DEFAULT_PORT;

View File

@@ -44,6 +44,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.ReadOnlyFileSystemException;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
@@ -115,15 +116,22 @@ public class Vault {
private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException {
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
if (vaultSettings.usesReadOnlyMode.get()) {
var createReadOnly = vaultSettings.usesReadOnlyMode.get();
try {
FileSystemCapabilityChecker.assertWriteAccess(getPath());
} catch (FileSystemCapabilityChecker.MissingCapabilityException e) {
if (!createReadOnly) {
throw new ReadOnlyFileSystemException();
}
}
if (createReadOnly) {
flags.add(FileSystemFlags.READONLY);
} else if (vaultSettings.maxCleartextFilenameLength.get() == -1) {
LOG.debug("Determining cleartext filename length limitations...");
var checker = new FileSystemCapabilityChecker();
int shorteningThreshold = configCache.get().allegedShorteningThreshold();
int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
int ciphertextLimit = FileSystemCapabilityChecker.determineSupportedCiphertextFileNameLength(getPath());
if (ciphertextLimit < shorteningThreshold) {
int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
int cleartextLimit = FileSystemCapabilityChecker.determineSupportedCleartextFileNameLength(getPath());
vaultSettings.maxCleartextFilenameLength.set(cleartextLimit);
} else {
vaultSettings.maxCleartextFilenameLength.setValue(UNLIMITED_FILENAME_LENGTH);

View File

@@ -29,6 +29,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.stream.Stream;
@@ -54,9 +55,9 @@ public class VaultListManager {
@Inject
public VaultListManager(ObservableList<Vault> vaultList, //
AutoLocker autoLocker, //
List<MountService> mountServices,
VaultComponent.Factory vaultComponentFactory,
ResourceBundle resourceBundle,
List<MountService> mountServices, //
VaultComponent.Factory vaultComponentFactory, //
ResourceBundle resourceBundle, //
Settings settings) {
this.vaultList = vaultList;
this.autoLocker = autoLocker;
@@ -123,6 +124,10 @@ public class VaultListManager {
private Vault create(VaultSettings vaultSettings) {
var wrapper = new VaultConfigCache(vaultSettings);
try {
if (Objects.isNull(vaultSettings.lastKnownKeyLoader.get())) {
var keyIdScheme = wrapper.get().getKeyId().getScheme();
vaultSettings.lastKnownKeyLoader.set(keyIdScheme);
}
var vaultState = determineVaultState(vaultSettings.path.get());
if (vaultState == LOCKED) { //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state
wrapper.reloadConfig();

View File

@@ -9,8 +9,9 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.SubstitutingProperties;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.SubstitutingProperties;
import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.ipc.IpcCommunicator;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
@@ -19,8 +20,10 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.net.ssl.SSLContext;
import javafx.application.Application;
import javafx.stage.Stage;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -48,14 +51,16 @@ public class Cryptomator {
private final Environment env;
private final Lazy<IpcMessageHandler> ipcMessageHandler;
private final ShutdownHook shutdownHook;
private final SecureRandom csprng;
@Inject
Cryptomator(DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
Cryptomator(DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook, SecureRandom csprng) {
this.debugMode = debugMode;
this.supportedLanguages = supportedLanguages;
this.env = env;
this.ipcMessageHandler = ipcMessageHandler;
this.shutdownHook = shutdownHook;
this.csprng = csprng;
}
public static void main(String[] args) {
@@ -89,7 +94,7 @@ public class Cryptomator {
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
debugMode.initialize();
supportedLanguages.applyPreferred();
changeDefaultSSLContext();
/*
* Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args.
* If no external process could be reached, the args will be handled by the loopback IPC endpoint.
@@ -115,6 +120,17 @@ public class Cryptomator {
}
}
private void changeDefaultSSLContext() {
SSLContextProvider.loadAll().findFirst().ifPresent(p -> {
try {
var context = p.getContext(csprng);
SSLContext.setDefault(context);
} catch (SSLContextProvider.SSLContextBuildException e) {
LOG.warn("Failed to change default SSL context with provider {}", p.getClass().getName(), e);
}
});
}
/**
* Launches the JavaFX application, blocking the main thread until shuts down.
*

View File

@@ -0,0 +1,33 @@
package org.cryptomator.networking;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
abstract class SSLContextDifferentTrustStoreBase implements SSLContextProvider {
abstract KeyStore getTruststore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException;
@Override
public SSLContext getContext(SecureRandom csprng) throws SSLContextBuildException {
try {
KeyStore truststore = getTruststore();
truststore.load(null, null);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(truststore);
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), csprng);
return context;
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | KeyManagementException | IOException e) {
throw new SSLContextBuildException(e);
}
}
}

View File

@@ -0,0 +1,24 @@
package org.cryptomator.networking;
import org.cryptomator.integrations.common.IntegrationsLoader;
import javax.net.ssl.SSLContext;
import java.security.SecureRandom;
import java.util.ServiceLoader;
import java.util.stream.Stream;
public interface SSLContextProvider {
SSLContext getContext(SecureRandom csprng) throws SSLContextBuildException;
class SSLContextBuildException extends Exception {
SSLContextBuildException(Throwable t) {
super(t);
}
}
static Stream<SSLContextProvider> loadAll() {
return IntegrationsLoader.loadAll(ServiceLoader.load(SSLContextProvider.class), SSLContextProvider.class);
}
}

View File

@@ -0,0 +1,21 @@
package org.cryptomator.networking;
import org.cryptomator.integrations.common.OperatingSystem;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
/**
* SSLContextProvider for macOS using the macOS Keychain as truststore
*/
@OperatingSystem(OperatingSystem.Value.MAC)
public class SSLContextWithMacKeychain extends SSLContextDifferentTrustStoreBase implements SSLContextProvider {
@Override
KeyStore getTruststore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
return KeyStore.getInstance("KeychainStore-ROOT");
}
}

View File

@@ -0,0 +1,42 @@
package org.cryptomator.networking;
import org.cryptomator.integrations.common.CheckAvailability;
import org.cryptomator.integrations.common.OperatingSystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Optional;
/**
* SSLContextProvider for Linux using a PKCS#12 file as trust store
*/
@OperatingSystem(OperatingSystem.Value.LINUX)
@CheckAvailability
public class SSLContextWithPKCS12TrustStore extends SSLContextDifferentTrustStoreBase implements SSLContextProvider {
private static final String CERT_FILE_LOCATION_PROPERTY = "cryptomator.networking.truststore.p12Path";
@Override
KeyStore getTruststore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
var pkcs12FilePath = Path.of(System.getProperty(CERT_FILE_LOCATION_PROPERTY));
try {
return KeyStore.getInstance(pkcs12FilePath.toFile(), new char[]{});
} catch (IllegalArgumentException e) {
throw new NoSuchFileException(pkcs12FilePath.toString());
}
}
@CheckAvailability
public static boolean isSupported() {
var pkcs12Path = System.getProperty(CERT_FILE_LOCATION_PROPERTY);
return Optional.ofNullable(pkcs12Path) //
.map(Path::of) //
.map(Files::exists).orElse(false);
}
}

View File

@@ -0,0 +1,21 @@
package org.cryptomator.networking;
import org.cryptomator.integrations.common.OperatingSystem;
import java.security.KeyStore;
import java.security.KeyStoreException;
/**
* SSLContextProvider for Windows using the Windows certificate store as trust store
* <p>
* In order to work, the jdk.crypto.mscapi jmod is needed
*/
@OperatingSystem(OperatingSystem.Value.WINDOWS)
public class SSLContextWithWindowsCertStore extends SSLContextDifferentTrustStoreBase implements SSLContextProvider {
@Override
KeyStore getTruststore() throws KeyStoreException {
return KeyStore.getInstance("WINDOWS-ROOT");
}
}

View File

@@ -26,7 +26,7 @@ public class CreateNewVaultExpertSettingsController implements FxController {
public static final int MAX_SHORTENING_THRESHOLD = 220;
public static final int MIN_SHORTENING_THRESHOLD = 36;
private static final String DOCS_NAME_SHORTENING_URL = "https://docs.cryptomator.org/en/1.7/security/architecture/#name-shortening";
private static final String DOCS_NAME_SHORTENING_URL = "https://docs.cryptomator.org/security/architecture/#name-shortening";
private final Stage window;
private final Lazy<Application> application;

View File

@@ -40,7 +40,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
@@ -50,7 +49,7 @@ 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 static final String TEMP_FILE_FORMAT = ".locationTest.cryptomator.tmp";
private static final String TEMP_FILE_PREFIX = ".locationTest.cryptomator";
private final Stage window;
private final Lazy<Scene> chooseNameScene;
@@ -126,16 +125,19 @@ public class CreateNewVaultLocationController implements FxController {
private boolean isActuallyWritable(Path p) {
Path tmpFile = p.resolve(TEMP_FILE_FORMAT);
try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
Path tmpDir = null;
try {
tmpDir = Files.createTempDirectory(p, TEMP_FILE_PREFIX );
return true;
} catch (IOException e) {
return false;
} finally {
try {
Files.deleteIfExists(tmpFile);
} catch (IOException e) {
LOG.warn("Unable to delete temporary file {}. Needs to be deleted manually.", tmpFile);
if (tmpDir != null) {
try {
Files.deleteIfExists(tmpDir);
} catch (IOException e) {
LOG.warn("Unable to delete temporary directory {}. Needs to be deleted manually.", tmpDir);
}
}
}
}

View File

@@ -12,7 +12,6 @@ public enum FxmlFile {
CONVERTVAULT_HUBTOPASSWORD_START("/fxml/convertvault_hubtopassword_start.fxml"), //
CONVERTVAULT_HUBTOPASSWORD_CONVERT("/fxml/convertvault_hubtopassword_convert.fxml"), //
CONVERTVAULT_HUBTOPASSWORD_SUCCESS("/fxml/convertvault_hubtopassword_success.fxml"), //
DOKANY_SUPPORT_END("/fxml/dokany_support_end.fxml"), //
ERROR("/fxml/error.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
HEALTH_START("/fxml/health_start.fxml"), //
@@ -47,9 +46,8 @@ public enum FxmlFile {
RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), //
RECOVERYKEY_RESET_VAULT_CONFIG_SUCCESS("/fxml/recoverykey_reset_vault_config_success.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_CERT("/fxml/remove_cert.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
SHARE_VAULT("/fxml/share_vault.fxml"), //
SIMPLE_DIALOG("/fxml/simple_dialog.fxml"), //
UPDATE_REMINDER("/fxml/update_reminder.fxml"), //
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
UNLOCK_REQUIRES_RESTART("/fxml/unlock_requires_restart.fxml"), //

View File

@@ -15,6 +15,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
@@ -108,6 +109,7 @@ public class HubToPasswordConvertController implements FxController {
.thenRunAsync(this::convertInternal, backgroundExecutorService) //
.whenCompleteAsync((result, exception) -> {
if (exception == null) {
vault.getVaultSettings().lastKnownKeyLoader.set(MasterkeyFileLoadingStrategy.SCHEME);
LOG.info("Conversion of vault {} succeeded.", vault.getPath());
window.setScene(successScene.get());
} else {

View File

@@ -0,0 +1,90 @@
package org.cryptomator.ui.dialogs;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.collections.ObservableList;
import javafx.stage.Stage;
import java.util.ResourceBundle;
import java.util.function.Consumer;
@FxApplicationScoped
public class Dialogs {
private final ResourceBundle resourceBundle;
private final StageFactory stageFactory;
@Inject
public Dialogs(ResourceBundle resourceBundle, StageFactory stageFactory) {
this.resourceBundle = resourceBundle;
this.stageFactory = stageFactory;
}
private static final Logger LOG = LoggerFactory.getLogger(Dialogs.class);
private SimpleDialog.Builder createDialogBuilder() {
return new SimpleDialog.Builder(resourceBundle, stageFactory);
}
public SimpleDialog.Builder prepareRemoveVaultDialog(Stage window, Vault vault, ObservableList<Vault> vaults) {
return createDialogBuilder().setOwner(window) //
.setTitleKey("removeVault.title", vault.getDisplayName()) //
.setMessageKey("removeVault.message") //
.setDescriptionKey("removeVault.description") //
.setIcon(FontAwesome5Icon.QUESTION) //
.setOkButtonKey("removeVault.confirmBtn") //
.setCancelButtonKey("generic.button.cancel") //
.setOkAction(stage -> {
LOG.debug("Removing vault {}.", vault.getDisplayName());
vaults.remove(vault);
stage.close();
});
}
public SimpleDialog.Builder prepareRemoveCertDialog(Stage window, Settings settings) {
return createDialogBuilder() //
.setOwner(window) //
.setTitleKey("removeCert.title") //
.setMessageKey("removeCert.message") //
.setDescriptionKey("removeCert.description") //
.setIcon(FontAwesome5Icon.QUESTION) //
.setOkButtonKey("removeCert.confirmBtn") //
.setCancelButtonKey("generic.button.cancel") //
.setOkAction(stage -> {
settings.licenseKey.set(null);
stage.close();
});
}
public SimpleDialog.Builder prepareDokanySupportEndDialog(Stage window, Consumer<Stage> cancelAction) {
return createDialogBuilder() //
.setOwner(window) //
.setTitleKey("dokanySupportEnd.title") //
.setMessageKey("dokanySupportEnd.message") //
.setDescriptionKey("dokanySupportEnd.description") //
.setIcon(FontAwesome5Icon.EXCLAMATION) //
.setOkButtonKey("generic.button.close") //
.setCancelButtonKey("dokanySupportEnd.preferencesBtn") //
.setOkAction(Stage::close) //
.setCancelAction(cancelAction);
}
public SimpleDialog.Builder prepareRetryIfReadonlyDialog(Stage window, Consumer<Stage> okAction) {
return createDialogBuilder() //
.setOwner(window) //
.setTitleKey("retryIfReadonly.title") //
.setMessageKey("retryIfReadonly.message") //
.setDescriptionKey("retryIfReadonly.description") //
.setIcon(FontAwesome5Icon.EXCLAMATION) //
.setOkButtonKey("retryIfReadonly.retry") //
.setCancelButtonKey("generic.button.close") //
.setOkAction(okAction) //
.setCancelAction(Stage::close);
}
}

View File

@@ -0,0 +1,140 @@
package org.cryptomator.ui.dialogs;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.IllegalFormatException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.function.Consumer;
public class SimpleDialog {
private final ResourceBundle resourceBundle;
private final Stage dialogStage;
SimpleDialog(Builder builder) throws IOException {
this.resourceBundle = builder.resourceBundle;
dialogStage = builder.stageFactory.create();
dialogStage.initOwner(builder.owner);
dialogStage.initModality(Modality.WINDOW_MODAL);
dialogStage.setTitle(resolveText(builder.titleKey, builder.titleArgs));
dialogStage.setResizable(false);
FxmlLoaderFactory loaderFactory = FxmlLoaderFactory.forController( //
new SimpleDialogController(resolveText(builder.messageKey, null), //
resolveText(builder.descriptionKey, null), //
builder.icon, resolveText(builder.okButtonKey, null), //
resolveText(builder.cancelButtonKey, null), //
() -> builder.okAction.accept(dialogStage), //
() -> builder.cancelAction.accept(dialogStage)), //
Scene::new, builder.resourceBundle);
dialogStage.setScene(new Scene(loaderFactory.load(FxmlFile.SIMPLE_DIALOG.getRessourcePathString()).getRoot()));
}
public void showAndWait() {
dialogStage.showAndWait();
}
private String resolveText(String key, String[] args) {
if (key == null || key.isEmpty() || !resourceBundle.containsKey(key)) {
throw new IllegalArgumentException(String.format("Invalid key: '%s'. Key not found in ResourceBundle.", key));
}
String text = resourceBundle.getString(key);
try {
return args != null && args.length > 0 ? String.format(text, (Object[]) args) : text;
} catch (IllegalFormatException e) {
throw new IllegalArgumentException("Formatting error: Check if arguments match placeholders in the text.", e);
}
}
public static class Builder {
private Stage owner;
private final ResourceBundle resourceBundle;
private final StageFactory stageFactory;
private String titleKey;
private String[] titleArgs;
private String messageKey;
private String descriptionKey;
private String okButtonKey;
private String cancelButtonKey;
private FontAwesome5Icon icon;
private Consumer<Stage> okAction = Stage::close;
private Consumer<Stage> cancelAction = Stage::close;
public Builder(ResourceBundle resourceBundle, StageFactory stageFactory) {
this.resourceBundle = resourceBundle;
this.stageFactory = stageFactory;
}
public Builder setOwner(Stage owner) {
this.owner = owner;
return this;
}
public Builder setTitleKey(String titleKey, String... args) {
this.titleKey = titleKey;
this.titleArgs = args;
return this;
}
public Builder setMessageKey(String messageKey) {
this.messageKey = messageKey;
return this;
}
public Builder setDescriptionKey(String descriptionKey) {
this.descriptionKey = descriptionKey;
return this;
}
public Builder setIcon(FontAwesome5Icon icon) {
this.icon = icon;
return this;
}
public Builder setOkButtonKey(String okButtonKey) {
this.okButtonKey = okButtonKey;
return this;
}
public Builder setCancelButtonKey(String cancelButtonKey) {
this.cancelButtonKey = cancelButtonKey;
return this;
}
public Builder setOkAction(Consumer<Stage> okAction) {
this.okAction = okAction;
return this;
}
public Builder setCancelAction(Consumer<Stage> cancelAction) {
this.cancelAction = cancelAction;
return this;
}
public SimpleDialog build() {
Objects.requireNonNull(titleKey, "SimpleDialog titleKey must be set.");
Objects.requireNonNull(messageKey, "SimpleDialog messageKey must be set.");
Objects.requireNonNull(descriptionKey, "SimpleDialog descriptionKey must be set.");
Objects.requireNonNull(okButtonKey, "SimpleDialog okButtonKey must be set.");
Objects.requireNonNull(cancelButtonKey, "SimpleDialog cancelButtonKey must be set.");
try {
return new SimpleDialog(this);
} catch (IOException e) {
throw new UncheckedIOException("Failed to create SimpleDialog.", e);
}
}
}
}

View File

@@ -0,0 +1,61 @@
package org.cryptomator.ui.dialogs;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import javafx.fxml.FXML;
public class SimpleDialogController implements FxController {
private final String message;
private final String description;
private final FontAwesome5Icon icon;
private final String okButtonText;
private final String cancelButtonText;
private final Runnable okAction;
private final Runnable cancelAction;
public SimpleDialogController(String message, String description, FontAwesome5Icon icon, String okButtonText, String cancelButtonText, Runnable okAction, Runnable cancelAction) {
this.message = message;
this.description = description;
this.icon = icon;
this.okButtonText = okButtonText;
this.cancelButtonText = cancelButtonText;
this.okAction = okAction;
this.cancelAction = cancelAction;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
public FontAwesome5Icon getIcon() {
return icon;
}
public String getOkButtonText() {
return okButtonText;
}
public String getCancelButtonText() {
return cancelButtonText;
}
@FXML
private void handleOk() {
if (okAction != null) {
okAction.run();
}
}
@FXML
private void handleCancel() {
if (cancelAction != null) {
cancelAction.run();
}
}
}

View File

@@ -1,34 +0,0 @@
package org.cryptomator.ui.dokanysupportend;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javafx.scene.Scene;
import javafx.stage.Stage;
@DokanySupportEndScoped
@Subcomponent(modules = {DokanySupportEndModule.class})
public interface DokanySupportEndComponent {
@DokanySupportEndWindow
Stage window();
@FxmlScene(FxmlFile.DOKANY_SUPPORT_END)
Lazy<Scene> dokanySupportEndScene();
default void showDokanySupportEndWindow() {
Stage stage = window();
stage.setScene(dokanySupportEndScene().get());
stage.sizeToScene();
stage.show();
}
@Subcomponent.Factory
interface Factory {
DokanySupportEndComponent create();
}
}

View File

@@ -1,34 +0,0 @@
package org.cryptomator.ui.dokanysupportend;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@DokanySupportEndScoped
public class DokanySupportEndController implements FxController {
private final Stage window;
private final FxApplicationWindows applicationWindows;
@Inject
DokanySupportEndController(@DokanySupportEndWindow Stage window, FxApplicationWindows applicationWindows) {
this.window = window;
this.applicationWindows = applicationWindows;
}
@FXML
public void close() {
window.close();
}
public void openVolumePreferences() {
applicationWindows.showPreferencesWindow(SelectedPreferencesTab.VOLUME);
window.close();
}
}

View File

@@ -1,57 +0,0 @@
package org.cryptomator.ui.dokanysupportend;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Provider;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class DokanySupportEndModule {
@Provides
@DokanySupportEndWindow
@DokanySupportEndScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@DokanySupportEndWindow
@DokanySupportEndScoped
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("dokanySupportEnd.title"));
stage.setMinWidth(500);
stage.setMinHeight(100);
stage.initModality(Modality.APPLICATION_MODAL);
return stage;
}
@Provides
@FxmlScene(FxmlFile.DOKANY_SUPPORT_END)
@DokanySupportEndScoped
static Scene provideDokanySupportEndScene(@DokanySupportEndWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.DOKANY_SUPPORT_END);
}
@Binds
@IntoMap
@FxControllerKey(DokanySupportEndController.class)
abstract FxController bindDokanySupportEndController(DokanySupportEndController controller);
}

View File

@@ -1,13 +0,0 @@
package org.cryptomator.ui.dokanysupportend;
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 DokanySupportEndScoped {
}

View File

@@ -1,14 +0,0 @@
package org.cryptomator.ui.dokanysupportend;
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 DokanySupportEndWindow {
}

View File

@@ -43,8 +43,8 @@ public class AutoUnlocker {
private CompletionStage<Void> unlockSequentially(Stream<Vault> vaultStream) {
// this is an attempt to run all the unlock workflows sequentially, i.e. start the next workflow only after completing/failing the previous workflow.
return vaultStream.filter(Vault::isLocked).reduce(CompletableFuture.completedFuture(null),
(prevUnlock, nextVault) -> prevUnlock.thenCompose(unused -> appWindows.startUnlockWorkflow(nextVault, null)),
(prevUnlock, nextUnlock) -> nextUnlock.exceptionally(e -> null) // we don't care here about the exception, logged elsewhere
(prevUnlock, nextVault) -> prevUnlock.thenCompose(_ -> appWindows.startUnlockWorkflow(nextVault, null)),
(_, nextUnlock) -> nextUnlock.exceptionally(_ -> null) // we don't care here about the exception, logged elsewhere
);
}

View File

@@ -7,14 +7,12 @@ package org.cryptomator.ui.fxapp;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.ui.dokanysupportend.DokanySupportEndComponent;
import org.cryptomator.ui.error.ErrorComponent;
import org.cryptomator.ui.health.HealthCheckComponent;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.removecert.RemoveCertComponent;
import org.cryptomator.ui.sharevault.ShareVaultComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.cryptomator.ui.unlock.UnlockComponent;
@@ -35,8 +33,6 @@ import java.io.InputStream;
ErrorComponent.class, //
HealthCheckComponent.class, //
UpdateReminderComponent.class, //
DokanySupportEndComponent.class, //
RemoveCertComponent.class, //
ShareVaultComponent.class})
abstract class FxApplicationModule {

View File

@@ -5,7 +5,8 @@ import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.ui.dokanysupportend.DokanySupportEndComponent;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.dialogs.SimpleDialog;
import org.cryptomator.ui.error.ErrorComponent;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
@@ -49,13 +50,13 @@ public class FxApplicationWindows {
private final QuitComponent.Builder quitWindowBuilder;
private final UnlockComponent.Factory unlockWorkflowFactory;
private final UpdateReminderComponent.Factory updateReminderWindowFactory;
private final DokanySupportEndComponent.Factory dokanySupportEndWindowBuilder;
private final LockComponent.Factory lockWorkflowFactory;
private final ErrorComponent.Factory errorWindowFactory;
private final ExecutorService executor;
private final VaultOptionsComponent.Factory vaultOptionsWindow;
private final ShareVaultComponent.Factory shareVaultWindow;
private final FilteredList<Window> visibleWindows;
private final Dialogs dialogs;
@Inject
public FxApplicationWindows(@PrimaryStage Stage primaryStage, //
@@ -65,12 +66,12 @@ public class FxApplicationWindows {
QuitComponent.Builder quitWindowBuilder, //
UnlockComponent.Factory unlockWorkflowFactory, //
UpdateReminderComponent.Factory updateReminderWindowFactory, //
DokanySupportEndComponent.Factory dokanySupportEndWindowBuilder, //
LockComponent.Factory lockWorkflowFactory, //
ErrorComponent.Factory errorWindowFactory, //
VaultOptionsComponent.Factory vaultOptionsWindow, //
ShareVaultComponent.Factory shareVaultWindow, //
ExecutorService executor) {
ExecutorService executor, //
Dialogs dialogs) {
this.primaryStage = primaryStage;
this.trayIntegration = trayIntegration;
this.mainWindow = mainWindow;
@@ -78,13 +79,13 @@ public class FxApplicationWindows {
this.quitWindowBuilder = quitWindowBuilder;
this.unlockWorkflowFactory = unlockWorkflowFactory;
this.updateReminderWindowFactory = updateReminderWindowFactory;
this.dokanySupportEndWindowBuilder = dokanySupportEndWindowBuilder;
this.lockWorkflowFactory = lockWorkflowFactory;
this.errorWindowFactory = errorWindowFactory;
this.executor = executor;
this.vaultOptionsWindow = vaultOptionsWindow;
this.shareVaultWindow = shareVaultWindow;
this.visibleWindows = Window.getWindows().filtered(Window::isShowing);
this.dialogs = dialogs;
}
public void initialize() {
@@ -93,17 +94,17 @@ public class FxApplicationWindows {
// register preferences shortcut
if (desktop.isSupported(Desktop.Action.APP_PREFERENCES)) {
desktop.setPreferencesHandler(evt -> showPreferencesWindow(SelectedPreferencesTab.ANY));
desktop.setPreferencesHandler(_ -> showPreferencesWindow(SelectedPreferencesTab.ANY));
}
// register preferences shortcut
if (desktop.isSupported(Desktop.Action.APP_ABOUT)) {
desktop.setAboutHandler(evt -> showPreferencesWindow(SelectedPreferencesTab.ABOUT));
desktop.setAboutHandler(_ -> showPreferencesWindow(SelectedPreferencesTab.ABOUT));
}
// register app reopen listener
if (desktop.isSupported(Desktop.Action.APP_EVENT_REOPENED)) {
desktop.addAppEventListener((AppReopenedListener) e -> showMainWindow());
desktop.addAppEventListener((AppReopenedListener) _ -> showMainWindow());
}
// observe visible windows
@@ -135,11 +136,12 @@ public class FxApplicationWindows {
}
public CompletionStage<Stage> showVaultOptionsWindow(Vault vault, SelectedVaultOptionsTab tab) {
return showMainWindow().thenApplyAsync((window) -> vaultOptionsWindow.create(vault).showVaultOptionsWindow(tab), Platform::runLater).whenComplete(this::reportErrors);
return showMainWindow().thenApplyAsync(_ -> vaultOptionsWindow.create(vault).showVaultOptionsWindow(tab), Platform::runLater) //
.whenComplete(this::reportErrors);
}
public void showQuitWindow(QuitResponse response, boolean forced) {
CompletableFuture.runAsync(() -> quitWindowBuilder.build().showQuitWindow(response,forced), Platform::runLater);
CompletableFuture.runAsync(() -> quitWindowBuilder.build().showQuitWindow(response, forced), Platform::runLater);
}
public void showUpdateReminderWindow() {
@@ -147,9 +149,15 @@ public class FxApplicationWindows {
}
public void showDokanySupportEndWindow() {
CompletableFuture.runAsync(() -> dokanySupportEndWindowBuilder.create().showDokanySupportEndWindow(), Platform::runLater);
CompletableFuture.runAsync(() -> createDokanySupportEndDialog().showAndWait(), Platform::runLater);
}
private SimpleDialog createDokanySupportEndDialog() {
return dialogs.prepareDokanySupportEndDialog(mainWindow.get().window(), stage -> {
showPreferencesWindow(SelectedPreferencesTab.VOLUME);
stage.close();
}).build();
}
public CompletionStage<Void> startUnlockWorkflow(Vault vault, @Nullable Stage owner) {
return CompletableFuture.supplyAsync(() -> {
@@ -157,8 +165,7 @@ public class FxApplicationWindows {
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
return unlockWorkflowFactory.create(vault, owner).unlockWorkflow();
}, Platform::runLater) //
.thenAcceptAsync(UnlockWorkflow::run, executor)
.exceptionally(e -> {
.thenAcceptAsync(UnlockWorkflow::run, executor).exceptionally(e -> {
showErrorWindow(e, owner == null ? primaryStage : owner, null);
return null;
});

View File

@@ -3,6 +3,8 @@ package org.cryptomator.ui.keyloading;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,6 +30,33 @@ public interface KeyLoadingStrategy extends MasterkeyLoader {
@Override
Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
/**
* Determines whether the provided key loader scheme corresponds to a Hub Vault.
* <p>
* This method compares the {@code keyLoader} parameter with the known Hub Vault schemes
* {@link HubKeyLoadingStrategy#SCHEME_HUB_HTTP} and {@link HubKeyLoadingStrategy#SCHEME_HUB_HTTPS}.
*
* @param keyLoader A string representing the key loader scheme to be checked.
* @return {@code true} if the given key loader scheme represents a Hub Vault; {@code false} otherwise.
*/
static boolean isHubVault(String keyLoader) {
return HubKeyLoadingStrategy.SCHEME_HUB_HTTP.equals(keyLoader) || HubKeyLoadingStrategy.SCHEME_HUB_HTTPS.equals(keyLoader);
}
/**
* Determines whether the provided key loader scheme corresponds to a Masterkey File Vault.
* <p>
* This method checks if the {@code keyLoader} parameter matches the known Masterkey File Vault scheme
* {@link MasterkeyFileLoadingStrategy#SCHEME}.
* </p>
*
* @param keyLoader A string representing the key loader scheme to be checked.
* @return {@code true} if the given key loader scheme represents a Masterkey File Vault; {@code false} otherwise.
*/
static boolean isMasterkeyFileVault(String keyLoader) {
return MasterkeyFileLoadingStrategy.SCHEME.equals(keyLoader);
}
/**
* Allows the loader to try and recover from an exception thrown during the last attempt.
*

View File

@@ -20,7 +20,7 @@ public class HubConfig {
public String devicesResourceUrl;
/**
* A collection of String template processors to construct URIs related to this Hub instance.
* A collection of functions to construct URIs related to this Hub instance.
*/
@JsonIgnore
public final URIProcessors URIs = new URIProcessors();
@@ -49,14 +49,20 @@ public class HubConfig {
public class URIProcessors {
public final URIProcessor API = this::fromApiEndpoint;
/**
* Resolves paths relative to the <code>/api/</code> endpoint of this Hub instance.
*/
public final StringTemplate.Processor<URI, RuntimeException> API = template -> {
var path = template.interpolate();
public URI fromApiEndpoint(String path) {
var relPath = path.startsWith("/") ? path.substring(1) : path;
return getApiBaseUrl().resolve(relPath);
};
}
}
@FunctionalInterface
public interface URIProcessor {
URI resolve(String path);
}
}

View File

@@ -88,7 +88,7 @@ public class ReceiveKeyController implements FxController {
* STEP 0 (Request): GET /api/config
*/
private void requestApiConfig() {
var configUri = hubConfig.URIs.API."config";
var configUri = hubConfig.URIs.API.resolve("config");
var request = HttpRequest.newBuilder(configUri) //
.GET() //
.timeout(REQ_TIMEOUT) //
@@ -122,7 +122,7 @@ public class ReceiveKeyController implements FxController {
* STEP 1 (Request): GET user key for this device
*/
private void requestDeviceData() {
var deviceUri = hubConfig.URIs.API."devices/\{deviceId}";
var deviceUri = hubConfig.URIs.API.resolve("devices/" + deviceId);
var request = HttpRequest.newBuilder(deviceUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //
@@ -162,9 +162,10 @@ public class ReceiveKeyController implements FxController {
* STEP 2 (Request): GET vault key for this user
*/
private void requestVaultMasterkey(String encryptedUserKey) {
var vaultKeyUri = hubConfig.URIs.API."vaults/\{vaultId}/access-token";
var vaultKeyUri = hubConfig.URIs.API.resolve("vaults/" + vaultId + "/access-token");
var request = HttpRequest.newBuilder(vaultKeyUri) //
.header("Authorization", "Bearer " + bearerToken) //
.header("Hub-Device-ID", deviceId) //
.GET() //
.timeout(REQ_TIMEOUT) //
.build();
@@ -205,7 +206,7 @@ public class ReceiveKeyController implements FxController {
*/
@Deprecated
private void requestLegacyAccessToken() {
var legacyAccessTokenUri = hubConfig.URIs.API."vaults/\{vaultId}/keys/\{deviceId}";
var legacyAccessTokenUri = hubConfig.URIs.API.resolve("vaults/" + vaultId + "/keys/" + deviceId);
var request = HttpRequest.newBuilder(legacyAccessTokenUri) //
.header("Authorization", "Bearer " + bearerToken) //
.GET() //

View File

@@ -115,7 +115,7 @@ public class RegisterDeviceController implements FxController {
workInProgress.set(true);
var userReq = HttpRequest.newBuilder(hubConfig.URIs.API."users/me") //
var userReq = HttpRequest.newBuilder(hubConfig.URIs.API.resolve("users/me")) //
.GET() //
.timeout(REQ_TIMEOUT) //
.header("Authorization", "Bearer " + bearerToken) //
@@ -143,7 +143,7 @@ public class RegisterDeviceController implements FxController {
var now = Instant.now().toString();
var dto = new CreateDeviceDto(deviceId, deviceNameField.getText(), BaseEncoding.base64().encode(deviceKeyPair.getPublic().getEncoded()), "DESKTOP", jwe.serialize(), now);
var json = toJson(dto);
var deviceUri = hubConfig.URIs.API."devices/\{deviceId}";
var deviceUri = hubConfig.URIs.fromApiEndpoint("devices/" + deviceId);
var putDeviceReq = HttpRequest.newBuilder(deviceUri) //
.PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
.timeout(REQ_TIMEOUT) //
@@ -164,7 +164,7 @@ public class RegisterDeviceController implements FxController {
private void migrateLegacyDevices(ECPublicKey userPublicKey) {
try {
// GET legacy access tokens
var getUri = hubConfig.URIs.API."devices/\{deviceId}/legacy-access-tokens";
var getUri = hubConfig.URIs.API.resolve("devices/" + deviceId + "/legacy-access-tokens");
var getReq = HttpRequest.newBuilder(getUri).GET().timeout(REQ_TIMEOUT).header("Authorization", "Bearer " + bearerToken).build();
var getRes = httpClient.send(getReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (getRes.statusCode() != 200) {
@@ -185,12 +185,12 @@ public class RegisterDeviceController implements FxController {
LOG.warn("Failed to decrypt legacy access token for vault {}. Skipping migration.", entry.getKey());
}
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
var postUri = hubConfig.URIs.API."users/me/access-tokens";
var postUri = hubConfig.URIs.fromApiEndpoint("users/me/access-tokens");
var postBody = JSON.writer().writeValueAsString(newAccessTokens);
var postReq = HttpRequest.newBuilder(postUri).POST(HttpRequest.BodyPublishers.ofString(postBody)).timeout(REQ_TIMEOUT).header("Authorization", "Bearer " + bearerToken).build();
var postRes = httpClient.send(postReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (postRes.statusCode() != 200) {
throw new IOException(STR."Unexpected response from POST \{postUri}: \{postRes.statusCode()}");
throw new IOException("Unexpected response from POST " + postUri + ": " + postRes.statusCode());
}
} catch (IOException e) {
// log and ignore: this is merely a best-effort attempt of migrating legacy devices. Failure is uncritical as this is merely a convenience feature.

View File

@@ -1,13 +1,5 @@
package org.cryptomator.ui.mainwindow;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
@@ -21,6 +13,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import javafx.stage.Screen;
import javafx.stage.Stage;
@MainWindowScoped
public class MainWindowController implements FxController {
@@ -63,27 +64,46 @@ public class MainWindowController implements FxController {
}
window.focusedProperty().addListener(this::mainWindowFocusChanged);
if (!neverTouched()) {
window.setHeight(settings.windowHeight.get() > window.getMinHeight() ? settings.windowHeight.get() : window.getMinHeight());
window.setWidth(settings.windowWidth.get() > window.getMinWidth() ? settings.windowWidth.get() : window.getMinWidth());
window.setX(settings.windowXPosition.get());
window.setY(settings.windowYPosition.get());
int x = settings.windowXPosition.get();
int y = settings.windowYPosition.get();
int width = settings.windowWidth.get();
int height = settings.windowHeight.get();
if (windowPositionSaved(x, y, width, height) ) {
if(isWithinDisplayBounds(x, y, width, height)) { //use stored window position
window.setX(x);
window.setY(y);
window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth()));
window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight()));
} else if(isWithinDisplayBounds((int) window.getX(), (int) window.getY(), width, height)) { //just reset position of upper left corner, keep window size
window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth()));
window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight()));
} //else reset window completely
}
window.widthProperty().addListener((_, _, _) -> savePositionalSettings());
window.heightProperty().addListener((_, _, _) -> savePositionalSettings());
window.xProperty().addListener((_, _, _) -> savePositionalSettings());
window.yProperty().addListener((_, _, _) -> savePositionalSettings());
settings.windowXPosition.bind(window.xProperty());
settings.windowYPosition.bind(window.yProperty());
settings.windowWidth.bind(window.widthProperty());
settings.windowHeight.bind(window.heightProperty());
}
private boolean neverTouched() {
return (settings.windowHeight.get() == 0) && (settings.windowWidth.get() == 0) && (settings.windowXPosition.get() == 0) && (settings.windowYPosition.get() == 0);
private boolean windowPositionSaved(int x, int y, int width, int height) {
return x != 0 || y != 0 || width != 0 || height != 0;
}
public void savePositionalSettings() {
settings.windowWidth.setValue(window.getWidth());
settings.windowHeight.setValue(window.getHeight());
settings.windowXPosition.setValue(window.getX());
settings.windowYPosition.setValue(window.getY());
private boolean isWithinDisplayBounds(int x, int y, int width, int height) {
// define a rect which is inset on all sides from the window's rect:
final int shrinkedX = x + 20; // 20px left
final int shrinkedY = y + 5; // 5px top
final int shrinkedWidth = width - 40; // 20px left + 20px right
final int shrinkedHeigth = height - 25; // 5px top + 20px bottom
return isRectangleWithinBounds(shrinkedX, shrinkedY, 0, shrinkedHeigth) // Left pixel column
&& isRectangleWithinBounds(shrinkedX + shrinkedWidth, shrinkedY, 0, shrinkedHeigth) // Right pixel column
&& isRectangleWithinBounds(shrinkedX, shrinkedY, shrinkedWidth, 0) // Top pixel row
&& isRectangleWithinBounds(shrinkedX, shrinkedY + shrinkedHeigth, shrinkedWidth, 0); // Bottom pixel row
}
private boolean isRectangleWithinBounds(int x, int y, int width, int height) {
return !Screen.getScreensForRectangle(x, y, width, height).isEmpty();
}
private void mainWindowFocusChanged(Observable observable) {

View File

@@ -1,6 +1,7 @@
package org.cryptomator.ui.mainwindow;
import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
@@ -14,11 +15,12 @@ import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.StageInitializer;
import org.cryptomator.ui.error.ErrorComponent;
import org.cryptomator.ui.fxapp.FxApplicationTerminator;
import org.cryptomator.ui.fxapp.PrimaryStage;
import org.cryptomator.ui.migration.MigrationComponent;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.stats.VaultStatisticsComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import javax.inject.Named;
@@ -31,17 +33,25 @@ import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class, RecoveryKeyComponent.class})
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class, ErrorComponent.class, RecoveryKeyComponent.class})
abstract class MainWindowModule {
@Provides
@MainWindow
@MainWindowScoped
static Stage provideMainWindow(@PrimaryStage Stage stage, StageInitializer initializer) {
static Stage provideMainWindow(@PrimaryStage Stage stage, StageInitializer initializer, FxApplicationTerminator terminator, Lazy<TrayMenuComponent> trayMenu) {
initializer.accept(stage);
stage.setTitle("Cryptomator");
stage.setMinWidth(650);
stage.setMinHeight(498);
stage.setOnCloseRequest(e -> {
if (!trayMenu.get().isInitialized()) {
terminator.terminate();
e.consume();
} else {
stage.close();
}
});
return stage;
}

View File

@@ -3,11 +3,12 @@ package org.cryptomator.ui.mainwindow;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
@@ -20,20 +21,25 @@ import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
public class VaultDetailMissingVaultController implements FxController {
private final ObjectProperty<Vault> vault;
private final RemoveVaultComponent.Builder removeVault;
private final ObservableList<Vault> vaults;
private final ResourceBundle resourceBundle;
private final Stage window;
private final RecoveryKeyComponent.Factory recoveryKeyWindow;
private final Dialogs dialogs;
@Inject
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault, RemoveVaultComponent.Builder removeVault, ResourceBundle resourceBundle, @MainWindow Stage window, RecoveryKeyComponent.Factory recoveryKeyWindow) {
public VaultDetailMissingVaultController(ObjectProperty<Vault> vault, //
ObservableList<Vault> vaults, //
ResourceBundle resourceBundle, //
@MainWindow Stage window, //
Dialogs dialogs, //
RecoveryKeyComponent.Factory recoveryKeyWindow) {
this.vault = vault;
this.removeVault = removeVault;
this.vaults = vaults;
this.resourceBundle = resourceBundle;
this.window = window;
this.recoveryKeyWindow = recoveryKeyWindow;
this.dialogs = dialogs;
}
@FXML
@@ -43,16 +49,16 @@ public class VaultDetailMissingVaultController implements FxController {
@FXML
void didClickRemoveVault() {
removeVault.vault(vault.get()).build().showRemoveVault();
dialogs.prepareRemoveVaultDialog(window, vault.get(), vaults).build().showAndWait();
}
@FXML
void restoreVaultConfig(){
void restoreVaultConfig() {
recoveryKeyWindow.create(vault.get(), window).showIsHubVaultDialogWindow();
}
@FXML
void restoreMasterkey(){
void restoreMasterkey() {
recoveryKeyWindow.create(vault.get(), window).showRecoveryKeyRecoverWindow("Recover Masterkey");
}

View File

@@ -3,12 +3,13 @@ package org.cryptomator.ui.mainwindow;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@@ -18,14 +19,23 @@ public class VaultDetailUnknownErrorController implements FxController {
private final ObjectProperty<Vault> vault;
private final FxApplicationWindows appWindows;
private final Stage errorWindow;
private final RemoveVaultComponent.Builder removeVault;
private final ObservableList<Vault> vaults;
private final Stage mainWindow;
private final Dialogs dialogs;
@Inject
public VaultDetailUnknownErrorController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, @Named("errorWindow") Stage errorWindow, RemoveVaultComponent.Builder removeVault) {
public VaultDetailUnknownErrorController(@MainWindow Stage mainWindow, //
ObjectProperty<Vault> vault, //
ObservableList<Vault> vaults, //
FxApplicationWindows appWindows, //
@Named("errorWindow") Stage errorWindow, //
Dialogs dialogs) {
this.mainWindow = mainWindow;
this.vault = vault;
this.vaults = vaults;
this.appWindows = appWindows;
this.errorWindow = errorWindow;
this.removeVault = removeVault;
this.dialogs = dialogs;
}
@FXML
@@ -40,6 +50,6 @@ public class VaultDetailUnknownErrorController implements FxController {
@FXML
void didClickRemoveVault() {
removeVault.vault(vault.get()).build().showRemoveVault();
dialogs.prepareRemoveVaultDialog(mainWindow, vault.get(), vaults).build().showAndWait();
}
}

View File

@@ -13,10 +13,16 @@ import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.layout.HBox;
// unscoped because each cell needs its own controller
public class VaultListCellController implements FxController {
private static final Insets COMPACT_INSETS = new Insets(6, 12, 6, 12);
private static final Insets DEFAULT_INSETS = new Insets(12);
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private final ObservableValue<FontAwesome5Icon> glyph;
private final ObservableValue<Boolean> compactMode;
@@ -25,6 +31,8 @@ public class VaultListCellController implements FxController {
/* FXML */
public FontAwesome5IconView vaultStateView;
@FXML
public HBox vaultListCell;
@Inject
VaultListCellController(Settings settings) {
@@ -37,6 +45,7 @@ public class VaultListCellController implements FxController {
.onCondition(vault.flatMap(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals).orElse(false)) //
.afterStop(() -> vaultStateView.setRotate(0)) //
.build();
this.vaultListCell.paddingProperty().bind(compactMode.map(c -> c ? COMPACT_INSETS : DEFAULT_INSETS));
}
// TODO deduplicate w/ VaultDetailController

View File

@@ -5,8 +5,8 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
@@ -14,6 +14,7 @@ import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.EnumSet;
@@ -35,23 +36,32 @@ public class VaultListContextMenuController implements FxController {
private final FxApplicationWindows appWindows;
private final VaultService vaultService;
private final KeychainManager keychain;
private final RemoveVaultComponent.Builder removeVault;
private final VaultOptionsComponent.Factory vaultOptionsWindow;
private final ObservableValue<VaultState.Value> selectedVaultState;
private final ObservableValue<Boolean> selectedVaultPassphraseStored;
private final ObservableValue<Boolean> selectedVaultRemovable;
private final ObservableValue<Boolean> selectedVaultUnlockable;
private final ObservableValue<Boolean> selectedVaultLockable;
private final ObservableList<Vault> vaults;
private final Dialogs dialogs;
@Inject
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplicationWindows appWindows, VaultService vaultService, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Factory vaultOptionsWindow) {
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, //
ObservableList<Vault> vaults, //
@MainWindow Stage mainWindow, //
FxApplicationWindows appWindows, //
VaultService vaultService, //
KeychainManager keychain, //
VaultOptionsComponent.Factory vaultOptionsWindow, //
Dialogs dialogs) {
this.selectedVault = selectedVault;
this.vaults = vaults;
this.mainWindow = mainWindow;
this.appWindows = appWindows;
this.vaultService = vaultService;
this.keychain = keychain;
this.removeVault = removeVault;
this.vaultOptionsWindow = vaultOptionsWindow;
this.dialogs = dialogs;
this.selectedVaultState = selectedVault.flatMap(Vault::stateProperty).orElse(null);
this.selectedVaultPassphraseStored = selectedVault.map(this::isPasswordStored).orElse(false);
@@ -67,7 +77,7 @@ public class VaultListContextMenuController implements FxController {
@FXML
public void didClickRemoveVault() {
var vault = Objects.requireNonNull(selectedVault.get());
removeVault.vault(vault).build().showRemoveVault();
dialogs.prepareRemoveVaultDialog(mainWindow, vault, vaults).build().showAndWait();
}
@FXML

View File

@@ -9,9 +9,9 @@ import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,6 +26,7 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListView;
import javafx.scene.input.ContextMenuEvent;
@@ -66,16 +67,17 @@ public class VaultListController implements FxController {
private final VaultListCellFactory cellFactory;
private final AddVaultWizardComponent.Builder addVaultWizard;
private final BooleanBinding emptyVaultList;
private final RemoveVaultComponent.Builder removeVaultDialogue;
private final VaultListManager vaultListManager;
private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
private final ResourceBundle resourceBundle;
private final FxApplicationWindows appWindows;
private final ObservableValue<Double> cellSize;
private final Dialogs dialogs;
public ListView<Vault> vaultList;
public StackPane root;
@FXML
private HBox addVaultButton;
private Button addVaultButton;
@FXML
private ContextMenu addVaultContextMenu;
@@ -86,21 +88,21 @@ public class VaultListController implements FxController {
VaultListCellFactory cellFactory, //
VaultService vaultService, //
AddVaultWizardComponent.Builder addVaultWizard, //
RemoveVaultComponent.Builder removeVaultDialogue, //
VaultListManager vaultListManager, //
ResourceBundle resourceBundle, //
FxApplicationWindows appWindows, //
Settings settings) {
Settings settings, //
Dialogs dialogs) {
this.mainWindow = mainWindow;
this.vaults = vaults;
this.selectedVault = selectedVault;
this.cellFactory = cellFactory;
this.vaultService = vaultService;
this.addVaultWizard = addVaultWizard;
this.removeVaultDialogue = removeVaultDialogue;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.appWindows = appWindows;
this.dialogs = dialogs;
this.emptyVaultList = Bindings.isEmpty(vaults);
@@ -212,7 +214,7 @@ public class VaultListController implements FxController {
private void pressedShortcutToRemoveVault() {
final var vault = selectedVault.get();
if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) {
removeVaultDialogue.vault(vault).build().showRemoveVault();
dialogs.prepareRemoveVaultDialog(mainWindow, vault, vaults).build().showAndWait();
}
}

View File

@@ -16,7 +16,7 @@ import javafx.fxml.FXML;
public class WelcomeController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
private static final String GETTING_STARTED_URI = "https://docs.cryptomator.org/en/1.7/desktop/getting-started/";
private static final String GETTING_STARTED_URI = "https://docs.cryptomator.org/desktop/getting-started/";
private final Application application;
private final BooleanBinding noVaultPresent;

View File

@@ -10,7 +10,7 @@ import javafx.stage.Stage;
public class MigrationImpossibleController implements FxController {
private static final String HELP_URI = "https://docs.cryptomator.org/en/1.7/help/manual-migration/";
private static final String HELP_URI = "https://docs.cryptomator.org/help/manual-migration/";
private final Application application;
private final Stage window;

View File

@@ -5,7 +5,7 @@ import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removecert.RemoveCertComponent;
import org.cryptomator.ui.dialogs.Dialogs;
import javax.inject.Inject;
import javafx.application.Application;
@@ -15,6 +15,7 @@ import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.stage.Stage;
@PreferencesScoped
public class SupporterCertificateController implements FxController {
@@ -26,18 +27,22 @@ public class SupporterCertificateController implements FxController {
private final Stage window;
private final LicenseHolder licenseHolder;
private final Settings settings;
private final RemoveCertComponent.Builder removeCert;
private final Dialogs dialogs;
@FXML
private TextArea supporterCertificateField;
@Inject
SupporterCertificateController(Application application, @PreferencesWindow Stage window, LicenseHolder licenseHolder, Settings settings, RemoveCertComponent.Builder removeCert) {
SupporterCertificateController(Application application, //
@PreferencesWindow Stage window, //
LicenseHolder licenseHolder, //
Settings settings, //
Dialogs dialogs) {
this.application = application;
this.window = window;
this.licenseHolder = licenseHolder;
this.settings = settings;
this.removeCert = removeCert;
this.dialogs = dialogs;
}
@FXML
@@ -84,10 +89,11 @@ public class SupporterCertificateController implements FxController {
@FXML
void didClickRemoveCert() {
removeCert.build().showRemoveCert(window);
dialogs.prepareRemoveCertDialog(window, settings).build().showAndWait();
}
public LicenseHolder getLicenseHolder() {
return licenseHolder;
}
}

View File

@@ -19,6 +19,8 @@ import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -32,7 +34,10 @@ import java.util.ResourceBundle;
@PreferencesScoped
public class UpdatesPreferencesController implements FxController {
private static final String DOWNLOADS_URI = "https://cryptomator.org/downloads";
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
+ "?utm_source=cryptomator-desktop" //
+ "&utm_medium=update-notification&" //
+ "utm_campaign=app-update-%s";
private final Application application;
private final Environment environment;
@@ -50,6 +55,7 @@ public class UpdatesPreferencesController implements FxController {
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
private final DateTimeFormatter formatter;
private final BooleanBinding upToDate;
private final String downloadsUri;
/* FXML */
public CheckBox checkForUpdatesCheckbox;
@@ -65,12 +71,13 @@ public class UpdatesPreferencesController implements FxController {
this.latestVersion = updateChecker.latestVersionProperty();
this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty();
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck);
this.currentVersion = updateChecker.getCurrentVersion();
this.currentVersion = environment.getAppVersion();
this.updateAvailable = updateChecker.updateAvailableProperty();
this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion));
this.checkFailed = updateChecker.checkFailedProperty();
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII));
}
public void initialize() {
@@ -93,7 +100,7 @@ public class UpdatesPreferencesController implements FxController {
@FXML
public void visitDownloadsPage() {
application.getHostServices().showDocument(DOWNLOADS_URI);
application.getHostServices().showDocument(downloadsUri);
}
@FXML

View File

@@ -22,7 +22,7 @@ import java.util.ResourceBundle;
@PreferencesScoped
public class VolumePreferencesController implements FxController {
public static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/";
public static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/desktop/volume-type/";
public static final int MIN_PORT = 1024;
public static final int MAX_PORT = 65535;

View File

@@ -1,34 +0,0 @@
package org.cryptomator.ui.removecert;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javafx.scene.Scene;
import javafx.stage.Stage;
@RemoveCertScoped
@Subcomponent(modules = {RemoveCertModule.class})
public interface RemoveCertComponent {
@RemoveCertWindow
Stage window();
@FxmlScene(FxmlFile.REMOVE_CERT)
Lazy<Scene> scene();
default void showRemoveCert(Stage owner) {
Stage stage = window();
stage.setScene(scene().get());
stage.sizeToScene();
stage.initOwner(owner);
stage.show();
}
@Subcomponent.Builder
interface Builder {
RemoveCertComponent build();
}
}

View File

@@ -1,32 +0,0 @@
package org.cryptomator.ui.removecert;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@RemoveCertScoped
public class RemoveCertController implements FxController {
private final Stage window;
private final Settings settings;
@Inject
public RemoveCertController(@RemoveCertWindow Stage window, Settings settings) {
this.window = window;
this.settings = settings;
}
@FXML
public void close() {
window.close();
}
@FXML
public void remove() {
settings.licenseKey.set(null);
window.close();
}
}

View File

@@ -1,56 +0,0 @@
package org.cryptomator.ui.removecert;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import javax.inject.Provider;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class RemoveCertModule {
@Provides
@RemoveCertWindow
@RemoveCertScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@RemoveCertWindow
@RemoveCertScoped
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("removeCert.title"));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
return stage;
}
@Provides
@FxmlScene(FxmlFile.REMOVE_CERT)
@RemoveCertScoped
static Scene provideRemoveCertScene(@RemoveCertWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.REMOVE_CERT);
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(RemoveCertController.class)
abstract FxController bindRemoveCertController(RemoveCertController controller);
}

View File

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

View File

@@ -1,14 +0,0 @@
package org.cryptomator.ui.removecert;
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 RemoveCertWindow {
}

View File

@@ -1,39 +0,0 @@
package org.cryptomator.ui.removevault;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javafx.scene.Scene;
import javafx.stage.Stage;
@RemoveVaultScoped
@Subcomponent(modules = {RemoveVaultModule.class})
public interface RemoveVaultComponent {
@RemoveVaultWindow
Stage window();
@FxmlScene(FxmlFile.REMOVE_VAULT)
Lazy<Scene> scene();
default void showRemoveVault() {
Stage stage = window();
stage.setScene(scene().get());
stage.sizeToScene();
stage.show();
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@RemoveVaultWindow Vault vault);
RemoveVaultComponent build();
}
}

View File

@@ -1,40 +0,0 @@
package org.cryptomator.ui.removevault;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@RemoveVaultScoped
public class RemoveVaultController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(RemoveVaultController.class);
private final Stage window;
private final Vault vault;
private final ObservableList<Vault> vaults;
@Inject
public RemoveVaultController(@RemoveVaultWindow Stage window, @RemoveVaultWindow Vault vault, ObservableList<Vault> vaults) {
this.window = window;
this.vault = vault;
this.vaults = vaults;
}
@FXML
public void close() {
window.close();
}
@FXML
public void finish() {
vaults.remove(vault);
LOG.debug("Removing vault {}.", vault.getDisplayName());
window.close();
}
}

View File

@@ -1,59 +0,0 @@
package org.cryptomator.ui.removevault;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.fxapp.PrimaryStage;
import javax.inject.Provider;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Map;
import java.util.ResourceBundle;
@Module
abstract class RemoveVaultModule {
@Provides
@RemoveVaultWindow
@RemoveVaultScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@RemoveVaultWindow
@RemoveVaultScoped
static Stage provideStage(StageFactory factory, @PrimaryStage Stage primaryStage, @RemoveVaultWindow Vault vault, ResourceBundle resourceBundle) {
Stage stage = factory.create();
stage.setTitle(String.format(resourceBundle.getString("removeVault.title"), vault.getDisplayName()));
stage.setResizable(false);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(primaryStage);
return stage;
}
@Provides
@FxmlScene(FxmlFile.REMOVE_VAULT)
@RemoveVaultScoped
static Scene provideRemoveVaultScene(@RemoveVaultWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.REMOVE_VAULT);
}
// ------------------
@Binds
@IntoMap
@FxControllerKey(RemoveVaultController.class)
abstract FxController bindRemoveVaultController(RemoveVaultController controller);
}

View File

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

View File

@@ -1,14 +0,0 @@
package org.cryptomator.ui.removevault;
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 RemoveVaultWindow {
}

View File

@@ -3,7 +3,7 @@ package org.cryptomator.ui.sharevault;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Inject;
import javafx.application.Application;
@@ -33,8 +33,7 @@ public class ShareVaultController implements FxController {
this.window = window;
this.application = application;
this.vault = vault;
var vaultScheme = vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme();
this.hubVault = (vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTP) || vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS));
this.hubVault = KeyLoadingStrategy.isHubVault(vault.getVaultSettings().lastKnownKeyLoader.get());
}
@FXML

View File

@@ -10,6 +10,7 @@ import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.cryptomator.ui.fxapp.PrimaryStage;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
@@ -25,6 +26,8 @@ import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.ReadOnlyFileSystemException;
import java.util.concurrent.TimeUnit;
/**
* A multi-step task that consists of background activities as well as user interaction.
@@ -46,6 +49,7 @@ public class UnlockWorkflow extends Task<Void> {
private final FxApplicationWindows appWindows;
private final KeyLoadingStrategy keyLoadingStrategy;
private final ObjectProperty<IllegalMountPointException> illegalMountPointException;
private final Dialogs dialogs;
@Inject
UnlockWorkflow(@PrimaryStage Stage mainWindow, //
@@ -57,7 +61,8 @@ public class UnlockWorkflow extends Task<Void> {
@FxmlScene(FxmlFile.UNLOCK_REQUIRES_RESTART) Lazy<Scene> restartRequiredScene, //
FxApplicationWindows appWindows, //
@UnlockWindow KeyLoadingStrategy keyLoadingStrategy, //
@UnlockWindow ObjectProperty<IllegalMountPointException> illegalMountPointException) {
@UnlockWindow ObjectProperty<IllegalMountPointException> illegalMountPointException, //
Dialogs dialogs) {
this.mainWindow = mainWindow;
this.window = window;
this.vault = vault;
@@ -68,6 +73,7 @@ public class UnlockWorkflow extends Task<Void> {
this.appWindows = appWindows;
this.keyLoadingStrategy = keyLoadingStrategy;
this.illegalMountPointException = illegalMountPointException;
this.dialogs = dialogs;
}
@Override
@@ -144,11 +150,36 @@ public class UnlockWorkflow extends Task<Void> {
switch (throwable) {
case IllegalMountPointException e -> handleIllegalMountPointError(e);
case ConflictingMountServiceException _ -> handleConflictingMountServiceException();
case ReadOnlyFileSystemException _ -> handleReadOnlyFileSystem();
default -> handleGenericError(throwable);
}
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
}
private void handleReadOnlyFileSystem() {
var readOnlyDialog = dialogs.prepareRetryIfReadonlyDialog(mainWindow, stage -> {
stage.close();
this.retry();
}).build();
Platform.runLater(readOnlyDialog::showAndWait);
}
private void retry() {
try {
vault.getVaultSettings().usesReadOnlyMode.set(true);
var isLocked = vault.stateProperty().awaitState(VaultState.Value.LOCKED, 5, TimeUnit.SECONDS);
if (!isLocked) {
LOG.error("Vault did not changed to LOCKED state within 5 seconds. Aborting unlock retry.");
} else {
appWindows.startUnlockWorkflow(vault, mainWindow);
}
} catch (InterruptedException e) {
LOG.error("Waiting for LOCKED vault state was interrupted. Aborting unlock retry.", e);
Thread.currentThread().interrupt();
}
}
@Override
protected void cancelled() {
LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());

View File

@@ -17,6 +17,8 @@ import org.cryptomator.ui.preferences.VolumePreferencesController;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -50,7 +52,6 @@ public class MountOptionsController implements FxController {
private final ObservableValue<String> defaultMountFlags;
private final ObservableValue<Boolean> mountpointDirSupported;
private final ObservableValue<Boolean> mountpointDriveLetterSupported;
private final ObservableValue<Boolean> readOnlySupported;
private final ObservableValue<Boolean> mountFlagsSupported;
private final ObservableValue<Boolean> defaultMountServiceSelected;
private final ObservableValue<String> directoryPath;
@@ -60,6 +61,7 @@ public class MountOptionsController implements FxController {
private final ObservableValue<MountService> selectedMountService;
private final ObservableValue<Boolean> selectedMountServiceRequiresRestart;
private final ObservableValue<Boolean> loopbackPortChangeable;
private final ObservableBooleanValue readOnlyOptionAllowed;
//-- FXML objects --
@@ -108,10 +110,10 @@ public class MountOptionsController implements FxController {
});
this.mountFlagsSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_FLAGS));
this.defaultMountServiceSelected = ObservableUtil.mapWithDefault(vaultSettings.mountService, _ -> false, true);
this.readOnlySupported = selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY));
this.mountpointDirSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_TO_EXISTING_DIR) || s.hasCapability(MountCapability.MOUNT_WITHIN_EXISTING_PARENT));
this.mountpointDriveLetterSupported = selectedMountService.map(s -> s.hasCapability(MountCapability.MOUNT_AS_DRIVE_LETTER));
this.loopbackPortChangeable = selectedMountService.map(s -> s.hasCapability(MountCapability.LOOPBACK_PORT) && vaultSettings.mountService.getValue() != null);
this.readOnlyOptionAllowed = BooleanBinding.booleanExpression(selectedMountService.map(s -> s.hasCapability(MountCapability.READ_ONLY))).or(vaultSettings.usesReadOnlyMode);
}
private MountService reselectMountService() {
@@ -345,12 +347,12 @@ public class MountOptionsController implements FxController {
return mountpointDriveLetterSupported.getValue();
}
public ObservableValue<Boolean> readOnlySupportedProperty() {
return readOnlySupported;
public ObservableValue<Boolean> readOnlyOptionAllowedProperty() {
return readOnlyOptionAllowed;
}
public boolean isReadOnlySupported() {
return readOnlySupported.getValue();
public boolean isReadOnlyOptionAllowed() {
return readOnlyOptionAllowed.getValue();
}
public ObservableValue<String> directoryPathProperty() {

View File

@@ -3,6 +3,7 @@ package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.slf4j.Logger;
@@ -42,11 +43,11 @@ public class VaultOptionsController implements FxController {
window.setOnShowing(this::windowWillAppear);
selectedTabProperty.addListener(observable -> this.selectChosenTab());
tabPane.getSelectionModel().selectedItemProperty().addListener(observable -> this.selectedTabChanged());
var vaultScheme = vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme();
if(!vaultScheme.equals(MasterkeyFileLoadingStrategy.SCHEME)){
var vaultKeyLoader = vault.getVaultSettings().lastKnownKeyLoader.get();
if(!KeyLoadingStrategy.isMasterkeyFileVault(vaultKeyLoader)){
tabPane.getTabs().remove(keyTab);
}
if(!(vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTP) || vaultScheme.equals(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS))){
if(!KeyLoadingStrategy.isHubVault(vaultKeyLoader)){
tabPane.getTabs().remove(hubTab);
}

View File

@@ -15,7 +15,7 @@ import java.io.UncheckedIOException;
@WrongFileAlertScoped
public class WrongFileAlertController implements FxController {
private static final String DOCUMENTATION_URI = "https://docs.cryptomator.org/en/1.7/desktop/accessing-vaults/";
private static final String DOCUMENTATION_URI = "https://docs.cryptomator.org/desktop/accessing-vaults/";
private final Application app;
private final Stage window;