diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 227de96..58ee700 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,8 +12,8 @@ jobs: runs-on: windows-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: 17 @@ -24,17 +24,26 @@ jobs: run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} - name: Build and Test id: buildAndTest - run: mvn -B clean install -Pdependency-check - - uses: actions/upload-artifact@v2 + run: mvn -B clean test -Pdependency-check + - name: Codesign DLL + uses: skymatic/code-sign-action@v2 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: 5FC94CE149E5B511E621F53A060AC67CBD446B3A + timestampUrl: 'http://timestamp.digicert.com' + folder: ./src/main/resources + - name: Package and Install + id: packAndInstall + run: mvn -B install -DskipNativeCompile + - uses: actions/upload-artifact@v3 with: name: artifacts path: target/*.jar - name: Create Release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') - env: - GITHUB_TOKEN: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} # release as "cryptobot" with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - prerelease: true \ No newline at end of file + prerelease: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + generate_release_notes: true \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ef97a85..ae1912a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -19,19 +19,19 @@ jobs: runs-on: windows-2019 if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 2 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: 17 cache: 'maven' - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: java - name: Build run: mvn -B compile - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 \ No newline at end of file + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index 747dc38..150b20c 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -15,10 +15,10 @@ jobs: publish: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: "refs/tags/${{ github.event.inputs.tag }}" - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: 17 diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index a63d99b..a24a7bb 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -12,8 +12,8 @@ jobs: runs-on: windows-latest if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: 17 @@ -26,4 +26,20 @@ jobs: run: mvn deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} \ No newline at end of file + MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + notify: + runs-on: ubuntu-latest + needs: [publish] + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_USERNAME: 'Cryptobot' + SLACK_ICON: + SLACK_ICON_EMOJI: ':bot:' + SLACK_CHANNEL: 'cryptomator-desktop' + SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}" + SLACK_MESSAGE: "Ready to ." + SLACK_FOOTER: + MSG_MINIMAL: true \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1971e34..919a692 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-win - 1.1.2 + 1.2.0 Cryptomator Integrations for Windows Provides optional Windows services used by Cryptomator @@ -37,7 +37,7 @@ 17 - 1.1.0 + 1.2.0-beta4 1.7.36 2.9.0 @@ -147,29 +147,6 @@ - - org.codehaus.mojo - exec-maven-plugin - 3.0.0 - - - - exec - - compile - - make - ${project.basedir} - - ${java.home} - - - install - - - - - maven-resources-plugin 3.2.0 @@ -285,6 +262,42 @@ + + compile-native + + + !skipNativeCompile + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + + exec + + compile + + make + ${project.basedir} + + ${java.home} + + + install + + + + + + + + + sign diff --git a/src/main/headers/org_cryptomator_windows_autostart_WinShellLinks_Native.h b/src/main/headers/org_cryptomator_windows_autostart_WinShellLinks_Native.h index 43e59bc..fe26551 100644 --- a/src/main/headers/org_cryptomator_windows_autostart_WinShellLinks_Native.h +++ b/src/main/headers/org_cryptomator_windows_autostart_WinShellLinks_Native.h @@ -15,6 +15,14 @@ extern "C" { JNIEXPORT jint JNICALL Java_org_cryptomator_windows_autostart_WinShellLinks_00024Native_createShortcut (JNIEnv *, jobject, jbyteArray, jbyteArray, jbyteArray); +/* + * Class: org_cryptomator_windows_autostart_WinShellLinks_Native + * Method: createAndGetStartupFolderPath + * Signature: ()Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_org_cryptomator_windows_autostart_WinShellLinks_00024Native_createAndGetStartupFolderPath + (JNIEnv *, jobject); + #ifdef __cplusplus } #endif diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6a58be8..1e6e53a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,8 +1,10 @@ import org.cryptomator.integrations.autostart.AutoStartProvider; import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.cryptomator.integrations.revealpath.RevealPathService; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.windows.autostart.WindowsAutoStart; import org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess; +import org.cryptomator.windows.revealpath.ExplorerRevealPathService; import org.cryptomator.windows.uiappearance.WinUiAppearanceProvider; module org.cryptomator.integrations.win { @@ -15,4 +17,5 @@ provides AutoStartProvider with WindowsAutoStart; provides KeychainAccessProvider with WindowsProtectedKeychainAccess; provides UiAppearanceProvider with WinUiAppearanceProvider; + provides RevealPathService with ExplorerRevealPathService; } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/windows/autostart/WinShellLinks.java b/src/main/java/org/cryptomator/windows/autostart/WinShellLinks.java index 39caa91..a1b26f0 100644 --- a/src/main/java/org/cryptomator/windows/autostart/WinShellLinks.java +++ b/src/main/java/org/cryptomator/windows/autostart/WinShellLinks.java @@ -15,8 +15,8 @@ public class WinShellLinks { /** * Create a Windows shortcut file to the given target, at the specified location and with a description. * - * @param target path of the target the shortcut points to - * @param storagePath path where the shortcut is created + * @param target path of the shortcut target + * @param storagePath full path where the shortcut will be created (including filename & file extension) * @param description string inserted in the description field of the shortcut. * @return {@code 0} if everything worked, otherwise an HRESULT error code */ @@ -28,6 +28,15 @@ public int createShortcut(String target, String storagePath, String description) ); } + /** + * Gets the file system path of the startup folder. Creates all directories in the path, if necessary. + * + * @return path to the startup folder with no trailing \, or {@code null} if the folder is not defined and cannot be created + */ + public String getPathToStartupFolder() { + return Native.INSTANCE.createAndGetStartupFolderPath(); + } + // visible for testing byte[] getNullTerminatedUTF16Representation(String source) { byte[] bytes = source.getBytes(StandardCharsets.UTF_16LE); @@ -41,6 +50,8 @@ private Native() { NativeLibLoader.loadLib(); } - public synchronized native int createShortcut(byte[] target, byte[] storagePath, byte[] description); + synchronized native int createShortcut(byte[] target, byte[] storagePath, byte[] description); + + native String createAndGetStartupFolderPath(); } } diff --git a/src/main/java/org/cryptomator/windows/autostart/WindowsAutoStart.java b/src/main/java/org/cryptomator/windows/autostart/WindowsAutoStart.java index 411c394..381fa35 100644 --- a/src/main/java/org/cryptomator/windows/autostart/WindowsAutoStart.java +++ b/src/main/java/org/cryptomator/windows/autostart/WindowsAutoStart.java @@ -17,7 +17,7 @@ /** * Checks, en- and disables autostart for an application on Windows using the startup folder. *

