Fixed docker build.

This commit is contained in:
SamTV12345 2024-03-09 22:32:04 +01:00
parent 4d97c3c48f
commit fa5aed489f
36 changed files with 243 additions and 967 deletions

View file

@ -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',

View file

@ -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"
}
},

View file

@ -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();
};

View file

@ -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');

View file

@ -13,16 +13,6 @@ const UpdateCheck = require('../../utils/UpdateCheck');
const padManager = require('../../db/PadManager');
const api = require('../../db/API');
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: [],
}));
});
};
const queryPadLimit = 12;

View file

@ -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.

View file

@ -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));

View file

@ -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);

View file

@ -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);
});

View file

@ -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
}
};

View file

@ -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>

View file

@ -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>

View file

@ -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>&nbsp;</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>

View file

@ -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>

View file

@ -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>

View file

@ -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',