diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java index 7fc5bd652..9fc8d2ae4 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java @@ -81,6 +81,11 @@ public class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider return builder.build(); } + @Override + public boolean automaticallyResolveLinks() { + return false; + } + private FilePath getTarget(FilePath name) { return FilePath.of(name.toString() .replaceAll("\\.tar$", "") diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java index 24ede7343..6b8024dbb 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java @@ -35,6 +35,11 @@ public abstract class BaseUnzipUnixMenuProvider implements BrowserMenuLeafProvid return BrowserMenuCategory.CUSTOM; } + @Override + public boolean automaticallyResolveLinks() { + return false; + } + @Override public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { var sep = OsFileSystem.of(model.getFileSystem().getShell().orElseThrow().getOsType()) diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java index f7b20e6f2..986a7497b 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java @@ -24,6 +24,11 @@ public abstract class BaseUnzipWindowsActionProvider implements BrowserMenuLeafP this.toDirectory = toDirectory; } + @Override + public boolean automaticallyResolveLinks() { + return false; + } + @Override public LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java index b6b892fc1..dc84c87fe 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java @@ -100,6 +100,11 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { this.directory = directory; } + @Override + public boolean automaticallyResolveLinks() { + return false; + } + @Override public void execute(BrowserFileSystemTabModel model, List entries) { var name = new SimpleStringProperty(directory ? entries.getFirst().getFileName() : null); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/OsLogoComp.java b/app/src/main/java/io/xpipe/app/hub/comp/OsLogoComp.java index 5f23fcca9..1dc757985 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/OsLogoComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/OsLogoComp.java @@ -62,6 +62,10 @@ public class OsLogoComp extends SimpleRegionBuilder { return null; } + if (name.contains("Cisco")) { + return null; + } + if (ICONS.isEmpty()) { AppResources.with(AppResources.MAIN_MODULE, "os", file -> { try (var list = Files.list(file)) { diff --git a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java index 23df87f6a..775b4b229 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java @@ -64,6 +64,7 @@ public class SyncCategory extends AppPrefsCategory { .apply(struc -> struc.setAlignment(Pos.CENTER_LEFT)); var remoteRepo = new TextFieldComp(prefs.storageGitRemote).hgrow(); + remoteRepo.apply(textField -> textField.setPromptText("https://... | ssh://... | directory path")); remoteRepo.disable(prefs.enableGitStorage.not()); var builder = new OptionsBuilder(); diff --git a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java index 8be452e73..d8a08815c 100644 --- a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java @@ -81,16 +81,9 @@ public class KeeperPasswordManager implements PasswordManager { } @Override - public synchronized CredentialResult retrieveCredentials(String rawKey) { - return retrieveCredentials(rawKey, 0); - } - - private CredentialResult retrieveCredentials(String rawKey, int retries) { + public synchronized CredentialResult retrieveCredentials(String key) { // The copy UID button copies the whole URL in the Keeper UI. Why? ... - rawKey = rawKey.replaceFirst("https://\\w+\\.\\w+/vault/#detail/", ""); - - var username = rawKey.contains("/") ? rawKey.split("/", 2)[1] : null; - var key = rawKey.contains("/") ? rawKey.split("/", 2)[0] : rawKey; + key = key.replaceFirst("https://\\w+\\.\\w+/vault/#detail/", ""); try { CommandSupport.isInLocalPathOrThrow("Keeper Commander CLI", "keeper-commander"); @@ -135,8 +128,9 @@ public class KeeperPasswordManager implements PasswordManager { } var b = CommandBuilder.of() - .add(getExecutable(sc), "find-password") + .add(getExecutable(sc), "get") .addLiteral(key) + .add("--format", "json", "--unmask") .add("--password") .addLiteral(r.getSecretValue()); FilePath file = sc.getSystemTemporaryDirectory().join("keeper" + Math.abs(new Random().nextInt()) + ".txt"); @@ -161,8 +155,8 @@ public class KeeperPasswordManager implements PasswordManager { %s """.formatted( - index != -1 ? "\n" + getTotpDurationValues().get(index) : "", - totp.getSecret().getSecretValue()); + index != -1 ? "\n" + getTotpDurationValues().get(index) : "", + totp.getSecret().getSecretValue()); sc.view().writeTextFile(file, input); } } else { @@ -207,16 +201,21 @@ public class KeeperPasswordManager implements PasswordManager { .strip(); var err = result[1].replace("\r\n", "\n").replace("EOF when reading a line", "").strip(); - var outLines = out.lines().toList(); - var message = !err.isEmpty() ? out + "\n" + err : out; - if (exitCode != 0 || (outLines.size() > 0 && outLines.getLast().contains("Invalid entry"))) { - // Another password prompt was made - var wrongPw = out.contains("Enter password for") || exitCode == CommandControl.EXIT_TIMEOUT_EXIT_CODE; - if (wrongPw && hasCompletedRequestInSession) { - if (retries == 0) { - return retrieveCredentials(rawKey, retries + 1); - } + var jsonStart = out.indexOf("{\n"); + var jsonEnd = out.indexOf("\n}"); + if (jsonEnd != -1) { + jsonEnd += 2; + } + var outPrefix = jsonStart <= 0 ? out : out.substring(0, jsonStart); + var outJson = jsonStart <= 0 + ? (jsonEnd != -1 ? out.substring(0, jsonEnd) : out) + : (jsonEnd != -1 ? out.substring(jsonStart, jsonEnd) : out.substring(jsonStart)); + + if (exitCode != 0) { + // Another password prompt was made + var wrongPw = (outPrefix.contains("Enter password for") || exitCode == CommandControl.EXIT_TIMEOUT_EXIT_CODE) && !hasCompletedRequestInSession; + if (wrongPw) { SecretManager.clearAll(KEEPER_PASSWORD_ID); ErrorEventFactory.fromMessage("Master password was not accepted by Keeper. Is it correct?") .expected() @@ -224,18 +223,69 @@ public class KeeperPasswordManager implements PasswordManager { return null; } + var message = !err.isEmpty() ? outPrefix + "\n" + err : outPrefix; + ErrorEventFactory.fromMessage(message).expected().handle(); + return null; + } + + JsonNode tree; + try { + tree = JacksonMapper.getDefault().readTree(outJson); + } catch (JsonProcessingException e) { + var message = !err.isEmpty() ? outPrefix + "\n" + err : outPrefix; ErrorEventFactory.fromMessage(message).expected().handle(); return null; } hasCompletedRequestInSession = true; - if (outLines.isEmpty()) { - return null; + var fields = tree.get("fields"); + // There multiple schemas + if (fields == null || !fields.isArray()) { + String login = null; + String password = null; + + var l = tree.get("login"); + if (l != null && l.isTextual()) { + login = l.asText(); + } + + var p = tree.get("password"); + if (p != null && p.isTextual()) { + password = p.asText(); + } + + if (login == null && password == null) { + var message = !err.isEmpty() ? out + "\n" + err : out; + ErrorEventFactory.fromMessage(message) + .description("Received invalid response") + .expected() + .handle(); + return null; + } + + return new CredentialResult(login, password != null ? InPlaceSecretValue.of(password) : null); } - var lastLine = outLines.getLast(); - return new CredentialResult(username, InPlaceSecretValue.of(lastLine)); + String login = null; + String password = null; + for (JsonNode field : fields) { + var type = field.required("type").asText(); + if (type.equals("login")) { + var v = field.required("value"); + if (v.size() > 0) { + login = v.get(0).asText(); + } + } + if (type.equals("password")) { + var v = field.required("value"); + if (v.size() > 0) { + password = v.get(0).asText(); + } + } + } + + return new CredentialResult(login, password != null ? InPlaceSecretValue.of(password) : null); } catch (Exception ex) { ErrorEventFactory.fromThrowable(ex).handle(); return null; diff --git a/app/src/main/java/io/xpipe/app/util/StoreStateFormat.java b/app/src/main/java/io/xpipe/app/util/StoreStateFormat.java index 6529dffea..19658d87d 100644 --- a/app/src/main/java/io/xpipe/app/util/StoreStateFormat.java +++ b/app/src/main/java/io/xpipe/app/util/StoreStateFormat.java @@ -55,7 +55,7 @@ public class StoreStateFormat { return new StoreStateFormat(List.of(), null, info).format(); } - return new StoreStateFormat(features, s.getShellDialect().getDisplayName(), info).format(); + return new StoreStateFormat(features, s.getOsName() != null ? s.getOsName() : s.getShellDialect().getDisplayName(), info).format(); } var joined = Stream.concat( diff --git a/build.gradle b/build.gradle index 71823868a..d1621c264 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ import java.util.stream.Stream plugins { id("io.github.gradle-nexus.publish-plugin") version "2.0.0" - id 'org.gradlex.extra-java-module-info' version '1.13.1' apply false + id 'org.gradlex.extra-java-module-info' version '1.14' apply false id("com.diffplug.spotless") version "8.2.1" apply false } diff --git a/dist/build.gradle b/dist/build.gradle index ccb9e3da3..1f7c58a86 100644 --- a/dist/build.gradle +++ b/dist/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'org.beryx.jlink' version '3.1.5' + id 'org.beryx.jlink' version '3.2.1' id("com.netflix.nebula.ospackage") version "12.1.1" id 'org.gradle.crypto.checksum' version '1.4.0' id 'signing' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6954be875..5f38436fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME