Merge branch 'develop'

This commit is contained in:
SamTV12345 2024-09-21 13:07:21 +02:00
commit 1c459b3e41
37 changed files with 3316 additions and 1349 deletions

View file

@ -1,3 +1,11 @@
# 2.2.5
### Notable enhancements and fixes
- Fixed timeslider not scrolling when the revision count is a multiple of 100
- Added new Restful API for version 2 of Etherpad. It is available at /api-docs
# 2.2.4
### Notable enhancements and fixes

View file

@ -1,7 +1,7 @@
{
"name": "admin",
"private": true,
"version": "2.2.4",
"version": "2.2.5",
"type": "module",
"scripts": {
"dev": "vite",
@ -16,25 +16,25 @@
"devDependencies": {
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-toast": "^1.2.1",
"@types/react": "^18.3.5",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.2.25",
"@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.9.1",
"eslint": "^9.10.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.11",
"i18next": "^23.14.0",
"eslint-plugin-react-refresh": "^0.4.12",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.439.0",
"lucide-react": "^0.441.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.1",
"react-router-dom": "^6.26.1",
"react-i18next": "^15.0.2",
"react-router-dom": "^6.26.2",
"socket.io-client": "^4.7.5",
"typescript": "^5.5.4",
"vite": "^5.4.3",
"typescript": "^5.6.2",
"vite": "^5.4.7",
"vite-plugin-static-copy": "^1.0.6",
"vite-plugin-svgr": "^4.2.0",
"zustand": "^4.5.5"

View file

@ -14,6 +14,7 @@
"ep_adminpads2_autoupdate.title": "Aktiviert oder deaktiviert automatische Aktualisierungen für die aktuelle Abfrage.",
"ep_adminpads2_confirm": "Willst du das Pad {{padID}} wirklich löschen?",
"ep_adminpads2_delete.value": "Löschen",
"ep_adminpads2_cleanup": "Historie aufräumen",
"ep_adminpads2_last-edited": "Zuletzt bearbeitet",
"ep_adminpads2_loading": "Lädt...",
"ep_adminpads2_manage-pads": "Pads verwalten",

View file

@ -4,6 +4,7 @@
"ep_adminpads2_autoupdate.title": "Enables or disables automatic updates for the current query.",
"ep_adminpads2_confirm": "Do you really want to delete the pad {{padID}}?",
"ep_adminpads2_delete.value": "Delete",
"ep_adminpads2_cleanup": "Cleanup revisions",
"ep_adminpads2_last-edited": "Last edited",
"ep_adminpads2_loading": "Loading…",
"ep_adminpads2_manage-pads": "Manage pads",

View file

@ -1,4 +1,4 @@
import {useEffect} from 'react'
import {useEffect, useState} from 'react'
import './App.css'
import {connect} from 'socket.io-client'
import {isJSONClean} from './utils/utils.ts'
@ -6,107 +6,113 @@ import {NavLink, Outlet, useNavigate} from "react-router-dom";
import {useStore} from "./store/store.ts";
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
import {Trans, useTranslation} from "react-i18next";
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall} from "lucide-react";
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall, LucideMenu} from "lucide-react";
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()
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()
const [sidebarOpen, setSidebarOpen] = useState<boolean>(true)
useEffect(() => {
fetch('/admin-auth/', {
method: 'POST'
}).then((value)=>{
if(!value.ok){
navigate('/login')
}
}).catch(()=>{
navigate('/login')
})
}, []);
useEffect(() => {
fetch('/admin-auth/', {
method: 'POST'
}).then((value) => {
if (!value.ok) {
navigate('/login')
}
}).catch(() => {
navigate('/login')
})
}, []);
useEffect(() => {
document.title = t('admin.page-title')
useEffect(() => {
document.title = t('admin.page-title')
useStore.getState().setShowLoading(true);
const settingSocket = connect(`${WS_URL}/settings`, {
transports: ['websocket'],
});
useStore.getState().setShowLoading(true);
const settingSocket = connect(`${WS_URL}/settings`, {
transports: ['websocket'],
});
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
transports: ['websocket'],
})
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
transports: ['websocket'],
})
pluginsSocket.on('connect', () => {
useStore.getState().setPluginsSocket(pluginsSocket);
});
pluginsSocket.on('connect', () => {
useStore.getState().setPluginsSocket(pluginsSocket);
});
settingSocket.on('connect', () => {
useStore.getState().setSettingsSocket(settingSocket);
useStore.getState().setShowLoading(false)
settingSocket.emit('load');
console.log('connected');
});
settingSocket.on('connect', () => {
useStore.getState().setSettingsSocket(settingSocket);
useStore.getState().setShowLoading(false)
settingSocket.emit('load');
console.log('connected');
});
settingSocket.on('disconnect', (reason) => {
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
useStore.getState().setShowLoading(true)
if (reason === 'io server disconnect') {
settingSocket.connect();
}
});
settingSocket.on('disconnect', (reason) => {
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
useStore.getState().setShowLoading(true)
if (reason === 'io server disconnect') {
settingSocket.connect();
}
});
settingSocket.on('settings', (settings) => {
/* Check whether the settings.json is authorized to be viewed */
if (settings.results === 'NOT_ALLOWED') {
console.log('Not allowed to view settings.json')
return;
}
settingSocket.on('settings', (settings) => {
/* Check whether the settings.json is authorized to be viewed */
if (settings.results === 'NOT_ALLOWED') {
console.log('Not allowed to view settings.json')
return;
}
/* Check to make sure the JSON is clean before proceeding */
if (isJSONClean(settings.results)) {
setSettings(settings.results);
} else {
alert('Invalid JSON');
}
useStore.getState().setShowLoading(false);
});
/* Check to make sure the JSON is clean before proceeding */
if (isJSONClean(settings.results)) {
setSettings(settings.results);
} else {
alert('Invalid JSON');
}
useStore.getState().setShowLoading(false);
});
settingSocket.on('saveprogress', (status)=>{
console.log(status)
})
settingSocket.on('saveprogress', (status) => {
console.log(status)
})
return () => {
settingSocket.disconnect();
pluginsSocket.disconnect()
}
}, []);
return () => {
settingSocket.disconnect();
pluginsSocket.disconnect()
}
}, []);
return <div id="wrapper">
<LoadingScreen/>
<div className="menu">
<div className="inner-menu">
<span>
return <div id="wrapper" className={`${sidebarOpen ? '': 'closed' }`}>
<LoadingScreen/>
<div className="menu">
<div className="inner-menu">
<span>
<Crown width={40} height={40}/>
<h1>Etherpad</h1>
</span>
<ul>
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
<li><NavLink to={"/pads"}><NotepadText/><Trans
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
</ul>
</div>
</div>
<div className="innerwrapper">
<Outlet/>
</div>
<ul onClick={()=>{
setSidebarOpen(false)
}}>
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
<li><NavLink to={"/pads"}><NotepadText/><Trans
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
</ul>
</div>
</div>
<button id="icon-button" onClick={() => {
setSidebarOpen(!sidebarOpen)
}}><LucideMenu/></button>
<div className="innerwrapper">
<Outlet/>
</div>
</div>
}
export default App

