mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-23 00:46:16 -04:00
Feat/admin react (#6211)
* Added vite react admin ui. * Added react i18next. * Added pads manager. * Fixed docker build. * Fixed windows build. * Fixed installOnWindows script. * Install only if path exists.
This commit is contained in:
parent
d34b964cc2
commit
db46ffb63b
112 changed files with 3327 additions and 946 deletions
|
@ -5,8 +5,6 @@ require('eslint-config-etherpad/patch/modern-module-resolution');
|
|||
|
||||
module.exports = {
|
||||
ignorePatterns: [
|
||||
'/static/js/admin/jquery.autosize.js',
|
||||
'/static/js/admin/minify.json.js',
|
||||
'/static/js/vendors/browser.js',
|
||||
'/static/js/vendors/farbtastic.js',
|
||||
'/static/js/vendors/gritter.js',
|
||||
|
|
|
@ -92,14 +92,12 @@
|
|||
{
|
||||
"name": "adminplugins",
|
||||
"hooks": {
|
||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins",
|
||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "adminsettings",
|
||||
"hooks": {
|
||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings",
|
||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
*/
|
||||
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
import {PadType} from "../types/PadType";
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const Pad = require('../db/Pad');
|
||||
|
@ -105,7 +106,7 @@ const padList = new class {
|
|||
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
|
||||
* applicable).
|
||||
*/
|
||||
exports.getPad = async (id: string, text: string|null, authorId:string = '') => {
|
||||
exports.getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise<PadType> => {
|
||||
// check if this is a valid padId
|
||||
if (!exports.isValidPadId(id)) {
|
||||
throw new CustomError(`${id} is not a valid padId`, 'apierror');
|
||||
|
@ -148,6 +149,9 @@ exports.listAllPads = async () => {
|
|||
return {padIDs};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// checks if a pad exists
|
||||
exports.doesPadExist = async (padId: string) => {
|
||||
const value = await db.get(`pad:${padId}`);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
'use strict';
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
import path from "path";
|
||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||
|
||||
const eejs = require('../../eejs');
|
||||
const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
|
||||
|
||||
/**
|
||||
* Add the admin navigation link
|
||||
|
@ -11,9 +13,19 @@ const eejs = require('../../eejs');
|
|||
* @return {*}
|
||||
*/
|
||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
|
||||
args.app.get('/admin', (req:any, res:any) => {
|
||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req}));
|
||||
});
|
||||
args.app.get('/admin/*', (req:any, res:any, next:Function) => {
|
||||
if (req.path.includes('.')) {
|
||||
const relativPath = req.path.split('/admin/')[1];
|
||||
res.sendFile(path.join(ADMIN_PATH, relativPath));
|
||||
} else {
|
||||
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
||||
res.header('Expires', '-1');
|
||||
res.header('Pragma', 'no-cache');
|
||||
res.sendFile(path.join(ADMIN_PATH, 'index.html'));
|
||||
}
|
||||
});
|
||||
args.app.get('/admin', (req:any, res:any, next:Function) => {
|
||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
||||
})
|
||||
return cb();
|
||||
};
|
||||
|
|
|
@ -12,35 +12,7 @@ const installer = require('../../../static/js/pluginfw/installer');
|
|||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const plugins = require('../../../static/js/pluginfw/plugins');
|
||||
const semver = require('semver');
|
||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
|
||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function) => {
|
||||
args.app.get('/admin/plugins', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', {
|
||||
plugins: pluginDefs.plugins,
|
||||
req,
|
||||
errors: [],
|
||||
}));
|
||||
});
|
||||
|
||||
args.app.get('/admin/plugins/info', (req:any, res:any) => {
|
||||
const gitCommit = settings.getGitCommit();
|
||||
const epVersion = settings.getEpVersion();
|
||||
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', {
|
||||
gitCommit,
|
||||
epVersion,
|
||||
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
|
||||
installedParts: `<pre>${plugins.formatParts()}</pre>`,
|
||||
installedServerHooks: `<div>${plugins.formatHooks('hooks', true)}</div>`,
|
||||
installedClientHooks: `<div>${plugins.formatHooks('client_hooks', true)}</div>`,
|
||||
latestVersion: UpdateCheck.getLatestVersion(),
|
||||
req,
|
||||
}));
|
||||
});
|
||||
|
||||
return cb();
|
||||
};
|
||||
|
||||
exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
const io = args.io.of('/pluginfw/installer');
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
import {PadQueryResult, PadSearchQuery} from "../../types/PadSearchQuery";
|
||||
import {PadType} from "../../types/PadType";
|
||||
|
||||
const eejs = require('../../eejs');
|
||||
const fsp = require('fs').promises;
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const plugins = require('../../../static/js/pluginfw/plugins');
|
||||
const settings = require('../../utils/Settings');
|
||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
const padManager = require('../../db/PadManager');
|
||||
const api = require('../../db/API');
|
||||
|
||||
|
||||
const queryPadLimit = 12;
|
||||
|
||||
exports.expressCreateServer = (hookName:string, {app}:any) => {
|
||||
app.get('/admin/settings', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', {
|
||||
req,
|
||||
settings: '',
|
||||
errors: [],
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
exports.socketio = (hookName:string, {io}:any) => {
|
||||
io.of('/settings').on('connection', (socket: any ) => {
|
||||
|
@ -38,10 +39,160 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
});
|
||||
|
||||
socket.on('saveSettings', async (newSettings:string) => {
|
||||
console.log('Admin request to save settings through a socket on /admin/settings');
|
||||
await fsp.writeFile(settings.settingsFilename, newSettings);
|
||||
socket.emit('saveprogress', 'saved');
|
||||
});
|
||||
|
||||
|
||||
socket.on('help', ()=> {
|
||||
const gitCommit = settings.getGitCommit();
|
||||
const epVersion = settings.getEpVersion();
|
||||
|
||||
const hooks:Map<string, Map<string,string>> = plugins.getHooks('hooks', false);
|
||||
const clientHooks:Map<string, Map<string,string>> = plugins.getHooks('client_hooks', false);
|
||||
|
||||
function mapToObject(map: Map<string,any>) {
|
||||
let obj = Object.create(null);
|
||||
for (let [k,v] of map) {
|
||||
if(v instanceof Map) {
|
||||
obj[k] = mapToObject(v);
|
||||
} else {
|
||||
obj[k] = v;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
socket.emit('reply:help', {
|
||||
gitCommit,
|
||||
epVersion,
|
||||
installedPlugins: plugins.getPlugins(),
|
||||
installedParts: plugins.getParts(),
|
||||
installedServerHooks: mapToObject(hooks),
|
||||
installedClientHooks: mapToObject(clientHooks),
|
||||
latestVersion: UpdateCheck.getLatestVersion(),
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
socket.on('padLoad', async (query: PadSearchQuery) => {
|
||||
const {padIDs} = await padManager.listAllPads();
|
||||
|
||||
const data:{
|
||||
total: number,
|
||||
results?: PadQueryResult[]
|
||||
} = {
|
||||
total: padIDs.length,
|
||||
};
|
||||
let result: string[] = padIDs;
|
||||
let maxResult;
|
||||
|
||||
// Filter out matches
|
||||
if (query.pattern) {
|
||||
result = result.filter((padName: string) => padName.includes(query.pattern));
|
||||
}
|
||||
|
||||
data.total = result.length;
|
||||
|
||||
maxResult = result.length - 1;
|
||||
if (maxResult < 0) {
|
||||
maxResult = 0;
|
||||
}
|
||||
|
||||
if (query.offset && query.offset < 0) {
|
||||
query.offset = 0;
|
||||
} else if (query.offset > maxResult) {
|
||||
query.offset = maxResult;
|
||||
}
|
||||
|
||||
if (query.limit && query.limit < 0) {
|
||||
query.limit = 0;
|
||||
} else if (query.limit > queryPadLimit) {
|
||||
query.limit = queryPadLimit;
|
||||
}
|
||||
|
||||
if (query.sortBy === 'padName') {
|
||||
result = result.sort((a,b)=>{
|
||||
if(a < b) return query.ascending ? -1 : 1;
|
||||
if(a > b) return query.ascending ? 1 : -1;
|
||||
return 0;
|
||||
}).slice(query.offset, query.offset + query.limit);
|
||||
|
||||
data.results = await Promise.all(result.map(async (padName: string) => {
|
||||
const pad = await padManager.getPad(padName);
|
||||
const revisionNumber = pad.getHeadRevisionNumber()
|
||||
const userCount = api.padUsersCount(padName).padUsersCount;
|
||||
const lastEdited = await pad.getLastEdit();
|
||||
|
||||
return {
|
||||
padName,
|
||||
lastEdited,
|
||||
userCount,
|
||||
revisionNumber
|
||||
}}));
|
||||
} else {
|
||||
const currentWinners: PadQueryResult[] = []
|
||||
let queryOffsetCounter = 0
|
||||
for (let res of result) {
|
||||
|
||||
const pad = await padManager.getPad(res);
|
||||
const padType = {
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
};
|
||||
|
||||
if (currentWinners.length < query.limit) {
|
||||
if(queryOffsetCounter < query.offset){
|
||||
queryOffsetCounter++
|
||||
continue
|
||||
}
|
||||
currentWinners.push({
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
})
|
||||
} else {
|
||||
// Kick out worst pad and replace by current pad
|
||||
let worstPad = currentWinners.sort((a, b) => {
|
||||
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
|
||||
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
|
||||
return 0;
|
||||
})
|
||||
if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){
|
||||
if(queryOffsetCounter < query.offset){
|
||||
queryOffsetCounter++
|
||||
continue
|
||||
}
|
||||
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
|
||||
currentWinners.push({
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
data.results = currentWinners;
|
||||
}
|
||||
|
||||
socket.emit('results:padLoad', data);
|
||||
})
|
||||
|
||||
|
||||
socket.on('deletePad', async (padId: string) => {
|
||||
const padExists = await padManager.doesPadExists(padId);
|
||||
if (padExists) {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.remove();
|
||||
socket.emit('results:deletePad', padId);
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('restartServer', async () => {
|
||||
console.log('Admin request to restart server through a socket on /admin/settings');
|
||||
settings.reloadSettings();
|
||||
|
@ -51,3 +202,10 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const searchPad = async (query:PadSearchQuery) => {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -76,10 +76,12 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
||||
})
|
||||
|
||||
io.on('connection', (socket:any) => {
|
||||
|
||||
const handleConnection = (socket:Socket) => {
|
||||
sockets.add(socket);
|
||||
socketsEvents.emit('updated');
|
||||
// https://socket.io/docs/v3/faq/index.html
|
||||
// @ts-ignore
|
||||
const session = socket.request.session;
|
||||
session.connections++;
|
||||
session.save();
|
||||
|
@ -87,15 +89,9 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
sockets.delete(socket);
|
||||
socketsEvents.emit('updated');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
io.use(socketSessionMiddleware(args));
|
||||
|
||||
// Temporary workaround so all clients go through middleware and handle connection
|
||||
io.of('/pluginfw/installer').use(socketSessionMiddleware(args))
|
||||
io.of('/settings').use(socketSessionMiddleware(args))
|
||||
|
||||
io.use((socket:any, next:Function) => {
|
||||
const renewSession = (socket:any, next:Function) => {
|
||||
socket.conn.on('packet', (packet:string) => {
|
||||
// Tell express-session that the session is still active. The session store can use these
|
||||
// touch events to defer automatic session cleanup, and if express-session is configured with
|
||||
|
@ -106,7 +102,24 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
if (socket.request.session != null) socket.request.session.touch();
|
||||
});
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
io.on('connection', handleConnection);
|
||||
|
||||
io.use(socketSessionMiddleware(args));
|
||||
|
||||
// Temporary workaround so all clients go through middleware and handle connection
|
||||
io.of('/pluginfw/installer')
|
||||
.on('connection',handleConnection)
|
||||
.use(socketSessionMiddleware(args))
|
||||
.use(renewSession)
|
||||
io.of('/settings')
|
||||
.on('connection',handleConnection)
|
||||
.use(socketSessionMiddleware(args))
|
||||
.use(renewSession)
|
||||
|
||||
io.use(renewSession);
|
||||
|
||||
// var socketIOLogger = log4js.getLogger("socket.io");
|
||||
// Debug logging now has to be set at an environment level, this is stupid.
|
||||
|
|
|
@ -50,7 +50,7 @@ exports.userCanModify = (padId: string, req: SocketClientRequest) => {
|
|||
exports.authnFailureDelayMs = 1000;
|
||||
|
||||
const checkAccess = async (req:any, res:any, next: Function) => {
|
||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin');
|
||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth');
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin
|
||||
|
@ -126,7 +126,13 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
// completed, or maybe different credentials are required), go to the next step.
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (await authorize()) return next();
|
||||
if (await authorize()) {
|
||||
if(requireAdmin) {
|
||||
res.status(200).send('Authorized')
|
||||
return
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Step 3: Authenticate the user. (Or, if already logged in, reauthenticate with different
|
||||
|
@ -163,7 +169,7 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
if (await aCallFirst0('authnFailure', {req, res})) return;
|
||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
||||
// No plugin handled the authentication failure. Fall back to basic authentication.
|
||||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||
//res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||
// Delay the error response for 1s to slow down brute force attacks.
|
||||
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
|
||||
res.status(401).send('Authentication Required');
|
||||
|
@ -188,7 +194,13 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
// a login page).
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (await authorize()) return next();
|
||||
const auth = await authorize()
|
||||
if (auth && !requireAdmin) return next();
|
||||
if(auth && requireAdmin) {
|
||||
res.status(200).send('Authorized')
|
||||
return
|
||||
}
|
||||
|
||||
if (await aCallFirst0('authzFailure', {req, res})) return;
|
||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
||||
// No plugin handled the authorization failure.
|
||||
|
|
15
src/node/types/PadSearchQuery.ts
Normal file
15
src/node/types/PadSearchQuery.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export type PadSearchQuery = {
|
||||
pattern: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
ascending: boolean;
|
||||
sortBy: "padName" | "lastEdited" | "userCount" | "revisionNumber";
|
||||
}
|
||||
|
||||
|
||||
export type PadQueryResult = {
|
||||
padName: string,
|
||||
lastEdited: string,
|
||||
userCount: number,
|
||||
revisionNumber: number
|
||||
}
|
|
@ -42,7 +42,7 @@ exports.getLatestVersion = () => {
|
|||
return infos?.latestVersion;
|
||||
};
|
||||
|
||||
exports.needsUpdate = async (cb: Function) => {
|
||||
exports.needsUpdate = async (cb?: Function) => {
|
||||
await loadEtherpadInformations()
|
||||
.then((info:Infos) => {
|
||||
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
|
||||
|
|
|
@ -1,180 +0,0 @@
|
|||
// Autosize 1.13 - jQuery plugin for textareas
|
||||
// (c) 2012 Jack Moore - jacklmoore.com
|
||||
// license: www.opensource.org/licenses/mit-license.php
|
||||
|
||||
(function ($) {
|
||||
var
|
||||
defaults = {
|
||||
className: 'autosizejs',
|
||||
append: "",
|
||||
callback: false
|
||||
},
|
||||
hidden = 'hidden',
|
||||
borderBox = 'border-box',
|
||||
lineHeight = 'lineHeight',
|
||||
copy = '<textarea tabindex="-1" style="position:absolute; top:-9999px; left:-9999px; right:auto; bottom:auto; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
|
||||
// line-height is omitted because IE7/IE8 doesn't return the correct value.
|
||||
copyStyle = [
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'fontStyle',
|
||||
'letterSpacing',
|
||||
'textTransform',
|
||||
'wordSpacing',
|
||||
'textIndent'
|
||||
],
|
||||
oninput = 'oninput',
|
||||
onpropertychange = 'onpropertychange',
|
||||
test = $(copy)[0];
|
||||
|
||||
// For testing support in old FireFox
|
||||
test.setAttribute(oninput, "return");
|
||||
|
||||
if ($.isFunction(test[oninput]) || onpropertychange in test) {
|
||||
|
||||
// test that line-height can be accurately copied to avoid
|
||||
// incorrect value reporting in old IE and old Opera
|
||||
$(test).css(lineHeight, '99px');
|
||||
if ($(test).css(lineHeight) === '99px') {
|
||||
copyStyle.push(lineHeight);
|
||||
}
|
||||
|
||||
$.fn.autosize = function (options) {
|
||||
options = $.extend({}, defaults, options || {});
|
||||
|
||||
return this.each(function () {
|
||||
var
|
||||
ta = this,
|
||||
$ta = $(ta),
|
||||
mirror,
|
||||
minHeight = $ta.height(),
|
||||
maxHeight = parseInt($ta.css('maxHeight'), 10),
|
||||
active,
|
||||
i = copyStyle.length,
|
||||
resize,
|
||||
boxOffset = 0,
|
||||
value = ta.value,
|
||||
callback = $.isFunction(options.callback);
|
||||
|
||||
if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox){
|
||||
boxOffset = $ta.outerHeight() - $ta.height();
|
||||
}
|
||||
|
||||
if ($ta.data('mirror') || $ta.data('ismirror')) {
|
||||
// if autosize has already been applied, exit.
|
||||
// if autosize is being applied to a mirror element, exit.
|
||||
return;
|
||||
} else {
|
||||
mirror = $(copy).data('ismirror', true).addClass(options.className)[0];
|
||||
|
||||
resize = $ta.css('resize') === 'none' ? 'none' : 'horizontal';
|
||||
|
||||
$ta.data('mirror', $(mirror)).css({
|
||||
overflow: hidden,
|
||||
overflowY: hidden,
|
||||
wordWrap: 'break-word',
|
||||
resize: resize
|
||||
});
|
||||
}
|
||||
|
||||
// Opera returns '-1px' when max-height is set to 'none'.
|
||||
maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
|
||||
|
||||
// Using mainly bare JS in this function because it is going
|
||||
// to fire very often while typing, and needs to very efficient.
|
||||
function adjust() {
|
||||
var height, overflow, original;
|
||||
|
||||
// the active flag keeps IE from tripping all over itself. Otherwise
|
||||
// actions in the adjust function will cause IE to call adjust again.
|
||||
if (!active) {
|
||||
active = true;
|
||||
mirror.value = ta.value + options.append;
|
||||
mirror.style.overflowY = ta.style.overflowY;
|
||||
original = parseInt(ta.style.height,10);
|
||||
|
||||
// Update the width in case the original textarea width has changed
|
||||
mirror.style.width = $ta.css('width');
|
||||
|
||||
// Needed for IE to reliably return the correct scrollHeight
|
||||
mirror.scrollTop = 0;
|
||||
|
||||
// Set a very high value for scrollTop to be sure the
|
||||
// mirror is scrolled all the way to the bottom.
|
||||
mirror.scrollTop = 9e4;
|
||||
|
||||
height = mirror.scrollTop;
|
||||
overflow = hidden;
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight;
|
||||
overflow = 'scroll';
|
||||
} else if (height < minHeight) {
|
||||
height = minHeight;
|
||||
}
|
||||
height += boxOffset;
|
||||
ta.style.overflowY = overflow;
|
||||
|
||||
if (original !== height) {
|
||||
ta.style.height = height + 'px';
|
||||
if (callback) {
|
||||
options.callback.call(ta);
|
||||
}
|
||||
}
|
||||
|
||||
// This small timeout gives IE a chance to draw it's scrollbar
|
||||
// before adjust can be run again (prevents an infinite loop).
|
||||
setTimeout(function () {
|
||||
active = false;
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// mirror is a duplicate textarea located off-screen that
|
||||
// is automatically updated to contain the same text as the
|
||||
// original textarea. mirror always has a height of 0.
|
||||
// This gives a cross-browser supported way getting the actual
|
||||
// height of the text, through the scrollTop property.
|
||||
while (i--) {
|
||||
mirror.style[copyStyle[i]] = $ta.css(copyStyle[i]);
|
||||
}
|
||||
|
||||
$('body').append(mirror);
|
||||
|
||||
if (onpropertychange in ta) {
|
||||
if (oninput in ta) {
|
||||
// Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
|
||||
// so binding to onkeyup to catch most of those occassions. There is no way that I
|
||||
// know of to detect something like 'cut' in IE9.
|
||||
ta[oninput] = ta.onkeyup = adjust;
|
||||
} else {
|
||||
// IE7 / IE8
|
||||
ta[onpropertychange] = adjust;
|
||||
}
|
||||
} else {
|
||||
// Modern Browsers
|
||||
ta[oninput] = adjust;
|
||||
|
||||
// The textarea overflow is now hidden. But Chrome doesn't reflow the text after the scrollbars are removed.
|
||||
// This is a hack to get Chrome to reflow it's text.
|
||||
ta.value = '';
|
||||
ta.value = value;
|
||||
}
|
||||
|
||||
$(window).resize(adjust);
|
||||
|
||||
// Allow for manual triggering if needed.
|
||||
$ta.on('autosize', adjust);
|
||||
|
||||
// Call adjust in case the textarea already contains text.
|
||||
adjust();
|
||||
});
|
||||
};
|
||||
} else {
|
||||
// Makes no changes for older browsers (FireFox3- and Safari4-)
|
||||
$.fn.autosize = function (callback) {
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
}(jQuery));
|
|
@ -1,61 +0,0 @@
|
|||
/*! JSON.minify()
|
||||
v0.1 (c) Kyle Simpson
|
||||
MIT License
|
||||
*/
|
||||
|
||||
(function(global){
|
||||
if (typeof global.JSON == "undefined" || !global.JSON) {
|
||||
global.JSON = {};
|
||||
}
|
||||
|
||||
global.JSON.minify = function(json) {
|
||||
|
||||
var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
|
||||
in_string = false,
|
||||
in_multiline_comment = false,
|
||||
in_singleline_comment = false,
|
||||
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
|
||||
;
|
||||
|
||||
tokenizer.lastIndex = 0;
|
||||
|
||||
while (tmp = tokenizer.exec(json)) {
|
||||
lc = RegExp.leftContext;
|
||||
rc = RegExp.rightContext;
|
||||
if (!in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.substring(from);
|
||||
if (!in_string) {
|
||||
tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
|
||||
}
|
||||
new_str[ns++] = tmp2;
|
||||
}
|
||||
from = tokenizer.lastIndex;
|
||||
|
||||
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.match(/(\\)*$/);
|
||||
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string
|
||||
in_string = !in_string;
|
||||
}
|
||||
from--; // include " character in next catch
|
||||
rc = json.substring(from);
|
||||
}
|
||||
else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = true;
|
||||
}
|
||||
else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = false;
|
||||
}
|
||||
else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_singleline_comment = true;
|
||||
}
|
||||
else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
|
||||
in_singleline_comment = false;
|
||||
}
|
||||
else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
|
||||
new_str[ns++] = tmp[0];
|
||||
}
|
||||
}
|
||||
new_str[ns++] = rc;
|
||||
return new_str.join("");
|
||||
};
|
||||
})(this);
|
|
@ -1,273 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/* global socketio */
|
||||
|
||||
$(document).ready(() => {
|
||||
const socket = socketio.connect('..', '/pluginfw/installer');
|
||||
socket.on('disconnect', (reason) => {
|
||||
// The socket.io client will automatically try to reconnect for all reasons other than "io
|
||||
// server disconnect".
|
||||
if (reason === 'io server disconnect') socket.connect();
|
||||
});
|
||||
|
||||
const search = (searchTerm, limit) => {
|
||||
if (search.searchTerm !== searchTerm) {
|
||||
search.offset = 0;
|
||||
search.results = [];
|
||||
search.end = false;
|
||||
}
|
||||
limit = limit ? limit : search.limit;
|
||||
search.searchTerm = searchTerm;
|
||||
socket.emit('search', {
|
||||
searchTerm,
|
||||
offset: search.offset,
|
||||
limit,
|
||||
sortBy: search.sortBy,
|
||||
sortDir: search.sortDir,
|
||||
});
|
||||
search.offset += limit;
|
||||
|
||||
$('#search-progress').show();
|
||||
search.messages.show('fetching');
|
||||
search.searching = true;
|
||||
};
|
||||
search.searching = false;
|
||||
search.offset = 0;
|
||||
search.limit = 999;
|
||||
search.results = [];
|
||||
search.sortBy = 'name';
|
||||
search.sortDir = /* DESC?*/true;
|
||||
search.end = true;// have we received all results already?
|
||||
search.messages = {
|
||||
show: (msg) => {
|
||||
// $('.search-results .messages').show()
|
||||
$(`.search-results .messages .${msg}`).show();
|
||||
$(`.search-results .messages .${msg} *`).show();
|
||||
},
|
||||
hide: (msg) => {
|
||||
$('.search-results .messages').hide();
|
||||
$(`.search-results .messages .${msg}`).hide();
|
||||
$(`.search-results .messages .${msg} *`).hide();
|
||||
},
|
||||
};
|
||||
|
||||
const installed = {
|
||||
progress: {
|
||||
show: (plugin, msg) => {
|
||||
$(`.installed-results .${plugin} .progress`).show();
|
||||
$(`.installed-results .${plugin} .progress .message`).text(msg);
|
||||
if ($(window).scrollTop() > $(`.${plugin}`).offset().top) {
|
||||
$(window).scrollTop($(`.${plugin}`).offset().top - 100);
|
||||
}
|
||||
},
|
||||
hide: (plugin) => {
|
||||
$(`.installed-results .${plugin} .progress`).hide();
|
||||
$(`.installed-results .${plugin} .progress .message`).text('');
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
show: (msg) => {
|
||||
$('.installed-results .messages').show();
|
||||
$(`.installed-results .messages .${msg}`).show();
|
||||
},
|
||||
hide: (msg) => {
|
||||
$('.installed-results .messages').hide();
|
||||
$(`.installed-results .messages .${msg}`).hide();
|
||||
},
|
||||
},
|
||||
list: [],
|
||||
};
|
||||
|
||||
const displayPluginList = (plugins, container, template) => {
|
||||
plugins.forEach((plugin) => {
|
||||
const row = template.clone();
|
||||
|
||||
for (const attr in plugin) {
|
||||
if (attr === 'name') { // Hack to rewrite URLS into name
|
||||
const link = $('<a>')
|
||||
.attr('href', `https://npmjs.org/package/${plugin.name}`)
|
||||
.attr('plugin', 'Plugin details')
|
||||
.attr('rel', 'noopener noreferrer')
|
||||
.attr('target', '_blank')
|
||||
.text(plugin.name.substr(3));
|
||||
row.find('.name').append(link);
|
||||
} else {
|
||||
row.find(`.${attr}`).text(plugin[attr]);
|
||||
}
|
||||
}
|
||||
row.find('.version').text(plugin.version);
|
||||
row.addClass(plugin.name);
|
||||
row.data('plugin', plugin.name);
|
||||
container.append(row);
|
||||
});
|
||||
updateHandlers();
|
||||
};
|
||||
|
||||
const sortPluginList = (plugins, property, /* ASC?*/dir) => plugins.sort((a, b) => {
|
||||
if (a[property] < b[property]) return dir ? -1 : 1;
|
||||
if (a[property] > b[property]) return dir ? 1 : -1;
|
||||
// a must be equal to b
|
||||
return 0;
|
||||
});
|
||||
|
||||
const updateHandlers = () => {
|
||||
// Search
|
||||
$('#search-query').off('keyup').on('keyup', () => {
|
||||
search($('#search-query').val());
|
||||
});
|
||||
|
||||
// Prevent form submit
|
||||
$('#search-query').parent().on('submit', () => false);
|
||||
|
||||
// update & install
|
||||
$('.do-install, .do-update').off('click').on('click', function (e) {
|
||||
const $row = $(e.target).closest('tr');
|
||||
const plugin = $row.data('plugin');
|
||||
if ($(this).hasClass('do-install')) {
|
||||
$row.remove().appendTo('#installed-plugins');
|
||||
installed.progress.show(plugin, 'Installing');
|
||||
} else {
|
||||
installed.progress.show(plugin, 'Updating');
|
||||
}
|
||||
socket.emit('install', plugin);
|
||||
installed.messages.hide('nothing-installed');
|
||||
});
|
||||
|
||||
// uninstall
|
||||
$('.do-uninstall').off('click').on('click', (e) => {
|
||||
const $row = $(e.target).closest('tr');
|
||||
const pluginName = $row.data('plugin');
|
||||
socket.emit('uninstall', pluginName);
|
||||
installed.progress.show(pluginName, 'Uninstalling');
|
||||
installed.list = installed.list.filter((plugin) => plugin.name !== pluginName);
|
||||
});
|
||||
|
||||
// Sort
|
||||
$('.sort.up').off('click').on('click', function () {
|
||||
search.sortBy = $(this).attr('data-label').toLowerCase();
|
||||
search.sortDir = false;
|
||||
search.offset = 0;
|
||||
search(search.searchTerm, search.results.length);
|
||||
search.results = [];
|
||||
});
|
||||
$('.sort.down, .sort.none').off('click').on('click', function () {
|
||||
search.sortBy = $(this).attr('data-label').toLowerCase();
|
||||
search.sortDir = true;
|
||||
search.offset = 0;
|
||||
search(search.searchTerm, search.results.length);
|
||||
search.results = [];
|
||||
});
|
||||
};
|
||||
|
||||
socket.on('results:search', (data) => {
|
||||
if (!data.results.length) search.end = true;
|
||||
if (data.query.offset === 0) search.results = [];
|
||||
search.messages.hide('nothing-found');
|
||||
search.messages.hide('fetching');
|
||||
$('#search-query').prop('disabled', false);
|
||||
|
||||
console.log('got search results', data);
|
||||
|
||||
// add to results
|
||||
search.results = search.results.concat(data.results);
|
||||
|
||||
// Update sorting head
|
||||
$('.sort')
|
||||
.removeClass('up down')
|
||||
.addClass('none');
|
||||
$(`.search-results thead th[data-label=${data.query.sortBy}]`)
|
||||
.removeClass('none')
|
||||
.addClass(data.query.sortDir ? 'up' : 'down');
|
||||
|
||||
// re-render search results
|
||||
const searchWidget = $('.search-results');
|
||||
searchWidget.find('.results *').remove();
|
||||
if (search.results.length > 0) {
|
||||
displayPluginList(
|
||||
search.results, searchWidget.find('.results'), searchWidget.find('.template tr'));
|
||||
} else {
|
||||
search.messages.show('nothing-found');
|
||||
}
|
||||
search.messages.hide('fetching');
|
||||
$('#search-progress').hide();
|
||||
search.searching = false;
|
||||
});
|
||||
|
||||
socket.on('results:installed', (data) => {
|
||||
installed.messages.hide('fetching');
|
||||
installed.messages.hide('nothing-installed');
|
||||
|
||||
installed.list = data.installed;
|
||||
sortPluginList(installed.list, 'name', /* ASC?*/true);
|
||||
|
||||
// filter out epl
|
||||
installed.list = installed.list.filter((plugin) => plugin.name !== 'ep_etherpad-lite');
|
||||
|
||||
// remove all installed plugins (leave plugins that are still being installed)
|
||||
installed.list.forEach((plugin) => {
|
||||
$(`#installed-plugins .${plugin.name}`).remove();
|
||||
});
|
||||
|
||||
if (installed.list.length > 0) {
|
||||
displayPluginList(installed.list, $('#installed-plugins'), $('#installed-plugin-template'));
|
||||
socket.emit('checkUpdates');
|
||||
} else {
|
||||
installed.messages.show('nothing-installed');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('results:updatable', (data) => {
|
||||
data.updatable.forEach((pluginName) => {
|
||||
const actions = $(`#installed-plugins > tr.${pluginName} .actions`);
|
||||
actions.find('.do-update').remove();
|
||||
actions.append(
|
||||
$('<input>').addClass('do-update').attr('type', 'button').attr('value', 'Update'));
|
||||
});
|
||||
updateHandlers();
|
||||
});
|
||||
|
||||
socket.on('finished:install', (data) => {
|
||||
if (data.error) {
|
||||
if (data.code === 'EPEERINVALID') {
|
||||
alert("This plugin requires that you update Etherpad so it can operate in it's true glory");
|
||||
}
|
||||
alert(`An error occurred while installing ${data.plugin} \n${data.error}`);
|
||||
$(`#installed-plugins .${data.plugin}`).remove();
|
||||
}
|
||||
|
||||
socket.emit('getInstalled');
|
||||
|
||||
// update search results
|
||||
search.offset = 0;
|
||||
search(search.searchTerm, search.results.length);
|
||||
search.results = [];
|
||||
});
|
||||
|
||||
socket.on('finished:uninstall', (data) => {
|
||||
if (data.error) {
|
||||
alert(`An error occurred while uninstalling the ${data.plugin} \n${data.error}`);
|
||||
}
|
||||
|
||||
// remove plugin from installed list
|
||||
$(`#installed-plugins .${data.plugin}`).remove();
|
||||
|
||||
socket.emit('getInstalled');
|
||||
|
||||
// update search results
|
||||
search.offset = 0;
|
||||
search(search.searchTerm, search.results.length);
|
||||
search.results = [];
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
updateHandlers();
|
||||
socket.emit('getInstalled');
|
||||
search.searchTerm = null;
|
||||
search($('#search-query').val());
|
||||
});
|
||||
|
||||
// check for updates every 5mins
|
||||
setInterval(() => {
|
||||
socket.emit('checkUpdates');
|
||||
}, 1000 * 60 * 5);
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
$(document).ready(() => {
|
||||
const socket = window.socketio.connect('..', '/settings');
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('load');
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
// The socket.io client will automatically try to reconnect for all reasons other than "io
|
||||
// server disconnect".
|
||||
if (reason === 'io server disconnect') socket.connect();
|
||||
});
|
||||
|
||||
socket.on('settings', (settings) => {
|
||||
/* Check whether the settings.json is authorized to be viewed */
|
||||
if (settings.results === 'NOT_ALLOWED') {
|
||||
$('.innerwrapper').hide();
|
||||
$('.innerwrapper-err').show();
|
||||
$('.err-message').html('Settings json is not authorized to be viewed in Admin page!!');
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check to make sure the JSON is clean before proceeding */
|
||||
if (isJSONClean(settings.results)) {
|
||||
$('.settings').append(settings.results);
|
||||
$('.settings').trigger('focus');
|
||||
$('.settings').autosize();
|
||||
} else {
|
||||
alert('Invalid JSON');
|
||||
}
|
||||
});
|
||||
|
||||
/* When the admin clicks save Settings check the JSON then send the JSON back to the server */
|
||||
$('#saveSettings').on('click', () => {
|
||||
const editedSettings = $('.settings').val();
|
||||
if (isJSONClean(editedSettings)) {
|
||||
// JSON is clean so emit it to the server
|
||||
socket.emit('saveSettings', $('.settings').val());
|
||||
} else {
|
||||
alert('Invalid JSON');
|
||||
$('.settings').trigger('focus');
|
||||
}
|
||||
});
|
||||
|
||||
/* Tell Etherpad Server to restart */
|
||||
$('#restartEtherpad').on('click', () => {
|
||||
socket.emit('restartServer');
|
||||
});
|
||||
|
||||
socket.on('saveprogress', (progress) => {
|
||||
$('#response').show();
|
||||
$('#response').text(progress);
|
||||
$('#response').fadeOut('slow');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const isJSONClean = (data) => {
|
||||
let cleanSettings = JSON.minify(data);
|
||||
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
|
||||
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
|
||||
try {
|
||||
return typeof JSON.parse(cleanSettings) === 'object';
|
||||
} catch (e) {
|
||||
return false; // the JSON failed to be parsed
|
||||
}
|
||||
};
|
|
@ -9,7 +9,7 @@ const tsort = require('./tsort');
|
|||
const pluginUtils = require('./shared');
|
||||
const defs = require('./plugin_defs');
|
||||
const {manager} = require('./installer');
|
||||
const settings = require("../../../node/utils/Settings");
|
||||
const settings = require('../../../node/utils/Settings');
|
||||
|
||||
const logger = log4js.getLogger('plugins');
|
||||
|
||||
|
@ -28,10 +28,13 @@ exports.prefix = 'ep_';
|
|||
|
||||
exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
|
||||
|
||||
exports.getPlugins = () => Object.keys(defs.plugins);
|
||||
|
||||
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
|
||||
|
||||
exports.formatHooks = (hookSetName, html) => {
|
||||
let hooks = new Map();
|
||||
exports.getParts = () => defs.parts.map((part) => part.full_name);
|
||||
|
||||
const sortHooks = (hookSetName, hooks) => {
|
||||
for (const [pluginName, def] of Object.entries(defs.plugins)) {
|
||||
for (const part of def.parts) {
|
||||
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||
|
@ -49,6 +52,18 @@ exports.formatHooks = (hookSetName, html) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports.getHooks = (hookSetName) => {
|
||||
const hooks = new Map();
|
||||
sortHooks(hookSetName, hooks);
|
||||
return hooks;
|
||||
};
|
||||
|
||||
exports.formatHooks = (hookSetName, html) => {
|
||||
let hooks = new Map();
|
||||
sortHooks(hookSetName, hooks);
|
||||
const lines = [];
|
||||
const sortStringKeys = (a, b) => String(a[0]).localeCompare(b[0]);
|
||||
if (html) lines.push('<dl>');
|
||||
|
@ -107,8 +122,8 @@ exports.update = async () => {
|
|||
};
|
||||
|
||||
exports.getPackages = async () => {
|
||||
let plugins = manager.list()
|
||||
let newDependencies = {}
|
||||
const plugins = manager.list();
|
||||
const newDependencies = {};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (!plugin.name.startsWith(exports.prefix)) {
|
||||
|
@ -116,7 +131,7 @@ exports.getPackages = async () => {
|
|||
}
|
||||
plugin.realPath = await fs.realpath(plugin.location);
|
||||
plugin.path = plugin.realPath;
|
||||
newDependencies[plugin.name] = plugin
|
||||
newDependencies[plugin.name] = plugin;
|
||||
}
|
||||
|
||||
newDependencies['ep_etherpad-lite'] = {
|
||||
|
@ -124,7 +139,7 @@ exports.getPackages = async () => {
|
|||
version: settings.getEpVersion(),
|
||||
path: path.join(settings.root, 'node_modules/ep_etherpad-lite'),
|
||||
realPath: path.join(settings.root, 'src'),
|
||||
}
|
||||
};
|
||||
|
||||
return newDependencies;
|
||||
};
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title data-l10n-id="admin.page-title">Admin Dashboard - Etherpad</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="../static/css/admin.css">
|
||||
<script src="../static/js/vendors/jquery.js"></script>
|
||||
<script src="../socket.io/socket.io.js"></script>
|
||||
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
|
||||
<script src="../static/js/vendors/html10n.js"></script>
|
||||
<script src="../static/js/l10n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<div class="menu">
|
||||
<h1><a href="../">Etherpad</a></h1>
|
||||
<ul>
|
||||
<% e.begin_block("adminMenu"); %>
|
||||
<li><a href="plugins" data-l10n-id="admin_plugins">Plugin manager</a></li>
|
||||
<li><a href="settings" data-l10n-id="admin_settings">Settings</a></li>
|
||||
<li><a href="plugins/info" data-l10n-id="admin_plugins_info">Troubleshooting information</a></li>
|
||||
<% e.end_block(); %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,47 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title data-l10n-id="admin_plugins_info.page-title">Plugin information - Etherpad</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="../../static/css/admin.css">
|
||||
<link rel="localizations" type="application/l10n+json" href="../../locales.json" />
|
||||
<script src="../../static/js/vendors/html10n.js"></script>
|
||||
<script src="../../static/js/l10n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<div class="menu">
|
||||
<h1><a href="../../">Etherpad</a></h1>
|
||||
<ul>
|
||||
<% e.begin_block("adminMenu"); %>
|
||||
<li><a href="../plugins" data-l10n-id="admin_plugins">Plugin manager</a></li>
|
||||
<li><a href="../settings" data-l10n-id="admin_settings">Settings</a></li>
|
||||
<li><a href="../plugins/info" data-l10n-id="admin_plugins_info">Troubleshooting information</a></li>
|
||||
<% e.end_block(); %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="innerwrapper">
|
||||
<h2 data-l10n-id="admin_plugins_info.version">Etherpad version</h2>
|
||||
<p><span data-l10n-id="admin_plugins_info.version_number">Version number</span>: <%= epVersion %></p>
|
||||
<p><span data-l10n-id="admin_plugins_info.version_latest">Latest available version</span>: <%= latestVersion %></p>
|
||||
<p>Git sha: <a href='https://github.com/ether/etherpad-lite/commit/<%= gitCommit %>'><%= gitCommit %></a></p>
|
||||
|
||||
<h2 data-l10n-id="admin_plugins_info.plugins">Installed plugins</h2>
|
||||
<%- installedPlugins %>
|
||||
|
||||
<h2 data-l10n-id="admin_plugins_info.parts">Installed parts</h2>
|
||||
<%- installedParts %>
|
||||
|
||||
<h2 data-l10n-id="admin_plugins_info.hooks">Installed hooks</h2>
|
||||
<h3 data-l10n-id="admin_plugins_info.hooks_server">Server-side hooks</h3>
|
||||
<%- installedServerHooks %>
|
||||
|
||||
<h3 data-l10n-id="admin_plugins_info.hooks_client">Client-side hooks</h3>
|
||||
<%- installedClientHooks %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,121 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title data-l10n-id="admin_plugins.page-title">Plugin manager - Etherpad</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="../static/css/admin.css">
|
||||
<script src="../static/js/vendors/jquery.js"></script>
|
||||
<script src="../socket.io/socket.io.js"></script>
|
||||
<script src="../static/js/socketio.js"></script>
|
||||
<script src="../static/js/admin/plugins.js"></script>
|
||||
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
|
||||
<script src="../static/js/vendors/html10n.js"></script>
|
||||
<script src="../static/js/l10n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
|
||||
<% if (errors.length) { %>
|
||||
<div class="errors">
|
||||
<% errors.forEach(function (item) { %>
|
||||
<div class="error"><%= item.toString() %></div>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="menu">
|
||||
<h1><a href="../">Etherpad</a></h1>
|
||||
<ul>
|
||||
<% e.begin_block("adminMenu"); %>
|
||||
<li><a href="plugins" data-l10n-id="admin_plugins">Plugin manager</a></li>
|
||||
<li><a href="settings" data-l10n-id="admin_settings">Settings</a></li>
|
||||
<li><a href="plugins/info" data-l10n-id="admin_plugins_info">Troubleshooting information</a></li>
|
||||
<% e.end_block(); %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="innerwrapper">
|
||||
<h2 data-l10n-id="admin_plugins.installed">Installed plugins</h2>
|
||||
<table class="installed-results">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-l10n-id="admin_plugins.name">Name</th>
|
||||
<th data-l10n-id="admin_plugins.description">Description</th>
|
||||
<th data-l10n-id="admin_plugins.version">Version</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="template">
|
||||
<tr id="installed-plugin-template">
|
||||
<td class="name" data-label="Name"></td>
|
||||
<td class="description" data-label="Description"></td>
|
||||
<td class="version" data-label="Version"></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<input type="button" value="Uninstall" class="do-uninstall" data-l10n-id="admin_plugins.installed_uninstall.value">
|
||||
<div class="progress"><p class="loadingAnimation"></p><p><span class="message"></span></p></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody id="installed-plugins">
|
||||
</tbody>
|
||||
<tbody class="messages">
|
||||
<tr><td></td><td>
|
||||
<p class="nothing-installed" data-l10n-id="admin_plugins.installed_nothing">You haven't installed any plugins yet.</p>
|
||||
<p class="fetching"><p class="loadingAnimation"></p><br/><span data-l10n-id="admin_plugins.installed_fetching">Fetching installed plugins…</span></p>
|
||||
</td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="paged listing search-results">
|
||||
<div class="separator"></div>
|
||||
|
||||
<h2 data-l10n-id="admin_plugins.available">Available plugins</h2>
|
||||
<form>
|
||||
<input type="text" name="search" disabled placeholder="Search for plugins to install" id="search-query" data-l10n-id="admin_plugins.available_search.placeholder">
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sort up" data-label="name" data-l10n-id="admin_plugins.name">Name</th>
|
||||
<th class="sort none" data-label="description" data-l10n-id="admin_plugins.description">Description</th>
|
||||
<th class="sort none" data-label="version" data-l10n-id="admin_plugins.version">Version</th>
|
||||
<th class="sort none" data-label="time" data-l10n-id="admin_plugins.last-update">Last update</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="template">
|
||||
<tr>
|
||||
<td class="name" data-label="Name"></td>
|
||||
<td class="description" data-label="Description"></td>
|
||||
<td class="version" data-label="Version"></td>
|
||||
<td class="time" data-label="Time"></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<input type="button" value="Install" class="do-install" data-l10n-id="admin_plugins.available_install.value">
|
||||
<div class="progress"><p><p class="loadingAnimation"></p></p><p><span class="message"></span></p></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody class="results">
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr><td></td><td>
|
||||
<div class="messages">
|
||||
<div id="search-progress" class="progress"><p> </p></div>
|
||||
<p class="nothing-found" data-l10n-id="admin_plugins.available_not-found">No plugins found.</p>
|
||||
<p class="fetching"><p class="loadingAnimation"></p><br/><span data-l10n-id="admin_plugins.available_fetching">Fetching…</span></p>
|
||||
</div>
|
||||
</td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,58 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title data-l10n-id="admin_settings.page-title">Settings - Etherpad</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="../static/css/admin.css">
|
||||
<script src="../static/js/vendors/jquery.js"></script>
|
||||
<script src="../socket.io/socket.io.js"></script>
|
||||
<script src="../static/js/socketio.js"></script>
|
||||
<script src="../static/js/admin/minify.json.js"></script>
|
||||
<script src="../static/js/admin/settings.js"></script>
|
||||
<script src="../static/js/admin/jquery.autosize.js"></script>
|
||||
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
|
||||
<script src="../static/js/vendors/html10n.js"></script>
|
||||
<script src="../static/js/l10n.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
|
||||
<% if (errors.length) { %>
|
||||
<div class="errors">
|
||||
<% errors.forEach(function (item) { %>
|
||||
<div class="error"><%= item.toString() %></div>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
|
||||
<div class="menu">
|
||||
<h1><a href="../">Etherpad</a></h1>
|
||||
<ul>
|
||||
<% e.begin_block("adminMenu"); %>
|
||||
<li><a href="plugins" data-l10n-id="admin_plugins">Plugin manager</a></li>
|
||||
<li><a href="settings" data-l10n-id="admin_settings">Settings</a></li>
|
||||
<li><a href="plugins/info" data-l10n-id="admin_plugins_info">Troubleshooting information</a></li>
|
||||
<% e.end_block(); %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="innerwrapper">
|
||||
<h2 data-l10n-id="admin_settings.current">Current configuration</h2>
|
||||
<textarea class="settings"></textarea>
|
||||
<input type="button" class="settingsButton" id="saveSettings" value="Save Settings" data-l10n-id="admin_settings.current_save.value">
|
||||
<input type="button" class="settingsButton" id="restartEtherpad" value="Restart Etherpad" data-l10n-id="admin_settings.current_restart.value">
|
||||
<div id="response"></div>
|
||||
<div class="separator"></div>
|
||||
<a href='https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON' data-l10n-id="admin_settings.current_example-prod">Example production settings template</a>
|
||||
<a href='https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON' data-l10n-id="admin_settings.current_example-devel">Example development settings template</a>
|
||||
</div>
|
||||
|
||||
<div class="innerwrapper-err" >
|
||||
<h2 class="err-message"></h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
||||
</body>
|
||||
</html>
|
|
@ -34,24 +34,16 @@
|
|||
<td><a href="/static/js/require-kernel.js">require-kernel.js</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/static/js/admin/plugins.js">plugins.js</a></td>
|
||||
<td><a href="http://www.apache.org/licenses/LICENSE-2.0">Apache-2.0-only</a></td>
|
||||
<td><a href="/static/js/admin/plugins.js">plugins.js</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/static/js/admin/minify.json.js">minify.json.js</a></td>
|
||||
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
|
||||
<td><a href="/static/js/admin/minify.json.js">minify.json.js</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/static/js/admin/settings.js">settings.js</a></td>
|
||||
<td><a href="http://www.apache.org/licenses/LICENSE-2.0">Apache-2.0-only</a></td>
|
||||
<td><a href="/static/js/admin/settings.js">settings.js</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/static/js/admin/jquery.autosize.js">jquery.autosize.js</a></td>
|
||||
<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a></td>
|
||||
<td><a href="/static/js/admin/jquery.autosize.js">jquery.autosize.js</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
|
|
@ -54,10 +54,10 @@ describe(__filename, function () {
|
|||
await agent.get('/').expect(200);
|
||||
});
|
||||
|
||||
it('!authn !authz anonymous /admin/ -> 401', async function () {
|
||||
it('!authn !authz anonymous /admin-auth// -> 401', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').expect(401);
|
||||
await agent.get('/admin-auth/').expect(401);
|
||||
});
|
||||
|
||||
it('authn !authz anonymous / -> 401', async function () {
|
||||
|
@ -72,10 +72,10 @@ describe(__filename, function () {
|
|||
await agent.get('/').auth('user', 'user-password').expect(200);
|
||||
});
|
||||
|
||||
it('authn !authz user /admin/ -> 403', async function () {
|
||||
it('authn !authz user //admin-auth// -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||
await agent.get('/admin-auth//').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
|
||||
it('authn !authz admin / -> 200', async function () {
|
||||
|
@ -84,10 +84,10 @@ describe(__filename, function () {
|
|||
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
|
||||
it('authn !authz admin /admin/ -> 200', async function () {
|
||||
it('authn !authz admin /admin-auth/ -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
|
||||
it('authn authz anonymous /robots.txt -> 200', async function () {
|
||||
|
@ -102,10 +102,10 @@ describe(__filename, function () {
|
|||
await agent.get('/').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
|
||||
it('authn authz user /admin/ -> 403', async function () {
|
||||
it('authn authz user //admin-auth// -> 403', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/admin/').auth('user', 'user-password').expect(403);
|
||||
await agent.get('/admin-auth//').auth('user', 'user-password').expect(403);
|
||||
});
|
||||
|
||||
it('authn authz admin / -> 200', async function () {
|
||||
|
@ -114,10 +114,10 @@ describe(__filename, function () {
|
|||
await agent.get('/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
|
||||
it('authn authz admin /admin/ -> 200', async function () {
|
||||
it('authn authz admin /admin-auth/ -> 200', async function () {
|
||||
settings.requireAuthentication = true;
|
||||
settings.requireAuthorization = true;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200);
|
||||
});
|
||||
|
||||
describe('login fails if password is nullish', function () {
|
||||
|
@ -130,7 +130,7 @@ describe(__filename, function () {
|
|||
it(`admin password: ${adminPassword} credentials: ${creds}`, async function () {
|
||||
settings.users.admin.password = adminPassword;
|
||||
const encCreds = Buffer.from(creds).toString('base64');
|
||||
await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401);
|
||||
await agent.get('/admin-auth/').set('Authorization', `Basic ${encCreds}`).expect(401);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -228,11 +228,11 @@ describe(__filename, function () {
|
|||
|
||||
it('cannot grant access to /admin', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [true];
|
||||
await agent.get('/admin/').expect(401);
|
||||
await agent.get('/admin-auth/').expect(401);
|
||||
// Notes:
|
||||
// * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because
|
||||
// 'true' entries are ignored for /admin/* requests.
|
||||
// * The authenticate hook always runs for /admin/* requests even if
|
||||
// 'true' entries are ignored for /admin-auth//* requests.
|
||||
// * The authenticate hook always runs for /admin-auth//* requests even if
|
||||
// settings.requireAuthentication is false.
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
|
@ -240,9 +240,9 @@ describe(__filename, function () {
|
|||
'authenticate_1']);
|
||||
});
|
||||
|
||||
it('can deny access to /admin', async function () {
|
||||
it('can deny access to /admin-auth/', async function () {
|
||||
handlers.preAuthorize[0].innerHandle = () => [false];
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(403);
|
||||
await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(403);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0']);
|
||||
});
|
||||
|
||||
|
@ -258,7 +258,7 @@ describe(__filename, function () {
|
|||
res.status(200).send('injected');
|
||||
return cb([true]);
|
||||
})];
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected');
|
||||
await agent.get('/admin-auth//').auth('admin', 'admin-password').expect(200, 'injected');
|
||||
assert(called);
|
||||
});
|
||||
|
||||
|
@ -274,15 +274,15 @@ describe(__filename, function () {
|
|||
settings.requireAuthorization = false;
|
||||
});
|
||||
|
||||
it('is not called if !requireAuthentication and not /admin/*', async function () {
|
||||
it('is not called if !requireAuthentication and not /admin-auth/*', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
await agent.get('/').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
|
||||
});
|
||||
|
||||
it('is called if !requireAuthentication and /admin/*', async function () {
|
||||
it('is called if !requireAuthentication and /admin-auth//*', async function () {
|
||||
settings.requireAuthentication = false;
|
||||
await agent.get('/admin/').expect(401);
|
||||
await agent.get('/admin-auth/').expect(401);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
|
@ -393,7 +393,7 @@ describe(__filename, function () {
|
|||
|
||||
it('is not called if !requireAuthorization (/admin)', async function () {
|
||||
settings.requireAuthorization = false;
|
||||
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
|
||||
await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200);
|
||||
assert.deepEqual(callOrder, ['preAuthorize_0',
|
||||
'preAuthorize_1',
|
||||
'authenticate_0',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue