Usable Browser

This commit is contained in:
UnlegitDqrk
2026-02-14 22:16:15 +01:00
parent c525fd3ae6
commit 016051e2ed
24 changed files with 2706 additions and 561 deletions

View File

@@ -0,0 +1,192 @@
package org.openautonomousconnection.webclient.settings;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* In-memory settings model.
*
* <p>Persisted by {@link SettingsManager}.</p>
*/
public final class AppSettings {
private final List<InsEndpoint> insEndpoints = new ArrayList<>();
private final List<String> favorites = new ArrayList<>();
private String startPageUrl = "web://info.oac/";
private boolean sslEnabled = true;
private boolean luaEnabled = true;
private boolean historyEnabled = true;
private InsEndpoint selectedIns;
private LuaExecutionPolicy luaPolicy = LuaExecutionPolicy.uiDefault();
/**
* Creates settings with defaults.
*/
public AppSettings() {
// Defaults: include the current INSList defaults as initial endpoint.
insEndpoints.add(new InsEndpoint("open-autonomous-connection.org", 1026));
selectedIns = insEndpoints.get(0);
}
/**
* Returns the configured start page URL.
*
* @return start page URL
*/
public String getStartPageUrl() {
return startPageUrl;
}
/**
* Sets the start page URL.
*
* @param startPageUrl URL (non-null, non-blank)
*/
public void setStartPageUrl(String startPageUrl) {
String s = Objects.requireNonNull(startPageUrl, "startPageUrl").trim();
if (s.isEmpty()) throw new IllegalArgumentException("startPageUrl must not be blank");
this.startPageUrl = s;
}
/**
* Returns whether SSL is enabled for protocol connections.
*
* @return true if enabled
*/
public boolean isSslEnabled() {
return sslEnabled;
}
/**
* Enables/disables SSL.
*
* @param sslEnabled enabled
*/
public void setSslEnabled(boolean sslEnabled) {
this.sslEnabled = sslEnabled;
}
/**
* Returns whether Lua runtime is enabled in WebView.
*
* @return true if enabled
*/
public boolean isLuaEnabled() {
return luaEnabled;
}
/**
* Enables/disables Lua.
*
* @param luaEnabled enabled
*/
public void setLuaEnabled(boolean luaEnabled) {
this.luaEnabled = luaEnabled;
}
/**
* Returns whether history is enabled.
*
* @return true if enabled
*/
public boolean isHistoryEnabled() {
return historyEnabled;
}
/**
* Enables/disables history tracking.
*
* @param historyEnabled enabled
*/
public void setHistoryEnabled(boolean historyEnabled) {
this.historyEnabled = historyEnabled;
}
/**
* Returns a mutable INS endpoint list.
*
* @return list (mutable)
*/
public List<InsEndpoint> getInsEndpointsMutable() {
return insEndpoints;
}
/**
* Returns an immutable view of INS endpoints.
*
* @return endpoints
*/
public List<InsEndpoint> getInsEndpoints() {
return Collections.unmodifiableList(insEndpoints);
}
/**
* Returns currently selected INS.
*
* @return selected endpoint
*/
public InsEndpoint getSelectedIns() {
return selectedIns;
}
/**
* Sets selected INS endpoint.
*
* @param selectedIns endpoint (must exist in list or will be added)
*/
public void setSelectedIns(InsEndpoint selectedIns) {
Objects.requireNonNull(selectedIns, "selectedIns");
if (!insEndpoints.contains(selectedIns)) {
insEndpoints.add(selectedIns);
}
this.selectedIns = selectedIns;
}
/**
* Returns a mutable favorites list.
*
* @return favorites (mutable)
*/
public List<String> getFavoritesMutable() {
return favorites;
}
/**
* Returns immutable favorites.
*
* @return favorites
*/
public List<String> getFavorites() {
return Collections.unmodifiableList(favorites);
}
/**
* Returns Lua execution policy.
*
* @return policy
*/
public LuaExecutionPolicy getLuaPolicy() {
return luaPolicy;
}
/**
* Sets Lua execution policy.
*
* @param luaPolicy policy (non-null)
*/
public void setLuaPolicy(LuaExecutionPolicy luaPolicy) {
this.luaPolicy = Objects.requireNonNull(luaPolicy, "luaPolicy");
}
/**
* Resets the Lua policy back to ui default.
*/
public void resetLuaPolicyToUiDefault() {
this.luaPolicy = new LuaExecutionPolicy(Duration.ofMillis(50L), 200_000L, 5_000);
}
}

