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

@ -24,3 +24,4 @@ Dockerfile
settings.json
src/node_modules
admin/node_modules

View file

@ -54,6 +54,12 @@ jobs:
-
name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: Install admin ui
working-directory: admin
run: pnpm install
- name: Build admin ui
working-directory: admin
run: pnpm build
-
name: Run the backend tests
run: pnpm test
@ -105,6 +111,12 @@ jobs:
-
name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh
- name: Install admin ui
working-directory: admin
run: pnpm install
- name: Build admin ui
working-directory: admin
run: pnpm build
-
name: Install Etherpad plugins
run: >
@ -163,6 +175,12 @@ jobs:
-
name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installOnWindows.bat
- name: Install admin ui
working-directory: admin
run: pnpm install
- name: Build admin ui
working-directory: admin
run: pnpm build
-
name: Fix up the settings.json
run: |
@ -207,6 +225,12 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: Only install direct dependencies
run: pnpm config set auto-install-peers false
- name: Install admin ui
working-directory: admin
run: pnpm install
- name: Build admin ui
working-directory: admin
run: pnpm build
-
name: Install Etherpad plugins
# The --legacy-peer-deps flag is required to work around a bug in npm

View file

@ -12,7 +12,6 @@ jobs:
name: with plugins
runs-on: ubuntu-latest
# node: [16, 19, 20] >> Disabled node 16 and 18 because they do not work
strategy:
fail-fast: false
matrix:
@ -83,11 +82,11 @@ jobs:
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json"
-
name: increase maxHttpBufferSize
run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 100000/' settings.json"
run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 10000000/' settings.json"
-
name: Disable import/export rate limiting
run: |
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 1000000/' -i settings.json
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 100000000/' -i settings.json
-
name: Remove standard frontend test files, so only admin tests are run
run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs

1
.gitignore vendored
View file

@ -24,3 +24,4 @@ out/
/src/bin/node.exe
plugin_packages
pnpm-lock.yaml
/src/templates/admin

View file

@ -4,6 +4,13 @@
#
# Author: muxator
FROM node:alpine as adminBuild
WORKDIR /opt/etherpad-lite
COPY ./admin ./admin
RUN cd ./admin && npm install -g pnpm && pnpm install && pnpm run build --outDir ./dist
FROM node:alpine as build
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
@ -99,6 +106,7 @@ COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./
FROM build as development
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/pnpm-lock.yaml ./src/
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin
RUN bin/installDeps.sh && { [ -z "${ETHERPAD_PLUGINS}" ] || \
pnpm install --workspace-root ${ETHERPAD_PLUGINS}; }
@ -109,6 +117,7 @@ ENV NODE_ENV=production
ENV ETHERPAD_PRODUCTION=true
COPY --chown=etherpad:etherpad ./src ./src
COPY --chown=etherpad:etherpad --from=adminBuild /opt/etherpad-lite/admin/dist ./src/templates/admin
RUN bin/installDeps.sh && { [ -z "${ETHERPAD_PLUGINS}" ] || \
pnpm install --workspace-root ${ETHERPAD_PLUGINS}; } && \

View file

@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Etherpad Admin Dashboard</title>
<link rel="icon" href="/favicon.ico">
</head>
<body>
<div id="root"></div>

View file

View file

@ -2,26 +2,38 @@ import {useEffect} from 'react'
import './App.css'
import {connect} from 'socket.io-client'
import {isJSONClean} from './utils/utils.ts'
import {NavLink, Outlet} from "react-router-dom";
import {NavLink, Outlet, useNavigate} from "react-router-dom";
import {useStore} from "./store/store.ts";
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
import {ToastDialog} from "./utils/Toast.tsx";
import {Trans, useTranslation} from "react-i18next";
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
export const App = ()=> {
const setSettings = useStore(state => state.setSettings);
const {t} = useTranslation()
const navigate = useNavigate()
useEffect(() => {
fetch('/admin-auth/', {
method: 'POST'
}).then((value)=>{
if(!value.ok){
navigate('/login')
}
}).catch(()=>{
navigate('/login')
})
}, []);
useEffect(() => {
document.title = t('admin.page-title')
useStore.getState().setShowLoading(true);
const settingSocket = connect('http://localhost:9001/settings', {
const settingSocket = connect(`${WS_URL}/settings`, {
transports: ['websocket'],
});
const pluginsSocket = connect('http://localhost:9001/pluginfw/installer', {
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
transports: ['websocket'],
})
@ -74,7 +86,6 @@ export const App = ()=> {
return <div id="wrapper">
<LoadingScreen/>
<ToastDialog/>
<div className="menu">
<h1>Etherpad</h1>
<ul>

View file

@ -339,6 +339,25 @@ pre {
}
.dialog-confirm-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 100;
}
.dialog-confirm-content {
position: fixed;
top: 50%;
left: 50%;
background-color: white;
transform: translate(-50%, -50%);
padding: 20px;
z-index: 101;
}
.dialog-content {
position: fixed;
top: 50%;
@ -463,3 +482,8 @@ pre {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px
}
.search-field {
width: 50%;
padding: 5px;
}

View file

@ -28,17 +28,17 @@ const LazyImportPlugin: BackendModule = {
try {
json = JSON.parse(await localeJSON.text())
} catch(e) {
callback(true, null);
callback(new Error("Error loading"), null);
}
callback(null, json);
},
save: function (language, namespace, data) {
save: function () {
},
create: function (languages, namespace, key, fallbackValue) {
create: function () {
/* save the missing translation */
},
};

View file

@ -11,6 +11,7 @@ import * as Toast from '@radix-ui/react-toast'
import {I18nextProvider} from "react-i18next";
import i18n from "./localization/i18n.ts";
import {PadPage} from "./pages/PadPage.tsx";
import {ToastDialog} from "./utils/Toast.tsx";
const router = createBrowserRouter(createRoutesFromElements(
<><Route element={<App/>}>
@ -31,7 +32,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<I18nextProvider i18n={i18n}>
<Toast.Provider>
<RouterProvider router={router}/>
<ToastDialog/>
<RouterProvider router={router}/>
</Toast.Provider>
</I18nextProvider>
</React.StrictMode>,

View file

@ -10,7 +10,6 @@ export const HelpPage = () => {
useEffect(() => {
if(!settingsSocket) return;
settingsSocket?.on('reply:help', (data) => {
console.log(data)
setHelpData(data)
});
@ -19,11 +18,11 @@ export const HelpPage = () => {
const renderHooks = (hooks:Record<string, Record<string, string>>) => {
return Object.keys(hooks).map((hookName, i) => {
return <div key={i}>
return <div key={hookName+i}>
<h3>{hookName}</h3>
<ul>
{Object.keys(hooks[hookName]).map((hook, i) => <li>{hook}
<ul key={i}>
<ul key={hookName+hook+i}>
{Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)}
</ul>
</li>)}

View file

@ -2,7 +2,7 @@ import {useStore} from "../store/store.ts";
import {useEffect, useState} from "react";
import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts";
import {useDebounce} from "../utils/useDebounce.ts";
import {Trans} from "react-i18next";
import {Trans, useTranslation} from "react-i18next";
export const HomePage = () => {
@ -17,6 +17,7 @@ export const HomePage = () => {
searchTerm: ''
})
const [searchTerm, setSearchTerm] = useState<string>('')
const {t} = useTranslation()
useEffect(() => {
@ -30,8 +31,18 @@ export const HomePage = () => {
setInstalledPlugins(data.installed)
})
pluginsSocket.on('results:updatable', () => {
console.log("Finished install")
pluginsSocket.on('results:updatable', (data) => {
data.updatable.forEach((pluginName: string) => {
setInstalledPlugins(installedPlugins.map(plugin => {
if (plugin.name === pluginName) {
return {
...plugin,
updatable: true
}
}
return plugin
}))
})
})
pluginsSocket.on('finished:install', () => {
@ -118,19 +129,25 @@ export const HomePage = () => {
return <tr key={index}>
<td>{plugin.name}</td>
<td>{plugin.version}</td>
<td onClick={() => {
}}>
<button disabled={plugin.name == "ep_etherpad-lite"} onClick={() => uninstallPlugin(plugin.name)}><Trans i18nKey="admin_plugins.installed_uninstall.value"/></button>
<td>
{
plugin.updatable ?
<button onClick={() => installPlugin(plugin.name)}>Update</button>
: <button disabled={plugin.name == "ep_etherpad-lite"}
onClick={() => uninstallPlugin(plugin.name)}><Trans
i18nKey="admin_plugins.installed_uninstall.value"/></button>
}
</td>
</tr>
})}
</tbody>
</table>
</tr>
})}
</tbody>
</table>
<h2><Trans i18nKey="admin_plugins.available"/></h2>
<h2><Trans i18nKey="admin_plugins.available"/></h2>
<input type="text" value={searchTerm} onChange={v=>{
<input className="search-field" placeholder={t('admin_plugins.available_search.placeholder')} type="text" value={searchTerm} onChange={v=>{
setSearchTerm(v.target.value)
}}/>

View file

@ -1,17 +1,28 @@
import {useState} from "react";
import {useStore} from "../store/store.ts";
import {useNavigate} from "react-router-dom";
export const LoginScreen = ()=>{
const navigate = useNavigate()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const login = ()=>{
fetch('/api/auth', {
method: 'GET',
fetch('/admin-auth/', {
method: 'POST',
headers:{
Authorization: `Basic ${btoa(`${username}:${password}`)}`
}
}).then(r=>{
console.log(r.status)
if(!r.ok) {
useStore.getState().setToastState({
open: true,
title: "Login failed",
success: false
})
} else {
navigate('/')
}
}).catch(e=>{
console.error(e)
})

View file

@ -1,9 +1,10 @@
import {Trans} from "react-i18next";
import {Trans, useTranslation} from "react-i18next";
import {useEffect, useMemo, useState} from "react";
import {useStore} from "../store/store.ts";
import {PadSearchQuery, PadSearchResult} from "../utils/PadSearch.ts";
import {useDebounce} from "../utils/useDebounce.ts";
import {determineSorting} from "../utils/sorting.ts";
import * as Dialog from "@radix-ui/react-dialog";
export const PadPage = ()=>{
const settingsSocket = useStore(state=>state.settingsSocket)
@ -14,7 +15,7 @@ export const PadPage = ()=>{
sortBy: 'padName',
ascending: true
})
const {t} = useTranslation()
const [searchTerm, setSearchTerm] = useState<string>('')
const pads = useStore(state=>state.pads)
const pages = useMemo(()=>{
@ -24,7 +25,9 @@ export const PadPage = ()=>{
const totalPages = Math.ceil(pads!.total / searchParams.limit)
return Array.from({length: totalPages}, (_, i) => i+1)
},[pads])
},[pads, searchParams.limit])
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
const [padToDelete, setPadToDelete] = useState<string>('')
useDebounce(()=>{
setSearchParams({
@ -64,11 +67,39 @@ export const PadPage = ()=>{
})
}, [settingsSocket, pads]);
const deletePad = (padID: string)=>{
settingsSocket?.emit('deletePad', padID)
}
return <div>
<Dialog.Root open={deleteDialog}><Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay" />
<Dialog.Content className="dialog-confirm-content">
<div className="">
<div className=""></div>
<div className="">
{t("ep_admin_pads:ep_adminpads2_confirm", {
padID: padToDelete,
})}
</div>
<div className="settings-button-bar">
<button onClick={()=>{
setDeleteDialog(false)
}}>Cancel</button>
<button onClick={()=>{
deletePad(padToDelete)
setDeleteDialog(false)
}}>Ok</button>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<h1><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></h1>
<input type="text" value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder="Pads suchen"/>
<input type="text" value={searchTerm} onChange={v=>setSearchTerm(v.target.value)}
placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
<table>
<thead>
<tr>
@ -78,21 +109,21 @@ export const PadPage = ()=>{
sortBy: 'padName',
ascending: !searchParams.ascending
})
}}>PadId</th>
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></th>
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'lastEdited')} onClick={()=>{
setSearchParams({
...searchParams,
sortBy: 'lastEdited',
ascending: !searchParams.ascending
})
}}>Users</th>
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_pad-user-count"/></th>
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'userCount')} onClick={()=>{
setSearchParams({
...searchParams,
sortBy: 'userCount',
ascending: !searchParams.ascending
})
}}>Last Edited</th>
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_last-edited"/></th>
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'revisionNumber')} onClick={()=>{
setSearchParams({
...searchParams,
@ -100,7 +131,7 @@ export const PadPage = ()=>{
ascending: !searchParams.ascending
})
}}>Revision number</th>
<th>Actions</th>
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
</tr>
</thead>
<tbody>
@ -114,8 +145,9 @@ export const PadPage = ()=>{
<td>
<div className="settings-button-bar">
<button onClick={()=>{
settingsSocket?.emit('deletePad', pad.padName)
}}>delete</button>
setPadToDelete(pad.padName)
setDeleteDialog(true)
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_delete.value"/></button>
<button onClick={()=>{
window.open(`/p/${pad.padName}`, '_blank')
}}>view</button>

View file

@ -12,7 +12,7 @@ export type InstalledPlugin = {
path: string,
realPath: string,
version:string,
updatable: boolean
updatable?: boolean
}

View file

@ -1,6 +1,6 @@
import {create} from "zustand";
import {Socket} from "socket.io-client";
import {PadSearchResult, PadType} from "../utils/PadSearch.ts";
import {PadSearchResult} from "../utils/PadSearch.ts";
type ToastState = {
description?:string,

View file

@ -14,14 +14,17 @@ export default defineConfig({
]
})],
base: '/admin',
build:{
outDir: '../src/templates/admin'
},
server:{
proxy: {
'/socket.io/': {
'/socket.io/*': {
target: 'http://localhost:9001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
'/api/auth': {
'/admin-auth/': {
target: 'http://localhost:9001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/')

View file

@ -49,6 +49,13 @@ rm -rf src/node_modules || true
#log "do a normal unix install first..."
#$(try cd ./bin/installDeps.sh)
# Install admin frontend
cd admin
try pnpm install
try pnpm run build
cd ..
log "copy the windows settings template..."
try cp settings.json.template settings.json

View file

@ -1,3 +1,2 @@
packages:
- src
- admin

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