Files

77 lines
4.3 KiB
JavaScript

// Host setter for the organization file's `plugins` switch (REQ-PLUG-1,
// REQ-ROLE-3). It is the only writer of that key: no broker verb, mosaic
// command, Settings form or chief-of-staff write reaches it (sage-0301).
// Writes go through the organization writer, which logs old and new names
// like a limit change. A write is a patch per kind (see set()): kinds it doesn't name are kept. A refused write throws a code and logs nothing.
import { organizationWriter, OrganizationError } from "./organization.mjs";
import { verifiedPlugin } from "../../plugins/src/pinned.mjs";
import { hasModule } from "../../plugins/src/loader.mjs";
import { isPlainObject } from "./util.mjs";
import { KINDS, PluginError, checkPlugins, checkUsageSettings } from "../../plugins/src/kinds.mjs";
// `exists(kind, name)`, when given, refuses a name no module answers to (the host passes the
// loader's hasModule). Without it the setter checks only the shape, as before.
export function pluginSettings({ dir, rolesDir, exists = null, dataRoot = null }) {
const writer = organizationWriter({ dir, rolesDir, dataRoot });
function read(id) {
const { document, revision } = writer.read(id);
return { business: id, revision, plugins: structuredClone(document.plugins ?? {}),
...(document.usage === undefined ? {} : { usage: structuredClone(document.usage) }) };
}
function set(id, args, actor) {
if (!isPlainObject(args) || Object.keys(args).some(key => key !== "revision" && key !== "plugins" && key !== "usage")) {
throw new OrganizationError("invalid-request");
}
if (typeof args.revision !== "string" || !/^[a-f0-9]{64}$/.test(args.revision)) {
throw new OrganizationError("invalid-request");
}
// A Date or a Map has no own keys and would pass checkPlugins, then serialize
// as something else (Filbert F1, sage-0314). Only a JSON-shaped object passes.
const shaped = args.plugins !== null && typeof args.plugins === "object";
const proto = shaped ? Object.getPrototypeOf(args.plugins) : undefined;
if (proto !== Object.prototype && proto !== null) {
throw new OrganizationError("invalid-plugins", "plugins must be a plain object with one key per kind");
}
// The patch names the kinds it changes (vikunja:19/309): a kind with a value is switched to it,
// a kind with null is removed, and every kind it doesn't name keeps its setting. An empty patch
// changes nothing and is refused rather than written as a no-op.
const patch = Object.entries(args.plugins);
if (patch.length === 0) throw new OrganizationError("invalid-plugins", "the patch names no kind; to remove a kind give it null");
const removals = patch.filter(([, setting]) => setting === null).map(([kind]) => kind);
const sets = Object.fromEntries(patch.filter(([, setting]) => setting !== null));
for (const kind of removals) {
if (!Object.hasOwn(KINDS, kind)) throw new OrganizationError("invalid-plugins", `plugins names unknown kind ${JSON.stringify(kind)}; adding a kind is a core change`);
}
try {
checkPlugins(sets, "plugins");
} catch (e) {
if (e instanceof PluginError) throw new OrganizationError("invalid-plugins", e.message);
throw e;
}
if (exists) {
for (const [kind, setting] of Object.entries(sets)) {
for (const name of Array.isArray(setting) ? setting : [setting]) {
if (dataRoot !== null && !hasModule(kind,name)) {
try { verifiedPlugin(dataRoot,name,kind); }
catch(e) { throw new OrganizationError(e.code === "missing-lock" ? "unknown-plugin" : "invalid-plugins", "installed plugin verification refused"); }
}
if (!exists(kind, name)) throw new OrganizationError("unknown-plugin", `no ${kind} module named ${name} in this tree`);
}
}
}
const next = structuredClone(writer.read(id).document);
if (Object.hasOwn(args, "usage")) {
if (args.usage === null) delete next.usage;
else next.usage = checkUsageSettings(args.usage);
}
next.plugins = { ...structuredClone(next.plugins ?? {}), ...structuredClone(sets) };
for (const kind of removals) delete next.plugins[kind];
const receipt = writer.write(id, next, { revision: args.revision, actor });
return { ...read(id), transaction: receipt.transaction };
}
return { writer, read, set };
}