diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9635f1f..feb2e11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,8 +10,8 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: - distribution: 'temurin' - java-version: 19 + distribution: 'zulu' + java-version: 20 cache: 'maven' - name: Ensure to use tagged version if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c06a92a..cc951ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,8 +20,8 @@ jobs: fetch-depth: 2 - uses: actions/setup-java@v3 with: - distribution: 'temurin' - java-version: 19 + distribution: 'zulu' + java-version: 20 cache: 'maven' - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index fbca582..d9121a2 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -15,8 +15,8 @@ jobs: ref: "refs/tags/${{ github.event.inputs.tag }}" - uses: actions/setup-java@v3 with: - distribution: 'temurin' - java-version: 19 + distribution: 'zulu' + java-version: 20 cache: 'maven' server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml server-username: MAVEN_USERNAME # env variable for username in deploy diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index aaef56e..c0af882 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -10,8 +10,8 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: - distribution: 'temurin' - java-version: 19 + distribution: 'zulu' + java-version: 20 cache: 'maven' gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase diff --git a/.idea/misc.xml b/.idea/misc.xml index 4731638..8c0cac2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 4a7a447..9b3ecdf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # integrations-linux -Linux-specific implemenations of the integrations-api +Linux-specific implemenations of the [integrations-api](https://github.com/cryptomator/integrations-api). + +# Config + +This project uses the following JVM properties: +* `cryptomator.integrationsLinux.trayIconsDir` - specifies the directory from which svg images for the tray icon are loaded + diff --git a/pom.xml b/pom.xml index 530f3bd..fa77a77 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-linux - 1.2.1 + 1.3.0 integrations-linux Provides optional Linux services used by Cryptomator @@ -36,21 +36,23 @@ UTF-8 - 19 + 20 - 1.2.0 + 1.3.0 1.8.1-jdk17 - 1.2.8 - 31.1-jre + 1.3.2 + 1.3.4 + 32.0.0-jre 1.7.36 + 3.12.0 5.8.2 - 8.1.0 + 8.2.1 1.6.8 @@ -80,6 +82,18 @@ kdewallet ${kdewallet.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + org.purejava + libappindicator-gtk3-java-minimal + ${appindicator.version} + org.junit.jupiter junit-jupiter @@ -96,6 +110,9 @@ 3.9.0 ${project.jdk.version} + + --enable-preview + @@ -190,6 +207,7 @@ see + --enable-preview diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..a649eb4 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,24 @@ +import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.cryptomator.integrations.revealpath.RevealPathService; +import org.cryptomator.integrations.tray.TrayMenuController; +import org.cryptomator.linux.keychain.KDEWalletKeychainAccess; +import org.cryptomator.linux.keychain.SecretServiceKeychainAccess; +import org.cryptomator.linux.revealpath.DBusSendRevealPathService; +import org.cryptomator.linux.tray.AppindicatorTrayMenuController; + +module org.cryptomator.integrations.linux { + requires org.cryptomator.integrations.api; + requires org.slf4j; + requires com.google.common; + requires org.apache.commons.lang3; + requires org.freedesktop.dbus; + requires org.purejava.appindicator; + requires org.purejava.kwallet; + requires secret.service; + + provides KeychainAccessProvider with SecretServiceKeychainAccess, KDEWalletKeychainAccess; + provides RevealPathService with DBusSendRevealPathService; + provides TrayMenuController with AppindicatorTrayMenuController; + + opens org.cryptomator.linux.tray to org.cryptomator.integrations.api; +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java index f69afee..5478199 100644 --- a/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java +++ b/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java @@ -10,9 +10,9 @@ import org.freedesktop.dbus.exceptions.DBusConnectionException; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import org.kde.KWallet; -import org.kde.Static; -import org.purejava.KDEWallet; +import org.purejava.kwallet.KWallet; +import org.purejava.kwallet.KDEWallet; +import org.purejava.kwallet.Static; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/org/cryptomator/linux/tray/ActionItemCallback.java b/src/main/java/org/cryptomator/linux/tray/ActionItemCallback.java new file mode 100644 index 0000000..de1f525 --- /dev/null +++ b/src/main/java/org/cryptomator/linux/tray/ActionItemCallback.java @@ -0,0 +1,16 @@ +package org.cryptomator.linux.tray; + +import org.cryptomator.integrations.tray.ActionItem; +import org.purejava.appindicator.GCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +record ActionItemCallback (ActionItem actionItem) implements GCallback { + private static final Logger LOG = LoggerFactory.getLogger(ActionItemCallback.class); + + @Override + public void apply() { + LOG.trace("Hit tray menu action '{}'", actionItem.title()); + actionItem.action().run(); + } +} diff --git a/src/main/java/org/cryptomator/linux/tray/AppindicatorTrayMenuController.java b/src/main/java/org/cryptomator/linux/tray/AppindicatorTrayMenuController.java new file mode 100644 index 0000000..3fd8ea2 --- /dev/null +++ b/src/main/java/org/cryptomator/linux/tray/AppindicatorTrayMenuController.java @@ -0,0 +1,127 @@ +package org.cryptomator.linux.tray; + +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.tray.ActionItem; +import org.cryptomator.integrations.tray.SeparatorItem; +import org.cryptomator.integrations.tray.SubMenuItem; +import org.cryptomator.integrations.tray.TrayIconLoader; +import org.cryptomator.integrations.tray.TrayMenuController; +import org.cryptomator.integrations.tray.TrayMenuException; +import org.cryptomator.integrations.tray.TrayMenuItem; +import org.purejava.appindicator.GCallback; +import org.purejava.appindicator.NativeLibUtilities; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SegmentScope; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import static org.purejava.appindicator.app_indicator_h.*; + +@Priority(1000) +@CheckAvailability +@OperatingSystem(OperatingSystem.Value.LINUX) +public class AppindicatorTrayMenuController implements TrayMenuController { + private static final String APP_INDICATOR_ID = "org.cryptomator.Cryptomator"; + private static final String SVG_SOURCE_PROPERTY = "cryptomator.integrationsLinux.trayIconsDir"; + + private static final SegmentScope SCOPE = SegmentScope.global(); + private MemorySegment indicator; + private MemorySegment menu = gtk_menu_new(); + private Optional svgSourcePath; + + @CheckAvailability + public static boolean isAvailable() { + return NativeLibUtilities.isLoadedNativeLib(); + } + + @Override + public void showTrayIcon(Consumer iconLoader, Runnable runnable, String s) throws TrayMenuException { + TrayIconLoader.FreedesktopIconName callback = this::showTrayIconWithSVG; + iconLoader.accept(callback); + gtk_widget_show_all(menu); + app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE()); + } + + private void showTrayIconWithSVG(String s) { + try (var arena = Arena.openConfined()) { + svgSourcePath = Optional.ofNullable(System.getProperty(SVG_SOURCE_PROPERTY)); + // flatpak + if (svgSourcePath.isEmpty()) { + indicator = app_indicator_new(arena.allocateUtf8String(APP_INDICATOR_ID), + arena.allocateUtf8String(s), + APP_INDICATOR_CATEGORY_APPLICATION_STATUS()); + // AppImage and ppa + } else { + indicator = app_indicator_new_with_path(arena.allocateUtf8String(APP_INDICATOR_ID), + arena.allocateUtf8String(s), + APP_INDICATOR_CATEGORY_APPLICATION_STATUS(), + // find tray icons theme in mounted AppImage / installed on system by ppa + arena.allocateUtf8String(svgSourcePath.get())); + } + } + } + + @Override + public void updateTrayIcon(Consumer iconLoader) { + TrayIconLoader.FreedesktopIconName callback = this::updateTrayIconWithSVG; + iconLoader.accept(callback); + } + + private void updateTrayIconWithSVG(String s) { + try (var arena = Arena.openConfined()) { + app_indicator_set_icon(indicator, arena.allocateUtf8String(s)); + } + } + + @Override + public void updateTrayMenu(List items) throws TrayMenuException { + menu = gtk_menu_new(); + addChildren(menu, items); + gtk_widget_show_all(menu); + app_indicator_set_menu(indicator, menu); + } + + @Override + public void onBeforeOpenMenu(Runnable runnable) { + + } + + private void addChildren(MemorySegment menu, List items) { + for (var item : items) { + switch (item) { + case ActionItem a -> { + var gtkMenuItem = gtk_menu_item_new(); + try (var arena = Arena.openConfined()) { + gtk_menu_item_set_label(gtkMenuItem, arena.allocateUtf8String(a.title())); + g_signal_connect_object(gtkMenuItem, + arena.allocateUtf8String("activate"), + GCallback.allocate(new ActionItemCallback(a), SCOPE), + menu, + 0); + } + gtk_menu_shell_append(menu, gtkMenuItem); + } + case SeparatorItem separatorItem -> { + var gtkSeparator = gtk_menu_item_new(); + gtk_menu_shell_append(menu, gtkSeparator); + } + case SubMenuItem s -> { + var gtkMenuItem = gtk_menu_item_new(); + var gtkSubmenu = gtk_menu_new(); + try (var arena = Arena.openConfined()) { + gtk_menu_item_set_label(gtkMenuItem, arena.allocateUtf8String(s.title())); + } + addChildren(gtkSubmenu, s.items()); + gtk_menu_item_set_submenu(gtkMenuItem, gtkSubmenu); + gtk_menu_shell_append(menu, gtkMenuItem); + } + } + } + } +} diff --git a/src/main/resources/META-INF/services/org.cryptomator.integrations.tray.TrayMenuController b/src/main/resources/META-INF/services/org.cryptomator.integrations.tray.TrayMenuController new file mode 100644 index 0000000..caae17c --- /dev/null +++ b/src/main/resources/META-INF/services/org.cryptomator.integrations.tray.TrayMenuController @@ -0,0 +1 @@ +org.cryptomator.linux.tray.AppindicatorTrayMenuController \ No newline at end of file diff --git a/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java b/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java index c2bd4bb..2001e51 100644 --- a/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java +++ b/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java @@ -3,8 +3,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnOs; -import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.condition.EnabledIf; import java.io.IOException; import java.util.List; @@ -13,14 +12,13 @@ /** * Unit tests for GNOME keyring access via DBUS. */ -@EnabledOnOs(OS.LINUX) public class SecretServiceKeychainAccessTest { private static boolean isInstalled; @BeforeAll public static void checkSystemAndSetup() throws IOException { - ProcessBuilder dbusSend = new ProcessBuilder("dbus-send","--print-reply","--dest=org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.ListNames"); + ProcessBuilder dbusSend = new ProcessBuilder("dbus-send", "--print-reply", "--dest=org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.ListNames"); ProcessBuilder grep = new ProcessBuilder("grep", "org.gnome.keyring"); try { Process end = ProcessBuilder.startPipeline(List.of(dbusSend, grep)).get(1); @@ -36,10 +34,13 @@ public static void checkSystemAndSetup() throws IOException { @Test - public void testIsSupported(){ + @EnabledIf("isXdgDisplayEnvVarSet") + public void testIsSupported() { SecretServiceKeychainAccess secretService = new SecretServiceKeychainAccess(); Assertions.assertEquals(isInstalled, secretService.isSupported()); } - -} + public boolean isXdgDisplayEnvVarSet() { + return System.getenv("DISPLAY") != null; + } +} \ No newline at end of file diff --git a/suppression.xml b/suppression.xml index aebb9df..cbc052c 100644 --- a/suppression.xml +++ b/suppression.xml @@ -1,19 +1,3 @@ - - - ^org\.cryptomator:.*$ - cpe:/a:cryptomator:cryptomator - CVE-2022-25366 - - - - ^de\.swiesend\:secret\-service:.*$ - CVE-2018-19358 - CVE-2018-20781 - \ No newline at end of file