Hydrate display storage preload state
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-15 19:00:02 +01:00
parent 750f7d9f43
commit 390cd600d8
3 changed files with 258 additions and 21 deletions

View file

@ -28,8 +28,12 @@ func (s *Service) InjectPreload(webview PreloadTarget, origin string) error {
// before page code runs.
// Use: script, _ := display.BuildPreloadScript("https://example.com")
func (s *Service) BuildPreloadScript(pageURL string) (string, error) {
storageBootstrap := map[string]map[string]string{}
if s.storage != nil {
storageBootstrap = s.storage.Snapshot(pageURL)
}
parts := []string{
s.injectStoragePolyfills(pageURL),
s.injectStoragePolyfills(pageURL, storageBootstrap),
s.injectBackgroundServiceShims(),
s.injectElectronShim(),
s.injectCoreMLShim(),
@ -41,16 +45,11 @@ func (s *Service) BuildPreloadScript(pageURL string) (string, error) {
return strings.Join(parts, "\n"), nil
}
func (s *Service) injectStoragePolyfills(pageOrigin string) string {
func (s *Service) injectStoragePolyfills(pageOrigin string, bootstrap map[string]map[string]string) string {
return `(function() {
const __coreOrigin = (() => {
try {
const parsed = new URL(` + core.JSONMarshalString(pageOrigin) + `, globalThis.location?.href || "http://localhost");
return parsed.origin || ` + core.JSONMarshalString(pageOrigin) + `;
} catch (_) {
return ` + core.JSONMarshalString(pageOrigin) + `;
}
})();
const __corePageURL = ` + core.JSONMarshalString(pageOrigin) + `;
const __coreOrigin = ` + core.JSONMarshalString(storageOriginForPageURL(pageOrigin)) + ` || __corePageURL;
const __coreBootstrapStorage = ` + core.JSONMarshalString(bootstrap) + `;
const __coreScopes = globalThis.__coreStorageScopes || (globalThis.__coreStorageScopes = {});
const __scope = __coreScopes[__coreOrigin] || (__coreScopes[__coreOrigin] = { localStorage: {}, sessionStorage: {}, cookies: {}, indexedDB: {}, caches: {}, buckets: {}, opfs: {} });
const __coreBridge = globalThis.__coreBridge || (globalThis.__coreBridge = {
@ -61,7 +60,84 @@ func (s *Service) injectStoragePolyfills(pageOrigin string) string {
return Promise.resolve({ route, payload, bridged: false });
}
});
const hydrateBucket = (bucketName, target, source) => {
if (!source || typeof source !== "object") {
return;
}
const parseMaybeJSON = (value) => {
if (typeof value !== "string") {
return value;
}
try {
return JSON.parse(value);
} catch (_) {
return value;
}
};
for (const [key, value] of Object.entries(source)) {
if (bucketName === "cookies") {
try {
target[key] = typeof value === "string" ? JSON.parse(value) : value;
} catch (_) {
target[key] = { value: String(value ?? "") };
}
continue;
}
if (bucketName.startsWith("cache:")) {
const cacheName = bucketName.slice("cache:".length);
target[cacheName] = target[cacheName] || {};
target[cacheName][key] = parseMaybeJSON(value);
continue;
}
if (bucketName.startsWith("indexeddb:")) {
const databaseName = bucketName.slice("indexeddb:".length);
const [storeName, recordKey] = key.split(":", 2);
target[databaseName] = target[databaseName] || { stores: {} };
target[databaseName].stores = target[databaseName].stores || {};
target[databaseName].stores[storeName] = target[databaseName].stores[storeName] || {};
target[databaseName].stores[storeName][recordKey] = parseMaybeJSON(value);
continue;
}
if (bucketName.startsWith("storageBucket:")) {
const bucketNameValue = bucketName.slice("storageBucket:".length);
target[bucketNameValue] = target[bucketNameValue] || { kv: {}, files: {} };
target[bucketNameValue].kv = target[bucketNameValue].kv || {};
target[bucketNameValue].kv[key] = value;
continue;
}
if (bucketName === "opfs") {
target[key] = { contents: value };
continue;
}
target[key] = value;
}
};
Object.entries(__coreBootstrapStorage || {}).forEach(([bucketName, bucket]) => {
if (bucketName.startsWith("cache:")) {
hydrateBucket(bucketName, __scope.caches, bucket);
return;
}
if (bucketName.startsWith("indexeddb:")) {
hydrateBucket(bucketName, __scope.indexedDB, bucket);
return;
}
if (bucketName.startsWith("storageBucket:")) {
hydrateBucket(bucketName, __scope.buckets, bucket);
return;
}
if (bucketName === "opfs") {
hydrateBucket(bucketName, __scope.opfs, bucket);
return;
}
if (!__scope[bucketName]) {
__scope[bucketName] = {};
}
hydrateBucket(bucketName, __scope[bucketName], bucket);
});
const persist = (bucket, key, value) => {
if (bucket === "sessionStorage") {
return;
}
__coreBridge.invoke('display.storage.set', { origin: __coreOrigin, bucket, key, value }).catch(() => undefined);
};
const createStorage = (bucketName, bucket) => ({
@ -77,10 +153,51 @@ func (s *Service) injectStoragePolyfills(pageOrigin string) string {
globalThis.core.storage.local = createStorage('localStorage', __scope.localStorage);
globalThis.core.storage.session = createStorage('sessionStorage', __scope.sessionStorage);
globalThis.core.storage.cookies = createStorage('cookies', __scope.cookies);
const cookieEntries = () => Object.entries(__scope.cookies).map(([name, value]) => name + "=" + value).join("; ");
const currentLocation = () => globalThis.location || {};
const cookieValue = (record) => {
if (record && typeof record === "object" && !Array.isArray(record)) {
return String(record.value ?? "");
}
return String(record ?? "");
};
const cookieIsExpired = (record) => {
if (!record || typeof record !== "object" || Array.isArray(record)) {
return false;
}
const expiresAt = record.expires ? Date.parse(record.expires) : NaN;
return Number.isFinite(expiresAt) && expiresAt <= Date.now();
};
const cookieMatchesPath = (record) => {
const path = String(currentLocation().pathname || "/");
const cookiePath = (record && typeof record === "object" && record.path) ? String(record.path) : "/";
return path === cookiePath || path.startsWith(cookiePath.endsWith("/") ? cookiePath : cookiePath + "/") || cookiePath === "/";
};
const cookieMatchesDomain = (record) => {
const hostname = String(currentLocation().hostname || "");
const domain = (record && typeof record === "object" && record.domain) ? String(record.domain).replace(/^\./, "") : "";
if (!domain) {
return true;
}
return hostname === domain || hostname.endsWith("." + domain);
};
const cookieMatchesSecure = (record) => {
if (!record || typeof record !== "object" || Array.isArray(record) || !record.secure) {
return true;
}
const protocol = String(currentLocation().protocol || "");
return protocol === "https:" || protocol === "wss:" || currentLocation().hostname === "localhost";
};
const cookieEntries = () => Object.entries(__scope.cookies)
.filter(([, record]) => !cookieIsExpired(record) && cookieMatchesPath(record) && cookieMatchesDomain(record) && cookieMatchesSecure(record))
.map(([name, record]) => name + "=" + cookieValue(record))
.join("; ");
const setCookie = (value) => {
const rawCookie = String(value ?? "");
const [pair] = rawCookie.split(";", 1);
const parts = rawCookie.split(";").map((part) => part.trim()).filter(Boolean);
const [pair, ...attributes] = parts;
if (!pair) {
return;
}
const separatorIndex = pair.indexOf("=");
if (separatorIndex < 0) {
return;
@ -89,8 +206,54 @@ func (s *Service) injectStoragePolyfills(pageOrigin string) string {
if (!name) {
return;
}
__scope.cookies[name] = pair.slice(separatorIndex + 1).trim();
persist('cookies', name, __scope.cookies[name]);
const record = {
value: pair.slice(separatorIndex + 1).trim(),
path: "/",
domain: String(currentLocation().hostname || ""),
secure: false,
sameSite: "Lax"
};
attributes.forEach((attribute) => {
const [rawKey, ...rawValue] = attribute.split("=");
const key = String(rawKey || "").trim().toLowerCase();
const value = rawValue.join("=").trim();
switch (key) {
case "expires":
if (value) {
const expiresAt = new Date(value);
if (!Number.isNaN(expiresAt.getTime())) {
record.expires = expiresAt.toISOString();
}
}
break;
case "max-age": {
const seconds = Number.parseInt(value, 10);
if (Number.isFinite(seconds)) {
record.expires = new Date(Date.now() + (seconds * 1000)).toISOString();
}
break;
}
case "path":
record.path = value || "/";
break;
case "domain":
record.domain = value.replace(/^\./, "");
break;
case "secure":
record.secure = true;
break;
case "samesite":
record.sameSite = value || "Lax";
break;
}
});
if (cookieIsExpired(record)) {
delete __scope.cookies[name];
persist('cookies', name, '');
return;
}
__scope.cookies[name] = record;
persist('cookies', name, JSON.stringify(record));
};
const asyncResult = (value) => Promise.resolve(value);
const createIDBRequest = (result) => {
@ -101,12 +264,12 @@ func (s *Service) injectStoragePolyfills(pageOrigin string) string {
});
return request;
};
const createObjectStore = (database, storeName) => ({
const createObjectStore = (databaseName, database, storeName) => ({
put(value, key) {
database.stores[storeName] = database.stores[storeName] || {};
const resolvedKey = key ?? value?.id ?? crypto.randomUUID?.() ?? String(Date.now());
database.stores[storeName][resolvedKey] = value;
persist('indexeddb', storeName + ':' + resolvedKey, JSON.stringify(value));
persist('indexeddb:' + databaseName, storeName + ':' + resolvedKey, JSON.stringify(value));
return createIDBRequest(resolvedKey);
},
get(key) {
@ -119,7 +282,7 @@ func (s *Service) injectStoragePolyfills(pageOrigin string) string {
if (database.stores?.[storeName]) {
delete database.stores[storeName][key];
}
persist('indexeddb', storeName + ':' + key, '');
persist('indexeddb:' + databaseName, storeName + ':' + key, '');
return createIDBRequest(undefined);
},
clear() {
@ -136,14 +299,14 @@ func (s *Service) injectStoragePolyfills(pageOrigin string) string {
name,
createObjectStore(storeName) {
database.stores[storeName] = database.stores[storeName] || {};
return createObjectStore(database, storeName);
return createObjectStore(name, database, storeName);
},
transaction(storeNames) {
const names = Array.isArray(storeNames) ? storeNames : [storeNames];
return {
objectStore(storeName) {
const target = names.includes(storeName) ? storeName : names[0];
return createObjectStore(database, target);
return createObjectStore(name, database, target);
}
};
},
@ -160,12 +323,12 @@ func (s *Service) injectStoragePolyfills(pageOrigin string) string {
async put(request, response) {
const key = typeof request === 'string' ? request : request?.url;
bucket[key] = response;
persist('cache', name + ':' + key, JSON.stringify(response));
persist('cache:' + name, key, JSON.stringify(response));
},
async delete(request) {
const key = typeof request === 'string' ? request : request?.url;
delete bucket[key];
persist('cache', name + ':' + key, '');
persist('cache:' + name, key, '');
return true;
},
async keys() {

View file

@ -2,6 +2,7 @@ package display
import (
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
@ -65,6 +66,44 @@ func storageDatabasePath() string {
return core.Path(home, ".core", "state", fmt.Sprintf("gui-storage-%d.db", os.Getpid()))
}
func storageOriginForPageURL(pageURL string) string {
trimmed := strings.TrimSpace(pageURL)
if trimmed == "" {
return ""
}
parsed, err := url.Parse(trimmed)
if err != nil {
return trimmed
}
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "http", "https":
if parsed.Host == "" {
return trimmed
}
return parsed.Scheme + "://" + parsed.Host
case "core":
if parsed.Host == "" {
return "core://"
}
return "core://" + parsed.Host
case "file":
if parsed.Path == "" {
return trimmed
}
return "file://" + parsed.Path
default:
origin := parsed.Scheme + "://" + parsed.Host
if parsed.Path != "" {
origin += parsed.Path
}
origin = strings.TrimRight(origin, "/")
if strings.TrimSpace(origin) == "://" {
return trimmed
}
return origin
}
}
func makeStorageEntryKey(origin, bucket, key string) string {
return strings.Join([]string{origin, bucket, key}, "\x00")
}
@ -175,6 +214,26 @@ func (r *StorageRegistry) Search(query string) []StorageEntry {
return results
}
func (r *StorageRegistry) Snapshot(pageURL string) map[string]map[string]string {
r.mu.RLock()
defer r.mu.RUnlock()
origin := storageOriginForPageURL(pageURL)
snapshot := make(map[string]map[string]string)
for _, entry := range r.entries {
if origin != "" && !strings.EqualFold(entry.Origin, origin) {
continue
}
bucket := snapshot[entry.Bucket]
if bucket == nil {
bucket = make(map[string]string)
snapshot[entry.Bucket] = bucket
}
bucket[entry.Key] = entry.Value
}
return snapshot
}
func (r *StorageRegistry) Close() error {
if r == nil || r.store == nil {
return nil

View file

@ -92,3 +92,18 @@ func TestStorageRegistry_Search_Ugly(t *testing.T) {
results := r.Search("does-not-exist")
require.Empty(t, results)
}
func TestStorageRegistry_Snapshot_Good(t *testing.T) {
r := NewStorageRegistry()
r.Set("core://settings", "localStorage", "theme", "dark")
r.Set("core://settings", "cookies", "session", `{"value":"abc","path":"/","secure":false}`)
r.Set("core://other", "localStorage", "theme", "light")
snapshot := r.Snapshot("core://settings/profile")
require.Contains(t, snapshot, "localStorage")
require.Contains(t, snapshot, "cookies")
assert.Equal(t, "dark", snapshot["localStorage"]["theme"])
assert.Equal(t, `{"value":"abc","path":"/","secure":false}`, snapshot["cookies"]["session"])
_, otherOriginPresent := snapshot["other"]
assert.False(t, otherOriginPresent)
}