Browse Source

Merge branch 'develop' into feature/preprocess-properties

Armin Schrenk 1 year ago
parent
commit
5e52f715ce

+ 19 - 117
.github/SECURITY.md

@@ -2,123 +2,25 @@
 
 ## Reporting a Vulnerability
 
-For reporting security-related vulnerabilities or exploits that [haven't been reported yet](https://github.com/cryptomator/cryptomator/labels/type%3Asecurity-issue), contact us at: security@cryptomator.org
+We take security seriously at Cryptomator. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
 
-<details>
-<summary>PGP Key</summary>
+To report a security vulnerability, please use the [GitHub Security Advisory feature](https://github.com/cryptomator/cryptomator/security/advisories). This feature allows you to privately discuss, fix, and publish information about security vulnerabilities.
 
-```
------BEGIN PGP PUBLIC KEY BLOCK-----
-Comment: GPGTools - https://gpgtools.org
+If you prefer to report the vulnerability via email, please send an email to security@cryptomator.org.
 
-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
-=W1Ri
------END PGP PUBLIC KEY BLOCK-----
-```
-</details>
+PGP key fingerprint: `3647 9903 B23A E0A5 9359  9A3E 23B5 DBEF 94D4 D81D` ([public key](https://gist.github.com/cryptobot/864300b6b44ae2d2a15abedfe14bd040))
+
+## Expectations
+
+When reporting a vulnerability, please provide us with a detailed report that includes:
+
+- A description of the vulnerability
+- Steps to reproduce the vulnerability
+- Possible impact of the vulnerability
+- Any additional information that may be helpful
+
+We ask that you do not publicly disclose the vulnerability until we have had a chance to address it.
+
+## Thank You
+
+We appreciate your help in keeping Cryptomator secure. Thank you for your contributions to the security of our project.

+ 3 - 6
src/main/java/org/cryptomator/common/ErrorCode.java

@@ -32,18 +32,15 @@ public class ErrorCode {
 		this.rootCauseSpecificFrames = rootCauseSpecificFrames;
 	}
 
-	// visible for testing
-	String methodCode() {
+	public String methodCode() {
 		return format(traceCode(rootCause, LATEST_FRAME));
 	}
 
-	// visible for testing
-	String rootCauseCode() {
+	public String rootCauseCode() {
 		return format(traceCode(rootCause, rootCauseSpecificFrames));
 	}
 
-	// visible for testing
-	String throwableCode() {
+	public String throwableCode() {
 		return format(traceCode(throwable, ALL_FRAMES));
 	}
 

+ 168 - 7
src/main/java/org/cryptomator/ui/error/ErrorController.java

@@ -1,5 +1,7 @@
 package org.cryptomator.ui.error;
 
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
 import org.cryptomator.common.Environment;
 import org.cryptomator.common.ErrorCode;
 import org.cryptomator.common.Nullable;
@@ -9,20 +11,34 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javafx.application.Application;
 import javafx.application.Platform;
+import javafx.beans.binding.BooleanExpression;
 import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.fxml.FXML;
 import javafx.scene.Scene;
 import javafx.scene.input.Clipboard;
 import javafx.scene.input.ClipboardContent;
 import javafx.stage.Stage;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
 import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 
 public class ErrorController implements FxController {
 
+	private static final String ERROR_CODES_URL = "https://gist.githubusercontent.com/cryptobot/accba9fb9555e7192271b85606f97230/raw/errorcodes.json";
 	private static final String SEARCH_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/categories/errors?discussions_q=category:Errors+%s";
 	private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s";
 	private static final String SEARCH_ERRORCODE_DELIM = " OR ";
@@ -46,16 +62,28 @@ public class ErrorController implements FxController {
 	private final Stage window;
 	private final Environment environment;
 
-	private BooleanProperty copiedDetails = new SimpleBooleanProperty();
+	private final BooleanProperty copiedDetails = new SimpleBooleanProperty();
+	private final ObjectProperty<ErrorDiscussion> matchingErrorDiscussion = new SimpleObjectProperty<>();
+	private final BooleanExpression errorSolutionFound = matchingErrorDiscussion.isNotNull();
+	private final BooleanProperty isLoadingHttpResponse = new SimpleBooleanProperty();
 
 	@Inject
-	ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window, Environment environment) {
+	ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window, Environment environment, ExecutorService executorService) {
 		this.application = application;
 		this.stackTrace = stackTrace;
 		this.errorCode = errorCode;
 		this.previousScene = previousScene;
 		this.window = window;
 		this.environment = environment;
+
+		isLoadingHttpResponse.set(true);
+		HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
+		HttpRequest httpRequest = HttpRequest.newBuilder()//
+				.uri(URI.create(ERROR_CODES_URL))//
+				.build();
+		httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream())//
+				.thenAcceptAsync(this::loadHttpResponse, executorService)//
+				.whenCompleteAsync((r, e) -> isLoadingHttpResponse.set(false), Platform::runLater);
 	}
 
 	@FXML
@@ -70,6 +98,16 @@ public class ErrorController implements FxController {
 		window.close();
 	}
 
+	@FXML
+	public void showSolution() {
+		if (matchingErrorDiscussion.isNotNull().get()) {
+			var discussion = matchingErrorDiscussion.get();
+			if (discussion != null) {
+				application.getHostServices().showDocument(discussion.url);
+			}
+		}
+	}
+
 	@FXML
 	public void searchError() {
 		var searchTerm = URLEncoder.encode(getErrorCode().replace(ErrorCode.DELIM, SEARCH_ERRORCODE_DELIM), StandardCharsets.UTF_8);
@@ -95,13 +133,119 @@ public class ErrorController implements FxController {
 		Clipboard.getSystemClipboard().setContent(clipboardContent);
 
 		copiedDetails.set(true);
-		CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
-			copiedDetails.set(false);
-		});
+		CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> copiedDetails.set(false));
 	}
 
-	/* Getter/Setter */
+	private void loadHttpResponse(HttpResponse<InputStream> response) {
+		if (response.statusCode() == 200) {
+			Map<String, ErrorDiscussion> errorDiscussionMap = new Gson().fromJson(//
+					new InputStreamReader(response.body(), StandardCharsets.UTF_8),//
+					new TypeToken<Map<String, ErrorDiscussion>>() {
+					}.getType());
+
+			if (errorDiscussionMap.values().stream().anyMatch(this::containsMethodCode)) {
+				Comparator<ErrorDiscussion> comp = this::compareByFullErrorCode;
+				Optional<ErrorDiscussion> value = errorDiscussionMap.values().stream().filter(this::containsMethodCode)//
+						.min(comp//
+								.thenComparing(this::compareByRootCauseCode)//
+								.thenComparing(this::compareIsAnswered)//
+								.thenComparing(this::compareUpvoteCount));
+
+				if (value.isPresent()) {
+					matchingErrorDiscussion.set(value.get());
+				}
+			}
+		}
+	}
+
+	/**
+	 * Checks if an ErrorDiscussion object's title contains the error code's method code.
+	 *
+	 * @param errorDiscussion The ErrorDiscussion object to be checked.
+	 * @return A boolean value indicating if the ErrorDiscussion object's title contains the error code's method code:
+	 * - true if the title contains the method code,
+	 * - false otherwise.
+	 */
+	public boolean containsMethodCode(ErrorDiscussion errorDiscussion) {
+		return errorDiscussion.title.contains(" " + errorCode.methodCode());
+	}
+
+	/**
+	 * Compares two ErrorDiscussion objects based on their upvote counts and returns the result.
+	 *
+	 * @param ed1 The first ErrorDiscussion object.
+	 * @param ed2 The second ErrorDiscussion object.
+	 * @return An integer indicating which ErrorDiscussion object has a higher upvote count:
+	 * - A positive value if ed2 has a higher upvote count than ed1,
+	 * - A negative value if ed1 has a higher upvote count than ed2,
+	 * - Or 0 if both upvote counts are equal.
+	 */
+	public int compareUpvoteCount(ErrorDiscussion ed1, ErrorDiscussion ed2) {
+		return Integer.compare(ed2.upvoteCount, ed1.upvoteCount);
+	}
+
+	/**
+	 * Compares two ErrorDiscussion objects based on their answered status and returns the result.
+	 *
+	 * @param ed1 The first ErrorDiscussion object.
+	 * @param ed2 The second ErrorDiscussion object.
+	 * @return An integer indicating the answered status of the ErrorDiscussion objects:
+	 * - A negative value (-1) if ed1 is considered answered and ed2 is considered unanswered,
+	 * - A positive value (1) if ed1 is considered unanswered and ed2 is considered answered,
+	 * - Or 0 if both ErrorDiscussion objects are considered answered or unanswered.
+	 */
+	public int compareIsAnswered(ErrorDiscussion ed1, ErrorDiscussion ed2) {
+		if (ed1.answer != null && ed2.answer == null) {
+			return -1;
+		} else if (ed1.answer == null && ed2.answer != null) {
+			return 1;
+		} else {
+			return 0;
+		}
+	}
 
+	/**
+	 * Compares two ErrorDiscussion objects based on the presence of the full error code in their titles and returns the result.
+	 *
+	 * @param ed1 The first ErrorDiscussion object.
+	 * @param ed2 The second ErrorDiscussion object.
+	 * @return An integer indicating the comparison result based on the presence of the full error code in the titles:
+	 * - A negative value (-1) if ed1 contains the full error code in the title and ed2 does not have a match,
+	 * - A positive value (1) if ed1 does not have a match and ed2 contains the full error code in the title,
+	 * - Or 0 if both ErrorDiscussion objects either contain the full error code or do not have a match in the titles.
+	 */
+	public int compareByFullErrorCode(ErrorDiscussion ed1, ErrorDiscussion ed2) {
+		if (ed1.title.contains(getErrorCode()) && !ed2.title.contains(getErrorCode())) {
+			return -1;
+		} else if (!ed1.title.contains(getErrorCode()) && ed2.title.contains(getErrorCode())) {
+			return 1;
+		} else {
+			return 0;
+		}
+	}
+
+	/**
+	 * Compares two ErrorDiscussion objects based on the presence of the root cause code in their titles and returns the result.
+	 *
+	 * @param ed1 The first ErrorDiscussion object.
+	 * @param ed2 The second ErrorDiscussion object.
+	 * @return An integer indicating the comparison result based on the presence of the root cause code in the titles:
+	 * - A negative value (-1) if ed1 contains the root cause code in the title and ed2 does not have a match,
+	 * - A positive value (1) if ed1 does not have a match and ed2 contains the root cause code in the title,
+	 * - Or 0 if both ErrorDiscussion objects either contain the root cause code or do not have a match in the titles.
+	 */
+	public int compareByRootCauseCode(ErrorDiscussion ed1, ErrorDiscussion ed2) {
+		String value = " " + errorCode.methodCode() + ErrorCode.DELIM + errorCode.rootCauseCode();
+		if (ed1.title.contains(value) && !ed2.title.contains(value)) {
+			return -1;
+		} else if (!ed1.title.contains(value) && ed2.title.contains(value)) {
+			return 1;
+		} else {
+			return 0;
+		}
+	}
+
+	/* Getter/Setter */
 	public boolean isPreviousScenePresent() {
 		return previousScene != null;
 	}
@@ -125,4 +269,21 @@ public class ErrorController implements FxController {
 	public boolean getCopiedDetails() {
 		return copiedDetails.get();
 	}
-}
+
+	public BooleanExpression errorSolutionFoundProperty() {
+		return errorSolutionFound;
+	}
+
+	public boolean getErrorSolutionFound() {
+		return errorSolutionFound.get();
+	}
+
+	public BooleanProperty isLoadingHttpResponseProperty() {
+		return isLoadingHttpResponse;
+	}
+
+	public boolean getIsLoadingHttpResponse() {
+		return isLoadingHttpResponse.get();
+	}
+
+}

+ 13 - 0
src/main/java/org/cryptomator/ui/error/ErrorDiscussion.java

@@ -0,0 +1,13 @@
+package org.cryptomator.ui.error;
+
+public class ErrorDiscussion {
+
+	int upvoteCount;
+	String title;
+	String url;
+	Answer answer;
+
+	static class Answer {
+
+	}
+}

+ 21 - 12
src/main/resources/fxml/error.fxml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
 <?import org.cryptomator.ui.controls.FormattedLabel?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Button?>
@@ -31,20 +32,28 @@
 			</StackPane>
 			<VBox spacing="6" HBox.hgrow="ALWAYS">
 				<FormattedLabel styleClass="label-extra-large" format="%error.message" arg1="${controller.errorCode}"/>
-				<Label text="%error.description" wrapText="true"/>
-				<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.lookup" onAction="#searchError" contentDisplay="LEFT">
-					<graphic>
-						<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
-					</graphic>
-				</Hyperlink>
-				<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.report" onAction="#reportError" contentDisplay="LEFT">
-					<graphic>
-						<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
-					</graphic>
-				</Hyperlink>
+				<FontAwesome5Spinner glyphSize="24" visible="${controller.isLoadingHttpResponse}" managed="${controller.isLoadingHttpResponse}"/>
+				<VBox visible="${!controller.isLoadingHttpResponse}" managed="${!controller.isLoadingHttpResponse}">
+					<Label text="%error.existingSolutionDescription" wrapText="true" visible="${controller.errorSolutionFound}" managed="${controller.errorSolutionFound}"/>
+					<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.solution" onAction="#showSolution" contentDisplay="LEFT" visible="${controller.errorSolutionFound}" managed="${controller.errorSolutionFound}">
+						<graphic>
+							<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
+						</graphic>
+					</Hyperlink>
+					<Label text="%error.description" wrapText="true" visible="${!controller.errorSolutionFound}" managed="${!controller.errorSolutionFound}"/>
+					<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.lookup" onAction="#searchError" contentDisplay="LEFT" visible="${!controller.errorSolutionFound}" managed="${!controller.errorSolutionFound}">
+						<graphic>
+							<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
+						</graphic>
+					</Hyperlink>
+					<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.report" onAction="#reportError" contentDisplay="LEFT" visible="${!controller.errorSolutionFound}" managed="${!controller.errorSolutionFound}">
+						<graphic>
+							<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
+						</graphic>
+					</Hyperlink>
+				</VBox>
 			</VBox>
 		</HBox>
-
 		<VBox spacing="6" VBox.vgrow="ALWAYS">
 			<HBox>
 				<Label text="%error.technicalDetails"/>

+ 3 - 0
src/main/resources/i18n/strings.properties

@@ -21,6 +21,9 @@ error.description=Cryptomator didn't expect this to happen. You can look up exis
 error.hyperlink.lookup=Look up this error
 error.hyperlink.report=Report this error
 error.technicalDetails=Details:
+error.existingSolutionDescription=Cryptomator didn't expect this to happen. But we found an existing solution for this error. Please take a look at the following link.
+error.hyperlink.solution=Look up the solution
+
 
 # Defaults
 defaults.vault.vaultName=Vault

+ 145 - 0
src/test/java/org/cryptomator/ui/error/ErrorControllerTest.java

@@ -0,0 +1,145 @@
+package org.cryptomator.ui.error;
+
+import org.cryptomator.common.Environment;
+import org.cryptomator.common.ErrorCode;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.mockito.Mockito;
+
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import java.util.concurrent.ExecutorService;
+
+class ErrorControllerTest {
+
+	Application application;
+	String stackTrace;
+	ErrorCode errorCode;
+	Scene previousScene;
+	Stage window;
+	Environment environment;
+	ExecutorService executorService;
+	ErrorController errorController;
+
+	@BeforeEach
+	public void beforeEach() {
+		application = Mockito.mock(Application.class);
+		stackTrace = "This is a stackTrace mock";
+		errorCode = Mockito.mock(ErrorCode.class);
+		previousScene = Mockito.mock(Scene.class);
+		window = Mockito.mock(Stage.class);
+		environment = Mockito.mock(Environment.class);
+		executorService = Mockito.mock(ExecutorService.class);
+		errorController = new ErrorController(application, stackTrace, errorCode, previousScene, window, environment, executorService);
+	}
+
+	private ErrorDiscussion createErrorDiscussion(String title, int upvoteCount, ErrorDiscussion.Answer answer) {
+		ErrorDiscussion ed = new ErrorDiscussion();
+		ed.title = title;
+		ed.upvoteCount = upvoteCount;
+		ed.answer = answer;
+		return ed;
+	}
+
+	@DisplayName("compare error discussions by upvote count")
+	@ParameterizedTest
+	@CsvSource(textBlock = """
+			10, <, 5 
+			8, >, 15 
+			10, =, 10
+			""")
+	public void testCompareUpvoteCount(int leftUpvoteCount, char expected, int rightUpvoteCount) {
+		int expectedResult = switch (expected) {
+			case '<' -> -1;
+			case '>' -> +1;
+			default -> 0;
+		};
+		var left = createErrorDiscussion("", leftUpvoteCount, null);
+		var right = createErrorDiscussion("", rightUpvoteCount, null);
+		int result = errorController.compareUpvoteCount(left, right);
+		Assertions.assertEquals(expectedResult, Integer.signum(result));
+	}
+
+	@DisplayName("compare error discussions by existence of an answer")
+	@ParameterizedTest
+	@CsvSource(textBlock = """
+			false, =, false
+			true, =, true
+			true, <, false
+			false, >, true
+			""")
+	public void testCompareIsAnswered(boolean leftIsAnswered, char expected, boolean rightIsAnswered) {
+		var answer = new ErrorDiscussion.Answer();
+		int expectedResult = switch (expected) {
+			case '<' -> -1;
+			case '>' -> +1;
+			default -> 0;
+		};
+		var left = createErrorDiscussion("", 0, leftIsAnswered ? answer : null);
+		var right = createErrorDiscussion("", 0, rightIsAnswered ? answer : null);
+		int result = errorController.compareIsAnswered(left, right);
+		Assertions.assertEquals(expectedResult, result);
+	}
+
+	@DisplayName("compare error codes by full error code")
+	@ParameterizedTest
+	@CsvSource(textBlock = """
+			Error 0000:0000:0000, =, Error 0000:0000:0000
+			Error 6HU1:12H1:HU7J, <, Error 0000:0000:0000
+			Error 0000:0000:0000, >, Error 6HU1:12H1:HU7J
+			""")
+	public void testCompareByFullErrorCode(String leftTitle, char expected, String rightTitle) {
+		Mockito.when(errorCode.toString()).thenReturn("6HU1:12H1:HU7J");
+		int expectedResult = switch (expected) {
+			case '<' -> -1;
+			case '>' -> +1;
+			default -> 0;
+		};
+		var left = createErrorDiscussion(leftTitle, 0, null);
+		var right = createErrorDiscussion(rightTitle, 0, null);
+		int result = errorController.compareByFullErrorCode(left, right);
+		Assertions.assertEquals(expectedResult, result);
+	}
+
+	@DisplayName("compare error codes by root cause")
+	@ParameterizedTest
+	@CsvSource(textBlock = """
+			Error 6HU1:12H1:0000, =, Error 6HU1:12H1:0000
+			Error 6HU1:12H1:0007, =, Error 6HU1:12H1:0042
+			Error 0000:0000:0000, =, Error 0000:0000:0000
+			Error 6HU1:12H1:0000, <, Error 0000:0000:0000
+			Error 6HU1:12H1:0000, <, Error 6HU1:0000:0000
+			Error 0000:0000:0000, >, Error 6HU1:12H1:0000
+			Error 6HU1:0000:0000, >, Error 6HU1:12H1:0000
+			""")
+	public void testCompareByRootCauseCode(String leftTitle, char expected, String rightTitle) {
+		Mockito.when(errorCode.methodCode()).thenReturn("6HU1");
+		Mockito.when(errorCode.rootCauseCode()).thenReturn("12H1");
+		int expectedResult = switch (expected) {
+			case '<' -> -1;
+			case '>' -> +1;
+			default -> 0;
+		};
+		var left = createErrorDiscussion(leftTitle, 0, null);
+		var right = createErrorDiscussion(rightTitle, 0, null);
+		int result = errorController.compareByRootCauseCode(left, right);
+		Assertions.assertEquals(expectedResult, result);
+	}
+
+	@DisplayName("check if the error code contains the method code")
+	@ParameterizedTest
+	@CsvSource(textBlock = """
+			Error 6HU1:0000:0000, true
+			Error 0000:0000:0000, false
+			""")
+	public void testContainsMethodCode(String title, boolean expectedResult) {
+		Mockito.when(errorCode.methodCode()).thenReturn("6HU1");
+		var ed = createErrorDiscussion(title, 0, null);
+		boolean result = errorController.containsMethodCode(ed);
+		Assertions.assertEquals(expectedResult, result);
+	}
+}