View File

@@ -39,6 +39,7 @@ public final class FxEngine implements AutoCloseable {
*
* @param engine web engine
* @param webView web view
* @param logger web logger
*/
public FxEngine(WebEngine engine, WebView webView, WebLogger logger) {
this(engine, webView, LuaExecutionPolicy.uiDefault(), logger);
@@ -50,12 +51,13 @@ public final class FxEngine implements AutoCloseable {
* @param engine web engine
* @param webView web view
* @param policy execution policy
* @param logger web logger
*/
public FxEngine(WebEngine engine, WebView webView, LuaExecutionPolicy policy, WebLogger logger) {
this.engine = Objects.requireNonNull(engine, "engine");
this.webView = webView;
this.webView = Objects.requireNonNull(webView, "webView");
this.policy = Objects.requireNonNull(policy, "policy");
this.logger = logger;
this.logger = Objects.requireNonNull(logger, "logger");
}
/**
@@ -81,32 +83,24 @@ public final class FxEngine implements AutoCloseable {
closeRuntimeQuietly();
// DOM host must exist before event/UI tables, and must ensure stable ids.
FxDomHost dom = new FxDomHost(engine);
dom.ensureAllElementsHaveId();
// Create per-page globals; harden sandbox in production.
Globals globals = LuaGlobalsFactory.create(
new LuaGlobalsFactory.Options()
.enableDebug(false)
.sandbox(true)
);
// Create runtime first (router lives inside it).
ConsoleHostImpl console = new ConsoleHostImpl(logger);
UiHostImpl uiHost = new UiHostImpl(engine, webView, dom);
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
// runtime depends on services; events depends on runtime router.
// We'll create eventHost after runtime, then build HostServices with it.
LuaRuntime rt = new LuaRuntime(globals, new HostServices.Default(uiHost, dom, null, resourceHost, console), policy);
FxEventHost eventHost = new FxEventHost(dom, rt.eventRouter());
// Rebuild services including eventHost and reinstall tables.
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console);
// Replace runtime with correct services (clean and deterministic).
rt.close();
rt = new LuaRuntime(globals, services, policy);

View File

@@ -0,0 +1,82 @@
package org.openautonomousconnection.webclient.settings;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* In-memory history tracker (URL + timestamp).
*
* <p>Persistence can be added later; for now it follows settings toggle and supports clearing.</p>
*/
public final class HistoryManager {
private final List<Entry> entries = new ArrayList<>();
private volatile boolean enabled = true;
/**
* Returns whether history is enabled.
*
* @return enabled
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets whether history is enabled.
*
* @param enabled enabled
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
if (!enabled) {
clear();
}
}
/**
* Adds a URL to history if enabled.
*
* @param url url (non-null, non-blank)
*/
public void add(String url) {
if (!enabled) return;
String s = Objects.requireNonNull(url, "url").trim();
if (s.isEmpty()) return;
// De-dup consecutive duplicates
if (!entries.isEmpty()) {
Entry last = entries.get(entries.size() - 1);
if (last.url().equals(s)) return;
}
entries.add(new Entry(s, Instant.now()));
}
/**
* Clears history.
*/
public void clear() {
entries.clear();
}
/**
* Returns immutable entries.
*
* @return entries
*/
public List<Entry> entries() {
return Collections.unmodifiableList(entries);
}
/**
* Single history entry.
*
* @param url visited URL
* @param visitedAt timestamp
*/
public record Entry(String url, Instant visitedAt) {
}
}

View File

@@ -0,0 +1,125 @@
package org.openautonomousconnection.webclient.settings;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Persists browsing history to disk.
*
* <p>Format per line: {@code <epochMillis>\t<url>}</p>
*/
public final class HistoryStore {
private static final String FILE_NAME = "history.log";
private final File file;
/**
* Creates a history store in the default user settings directory.
*/
public HistoryStore() {
this(historyFile());
}
/**
* Creates a history store for a specific file.
*
* @param file history file
*/
public HistoryStore(File file) {
this.file = Objects.requireNonNull(file, "file");
File dir = file.getParentFile();
if (dir != null && !dir.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
}
/**
* Returns the default history file location.
*
* @return file
*/
public static File historyFile() {
return new File(FILE_NAME);
}
/**
* Loads history entries from disk.
*
* @return entries (immutable)
*/
public List<Entry> load() {
if (!file.isFile()) return List.of();
List<Entry> out = new ArrayList<>();
try (BufferedReader br = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
int tab = line.indexOf('\t');
if (tab <= 0) continue;
String tsS = line.substring(0, tab).trim();
String url = line.substring(tab + 1).trim();
if (url.isEmpty()) continue;
long ms;
try {
ms = Long.parseLong(tsS);
} catch (Exception ignored) {
continue;
}
out.add(new Entry(url, Instant.ofEpochMilli(ms)));
}
} catch (Exception ignored) {
return List.of();
}
return Collections.unmodifiableList(out);
}
/**
* Appends a single entry to disk (best-effort).
*
* @param url visited URL
* @param at timestamp
*/
public void append(String url, Instant at) {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(at, "at");
String u = url.trim();
if (u.isEmpty()) return;
try (Writer w = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file, true), StandardCharsets.UTF_8))) {
w.write(Long.toString(at.toEpochMilli()));
w.write('\t');
w.write(u.replace('\n', ' ').replace('\r', ' '));
w.write('\n');
} catch (Exception ignored) {
// Best-effort
}
}
/**
* Clears history file (best-effort).
*/
public void clear() {
try {
Files.deleteIfExists(file.toPath());
} catch (Exception ignored) {
// Best-effort
}
}
/**
* History entry.
*
* @param url visited URL
* @param visitedAt timestamp
*/
public record Entry(String url, Instant visitedAt) {
}
}

