mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
prevent native menus in browser
This commit is contained in:
@@ -4,7 +4,11 @@ const path = require("path");
|
|||||||
const compression = require("compression");
|
const compression = require("compression");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { getVersion } = require("./version");
|
const { getVersion } = require("./version");
|
||||||
const { setupWebSocket, watcher, writeCoalescer } = require("@ignis/server-core");
|
const {
|
||||||
|
setupWebSocket,
|
||||||
|
watcher,
|
||||||
|
writeCoalescer,
|
||||||
|
} = require("@ignis/server-core");
|
||||||
const { updateBridgePluginInAllVaults } = require("./bridge-plugin");
|
const { updateBridgePluginInAllVaults } = require("./bridge-plugin");
|
||||||
const { initPlugins, shutdownPlugins } = require("./plugin-system/manager");
|
const { initPlugins, shutdownPlugins } = require("./plugin-system/manager");
|
||||||
const pluginRoutes = require("./routes/plugins");
|
const pluginRoutes = require("./routes/plugins");
|
||||||
@@ -92,9 +96,7 @@ app.use("/vault-files", (req, res, next) => {
|
|||||||
express.static(vaultPath)(req, res, next);
|
express.static(vaultPath)(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serve our own index.html. Obsidian's scripts are discovered at startup
|
// Serve our own index.html. Obsidian's scripts are discovered at startup and injected dynamically by the client.
|
||||||
// and injected dynamically by the client -- no Obsidian files are read or
|
|
||||||
// transformed in the response.
|
|
||||||
let cachedHtml = null;
|
let cachedHtml = null;
|
||||||
|
|
||||||
function buildIndexHtml() {
|
function buildIndexHtml() {
|
||||||
|
|||||||
@@ -148,9 +148,7 @@ async function flushAll() {
|
|||||||
|
|
||||||
const timeout = new Promise((resolve) => {
|
const timeout = new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.warn(
|
console.warn("[write-coalesce] Flush timeout. Some writes may be lost");
|
||||||
"[write-coalesce] Flush timeout -- some writes may be lost",
|
|
||||||
);
|
|
||||||
resolve();
|
resolve();
|
||||||
}, FLUSH_TIMEOUT_MS);
|
}, FLUSH_TIMEOUT_MS);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,18 @@ export class menuShim {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closePopup() {}
|
closePopup() {}
|
||||||
|
|
||||||
|
// If the appearance guard in native-menu-guard.js ever fails to block the native-menu path, warn instead of throwing.
|
||||||
|
on(channel, listener) {
|
||||||
|
console.warn(
|
||||||
|
`[shim:Menu] Menu.on(${channel}) called; native-menu path escaped the guard.`,
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
off(channel, listener) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class menuItemShim {
|
export class menuItemShim {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe("ContentCache LRU eviction", () => {
|
|||||||
cache.set("b.md", "bbbb"); // 4, accessedAt=1001
|
cache.set("b.md", "bbbb"); // 4, accessedAt=1001
|
||||||
// Touch a.md so b.md becomes the LRU
|
// Touch a.md so b.md becomes the LRU
|
||||||
cache.get("a.md"); // a.md accessedAt=1002
|
cache.get("a.md"); // a.md accessedAt=1002
|
||||||
cache.set("c.md", "cccc"); // 4 -- should evict b.md (1001), not a.md (1002)
|
cache.set("c.md", "cccc"); // 4, should evict b.md (1001), not a.md (1002)
|
||||||
expect(cache.has("a.md")).toBe(true);
|
expect(cache.has("a.md")).toBe(true);
|
||||||
expect(cache.has("b.md")).toBe(false);
|
expect(cache.has("b.md")).toBe(false);
|
||||||
expect(cache.has("c.md")).toBe(true);
|
expect(cache.has("c.md")).toBe(true);
|
||||||
@@ -73,7 +73,7 @@ describe("ContentCache LRU eviction", () => {
|
|||||||
it("entry larger than maxSize still gets stored", () => {
|
it("entry larger than maxSize still gets stored", () => {
|
||||||
const cache = new ContentCache(5);
|
const cache = new ContentCache(5);
|
||||||
cache.set("small.md", "ab"); // 2
|
cache.set("small.md", "ab"); // 2
|
||||||
cache.set("big.md", "abcdefghij"); // 10 -- larger than maxSize
|
cache.set("big.md", "abcdefghij"); // 10, larger than maxSize
|
||||||
expect(cache.has("small.md")).toBe(false);
|
expect(cache.has("small.md")).toBe(false);
|
||||||
expect(cache.has("big.md")).toBe(true);
|
expect(cache.has("big.md")).toBe(true);
|
||||||
expect(cache.currentBytes).toBe(10);
|
expect(cache.currentBytes).toBe(10);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("MetadataCache populate and merge", () => {
|
|||||||
expect(cache.has("added.md")).toBe(true);
|
expect(cache.has("added.md")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("populate then merge -- pre-existing entries survive merge", () => {
|
it("populate then merge. pre-existing entries survive merge", () => {
|
||||||
const cache = new MetadataCache();
|
const cache = new MetadataCache();
|
||||||
cache.populate({
|
cache.populate({
|
||||||
"a.md": { type: "file", size: 1 },
|
"a.md": { type: "file", size: 1 },
|
||||||
@@ -112,7 +112,7 @@ describe("MetadataCache readdir", () => {
|
|||||||
"foo/sub/deep.md": { type: "file", size: 3 },
|
"foo/sub/deep.md": { type: "file", size: 3 },
|
||||||
"foobar/other.md": { type: "file", size: 4 },
|
"foobar/other.md": { type: "file", size: 4 },
|
||||||
"root.md": { type: "file", size: 5 },
|
"root.md": { type: "file", size: 5 },
|
||||||
"docs": { type: "directory", size: 0 },
|
docs: { type: "directory", size: 0 },
|
||||||
});
|
});
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "./workspace.js";
|
} from "./workspace.js";
|
||||||
import { prefetchVaultContent } from "./fs/indexer-prefetch.js";
|
import { prefetchVaultContent } from "./fs/indexer-prefetch.js";
|
||||||
import { autoTrustDemoVaults, maybeProvisionDemoVault } from "./demo.js";
|
import { autoTrustDemoVaults, maybeProvisionDemoVault } from "./demo.js";
|
||||||
|
import { initNativeMenuGuard } from "./native-menu-guard.js";
|
||||||
|
|
||||||
function resolveVaultId() {
|
function resolveVaultId() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -221,6 +222,7 @@ export function initialize() {
|
|||||||
resolveVaultId();
|
resolveVaultId();
|
||||||
resolveWorkspaceName();
|
resolveWorkspaceName();
|
||||||
loadPresetIfRequested();
|
loadPresetIfRequested();
|
||||||
|
initNativeMenuGuard(window.__currentVaultId);
|
||||||
|
|
||||||
const bootstrap = fetchBootstrap();
|
const bootstrap = fetchBootstrap();
|
||||||
|
|
||||||
|
|||||||
164
packages/shim/src/native-menu-guard.js
Normal file
164
packages/shim/src/native-menu-guard.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
// Obsidian's native-menu path uses Electron Menu APIs that can't render in a browser.
|
||||||
|
// Use transforms to keep nativeMenus = false in browser context while preserving user config on disk.
|
||||||
|
// Also disable the settings toggle and patch setConfig.
|
||||||
|
|
||||||
|
import {
|
||||||
|
registerReadTransform,
|
||||||
|
registerWriteTransform,
|
||||||
|
} from "./fs/transforms.js";
|
||||||
|
|
||||||
|
const APPEARANCE_PATH = ".obsidian/appearance.json";
|
||||||
|
|
||||||
|
// undefined = key absent on disk; write transform keeps it absent.
|
||||||
|
let preservedNativeMenus = undefined;
|
||||||
|
|
||||||
|
function snapshotAppearance(vaultId) {
|
||||||
|
if (!vaultId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const url =
|
||||||
|
"/api/fs/readFile?vault=" +
|
||||||
|
encodeURIComponent(vaultId) +
|
||||||
|
"&path=" +
|
||||||
|
encodeURIComponent(APPEARANCE_PATH) +
|
||||||
|
"&encoding=utf-8";
|
||||||
|
|
||||||
|
xhr.open("GET", url, false);
|
||||||
|
xhr.send();
|
||||||
|
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = JSON.parse(xhr.responseText);
|
||||||
|
|
||||||
|
if ("nativeMenus" in obj) {
|
||||||
|
preservedNativeMenus = obj.nativeMenus;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// File missing or malformed; preservedNativeMenus stays undefined.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTransform(data) {
|
||||||
|
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(text);
|
||||||
|
|
||||||
|
if (obj.nativeMenus) {
|
||||||
|
obj.nativeMenus = false;
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeTransform(data) {
|
||||||
|
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(text);
|
||||||
|
|
||||||
|
if (preservedNativeMenus === undefined) {
|
||||||
|
delete obj.nativeMenus;
|
||||||
|
} else {
|
||||||
|
obj.nativeMenus = preservedNativeMenus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
} catch {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent setting from being set during runtime.
|
||||||
|
function patchSetConfig() {
|
||||||
|
const tryPatch = () => {
|
||||||
|
const vault = window.app && window.app.vault;
|
||||||
|
|
||||||
|
if (!vault || typeof vault.setConfig !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vault.__ignisNativeMenuGuarded) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orig = vault.setConfig.bind(vault);
|
||||||
|
|
||||||
|
vault.setConfig = function (key, value) {
|
||||||
|
if (key === "nativeMenus") {
|
||||||
|
return orig("nativeMenus", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return orig(key, value);
|
||||||
|
};
|
||||||
|
vault.__ignisNativeMenuGuarded = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tryPatch()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (tryPatch()) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the "Native menus" toggle in appearance settings.
|
||||||
|
function disableNativeMenuToggle() {
|
||||||
|
const apply = () => {
|
||||||
|
document.querySelectorAll(".setting-item-name").forEach((nameEl) => {
|
||||||
|
if (!/native.?menu/i.test(nameEl.textContent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = nameEl.closest(".setting-item");
|
||||||
|
const input = item && item.querySelector('input[type="checkbox"]');
|
||||||
|
|
||||||
|
if (!input || input.__ignisDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.disabled = true;
|
||||||
|
input.__ignisDisabled = true;
|
||||||
|
|
||||||
|
const container = input.closest(".checkbox-container");
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
container.title =
|
||||||
|
"Forced off in Ignis - browser context can't render native menus.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new MutationObserver(apply);
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initNativeMenuGuard(vaultId) {
|
||||||
|
// Snapshot before registering transforms so the write transform has the original disk value to substitute back.
|
||||||
|
snapshotAppearance(vaultId);
|
||||||
|
registerReadTransform(APPEARANCE_PATH, readTransform);
|
||||||
|
registerWriteTransform(APPEARANCE_PATH, writeTransform);
|
||||||
|
patchSetConfig();
|
||||||
|
disableNativeMenuToggle();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user