mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
0139b91b5d
* chore: Replace lodash with es-toolkit Migrate all direct lodash imports to es-toolkit/compat for a smaller, faster, lodash-compatible utility library. Transitive lodash usage from other packages remains unchanged. * fix: Restore isPlainObject semantics in CanCan policy The lodash migration aliased `isObject` to `lodash/isPlainObject` and the codemod incorrectly mapped the local name to es-toolkit's `isObject`, which also returns true for arrays and functions. This caused condition objects in policy definitions to be skipped, breaking authorization checks across the codebase. * fix: Restore unicode-aware length counting in validators es-toolkit/compat's size() returns string.length, while lodash's _.size() counts unicode code points. Switch to [...value].length to preserve the previous behavior so multi-byte characters like emoji count as one.
154 lines
4.8 KiB
TypeScript
154 lines
4.8 KiB
TypeScript
import path from "node:path";
|
|
import { glob } from "glob";
|
|
import type Router from "koa-router";
|
|
import { isArray, sortBy } from "es-toolkit/compat";
|
|
import type BaseEmail from "@server/emails/templates/BaseEmail";
|
|
import env from "@server/env";
|
|
import Logger from "@server/logging/Logger";
|
|
import type BaseProcessor from "@server/queues/processors/BaseProcessor";
|
|
import type { BaseTask } from "@server/queues/tasks/base/BaseTask";
|
|
import type { UnfurlSignature, UninstallSignature } from "@server/types";
|
|
import type { BaseIssueProvider } from "./BaseIssueProvider";
|
|
import type { GroupSyncProvider } from "./GroupSyncProvider";
|
|
import type { BaseSearchProvider } from "./BaseSearchProvider";
|
|
|
|
export enum PluginPriority {
|
|
VeryHigh = 0,
|
|
High = 100,
|
|
Normal = 200,
|
|
Low = 300,
|
|
VeryLow = 500,
|
|
}
|
|
|
|
/**
|
|
* The different types of server plugins that can be registered.
|
|
*/
|
|
export enum Hook {
|
|
API = "api",
|
|
AuthProvider = "authProvider",
|
|
EmailTemplate = "emailTemplate",
|
|
IssueProvider = "issueProvider",
|
|
Processor = "processor",
|
|
SearchProvider = "searchProvider",
|
|
Task = "task",
|
|
UnfurlProvider = "unfurl",
|
|
Uninstall = "uninstall",
|
|
GroupSyncProvider = "groupSyncProvider",
|
|
}
|
|
|
|
/**
|
|
* A map of plugin types to their values, for example an API plugin would have a value of type
|
|
* Router. Registering an API plugin causes the router to be mounted.
|
|
*/
|
|
type PluginValueMap = {
|
|
[Hook.API]: Router;
|
|
[Hook.AuthProvider]: { router: Router | Promise<Router>; id: string };
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- typeof BaseEmail<EmailProps> isn't assignable from BaseEmail<Subtype>; plugins register heterogeneous template Props.
|
|
[Hook.EmailTemplate]: typeof BaseEmail<any>;
|
|
[Hook.IssueProvider]: BaseIssueProvider;
|
|
[Hook.Processor]: typeof BaseProcessor;
|
|
[Hook.SearchProvider]: BaseSearchProvider;
|
|
[Hook.Task]: typeof BaseTask<object>;
|
|
[Hook.Uninstall]: UninstallSignature;
|
|
[Hook.UnfurlProvider]: { unfurl: UnfurlSignature; cacheExpiry: number };
|
|
[Hook.GroupSyncProvider]: { id: string; provider: GroupSyncProvider };
|
|
};
|
|
|
|
export type Plugin<T extends Hook> = {
|
|
/** Plugin type */
|
|
type: T;
|
|
/** The plugin's display name */
|
|
name?: string;
|
|
/** A brief description of the plugin */
|
|
description?: string;
|
|
/** The plugin content */
|
|
value: PluginValueMap[T];
|
|
/** Priority will affect order in menus and execution. Lower is earlier. */
|
|
priority?: number;
|
|
};
|
|
|
|
/**
|
|
* Server plugin manager.
|
|
*/
|
|
export class PluginManager {
|
|
private static plugins = new Map<Hook, Plugin<Hook>[]>();
|
|
|
|
/**
|
|
* Add plugins to the manager.
|
|
*
|
|
* @param plugins
|
|
*/
|
|
public static add(plugins: Array<Plugin<Hook>> | Plugin<Hook>) {
|
|
if (isArray(plugins)) {
|
|
return plugins.forEach((plugin) => this.register(plugin));
|
|
}
|
|
|
|
this.register(plugins);
|
|
}
|
|
|
|
private static register<T extends Hook>(plugin: Plugin<T>) {
|
|
if (!this.plugins.has(plugin.type)) {
|
|
this.plugins.set(plugin.type, []);
|
|
}
|
|
|
|
this.plugins
|
|
.get(plugin.type)!
|
|
.push({ ...plugin, priority: plugin.priority ?? PluginPriority.Normal });
|
|
|
|
// Do not log plugin registration in forked worker processes, one log from the master process
|
|
// is enough. This can be detected by the presence of `process.send`.
|
|
if (process.send === undefined) {
|
|
Logger.debug(
|
|
"plugins",
|
|
`Plugin(type=${plugin.type}) registered ${
|
|
"name" in plugin.value ? plugin.value.name : ""
|
|
} ${plugin.description ? `(${plugin.description})` : ""}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns all the plugins of a given type in order of priority.
|
|
* Triggers loading of all plugins from disk if not already loaded.
|
|
*
|
|
* @param type - the type of plugin to filter by.
|
|
* @returns a list of plugins.
|
|
*/
|
|
public static getHooks<T extends Hook>(type: T) {
|
|
this.loadPlugins();
|
|
return sortBy(this.plugins.get(type) || [], "priority") as Plugin<T>[];
|
|
}
|
|
|
|
/**
|
|
* Returns the GroupSyncProvider for the given authentication provider name.
|
|
*
|
|
* @param name - the authentication provider name (e.g. "oidc", "google").
|
|
* @returns the GroupSyncProvider if one is registered, undefined otherwise.
|
|
*/
|
|
public static getGroupSyncProvider(
|
|
name: string
|
|
): GroupSyncProvider | undefined {
|
|
const hooks = this.getHooks(Hook.GroupSyncProvider);
|
|
return hooks.find((h) => h.value.id === name)?.value.provider;
|
|
}
|
|
|
|
/**
|
|
* Load plugin server components (anything in the `/server/` directory of a plugin will be loaded)
|
|
*/
|
|
public static loadPlugins() {
|
|
if (this.loaded) {
|
|
return;
|
|
}
|
|
const rootDir = env.ENVIRONMENT === "test" ? "" : "build";
|
|
|
|
glob
|
|
.sync(path.join(rootDir, "plugins/*/server/!(*.test|schema).[jt]s"))
|
|
.forEach((filePath: string) =>
|
|
require(path.join(process.cwd(), filePath))
|
|
);
|
|
this.loaded = true;
|
|
}
|
|
|
|
private static loaded = false;
|
|
}
|