- * The above actions are done by checking/adding/removing in the directory {@value RELATIVE_STARTUP_FOLDER} a shell link (.lnk). + * The above actions are done by checking/adding/removing a shell link (.lnk) in the Windows defined Startup directory (see also https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#constants). * The filename of the shell link is given by the JVM property {@value LNK_NAME_PROPERTY}. If the property is not set before object creation, the start command of the calling process is used. */ @Priority(1000) @@ -25,13 +25,12 @@ public class WindowsAutoStart implements AutoStartProvider { private static final Logger LOG = LoggerFactory.getLogger(WindowsAutoStart.class); - private static final String RELATIVE_STARTUP_FOLDER = "AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\"; private static final String LNK_FILE_EXTENSION = ".lnk"; private static final String LNK_NAME_PROPERTY = "cryptomator.integrationsWin.autoStartShellLinkName"; private final WinShellLinks winShellLinks; private final Optional shellLinkName; - private final Optional absStartupEntryPath; + private final Optional absShellLinkPath; private final Optional exePath; @SuppressWarnings("unused") // default constructor required by ServiceLoader @@ -39,12 +38,12 @@ public WindowsAutoStart() { this.winShellLinks = new WinShellLinks(); this.exePath = ProcessHandle.current().info().command().map(Path::of); this.shellLinkName = Optional.ofNullable(System.getProperty(LNK_NAME_PROPERTY)).or(() -> exePath.map(this::getExeBaseName)); - this.absStartupEntryPath = shellLinkName.map(name -> Path.of(System.getProperty("user.home"), RELATIVE_STARTUP_FOLDER, name + LNK_FILE_EXTENSION).toAbsolutePath()); + this.absShellLinkPath = Optional.ofNullable(winShellLinks.getPathToStartupFolder()).flatMap(p -> shellLinkName.map(name -> p + "\\" + name + LNK_FILE_EXTENSION)).map(Path::of); } @Override public boolean isEnabled() { - return absStartupEntryPath.map(Files::exists).orElse(false); + return absShellLinkPath.map(Files::exists).orElse(false); } @Override @@ -53,10 +52,10 @@ public synchronized void enable() throws ToggleAutoStartFailedException { throw new ToggleAutoStartFailedException("Enabling autostart using the startup folder failed: Path to executable is not set"); } - assert exePath.isPresent() && absStartupEntryPath.isPresent() && shellLinkName.isPresent(); - int returnCode = winShellLinks.createShortcut(exePath.get().toString(), absStartupEntryPath.get().toString(), shellLinkName.get()); + assert exePath.isPresent() && absShellLinkPath.isPresent() && shellLinkName.isPresent(); + int returnCode = winShellLinks.createShortcut(exePath.get().toString(), absShellLinkPath.get().toString(), shellLinkName.get()); if (returnCode == 0) { - LOG.debug("Successfully created {}.", absStartupEntryPath.get()); + LOG.debug("Successfully created {}.", absShellLinkPath.get()); } else { throw new ToggleAutoStartFailedException("Enabling autostart using the startup folder failed. Windows error code: " + Integer.toHexString(returnCode)); } @@ -65,13 +64,13 @@ public synchronized void enable() throws ToggleAutoStartFailedException { @Override public synchronized void disable() throws ToggleAutoStartFailedException { try { - Files.delete(absStartupEntryPath.get()); - LOG.debug("Successfully deleted {}.", absStartupEntryPath.get()); + Files.delete(absShellLinkPath.get()); + LOG.debug("Successfully deleted {}.", absShellLinkPath.get()); } catch (NoSuchElementException e) { //thrown by Optional::get throw new ToggleAutoStartFailedException("Disabling auto start failed using startup folder: Name of shell link is not defined."); } catch (NoSuchFileException e) { //also okay - LOG.debug("File {} not present. Nothing to do.", absStartupEntryPath.get()); + LOG.debug("File {} not present. Nothing to do.", absShellLinkPath.get()); } catch (IOException e) { LOG.debug("Failed to delete entry from auto start folder.", e); throw new ToggleAutoStartFailedException("Disabling auto start failed using startup folder.", e); diff --git a/src/main/java/org/cryptomator/windows/revealpath/ExplorerRevealPathService.java b/src/main/java/org/cryptomator/windows/revealpath/ExplorerRevealPathService.java new file mode 100644 index 0000000..687dd3e --- /dev/null +++ b/src/main/java/org/cryptomator/windows/revealpath/ExplorerRevealPathService.java @@ -0,0 +1,49 @@ +package org.cryptomator.windows.revealpath; + +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.revealpath.RevealFailedException; +import org.cryptomator.integrations.revealpath.RevealPathService; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.concurrent.TimeUnit; + +@Priority(100) +@OperatingSystem(OperatingSystem.Value.WINDOWS) +public class ExplorerRevealPathService implements RevealPathService { + + @Override + public void reveal(Path p) throws RevealFailedException { + try { + var attrs = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + ProcessBuilder pb = new ProcessBuilder(); + if(attrs.isDirectory()) { + pb.command("explorer.exe", "\""+p+"\""); + } else { + pb.command("explorer.exe ","/select,","\""+p+"\""); + } + + var process = pb.start(); + if (process.waitFor(5000, TimeUnit.MILLISECONDS)) { + int exitValue = process.exitValue(); + if (process.exitValue() != 1) { //explorer.exe seems to return always 1 + throw new RevealFailedException("Explorer.exe exited with value " + exitValue); + } + } + } catch (IOException e) { + throw new RevealFailedException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RevealFailedException(e); + } + } + + @Override + public boolean isSupported() { + return true; + } +} diff --git a/src/main/native/org_cryptomator_windows_autostart_WinShellLinks_Native.cpp b/src/main/native/org_cryptomator_windows_autostart_WinShellLinks_Native.cpp index 663c5db..504f3ff 100644 --- a/src/main/native/org_cryptomator_windows_autostart_WinShellLinks_Native.cpp +++ b/src/main/native/org_cryptomator_windows_autostart_WinShellLinks_Native.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -12,6 +14,32 @@ #include "org_cryptomator_windows_autostart_WinShellLinks_Native.h" HRESULT CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink, LPCWSTR lpszDesc); +//GUID of the Startup Folder: {B97D20BB-F46A-4C97-BA10-5E3608430854} +const GUID StartupFolderGUID = { + 0xB97D20BBu,0xF46Au,0x4C97u, 0xBAu, 0x10u, 0x5Eu,0x36u, 0x08u, 0x43u, 0x08u, 0x54u +}; + + +JNIEXPORT jstring JNICALL Java_org_cryptomator_windows_autostart_WinShellLinks_00024Native_createAndGetStartupFolderPath + (JNIEnv * env, jobject thisObj) { + HRESULT hres; + jstring result = NULL; + + PWSTR startupfolder_path; + hres = SHGetKnownFolderPath(StartupFolderGUID, KF_FLAG_CREATE | KF_FLAG_INIT, NULL, &startupfolder_path); //returns C:\Home, not C:\Home\ (NO trailing slash) + if(FAILED(hres)) { + return NULL; + } + + size_t length_startupfolder_path; + hres = StringCbLengthW(startupfolder_path, STRSAFE_MAX_CCH * sizeof(TCHAR), &length_startupfolder_path); + if(SUCCEEDED(hres)) { + result = env->NewString( (jchar *) startupfolder_path, length_startupfolder_path/sizeof(WCHAR) ); + } + CoTaskMemFree(startupfolder_path); + return result; +}; + JNIEXPORT jint JNICALL Java_org_cryptomator_windows_autostart_WinShellLinks_00024Native_createShortcut (JNIEnv * env, jobject thisObj, jbyteArray target, jbyteArray storage_path, jbyteArray description) { @@ -42,6 +70,7 @@ JNIEXPORT jint JNICALL Java_org_cryptomator_windows_autostart_WinShellLinks_0002 return hres; } + // CreateLink - Uses the Shell's IShellLink and IPersistFile interfaces // to create and store a shortcut to the specified object. // @@ -87,5 +116,5 @@ HRESULT CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink, LPCWSTR lpszDesc) } psl->Release(); } -return hres; -} + return hres; +} \ No newline at end of file diff --git a/src/main/resources/WinIntegrationsBundle_da.properties b/src/main/resources/WinIntegrationsBundle_da.properties new file mode 100644 index 0000000..b7b0217 --- /dev/null +++ b/src/main/resources/WinIntegrationsBundle_da.properties @@ -0,0 +1 @@ +org.cryptomator.windows.keychain.displayName=Windows Databeskyttelse \ No newline at end of file diff --git a/src/main/resources/WinIntegrationsBundle_pt.properties b/src/main/resources/WinIntegrationsBundle_pt.properties index 85f4a7a..cad370f 100644 --- a/src/main/resources/WinIntegrationsBundle_pt.properties +++ b/src/main/resources/WinIntegrationsBundle_pt.properties @@ -1 +1 @@ -org.cryptomator.windows.keychain.displayName=Windows Data Protection \ No newline at end of file +org.cryptomator.windows.keychain.displayName=Proteção de dados do Windows \ No newline at end of file diff --git a/src/main/resources/WinIntegrationsBundle_sl.properties b/src/main/resources/WinIntegrationsBundle_sl.properties new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/WinIntegrationsBundle_vi.properties b/src/main/resources/WinIntegrationsBundle_vi.properties index 9b7f58e..578ccfb 100644 --- a/src/main/resources/WinIntegrationsBundle_vi.properties +++ b/src/main/resources/WinIntegrationsBundle_vi.properties @@ -1 +1 @@ -org.cryptomator.windows.keychain.displayName=Bảo vệ Dữ liệu Windows \ No newline at end of file +org.cryptomator.windows.keychain.displayName=Thuật toán Bảo vệ Dữ liệu của Windows \ No newline at end of file diff --git a/src/test/java/org/cryptomator/windows/autostart/WinShellLinksTest.java b/src/test/java/org/cryptomator/windows/autostart/WinShellLinksTest.java index 661fe6a..f0a4b1d 100644 --- a/src/test/java/org/cryptomator/windows/autostart/WinShellLinksTest.java +++ b/src/test/java/org/cryptomator/windows/autostart/WinShellLinksTest.java @@ -23,18 +23,24 @@ public class WinShellLinksTest { private Path linkTarget; private Path shortcut; + private WinShellLinks inTest = new WinShellLinks(); + @BeforeEach public void setup(@TempDir Path tempDir) throws IOException { this.linkTarget = tempDir.resolve("link.target"); Files.createFile(linkTarget); } + @Test + public void testGetStartupFolderPath() { + Assertions.assertDoesNotThrow(() -> inTest.getPathToStartupFolder()); + } + @Test public void testShellLinkCreation() { - WinShellLinks winShellLinks = new WinShellLinks(); shortcut = linkTarget.getParent().resolve("short.lnk"); - int returnCode = winShellLinks.createShortcut(linkTarget.toString(), shortcut.toString(), "asd"); + int returnCode = inTest.createShortcut(linkTarget.toString(), shortcut.toString(), "asd"); Assertions.assertEquals(0, returnCode); Assertions.assertTrue(Files.exists(shortcut)); @@ -46,9 +52,7 @@ public void testShellLinkCreation() { "bar, '62 00 61 00 72 00 00 00'" }) public void testGetNullTerminatedUTF16Representation(String input, @ConvertWith(ByteArrayConverter.class) byte[] expected) { - WinShellLinks winShellLinks = new WinShellLinks(); - - var result = winShellLinks.getNullTerminatedUTF16Representation(input); + var result = inTest.getNullTerminatedUTF16Representation(input); Assertions.assertArrayEquals(expected, result); } diff --git a/src/test/java/org/cryptomator/windows/revealpath/ExplorerRevealPathServiceTest.java b/src/test/java/org/cryptomator/windows/revealpath/ExplorerRevealPathServiceTest.java new file mode 100644 index 0000000..eb77dd7 --- /dev/null +++ b/src/test/java/org/cryptomator/windows/revealpath/ExplorerRevealPathServiceTest.java @@ -0,0 +1,35 @@ +package org.cryptomator.windows.revealpath; + +import org.cryptomator.integrations.revealpath.RevealFailedException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ExplorerRevealPathServiceTest { + + ExplorerRevealPathService service = new ExplorerRevealPathService(); + + @Test + public void testReveal(@TempDir Path tmpDir) throws IOException { + Path foo = tmpDir.resolve("foo=.txt"); + Path bar = tmpDir.resolve("bar="); + + Files.createFile(foo); + Files.createDirectory(bar); + + Assertions.assertDoesNotThrow(() -> service.reveal(foo)); + Assertions.assertDoesNotThrow(() -> service.reveal(bar)); + Assertions.assertTrue(Files.exists(foo)); + } + + @Test + public void testRevealFailed(@TempDir Path tmpDir) { + Path foo = tmpDir.resolve("bar.txt"); + + Assertions.assertThrows(RevealFailedException.class, () -> service.reveal(foo)); + } +}