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 config = require("./config");
|
||||
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 { initPlugins, shutdownPlugins } = require("./plugin-system/manager");
|
||||
const pluginRoutes = require("./routes/plugins");
|
||||
@@ -92,9 +96,7 @@ app.use("/vault-files", (req, res, next) => {
|
||||
express.static(vaultPath)(req, res, next);
|
||||
});
|
||||
|
||||
// Serve our own index.html. Obsidian's scripts are discovered at startup
|
||||
// and injected dynamically by the client -- no Obsidian files are read or
|
||||
// transformed in the response.
|
||||
// Serve our own index.html. Obsidian's scripts are discovered at startup and injected dynamically by the client.
|
||||
let cachedHtml = null;
|
||||
|
||||
function buildIndexHtml() {
|
||||
|
||||
@@ -148,9 +148,7 @@ async function flushAll() {
|
||||
|
||||
const timeout = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.warn(
|
||||
"[write-coalesce] Flush timeout -- some writes may be lost",
|
||||
);
|
||||
console.warn("[write-coalesce] Flush timeout. Some writes may be lost");
|
||||
resolve();
|
||||
}, FLUSH_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
@@ -31,6 +31,18 @@ export class menuShim {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("ContentCache LRU eviction", () => {
|
||||
cache.set("b.md", "bbbb"); // 4, accessedAt=1001
|
||||
// Touch a.md so b.md becomes the LRU
|
||||
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("b.md")).toBe(false);
|
||||
expect(cache.has("c.md")).toBe(true);
|
||||
@@ -73,7 +73,7 @@ describe("ContentCache LRU eviction", () => {
|
||||
it("entry larger than maxSize still gets stored", () => {
|
||||
const cache = new ContentCache(5);
|
||||
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("big.md")).toBe(true);
|
||||
expect(cache.currentBytes).toBe(10);
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("MetadataCache populate and merge", () => {
|
||||
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();
|
||||
cache.populate({
|
||||
"a.md": { type: "file", size: 1 },
|
||||
@@ -112,7 +112,7 @@ describe("MetadataCache readdir", () => {
|
||||
"foo/sub/deep.md": { type: "file", size: 3 },
|
||||
"foobar/other.md": { type: "file", size: 4 },
|
||||
"root.md": { type: "file", size: 5 },
|
||||
"docs": { type: "directory", size: 0 },
|
||||
docs: { type: "directory", size: 0 },
|
||||
});
|
||||
return cache;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "./workspace.js";
|
||||
import { prefetchVaultContent } from "./fs/indexer-prefetch.js";
|
||||
import { autoTrustDemoVaults, maybeProvisionDemoVault } from "./demo.js";
|
||||
import { initNativeMenuGuard } from "./native-menu-guard.js";
|
||||
|
||||
function resolveVaultId() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -221,6 +222,7 @@ export function initialize() {
|
||||
resolveVaultId();
|
||||
resolveWorkspaceName();
|
||||
loadPresetIfRequested();
|
||||
initNativeMenuGuard(window.__currentVaultId);
|
||||
|
||||
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