mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-05 20:26:49 +02:00
* fix(plugins): updatePlugins.sh actually updates installed plugins (#6670) bin/updatePlugins.sh detected outdated plugins by running `pnpm --filter ep_etherpad-lite outdated --depth=0`, but installed plugins are not registered in src/package.json — bin/plugins.ts adds them via linkInstaller.installPlugin which writes to src/plugin_packages/.versions/<name>@<version>/ and tracks the result in var/installed_plugins.json. pnpm has no view of them, so `outdated` returns empty and the script always reported "All plugins are up-to-date" even when newer versions existed on the registry. PR #7468 fixed npm→pnpm and install→update but kept the same broken detection mechanism, which is why the issue stayed open after that PR landed. Read the plugin list from var/installed_plugins.json instead, then re-invoke linkInstaller.installPlugin(name) for each entry. Calling the installer without a version pin resolves the registry-latest and overwrites the existing pinned copy, so an outdated plugin is brought to head while plugins already at latest are no-ops apart from the pnpm cache hit. Add an `update`/`up` action to bin/plugins.ts so users can also run `pnpm run plugins update` directly, mirroring the existing install/remove/list actions. updatePlugins.sh becomes a one-line wrapper for backwards compatibility. Reproduction (verified): pnpm run install-plugins ep_markdown@11.0.5 # latest is 11.0.18 ./bin/updatePlugins.sh # → 11.0.18 Edge cases tested: no plugins installed, missing installed_plugins.json, already-at-latest re-run. Closes #6670. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(plugins): validate ep_ prefix and dedupe + add regression test Qodo flagged two issues on the original update() addition: 1. Security — update() trusted every name in var/installed_plugins.json, so a corrupted or hand-edited manifest could coerce the script into installing arbitrary npm packages. pluginfw/plugins.getPackages already gates on the ep_ prefix; mirror that gate here. 2. Reliability — no automated regression test, so a future refactor could silently bring back the broken behaviour. Extract the safe-name filter to filterUpdatablePluginNames in bin/commonPlugins.ts (pure, side-effect-free, prefix configurable, also de-duplicates repeats so a duplicated entry installs once). Use it from plugins.ts update(). Add src/tests/backend/specs/filterUpdatablePluginNames.ts covering: keep prefixed names, drop ep_etherpad-lite, reject non-prefixed entries, de-dupe repeats, tolerate missing/null/non-string name fields, empty input, custom prefix. Manually verified end-to-end on a live install: an installed_plugins.json containing ep_markdown@11.0.5, a duplicate ep_markdown, and a "malicious-package" entry runs `Updating plugins to latest from registry: ep_markdown` (only) and ep_markdown ends up at 11.0.18 — the bad entries are silently filtered out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
'use strict';
|
|
|
|
import {linkInstaller, checkForMigration} from "ep_etherpad-lite/static/js/pluginfw/installer";
|
|
import {persistInstalledPlugins, filterUpdatablePluginNames} from "./commonPlugins";
|
|
import fs from "node:fs";
|
|
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
|
|
|
if (process.argv.length === 2) {
|
|
console.error('Expected at least one argument!');
|
|
process.exit(1);
|
|
}
|
|
|
|
let args = process.argv.slice(2)
|
|
|
|
|
|
const possibleActions = [
|
|
"i",
|
|
"install",
|
|
"rm",
|
|
"remove",
|
|
"ls",
|
|
"list",
|
|
"up",
|
|
"update"
|
|
]
|
|
|
|
const install = ()=> {
|
|
const argsAsString: string = args.join(" ");
|
|
const regexRegistryPlugins = /(?<=(?:i|install)\s)(.*?)(?=--github|--path|$)/;
|
|
const regexLocalPlugins = /(?<=--path\s)(.*?)(?=--github|$)/;
|
|
const regexGithubPlugins = /(?<=--github\s)(.*?)(?=--path|$)/;
|
|
const registryPlugins = argsAsString.match(regexRegistryPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
|
const localPlugins = argsAsString.match(regexLocalPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
|
const githubPlugins = argsAsString.match(regexGithubPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
|
|
|
async function run() {
|
|
for (const plugin of registryPlugins) {
|
|
if (possibleActions.includes(plugin)){
|
|
continue
|
|
}
|
|
console.log(`Installing plugin from registry: ${plugin}`)
|
|
if (plugin.includes('@')) {
|
|
const [name, version] = plugin.split('@');
|
|
await linkInstaller.installPlugin(name, version);
|
|
continue;
|
|
}
|
|
await linkInstaller.installPlugin(plugin);
|
|
}
|
|
|
|
for (const plugin of localPlugins) {
|
|
console.log(`Installing plugin from path: ${plugin}`);
|
|
await linkInstaller.installFromPath(plugin);
|
|
}
|
|
|
|
for (const plugin of githubPlugins) {
|
|
console.log(`Installing plugin from github: ${plugin}`);
|
|
await linkInstaller.installFromGitHub(plugin);
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
await checkForMigration();
|
|
await run();
|
|
await persistInstalledPlugins();
|
|
})();
|
|
}
|
|
|
|
const list = ()=>{
|
|
const walk = async () => {
|
|
const plugins = fs.readFileSync(settings.root+"/var/installed_plugins.json", "utf-8")
|
|
const pluginNames = JSON.parse(plugins).plugins.map((plugin: any) => plugin.name).join(", ")
|
|
|
|
console.log("Installed plugins are:", pluginNames)
|
|
}
|
|
|
|
(async () => {
|
|
await walk();
|
|
})();
|
|
}
|
|
|
|
// Re-install every plugin in installed_plugins.json without a version pin so
|
|
// the registry-latest gets resolved and overwrites the existing pinned copy
|
|
// in src/plugin_packages/. ep_etherpad-lite is the vendored core, never
|
|
// installed via the plugin path. filterUpdatablePluginNames also enforces
|
|
// the ep_ prefix so a corrupted manifest cannot coerce us into installing
|
|
// arbitrary npm packages, and de-duplicates repeats.
|
|
const update = ()=> {
|
|
(async () => {
|
|
const path = settings.root+"/var/installed_plugins.json";
|
|
let entries: Array<{name?: unknown}>;
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(path, "utf-8"));
|
|
entries = Array.isArray(parsed?.plugins) ? parsed.plugins : [];
|
|
} catch (err: any) {
|
|
if (err.code === 'ENOENT') {
|
|
console.log("No installed_plugins.json found — nothing to update");
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
const names = filterUpdatablePluginNames(entries);
|
|
if (names.length === 0) {
|
|
console.log("No plugins installed — nothing to update");
|
|
return;
|
|
}
|
|
console.log(`Updating plugins to latest from registry: ${names.join(', ')}`);
|
|
await checkForMigration();
|
|
for (const name of names) {
|
|
await linkInstaller.installPlugin(name);
|
|
}
|
|
await persistInstalledPlugins();
|
|
})();
|
|
}
|
|
|
|
const remove = (plugins: string[])=>{
|
|
const walk = async () => {
|
|
for (const plugin of plugins) {
|
|
console.log(`Uninstalling plugin: ${plugin}`)
|
|
await linkInstaller.uninstallPlugin(plugin);
|
|
}
|
|
await persistInstalledPlugins();
|
|
}
|
|
|
|
(async () => {
|
|
await checkForMigration();
|
|
await walk();
|
|
})();
|
|
}
|
|
|
|
let action = args[0];
|
|
|
|
switch (action) {
|
|
case "install":
|
|
install();
|
|
break;
|
|
case "i":
|
|
install();
|
|
break;
|
|
case "ls":
|
|
list();
|
|
break;
|
|
case "list":
|
|
list();
|
|
break;
|
|
case "rm":
|
|
remove(args.slice(1));
|
|
break;
|
|
case "remove":
|
|
remove(args.slice(1));
|
|
break;
|
|
case "up":
|
|
update();
|
|
break;
|
|
case "update":
|
|
update();
|
|
break;
|
|
default:
|
|
console.error('Expected at least one argument!');
|
|
process.exit(1);
|
|
}
|
|
|
|
|