View File

@@ -1,16 +0,0 @@
package org.openautonomousconnection.webclient.settings;
import java.util.HashMap;
public class INSList {
public static String DEFAULT_INS = "open-autonomous-connection.org";
public static int DEFAULT_PORT = 1026;
private static HashMap<String, Integer> insList = new HashMap<>();
public static void registerINS(String host, int tcpPort) {
insList.put(host, tcpPort);
}
}

View File

@@ -0,0 +1,59 @@
package org.openautonomousconnection.webclient.settings;
import java.util.Objects;
/**
* Represents an INS endpoint (host + TCP port).
*/
public record InsEndpoint(String host, int port) {
/**
* Creates an INS endpoint.
*
* @param host endpoint host (non-null, non-blank)
* @param port tcp port (1..65535)
*/
public InsEndpoint(String host, int port) {
String h = Objects.requireNonNull(host, "host").trim();
if (h.isEmpty()) throw new IllegalArgumentException("host must not be blank");
if (port < 1 || port > 65535) throw new IllegalArgumentException("port out of range: " + port);
this.host = h;
this.port = port;
}
/**
* Returns the host.
*
* @return host
*/
@Override
public String host() {
return host;
}
/**
* Returns the tcp port.
*
* @return port
*/
@Override
public int port() {
return port;
}
@Override
public String toString() {
return host + ":" + port;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof InsEndpoint other)) return false;
return host.equalsIgnoreCase(other.host) && port == other.port;
}
@Override
public int hashCode() {
return host.toLowerCase().hashCode() * 31 + port;
}
}

View File