File diff suppressed because it is too large Load diff

View file

@ -193,6 +193,7 @@ export const HomePage = () => {
<h2><Trans i18nKey="admin_plugins.available"/></h2>
<SearchField onChange={v=>{setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/>
<div className="table-container">
<table id="available-plugins">
<thead>
<tr>
@ -240,5 +241,6 @@ export const HomePage = () => {
}
</tbody>
</table>
</div>
</div>
}

View file

@ -6,7 +6,7 @@ import {useDebounce} from "../utils/useDebounce.ts";
import {determineSorting} from "../utils/sorting.ts";
import * as Dialog from "@radix-ui/react-dialog";
import {IconButton} from "../components/IconButton.tsx";
import {ChevronLeft, ChevronRight, Eye, Trash2} from "lucide-react";
import {ChevronLeft, ChevronRight, Eye, Trash2, FileStack} from "lucide-react";
import {SearchField} from "../components/SearchField.tsx";
export const PadPage = ()=>{
@ -23,6 +23,7 @@ export const PadPage = ()=>{
const pads = useStore(state=>state.pads)
const [currentPage, setCurrentPage] = useState<number>(0)
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
const [errorText, setErrorText] = useState<string|null>(null)
const [padToDelete, setPadToDelete] = useState<string>('')
const pages = useMemo(()=>{
if(!pads){
@ -68,12 +69,35 @@ export const PadPage = ()=>{
results: newPads
})
})
settingsSocket.on('results:cleanupPadRevisions', (data)=>{
let newPads = useStore.getState().pads?.results ?? []
if (data.error) {
setErrorText(data.error)
return
}
newPads.forEach((pad)=>{
if (pad.padName === data.padId) {
pad.revisionNumber = data.keepRevisions
}
})
useStore.getState().setPads({
results: newPads,
total: useStore.getState().pads!.total
})
})
}, [settingsSocket, pads]);
const deletePad = (padID: string)=>{
settingsSocket?.emit('deletePad', padID)
}
const cleanupPad = (padID: string)=>{
settingsSocket?.emit('cleanupPadRevisions', padID)
}
return <div>
@ -100,6 +124,21 @@ export const PadPage = ()=>{
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<Dialog.Root open={errorText !== null}>
<Dialog.Portal>
<Dialog.Overlay className="dialog-confirm-overlay"/>
<Dialog.Content className="dialog-confirm-content">
<div>
<div>Error occured: {errorText}</div>
<div className="settings-button-bar">
<button onClick={() => {
setErrorText(null)
}}>OK</button>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<h1><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></h1>
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
<table>
@ -150,6 +189,9 @@ export const PadPage = ()=>{
setPadToDelete(pad.padName)
setDeleteDialog(true)
}}/>
<IconButton icon={<FileStack/>} title={<Trans i18nKey="ep_admin_pads:ep_adminpads2_cleanup"/>} onClick={()=>{
cleanupPad(pad.padName)
}}/>
<IconButton icon={<Eye/>} title="view" onClick={()=>window.open(`/p/${pad.padName}`, '_blank')}/>
</div>
</td>

View file

@ -57,7 +57,7 @@ createDirIfNotExists('../out/doc/api')
exec(`asciidoctor -D ../out/doc ../doc/index.adoc */**.adoc -a VERSION=${VERSION}`)
exec(`asciidoctor -D ../out/doc ../doc/index.adoc ../*/**.adoc -a VERSION=${VERSION}`)
exec(`asciidoctor -D ../out/doc/api ../doc/api/*.adoc -a VERSION=${VERSION}`)
copyFolderSync('../doc/public/', '../out/doc/')

View file

@ -1,6 +1,6 @@
{
"name": "bin",
"version": "2.2.4",
"version": "2.2.5",
"description": "",
"main": "checkAllPads.js",
"directories": {
@ -11,13 +11,13 @@
"ep_etherpad-lite": "workspace:../src",
"log4js": "^6.9.1",
"semver": "^7.6.3",
"tsx": "^4.19.0",
"ueberdb2": "^4.2.103"
"tsx": "^4.19.1",
"ueberdb2": "^5.0.2"
},
"devDependencies": {
"@types/node": "^22.5.4",
"@types/node": "^22.5.5",
"@types/semver": "^7.5.8",
"typescript": "^5.5.4"
"typescript": "^5.6.2"
},
"scripts": {
"makeDocs": "node --import tsx make_docs.ts",

View file

@ -32,6 +32,7 @@ export default defineConfig({
{ text: 'Stats', link: '/stats.md' },
{text: 'Skins', link: '/skins.md' },
{text: 'Demo', link: '/demo.md' },
{text: 'CLI', link: '/cli.md'},
]
},
{

View file

@ -58,7 +58,7 @@ services:
# ports:
# - "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/pgdata
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
postgres_data:

View file

@ -42,7 +42,7 @@ services:
# ports:
# - "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/pgdata
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:

View file

@ -50,6 +50,6 @@
"type": "git",
"url": "https://github.com/ether/etherpad-lite.git"
},
"version": "2.2.4",
"version": "2.2.5",
"license": "Apache-2.0"
}

File diff suppressed because it is too large Load diff

View file

@ -171,6 +171,14 @@
*/
"showSettingsInAdminPage": "${SHOW_SETTINGS_IN_ADMIN_PAGE:true}",
/*
* Settings for cleanup of pads
*/
"cleanup": {
"enabled": false,
"keepRevisions": 5
},
/*
The authentication method used by the server.
The default value is sso
@ -194,6 +202,15 @@
},
*/
/*
* Enables the use of a different server. We have a different one that syncs changes from the original server.
* It is hosted on GitHub and should not be blocked by many firewalls.
* https://etherpad.org/ep_infos
*/
"updateServer": "https://etherpad.org/ep_infos",
/*
* The type of the database.
*

View file

@ -162,6 +162,14 @@
*/
"showSettingsInAdminPage": true,
/*
* Settings for cleanup of pads
*/
"cleanup": {
"enabled": false,
"keepRevisions": 5
},
/*
* Node native SSL support
*
@ -271,6 +279,14 @@
"pageDown": true
},
/*
* Enables the use of a different server. We have a different one that syncs changes from the original server.
* It is hosted on GitHub and should not be blocked by many firewalls.
* https://etherpad.org/ep_infos
*/
"updateServer": "https://etherpad.org/ep_infos",
/*
* Should we suppress errors from being visible in the default Pad Text?
*/

View file

@ -82,6 +82,12 @@
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
}
},
{
"name": "restApi",
"hooks": {
"expressCreateServer": "ep_etherpad-lite/node/handler/RestAPI"
}
},
{
"name": "socketio",
"hooks": {

View file

@ -24,10 +24,10 @@ import {MapArrayType} from "../types/MapType";
const api = require('../db/API');
const padManager = require('../db/PadManager');
import createHTTPError from 'http-errors';
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
import {Http2ServerRequest} from "node:http2";
import {publicKeyExported} from "../security/OAuth2Provider";
import {jwtVerify} from "jose";
import {apikey} from './APIKeyHandler'
import {APIFields, apikey} from './APIKeyHandler'
// a list of all functions
const version:MapArrayType<any> = {};
@ -141,6 +141,7 @@ version['1.3.0'] = {
setText: ['padID', 'text', 'authorId'],
};
// set the latest available API version here
exports.latestApiVersion = '1.3.0';
@ -148,13 +149,6 @@ exports.latestApiVersion = '1.3.0';
exports.version = version;
type APIFields = {
apikey: string;
api_key: string;
padID: string;
padName: string;
authorization: string;
}
/**
* Handles an HTTP API call

View file

@ -7,6 +7,16 @@ const settings = require('../utils/Settings');
const apiHandlerLogger = log4js.getLogger('APIHandler');
export type APIFields = {
apikey: string;
api_key: string;
padID: string;
padName: string;
authorization: string;
}
// ensure we have an apikey
export let apikey:string|null = null;
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt');

View file

@ -1147,13 +1147,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
getPadLines(pad, startNum - 1),
// Get all needed composite Changesets.
...compositesChangesetNeeded.map(async (item) => {
const changeset = await composePadChangesets(pad, item.start, item.end);
const changeset = await exports.composePadChangesets(pad, item.start, item.end);
composedChangesets[`${item.start}/${item.end}`] = changeset;
}),
// Get all needed revision Dates.
...revTimesNeeded.map(async (revNum) => {
const revDate = await pad.getRevisionDate(revNum);
revisionDate[revNum] = Math.floor(revDate / 1000);
revisionDate[revNum] = revDate;
}),
]);
@ -1213,7 +1213,7 @@ const getPadLines = async (pad: PadType, revNum: number) => {
* Tries to rebuild the composePadChangeset function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
*/
const composePadChangesets = async (pad: PadType, startNum: number, endNum: number) => {
exports.composePadChangesets = async (pad: PadType, startNum: number, endNum: number) => {
// fetch all changesets we need
const headNum = pad.getHeadRevisionNumber();
endNum = Math.min(endNum, headNum + 1);

1527
src/node/handler/RestAPI.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ const settings = require('../../utils/Settings');
const UpdateCheck = require('../../utils/UpdateCheck');
const padManager = require('../../db/PadManager');
const api = require('../../db/API');
const cleanup = require('../../utils/Cleanup');
const queryPadLimit = 12;
@ -252,6 +253,40 @@ exports.socketio = (hookName: string, {io}: any) => {
}
})
socket.on('cleanupPadRevisions', async (padId: string) => {
if (!settings.cleanup.enabled) {
socket.emit('results:cleanupPadRevisions', {
error: 'Cleanup disabled. Enable cleanup in settings.json: cleanup.enabled => true',
});
return;
}
const padExists = await padManager.doesPadExists(padId);
if (padExists) {
logger.info(`Cleanup pad revisions: ${padId}`);
try {
const result = await cleanup.deleteRevisions(padId, settings.cleanup.keepRevisions)
if (result) {
socket.emit('results:cleanupPadRevisions', {
padId: padId,
keepRevisions: settings.cleanup.keepRevisions,
});
logger.info('successful cleaned up pad: ', padId)
} else {
socket.emit('results:cleanupPadRevisions', {
error: 'Error cleaning up pad',
});
}
} catch (err: any) {
logger.error(`Error in pad ${padId}: ${err.stack || err}`);
socket.emit('results:cleanupPadRevisions', {
error: err.toString(),
});
return;
}
}
})
socket.on('restartServer', async () => {
logger.info('Admin request to restart server through a socket on /admin/settings');
settings.reloadSettings();

View file

@ -12,6 +12,7 @@ const webaccess = require('./webaccess');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
import {build, buildSync} from 'esbuild'
import {ArgsExpressType} from "../../types/ArgsExpressType";
let ioI: { sockets: { sockets: any[]; }; } | null = null
exports.socketio = (hookName: string, {io}: any) => {
@ -19,7 +20,7 @@ exports.socketio = (hookName: string, {io}: any) => {
}
exports.expressPreSession = async (hookName:string, {app}:any) => {
exports.expressPreSession = async (hookName:string, {app}:ArgsExpressType) => {
// This endpoint is intended to conform to:
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
app.get('/health', (req:any, res:any) => {
@ -113,7 +114,7 @@ const convertTypescript = (content: string) => {
const handleLiveReload = async (args: any, padString: string, timeSliderString: string, indexString: any) => {
const chokidar = await import('chokidar')
const watcher = chokidar.watch(path.join(settings.root, 'src', 'static', 'js'));
const watcher = chokidar.watch(path.join(settings.root, 'src', 'static', 'js'), {});
let routeHandlers: { [key: string]: Function } = {};
const setRouteHandler = (path: string, newHandler: Function) => {
@ -243,7 +244,7 @@ const convertTypescriptWatched = (content: string, cb: (output:string, hash: str
})
}
exports.expressCreateServer = async (hookName: string, args: any, cb: Function) => {
exports.expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => {
const padString = eejs.require('ep_etherpad-lite/templates/padBootstrap.js', {
pluginModules: (() => {
const pluginModules = new Set();

View file

@ -153,7 +153,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
});
args.app.post('/interaction/:uid', async (req: Http2ServerRequest, res: Http2ServerResponse, next:Function) => {
args.app.post('/interaction/:uid', async (req, res, next) => {
const formid = new IncomingForm();
try {
// @ts-ignore
@ -226,7 +226,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
})
args.app.get('/interaction/:uid', async (req: Request, res: Response, next: Function) => {
args.app.get('/interaction/:uid', async (req, res, next) => {
try {
const {
uid, prompt, params, session,

View file

@ -1,5 +1,7 @@
import {Express} from "express";
export type ArgsExpressType = {
app:any,
app:Express,
io: any,
server:any
}
}

View file

@ -0,0 +1,9 @@
import {AChangeSet} from "./PadType";
export type Revision = {
changeset: AChangeSet,
meta: {
author: string,
timestamp: number,
}
}

168
src/node/utils/Cleanup.ts Normal file
View file

@ -0,0 +1,168 @@
'use strict'
import {AChangeSet} from "../types/PadType";
import {Revision} from "../types/Revision";
const promises = require('./promises');
const padManager = require('ep_etherpad-lite/node/db/PadManager');
const db = require('ep_etherpad-lite/node/db/DB');
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const padMessageHandler = require('ep_etherpad-lite/node/handler/PadMessageHandler');
const log4js = require('log4js');
const logger = log4js.getLogger('cleanup');
exports.deleteAllRevisions = async (padID: string): Promise<void> => {
const randomPadId = padID + 'aertdfdf' + Math.random().toString(10)
let pad = await padManager.getPad(padID);
await pad.copyPadWithoutHistory(randomPadId, false);
pad = await padManager.getPad(randomPadId);
await pad.copyPadWithoutHistory(padID, true);
await pad.remove();
}
const createRevision = async (aChangeset: AChangeSet, timestamp: number, isKeyRev: boolean, authorId: string, atext: any, pool: any) => {
if (authorId !== '') pool.putAttrib(['author', authorId]);
return {
changeset: aChangeset,
meta: {
author: authorId,
timestamp: timestamp,
...isKeyRev ? {
pool: pool,
atext: atext,
} : {},
},
};
}
exports.deleteRevisions = async (padId: string, keepRevisions: number): Promise<boolean> => {
logger.debug('Start cleanup revisions', padId)
let pad = await padManager.getPad(padId);
await pad.check()
logger.debug('Initial pad is valid')
if (pad.head <= keepRevisions) {
logger.debug('Pad has not enough revisions')
return false
}
padMessageHandler.kickSessionsFromPad(padId)
const cleanupUntilRevision = pad.head - keepRevisions
logger.debug('Composing changesets: ', cleanupUntilRevision)
const changeset = await padMessageHandler.composePadChangesets(pad, 0, cleanupUntilRevision + 1)
const revisions: Revision[] = [];
await promises.timesLimit(keepRevisions + 1, 500, async (i: number) => {
const rev = i + cleanupUntilRevision
revisions[rev] = await pad.getRevision(rev)
});
logger.debug('Loaded revisions: ', revisions.length)
await promises.timesLimit(pad.head + 1, 500, async (i: string) => {
await db.remove(`pad:${padId}:revs:${i}`, null);
});
let padContent = await db.get(`pad:${padId}`)
padContent.head = keepRevisions
if (padContent.savedRevisions) {
let newSavedRevisions = []
for (let i = 0; i < padContent.savedRevisions.length; i++) {
if (padContent.savedRevisions[i].revNum > cleanupUntilRevision) {
padContent.savedRevisions[i].revNum = padContent.savedRevisions[i].revNum - cleanupUntilRevision
newSavedRevisions.push(padContent.savedRevisions[i])
}
}
padContent.savedRevisions = newSavedRevisions
}
await db.set(`pad:${padId}`, padContent);
let newAText = Changeset.makeAText('\n');
let pool = pad.apool()
newAText = Changeset.applyToAText(changeset, newAText, pool);
const revision = await createRevision(
changeset,
revisions[cleanupUntilRevision].meta.timestamp,
0 === pad.getKeyRevisionNumber(0),
'',
newAText,
pool
);
const p: Promise<void>[] = [];
p.push(db.set(`pad:${padId}:revs:0`, revision))
p.push(promises.timesLimit(keepRevisions, 500, async (i: number) => {
const rev = i + cleanupUntilRevision + 1
const newRev = rev - cleanupUntilRevision;
newAText = Changeset.applyToAText(revisions[rev].changeset, newAText, pool);
const revision = await createRevision(
revisions[rev].changeset,
revisions[rev].meta.timestamp,
newRev === pad.getKeyRevisionNumber(newRev),
revisions[rev].meta.author,
newAText,
pool
);
await db.set(`pad:${padId}:revs:${newRev}`, revision);
}));
await Promise.all(p)
logger.debug('Finished migration. Checking pad now')
padManager.unloadPad(padId);
let newPad = await padManager.getPad(padId);
await newPad.check();
return true
}
exports.checkTodos = async () => {
await new Promise(resolve => setTimeout(resolve, 5000));
// TODO: Move to settings
const settings = {
minHead: 100,
keepRevisions: 100,
minAge: 1,//1000 * 60 * 60 * 24,
}
await Promise.all((await padManager.listAllPads()).padIDs.map(async (padId: string) => {
// TODO: Handle concurrency
const pad = await padManager.getPad(padId);
const revisionDate = await pad.getRevisionDate(pad.getHeadRevisionNumber())
if (pad.head < settings.minHead || padMessageHandler.padUsersCount(padId) > 0 || Date.now() < revisionDate + settings.minAge) {
return
}
try {
const result = await exports.deleteRevisions(padId, settings.keepRevisions)
if (result) {
logger.info('successful cleaned up pad: ', padId)
}
} catch (err: any) {
logger.error(`Error in pad ${padId}: ${err.stack || err}`);
return;
}
}));
}

View file

@ -107,6 +107,7 @@ exports.ttl = {
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
}
exports.updateServer = "https://static.etherpad.org"
/*
@ -379,6 +380,14 @@ exports.sso = {
*/
exports.showSettingsInAdminPage = true;
/*
* Settings for cleanup of pads
*/
exports.cleanup = {
enabled: false,
keepRevisions: 100,
}
/*
* By default, when caret is moved out of viewport, it scrolls the minimum
* height needed to make this line visible.

View file

@ -20,7 +20,7 @@ const loadEtherpadInformations = () => {
return infos;
}
return axios.get('https://static.etherpad.org/info.json', {headers: headers})
return axios.get(`${settings.updateServer}/info.json`, {headers: headers})
.then(async (resp: any) => {
infos = await resp.data;
if (infos === undefined || infos === null) {

View file

@ -38,13 +38,13 @@
"cross-spawn": "^7.0.3",
"ejs": "^3.1.10",
"esbuild": "^0.23.1",
"express": "4.19.2",
"express": "4.21.0",
"express-rate-limit": "^7.4.0",
"fast-deep-equal": "^3.1.3",
"find-root": "1.1.0",
"formidable": "^3.5.1",
"http-errors": "^2.0.0",
"jose": "^5.8.0",
"jose": "^5.9.2",
"js-cookie": "^3.0.5",
"jsdom": "^25.0.0",
"jsonminify": "0.4.2",
@ -57,31 +57,32 @@
"measured-core": "^2.0.0",
"mime-types": "^2.1.35",
"oidc-provider": "^8.5.1",
"openapi-backend": "^5.10.6",
"openapi-backend": "^5.11.0",
"proxy-addr": "^2.0.7",
"rate-limiter-flexible": "^5.0.3",
"rehype": "^13.0.1",
"rehype-minify-whitespace": "^6.0.0",
"rehype-minify-whitespace": "^6.0.1",
"resolve": "1.22.8",
"rusty-store-kv": "^1.3.1",
"security": "1.0.0",
"semver": "^7.6.3",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"superagent": "10.1.0",
"swagger-ui-express": "^5.0.1",
"tinycon": "0.6.8",
"tsx": "4.19.0",
"ueberdb2": "^4.2.103",
"tsx": "4.19.1",
"ueberdb2": "^5.0.2",
"underscore": "1.13.7",
"unorm": "1.6.0",
"wtfnode": "^0.9.3",
"rusty-store-kv": "^1.2.0"
"wtfnode": "^0.9.3"
},
"bin": {
"etherpad-healthcheck": "../bin/etherpad-healthcheck",
"etherpad-lite": "node/server.ts"
},
"devDependencies": {
"@playwright/test": "^1.47.0",
"@playwright/test": "^1.47.1",
"@types/async": "^3.2.24",
"@types/express": "^4.17.21",
"@types/formidable": "^3.4.5",
@ -89,17 +90,19 @@
"@types/jquery": "^3.5.30",
"@types/js-cookie": "^3.0.6",
"@types/jsdom": "^21.1.7",
"@types/jsonwebtoken": "^9.0.6",
"@types/jsonwebtoken": "^9.0.7",
"@types/mime-types": "^2.1.4",
"@types/mocha": "^10.0.7",
"@types/node": "^22.5.4",
"@types/mocha": "^10.0.8",
"@types/node": "^22.5.5",
"@types/oidc-provider": "^8.5.2",
"@types/semver": "^7.5.8",
"@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2",
"@types/swagger-ui-express": "^4.1.6",
"@types/underscore": "^1.11.15",
"chokidar": "^3.6.0",
"eslint": "^9.9.1",
"@types/whatwg-mimetype": "^3.0.2",
"chokidar": "^4.0.0",
"eslint": "^9.10.0",
"eslint-config-etherpad": "^4.0.4",
"etherpad-cli-client": "^3.0.2",
"mocha": "^10.7.3",
@ -107,11 +110,11 @@
"nodeify": "^1.0.1",
"openapi-schema-validation": "^0.4.2",
"set-cookie-parser": "^2.7.0",
"sinon": "^18.0.0",
"sinon": "^19.0.2",
"split-grid": "^1.0.11",
"supertest": "^7.0.0",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
"typescript": "^5.6.2",
"vitest": "^2.1.1"
},
"engines": {
"node": ">=18.18.2",
@ -138,6 +141,6 @@
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts",
"test:vitest": "vitest"
},
"version": "2.2.4",
"version": "2.2.5",
"license": "Apache-2.0"
}

View file

@ -167,7 +167,7 @@ function Ace2Inner(editorInfo, cssManagers) {
for (const name of names) console[name] = noop;
}
const scheduler = parent; // hack for opera required
const scheduler = window; // hack for opera required
const performDocumentReplaceRange = (start, end, newText) => {
if (start === undefined) start = rep.selStart;
@ -240,7 +240,7 @@ function Ace2Inner(editorInfo, cssManagers) {
bgcolor = fadeColor(bgcolor, info.fade);
}
const textColor =
colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName);
colorutils.textColorFromBackgroundColor(bgcolor, window.clientVars.skinName);
const styles = [
cssManagers.inner.selectorStyle(authorSelector),
cssManagers.parent.selectorStyle(authorSelector),
@ -1270,7 +1270,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const prevLine = rep.lines.prev(thisLine);
const prevLineText = prevLine.text;
let theIndent = /^ *(?:)/.exec(prevLineText)[0];
const shouldIndent = parent.parent.clientVars.indentationOnNewLine;
const shouldIndent = window.clientVars.indentationOnNewLine;
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
theIndent += THE_TAB;
}
@ -2023,7 +2023,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const isPadLoading = (t) => t === 'setup' || t === 'setBaseText' || t === 'importText';
const updateStyleButtonState = (attribName, hasStyleOnRepSelection) => {
const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a');
const $formattingButton = window.$(`[data-key="${attribName}"]`).find('a');
$formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection);
};
@ -2277,7 +2277,7 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const hideEditBarDropdowns = () => {
window.parent.parent.padeditbar.toggleDropDown('none');
window.padeditbar.toggleDropDown('none');
};
const renumberList = (lineNum) => {
@ -2582,7 +2582,7 @@ function Ace2Inner(editorInfo, cssManagers) {
specialHandled = specialHandledInHook.indexOf(true) !== -1;
}
const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled;
const padShortcutEnabled = window.clientVars.padShortcutEnabled;
if (!specialHandled && isTypeForSpecialKey &&
altKey && keyCode === 120 &&
padShortcutEnabled.altF9) {
@ -2591,7 +2591,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// As ubuntu cannot use Alt F10....
// Focus on the editbar.
// -- TODO: Move Focus back to previous state (we know it so we can use it)
const firstEditbarElement = parent.parent.$('#editbar')
const firstEditbarElement = window.$('#editbar')
.children('ul').first().children().first()
.children().first().children().first();
$(this).trigger('blur');
@ -2603,8 +2603,8 @@ function Ace2Inner(editorInfo, cssManagers) {
padShortcutEnabled.altC) {
// Alt c focuses on the Chat window
$(this).trigger('blur');
parent.parent.chat.show();
parent.parent.$('#chatinput').trigger('focus');
window.chat.show();
window.$('#chatinput').trigger('focus');
evt.preventDefault();
}
if (!specialHandled && type === 'keydown' &&
@ -2626,12 +2626,12 @@ function Ace2Inner(editorInfo, cssManagers) {
if (authorId) authorIds.add(authorId);
}
}
const idToName = new Map(parent.parent.pad.userList().map((a) => [a.userId, a.name]));
const myId = parent.parent.clientVars.userId;
const idToName = new Map(window.pad.userList().map((a) => [a.userId, a.name]));
const myId = window.clientVars.userId;
const authors =
[...authorIds].map((id) => id === myId ? 'me' : idToName.get(id) || 'unknown');
parent.parent.$.gritter.add({
window.$.gritter.add({
title: 'Line Authors',
text:
authors.length === 0 ? 'No author information is available'
@ -2680,7 +2680,7 @@ function Ace2Inner(editorInfo, cssManagers) {
specialHandled = true;
// close all gritters when the user hits escape key
parent.parent.$.gritter.removeAll();
window.$.gritter.removeAll();
}
if (!specialHandled && isTypeForCmdKey &&
/* Do a saved revision on ctrl S */
@ -2688,13 +2688,13 @@ function Ace2Inner(editorInfo, cssManagers) {
!evt.altKey &&
padShortcutEnabled.cmdS) {
evt.preventDefault();
const originalBackground = parent.parent.$('#revisionlink').css('background');
parent.parent.$('#revisionlink').css({background: 'lightyellow'});
const originalBackground = window.$('#revisionlink').css('background');
window.$('#revisionlink').css({background: 'lightyellow'});
scheduler.setTimeout(() => {
parent.parent.$('#revisionlink').css({background: originalBackground});
window.$('#revisionlink').css({background: originalBackground});
}, 1000);
/* The parent.parent part of this is BAD and I feel bad.. It may break something */
parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
window.pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
specialHandled = true;
}
if (!specialHandled && isTypeForSpecialKey &&

View file

@ -186,7 +186,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
mutateTextLines(changeset, padContents);
padContents.currentRevision = revision;
padContents.currentTime += timeDelta * 1000;
padContents.currentTime += timeDelta;
updateTimer();
@ -299,7 +299,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
// Loading changeset history for new revision
loadChangesetsForRevision(newRevision, update);
// Loading changeset history for old revision (to make diff between old and new revision)
loadChangesetsForRevision(padContents.currentRevision - 1);
loadChangesetsForRevision(padContents.currentRevision);
}
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);

View file

@ -140,7 +140,7 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
toSubmit = compose(submittedChangeset, userChangeset, apool);
} else {
// Get my authorID
const authorId = parent.parent.pad.myUserInfo.userId;
const authorId = window.pad.myUserInfo.userId;
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
// text was copied from another author.

View file

@ -171,7 +171,7 @@ export const getAvailablePlugins = (maxCacheAge: number|false) => {
return resolve(availablePlugins);
}
await axios.get('https://static.etherpad.org/plugins.json', {headers})
await axios.get(`${settings.updateServer}/plugins.json`, {headers})
.then((pluginsLoaded:AxiosResponse<MapArrayType<PackageInfo>>) => {
availablePlugins = pluginsLoaded.data;
cacheTimestamp = nowTimestamp;

View file

@ -15,7 +15,8 @@ class Scroll {
// DOM reference
this.outerWin = outerWin;
this.doc = this.outerWin.contentDocument!;
this.rootDocument = parent.parent.document;
this.rootDocument = document;
console.log(this.rootDocument)
}
scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep: RepModel, isScrollableEvent: boolean, innerHeight: number) {
@ -112,7 +113,7 @@ class Scroll {
};
_getEditorPositionTop() {
const editor = parent.document.getElementsByTagName('iframe');
const editor = document.getElementsByTagName('iframe');
const editorPositionTop = editor[0].offsetTop;
return editorPositionTop;
};

View file

@ -11,7 +11,7 @@
},
"devDependencies": {
"ep_etherpad-lite": "workspace:../src",
"typescript": "^5.5.4",
"vite": "^5.4.3"
"typescript": "^5.6.2",
"vite": "^5.4.7"
}
}