-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpad-toggle-server.js
More file actions
134 lines (119 loc) · 5.96 KB
/
Copy pathpad-toggle-server.js
File metadata and controls
134 lines (119 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
'use strict';
// padToggle (server side) — emits parallel checkboxes in the User Settings
// (mySettings) and Pad Wide Settings (padSettings) panels, mirroring native
// Etherpad behavior. Pad-wide values ride the existing padoptions COLLABROOM
// rail (stored at pad.padOptions[pluginName] = {enabled: bool}) when the core
// has the ep_* passthrough patch (Etherpad >= 3.0.0, PR #7698); on older
// cores the pad-wide block silently no-ops and the user-side cookie toggle
// alone still works.
//
// This module is intended for server-side import only. The companion
// `pad-toggle.js` provides the client-side init/handleClientMessage hooks.
// They share the same config; plugin authors should use identical
// `pluginName`, `settingId`, `l10nId`, and `defaultEnabled` on both sides so
// the checkbox ids and clientVars block line up.
const PLUGIN_NAME_RE = /^ep_[a-z0-9_]+$/;
let padOptionsPluginPassthrough = false;
try {
// The require lands on a leaf module on patched cores (Etherpad >= 3.0.0)
// and throws on older cores. Server-only: this file is never bundled for
// the browser, so esbuild's static analysis does not run here.
// eslint-disable-next-line global-require
const caps = require('ep_etherpad-lite/node/utils/PluginCapabilities');
padOptionsPluginPassthrough = caps && caps.padOptionsPluginPassthrough === true;
} catch (_e) { /* older core — leave as false */ }
const HTML_ESCAPE_RE = /[&<>"']/g;
const HTML_ESCAPES = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''};
const escapeHtml = (s) => String(s).replace(HTML_ESCAPE_RE, (c) => HTML_ESCAPES[c]);
const validateConfig = (config) => {
if (!config || typeof config !== 'object') {
throw new Error('padToggle requires a config object');
}
const {pluginName, settingId, l10nId, defaultLabel, defaultEnabled = true} = config;
if (!PLUGIN_NAME_RE.test(pluginName || '')) {
throw new Error(
`padToggle pluginName must match /^ep_[a-z0-9_]+$/, got: ${pluginName}`);
}
if (!settingId || typeof settingId !== 'string') {
throw new Error('padToggle requires settingId (string)');
}
if (!l10nId || typeof l10nId !== 'string') {
throw new Error('padToggle requires l10nId (string) — i18n is mandatory');
}
if (!defaultLabel || typeof defaultLabel !== 'string') {
throw new Error(
'padToggle requires defaultLabel (string) — accessibility fallback ' +
'rendered inside <label> so screen readers announce something before ' +
'html10n loads. html10n overwrites it at runtime via data-l10n-id.');
}
return {pluginName, settingId, l10nId, defaultLabel, defaultEnabled: !!defaultEnabled};
};
const renderCheckbox = (settingId, l10nId, defaultLabel, idPrefix) =>
`<p>` +
`<input type="checkbox" id="${idPrefix}options-${settingId}">` +
`<label for="${idPrefix}options-${settingId}" ` +
`data-l10n-id="${escapeHtml(l10nId)}">${escapeHtml(defaultLabel)}</label>` +
`</p>`;
const padToggleServer = (rawConfig) => {
const {pluginName, settingId, l10nId, defaultLabel, defaultEnabled} = validateConfig(rawConfig);
let cachedDefaultEnabled = defaultEnabled;
// Etherpad >= 3.0.0 introduced settings.enablePluginPadOptions as a runtime
// gate on the ep_* passthrough (default true on current cores; older 3.x
// releases shipped with it default false). We grab it from loadSettings so
// eejsBlock_padSettings + clientVars correctly no-op when the flag isn't
// enabled (absent on pre-flip cores, or explicitly false), even though
// PluginCapabilities reports the patch is present in the core.
let runtimeFlagEnabled = false;
const isPadWideActive = () => padOptionsPluginPassthrough && runtimeFlagEnabled;
const loadSettings = async (hookName, args) => {
const root = (args && args.settings) || {};
const ps = root[pluginName] || {};
if (typeof ps.defaultEnabled === 'boolean') cachedDefaultEnabled = ps.defaultEnabled;
runtimeFlagEnabled = root.enablePluginPadOptions === true;
};
const clientVars = async (hookName, ctx) => {
let initialPadEnabled = cachedDefaultEnabled;
try {
const padSettings = ctx && ctx.pad && typeof ctx.pad.getPadSettings === 'function'
? ctx.pad.getPadSettings() : null;
const stored = padSettings && padSettings[pluginName];
if (stored && typeof stored.enabled === 'boolean') initialPadEnabled = stored.enabled;
} catch (_e) { /* leave initialPadEnabled at instance default */ }
return {
ep_plugin_helpers: {
padToggle: {
[pluginName]: {
// True iff the running core has the patch AND the admin has
// opted in via settings.enablePluginPadOptions. Client-side
// init() reads this to decide whether to wire the pad-wide
// checkbox, log the degradation warning, etc.
padWideSupported: isPadWideActive(),
// Granular flags so the client's degradation warning can name
// the specific cause (missing patch vs. missing runtime flag)
// instead of guessing. Older client builds that don't read
// these still work — padWideSupported alone is sufficient
// for the gating logic; the warning just falls back to a
// generic line.
patchPresent: padOptionsPluginPassthrough,
runtimeEnabled: runtimeFlagEnabled,
settingId,
l10nId,
defaultEnabled: cachedDefaultEnabled,
initialPadEnabled,
},
},
},
};
};
const eejsBlock_mySettings = (hookName, args, cb) => {
args.content += renderCheckbox(settingId, l10nId, defaultLabel, '');
return cb();
};
const eejsBlock_padSettings = (hookName, args, cb) => {
if (!isPadWideActive()) return cb();
args.content += renderCheckbox(settingId, l10nId, defaultLabel, 'padsettings-');
return cb();
};
return {loadSettings, clientVars, eejsBlock_mySettings, eejsBlock_padSettings};
};
module.exports = {padToggle: padToggleServer, createPadToggle: padToggleServer};