@@ -0,0 +1,190 @@
package org.openautonomousconnection.webclient.settings;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
/**
* Loads/saves {@link AppSettings} to a simple properties file.
*
* <p>Location: {@code settings.properties}</p>
*/
public final class SettingsManager {
private static final String FILE_NAME = "settings.properties";
private SettingsManager() {
// Utility class
}
/**
* Returns the settings file location.
*
* @return settings file
*/
public static File settingsFile() {
return new File(FILE_NAME);
}
/**
* Loads settings from disk. If missing, returns defaults.
*
* @return settings
*/
public static AppSettings load() {
AppSettings s = new AppSettings();
File f = settingsFile();
if (!f.isFile()) return s;
Properties p = new Properties();
try (InputStream in = Files.newInputStream(f.toPath())) {
p.load(new InputStreamReader(in, StandardCharsets.UTF_8));
} catch (Exception ignored) {
return s;
}
trySetString(p, "startPageUrl", s::setStartPageUrl);
s.setSslEnabled(parseBool(p.getProperty("sslEnabled"), s.isSslEnabled()));
s.setLuaEnabled(parseBool(p.getProperty("luaEnabled"), s.isLuaEnabled()));
s.setHistoryEnabled(parseBool(p.getProperty("historyEnabled"), s.isHistoryEnabled()));
// INS endpoints
List<InsEndpoint> endpoints = new ArrayList<>();
int count = parseInt(p.getProperty("ins.count"), 0);
for (int i = 0; i < count; i++) {
String host = p.getProperty("ins." + i + ".host");
int port = parseInt(p.getProperty("ins." + i + ".port"), -1);
if (host == null || host.isBlank() || port < 1 || port > 65535) continue;
endpoints.add(new InsEndpoint(host.trim(), port));
}
if (!endpoints.isEmpty()) {
s.getInsEndpointsMutable().clear();
s.getInsEndpointsMutable().addAll(endpoints);
}
String selHost = p.getProperty("ins.selected.host");
int selPort = parseInt(p.getProperty("ins.selected.port"), -1);
if (selHost != null && !selHost.isBlank() && selPort >= 1 && selPort <= 65535) {
s.setSelectedIns(new InsEndpoint(selHost.trim(), selPort));
} else {
// Keep default selection, but ensure it exists in list.
s.setSelectedIns(s.getSelectedIns());
}
// Favorites
int favCount = parseInt(p.getProperty("favorites.count"), 0);
s.getFavoritesMutable().clear();
for (int i = 0; i < favCount; i++) {
String url = p.getProperty("favorites." + i);
if (url != null && !url.isBlank()) s.getFavoritesMutable().add(url.trim());
}
// Lua policy
long timeoutMs = parseLong(p.getProperty("lua.timeoutMs"), 50L);
long instr = parseLong(p.getProperty("lua.instructionLimit"), 200_000L);
int hook = parseInt(p.getProperty("lua.hookStep"), 5_000);
try {
s.setLuaPolicy(new LuaExecutionPolicy(Duration.ofMillis(timeoutMs), instr, hook));
} catch (Exception ignored) {
s.resetLuaPolicyToUiDefault();
}
return s;
}
/**
* Saves settings to disk (best-effort).
*
* @param s settings
*/
public static void save(AppSettings s) {
Objects.requireNonNull(s, "s");
File f = settingsFile();
File dir = f.getParentFile();
if (dir != null && !dir.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
Properties p = new Properties();
p.setProperty("startPageUrl", s.getStartPageUrl());
p.setProperty("sslEnabled", Boolean.toString(s.isSslEnabled()));
p.setProperty("luaEnabled", Boolean.toString(s.isLuaEnabled()));
p.setProperty("historyEnabled", Boolean.toString(s.isHistoryEnabled()));
List<InsEndpoint> endpoints = s.getInsEndpoints();
p.setProperty("ins.count", Integer.toString(endpoints.size()));
for (int i = 0; i < endpoints.size(); i++) {
InsEndpoint ep = endpoints.get(i);
p.setProperty("ins." + i + ".host", ep.host());
p.setProperty("ins." + i + ".port", Integer.toString(ep.port()));
}
InsEndpoint sel = s.getSelectedIns();
if (sel != null) {
p.setProperty("ins.selected.host", sel.host());
p.setProperty("ins.selected.port", Integer.toString(sel.port()));
}
List<String> fav = s.getFavorites();
p.setProperty("favorites.count", Integer.toString(fav.size()));
for (int i = 0; i < fav.size(); i++) {
p.setProperty("favorites." + i, fav.get(i));
}
p.setProperty("lua.timeoutMs", Long.toString(s.getLuaPolicy().timeout().toMillis()));
p.setProperty("lua.instructionLimit", Long.toString(s.getLuaPolicy().instructionLimit()));
p.setProperty("lua.hookStep", Integer.toString(s.getLuaPolicy().hookStep()));
try (OutputStream out = Files.newOutputStream(f.toPath())) {
p.store(new OutputStreamWriter(out, StandardCharsets.UTF_8), "OAC WebClient Settings");
} catch (Exception ignored) {
// Best-effort persistence
}
}
private static void trySetString(Properties p, String key, java.util.function.Consumer<String> setter) {
String v = p.getProperty(key);
if (v != null && !v.isBlank()) {
try {
setter.accept(v.trim());
} catch (Exception ignored) {
// Ignore malformed value
}
}
}
private static boolean parseBool(String v, boolean def) {
if (v == null) return def;
String s = v.trim().toLowerCase();
if (s.equals("true")) return true;
if (s.equals("false")) return false;
return def;
}
private static int parseInt(String v, int def) {
if (v == null) return def;
try {
return Integer.parseInt(v.trim());
} catch (Exception e) {
return def;
}
}
private static long parseLong(String v, long def) {
if (v == null) return def;
try {
return Long.parseLong(v.trim());
} catch (Exception e) {
return def;
}
}
}