diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java index 72b0730..cb04fa2 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/gameuicomponents/CasinoBrowserController.java @@ -4,7 +4,10 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashSet; +import java.util.Map; import java.util.Optional; import java.util.Set; import javafx.application.Platform; @@ -29,6 +32,10 @@ import javafx.scene.web.WebView; import javafx.stage.Stage; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; /** * Experimental embedded browser for Casono. @@ -63,6 +70,11 @@ public class CasinoBrowserController { private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class); + private static final ObservableList URL_SUGGESTIONS = + FXCollections.observableArrayList(TRUSTED_DOMAINS); + + private static ContextMenu autoCompleteMenu = new ContextMenu(); + private static final int LOGO_HEIGHT = 40; private static final int CORNER_RADIUS = 40; private static final int HBOX_SPACIN = 10; @@ -79,7 +91,9 @@ public class CasinoBrowserController { CookieHandler.setDefault(COOKIE_MANAGER); TRUSTED_DOMAINS.add("wikipedia.org"); + TRUSTED_DOMAINS.add("youtube.com"); TRUSTED_DOMAINS.add("github.com"); + TRUSTED_DOMAINS.add("search.brave.com"); TRUSTED_DOMAINS.add("oracle.com"); TRUSTED_DOMAINS.add("stackoverflow.com"); TRUSTED_DOMAINS.add("docs.oracle.com"); @@ -94,10 +108,62 @@ public class CasinoBrowserController { TRUSTED_DOMAINS.add("metager.de"); TRUSTED_DOMAINS.add("unibas.ch"); TRUSTED_DOMAINS.add("mojeek.com"); - TRUSTED_DOMAINS.add("searx.be "); + TRUSTED_DOMAINS.add("searx.be"); TRUSTED_DOMAINS.add("startpage.com"); } + /** + * Aliases for common websites. + */ + private static final java.util.Map ALIASES = Map.ofEntries( + Map.entry("wiki", "wikipedia.org"), + Map.entry("yt", "youtube.com"), + Map.entry("bra", "search.brave.com"), + Map.entry("bs", "search.brave.com"), + Map.entry("gh", "github.com"), + Map.entry("google", "google.com"), + Map.entry("duck", "duckduckgo.com"), + Map.entry("bing", "bing.com"), + Map.entry("meta", "metager.de"), + Map.entry("mojeek", "mojeek.com"), + Map.entry("searx", "searx.be"), + Map.entry("startpage", "startpage.com"), + Map.entry("stack", "stackoverflow.com"), + Map.entry("so", "stackoverflow.com"), + Map.entry("oracle", "oracle.com"), + Map.entry("docs", "docs.oracle.com"), + Map.entry("mdn", "developer.mozilla.org"), + Map.entry("maven", "maven.apache.org"), + Map.entry("gradle", "gradle.org"), + Map.entry("spring", "spring.io"), + Map.entry("jetbrains", "jetbrains.com"), + Map.entry("uni", "unibas.ch") + ); + + /** + * Resolves user input into a valid URL. + * + * @param input User input from the URL field, which can be a full URL, a domain name, or an alias. + * @return A properly formatted URL string that can be loaded by the browser, or the original input + * if it cannot be resolved. + */ + private static String resolveInputToUrl(String input) { + if (input == null || input.isBlank()) return input; + + input = input.trim(); + + String alias = ALIASES.get(input.toLowerCase()); + if (alias != null) { + return "https://" + alias; + } + + if (input.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) { + return input; + } + + return "https://" + input; + } + private static boolean javascriptEnabled = false; /** Deletes all cookies stored during the current browser session. */ @@ -203,9 +269,9 @@ public class CasinoBrowserController { engine.setCreatePopupHandler( config -> { Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle("Popup blockiert"); + alert.setTitle("Popup blocked"); alert.setHeaderText(null); - alert.setContentText("Popup wurde aus Sicherheitsgründen blockiert."); + alert.setContentText("The Casono browser has detected a threat and successfully blocked it."); try { var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH); @@ -283,6 +349,64 @@ public class CasinoBrowserController { urlField.getStyleClass().add("gray-input-field"); HBox.setHgrow(urlField, Priority.ALWAYS); + urlField.textProperty().addListener((obs, oldText, newText) -> { + if (newText == null || newText.isBlank()) { + autoCompleteMenu.hide(); + return; + } + + String input = newText.toLowerCase(); + + String aliasMatch = ALIASES.get(input); + if (aliasMatch != null) { + autoCompleteMenu.getItems().clear(); + + MenuItem item = new MenuItem(aliasMatch); + item.setOnAction(e -> { + urlField.setText("https://" + aliasMatch); + autoCompleteMenu.hide(); + }); + + autoCompleteMenu.getItems().add(item); + autoCompleteMenu.show(urlField, javafx.geometry.Side.BOTTOM, 0, 0); + return; + } + + autoCompleteMenu.getItems().clear(); + + URL_SUGGESTIONS.stream() + .filter(domain -> domain.toLowerCase().startsWith(input) + || domain.toLowerCase().contains(input)) + .sorted((a, b) -> { + boolean aStarts = a.startsWith(input); + boolean bStarts = b.startsWith(input); + return Boolean.compare(!aStarts, !bStarts); + }) + .limit(5) + .forEach(domain -> { + MenuItem item = new MenuItem(domain); + + item.setOnAction(e -> { + urlField.setText("https://" + domain); + autoCompleteMenu.hide(); + }); + + autoCompleteMenu.getItems().add(item); + }); + + if (!autoCompleteMenu.getItems().isEmpty()) { + autoCompleteMenu.show(urlField, + javafx.geometry.Side.BOTTOM, + 0, 0); + } else { + autoCompleteMenu.hide(); + } + }); + + urlField.focusedProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal) autoCompleteMenu.hide(); + }); + return urlField; } @@ -305,7 +429,7 @@ public class CasinoBrowserController { * @return Toggle button */ public static Button createJsToggle(WebEngine engine) { - Button jsToggle = new Button("JS EINSCHALTEN"); + Button jsToggle = new Button("JS TURN ON"); jsToggle.getStyleClass().add("red-button"); jsToggle.setOnAction( @@ -319,7 +443,7 @@ public class CasinoBrowserController { jsToggle.getStyleClass().add("yellow-button"); } else { - jsToggle.setText("JS EINSCHALTEN"); + jsToggle.setText("JS TURN ON"); jsToggle.getStyleClass().removeAll("yellow-button"); jsToggle.getStyleClass().add("red-button"); } @@ -410,7 +534,15 @@ public class CasinoBrowserController { */ public static void configureUrlEvents( WebEngine engine, TextField urlField, Label securityLabel) { - urlField.setOnAction(e -> loadUrlSafely(engine, urlField.getText(), securityLabel)); + urlField.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ENTER) { + if (!autoCompleteMenu.isShowing()) { + String resolved = resolveInputToUrl(urlField.getText()); + urlField.setText(resolved); + loadUrlSafely(engine, resolved, securityLabel); + } + } + }); engine.locationProperty().addListener((obs, o, n) -> urlField.setText(n)); } @@ -468,28 +600,46 @@ public class CasinoBrowserController { */ private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) { try { - if (!url.startsWith("http")) { + if (!url.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) { url = "https://" + url; } URI uri = new URI(url); + String scheme = uri.getScheme(); - // HTTPS required - if (!"https".equalsIgnoreCase(uri.getScheme())) { + if ("file".equalsIgnoreCase(scheme)) { + Path allowed = Paths.get( + System.getProperty("user.dir"), + "documents", + "docs", + "game-engine", + "manual.html" + ).normalize(); + + Path requested = Paths.get(uri).normalize(); + + if (requested.equals(allowed)) { + securityLabel.setText("LOCAL OK"); + engine.load(uri.toString()); + } else { + securityLabel.setText("BLOCKED"); + } + return; + } + + if (!"https".equalsIgnoreCase(scheme)) { securityLabel.setText("BLOCKED"); return; } String host = uri.getHost(); - if (host == null) { + if (host == null || host.isBlank()) { securityLabel.setText("ERROR"); return; } - // Secure Domain Check - boolean trusted = - TRUSTED_DOMAINS.stream() - .anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain)); + boolean trusted = TRUSTED_DOMAINS.stream() + .anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain)); if (!trusted) { if (!showUnknownWebsiteAlert(host)) { @@ -500,9 +650,11 @@ public class CasinoBrowserController { } else { securityLabel.setText("SAFE"); } + engine.load(uri.toString()); + } catch (Exception e) { - securityLabel.setText("ERROR"); + securityLabel.setText("INVALID"); } } @@ -519,7 +671,7 @@ public class CasinoBrowserController { alert.setHeaderText("This website is unknown"); String content = host - + "\n\nThis site has not been verified by the Casono browser.\n" + + "\n\nThis site has not been verified by the Casono Browser.\n" + "Do you still want to open it?"; alert.setContentText(content);