mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-31 19:02:59 +01:00
Merge branch 'develop'
This commit is contained in:
commit
1c459b3e41
37 changed files with 3316 additions and 1349 deletions
|
@ -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
|
# 2.2.4
|
||||||
|
|
||||||
### Notable enhancements and fixes
|
### Notable enhancements and fixes
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -16,25 +16,25 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.8",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
"@typescript-eslint/eslint-plugin": "^8.6.0",
|
||||||
"@typescript-eslint/parser": "^8.4.0",
|
"@typescript-eslint/parser": "^8.6.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.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-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.11",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
"i18next": "^23.14.0",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"lucide-react": "^0.439.0",
|
"lucide-react": "^0.441.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.2",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.2",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.3",
|
"vite": "^5.4.7",
|
||||||
"vite-plugin-static-copy": "^1.0.6",
|
"vite-plugin-static-copy": "^1.0.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zustand": "^4.5.5"
|
"zustand": "^4.5.5"
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"ep_adminpads2_autoupdate.title": "Aktiviert oder deaktiviert automatische Aktualisierungen für die aktuelle Abfrage.",
|
"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_confirm": "Willst du das Pad {{padID}} wirklich löschen?",
|
||||||
"ep_adminpads2_delete.value": "Löschen",
|
"ep_adminpads2_delete.value": "Löschen",
|
||||||
|
"ep_adminpads2_cleanup": "Historie aufräumen",
|
||||||
"ep_adminpads2_last-edited": "Zuletzt bearbeitet",
|
"ep_adminpads2_last-edited": "Zuletzt bearbeitet",
|
||||||
"ep_adminpads2_loading": "Lädt...",
|
"ep_adminpads2_loading": "Lädt...",
|
||||||
"ep_adminpads2_manage-pads": "Pads verwalten",
|
"ep_adminpads2_manage-pads": "Pads verwalten",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"ep_adminpads2_autoupdate.title": "Enables or disables automatic updates for the current query.",
|
"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_confirm": "Do you really want to delete the pad {{padID}}?",
|
||||||
"ep_adminpads2_delete.value": "Delete",
|
"ep_adminpads2_delete.value": "Delete",
|
||||||
|
"ep_adminpads2_cleanup": "Cleanup revisions",
|
||||||
"ep_adminpads2_last-edited": "Last edited",
|
"ep_adminpads2_last-edited": "Last edited",
|
||||||
"ep_adminpads2_loading": "Loading…",
|
"ep_adminpads2_loading": "Loading…",
|
||||||
"ep_adminpads2_manage-pads": "Manage pads",
|
"ep_adminpads2_manage-pads": "Manage pads",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {useEffect} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import {connect} from 'socket.io-client'
|
import {connect} from 'socket.io-client'
|
||||||
import {isJSONClean} from './utils/utils.ts'
|
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 {useStore} from "./store/store.ts";
|
||||||
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
||||||
import {Trans, useTranslation} from "react-i18next";
|
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' : ''
|
const WS_URL = import.meta.env.DEV ? 'http://localhost:9001' : ''
|
||||||
export const App = ()=> {
|
export const App = () => {
|
||||||
const setSettings = useStore(state => state.setSettings);
|
const setSettings = useStore(state => state.setSettings);
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState<boolean>(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/admin-auth/', {
|
fetch('/admin-auth/', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
}).then((value)=>{
|
}).then((value) => {
|
||||||
if(!value.ok){
|
if (!value.ok) {
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
}
|
}
|
||||||
}).catch(()=>{
|
}).catch(() => {
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
})
|
})
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t('admin.page-title')
|
document.title = t('admin.page-title')
|
||||||
|
|
||||||
useStore.getState().setShowLoading(true);
|
useStore.getState().setShowLoading(true);
|
||||||
const settingSocket = connect(`${WS_URL}/settings`, {
|
const settingSocket = connect(`${WS_URL}/settings`, {
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
|
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
})
|
})
|
||||||
|
|
||||||
pluginsSocket.on('connect', () => {
|
pluginsSocket.on('connect', () => {
|
||||||
useStore.getState().setPluginsSocket(pluginsSocket);
|
useStore.getState().setPluginsSocket(pluginsSocket);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
settingSocket.on('connect', () => {
|
settingSocket.on('connect', () => {
|
||||||
useStore.getState().setSettingsSocket(settingSocket);
|
useStore.getState().setSettingsSocket(settingSocket);
|
||||||
useStore.getState().setShowLoading(false)
|
useStore.getState().setShowLoading(false)
|
||||||
settingSocket.emit('load');
|
settingSocket.emit('load');
|
||||||
console.log('connected');
|
console.log('connected');
|
||||||
});
|
});
|
||||||
|
|
||||||
settingSocket.on('disconnect', (reason) => {
|
settingSocket.on('disconnect', (reason) => {
|
||||||
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
|
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
|
||||||
// server disconnect".
|
// server disconnect".
|
||||||
useStore.getState().setShowLoading(true)
|
useStore.getState().setShowLoading(true)
|
||||||
if (reason === 'io server disconnect') {
|
if (reason === 'io server disconnect') {
|
||||||
settingSocket.connect();
|
settingSocket.connect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
settingSocket.on('settings', (settings) => {
|
settingSocket.on('settings', (settings) => {
|
||||||
/* Check whether the settings.json is authorized to be viewed */
|
/* Check whether the settings.json is authorized to be viewed */
|
||||||
if (settings.results === 'NOT_ALLOWED') {
|
if (settings.results === 'NOT_ALLOWED') {
|
||||||
console.log('Not allowed to view settings.json')
|
console.log('Not allowed to view settings.json')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check to make sure the JSON is clean before proceeding */
|
/* Check to make sure the JSON is clean before proceeding */
|
||||||
if (isJSONClean(settings.results)) {
|
if (isJSONClean(settings.results)) {
|
||||||
setSettings(settings.results);
|
setSettings(settings.results);
|
||||||
} else {
|
} else {
|
||||||
alert('Invalid JSON');
|
alert('Invalid JSON');
|
||||||
}
|
}
|
||||||
useStore.getState().setShowLoading(false);
|
useStore.getState().setShowLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
settingSocket.on('saveprogress', (status)=>{
|
settingSocket.on('saveprogress', (status) => {
|
||||||
console.log(status)
|
console.log(status)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
settingSocket.disconnect();
|
settingSocket.disconnect();
|
||||||
pluginsSocket.disconnect()
|
pluginsSocket.disconnect()
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div id="wrapper">
|
return <div id="wrapper" className={`${sidebarOpen ? '': 'closed' }`}>
|
||||||
<LoadingScreen/>
|
<LoadingScreen/>
|
||||||
<div className="menu">
|
<div className="menu">
|
||||||
<div className="inner-menu">
|
<div className="inner-menu">
|
||||||
<span>
|
<span>
|
||||||
<Crown width={40} height={40}/>
|
<Crown width={40} height={40}/>
|
||||||
<h1>Etherpad</h1>
|
<h1>Etherpad</h1>
|
||||||
</span>
|
</span>
|
||||||
<ul>
|
<ul onClick={()=>{
|
||||||
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
|
setSidebarOpen(false)
|
||||||
<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="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
|
||||||
<li><NavLink to={"/pads"}><NotepadText/><Trans
|
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
|
||||||
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
|
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
|
||||||
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
|
<li><NavLink to={"/pads"}><NotepadText/><Trans
|
||||||
</ul>
|
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
|
||||||
</div>
|
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
|
||||||
</div>
|
</ul>
|
||||||
<div className="innerwrapper">
|
</div>
|
||||||
<Outlet/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button id="icon-button" onClick={() => {
|
||||||
|
setSidebarOpen(!sidebarOpen)
|
||||||
|
}}><LucideMenu/></button>
|
||||||
|
<div className="innerwrapper">
|
||||||
|
<Outlet/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -193,6 +193,7 @@ export const HomePage = () => {
|
||||||
<h2><Trans i18nKey="admin_plugins.available"/></h2>
|
<h2><Trans i18nKey="admin_plugins.available"/></h2>
|
||||||
<SearchField onChange={v=>{setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/>
|
<SearchField onChange={v=>{setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/>
|
||||||
|
|
||||||
|
<div className="table-container">
|
||||||
<table id="available-plugins">
|
<table id="available-plugins">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -240,5 +241,6 @@ export const HomePage = () => {
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {useDebounce} from "../utils/useDebounce.ts";
|
||||||
import {determineSorting} from "../utils/sorting.ts";
|
import {determineSorting} from "../utils/sorting.ts";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
import {IconButton} from "../components/IconButton.tsx";
|
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";
|
import {SearchField} from "../components/SearchField.tsx";
|
||||||
|
|
||||||
export const PadPage = ()=>{
|
export const PadPage = ()=>{
|
||||||
|
@ -23,6 +23,7 @@ export const PadPage = ()=>{
|
||||||
const pads = useStore(state=>state.pads)
|
const pads = useStore(state=>state.pads)
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0)
|
const [currentPage, setCurrentPage] = useState<number>(0)
|
||||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
|
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
|
||||||
|
const [errorText, setErrorText] = useState<string|null>(null)
|
||||||
const [padToDelete, setPadToDelete] = useState<string>('')
|
const [padToDelete, setPadToDelete] = useState<string>('')
|
||||||
const pages = useMemo(()=>{
|
const pages = useMemo(()=>{
|
||||||
if(!pads){
|
if(!pads){
|
||||||
|
@ -68,12 +69,35 @@ export const PadPage = ()=>{
|
||||||
results: newPads
|
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]);
|
}, [settingsSocket, pads]);
|
||||||
|
|
||||||
const deletePad = (padID: string)=>{
|
const deletePad = (padID: string)=>{
|
||||||
settingsSocket?.emit('deletePad', padID)
|
settingsSocket?.emit('deletePad', padID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanupPad = (padID: string)=>{
|
||||||
|
settingsSocket?.emit('cleanupPadRevisions', padID)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
|
@ -100,6 +124,21 @@ export const PadPage = ()=>{
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
</Dialog.Root>
|
</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>
|
<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')}/>
|
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||||
<table>
|
<table>
|
||||||
|
@ -150,6 +189,9 @@ export const PadPage = ()=>{
|
||||||
setPadToDelete(pad.padName)
|
setPadToDelete(pad.padName)
|
||||||
setDeleteDialog(true)
|
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')}/>
|
<IconButton icon={<Eye/>} title="view" onClick={()=>window.open(`/p/${pad.padName}`, '_blank')}/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -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}`)
|
exec(`asciidoctor -D ../out/doc/api ../doc/api/*.adoc -a VERSION=${VERSION}`)
|
||||||
|
|
||||||
copyFolderSync('../doc/public/', '../out/doc/')
|
copyFolderSync('../doc/public/', '../out/doc/')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bin",
|
"name": "bin",
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "checkAllPads.js",
|
"main": "checkAllPads.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
@ -11,13 +11,13 @@
|
||||||
"ep_etherpad-lite": "workspace:../src",
|
"ep_etherpad-lite": "workspace:../src",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.1",
|
||||||
"ueberdb2": "^4.2.103"
|
"ueberdb2": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.5",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"makeDocs": "node --import tsx make_docs.ts",
|
"makeDocs": "node --import tsx make_docs.ts",
|
||||||
|
|
|
@ -32,6 +32,7 @@ export default defineConfig({
|
||||||
{ text: 'Stats', link: '/stats.md' },
|
{ text: 'Stats', link: '/stats.md' },
|
||||||
{text: 'Skins', link: '/skins.md' },
|
{text: 'Skins', link: '/skins.md' },
|
||||||
{text: 'Demo', link: '/demo.md' },
|
{text: 'Demo', link: '/demo.md' },
|
||||||
|
{text: 'CLI', link: '/cli.md'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -58,7 +58,7 @@ services:
|
||||||
# ports:
|
# ports:
|
||||||
# - "5432:5432"
|
# - "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data/pgdata
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|
|
@ -42,7 +42,7 @@ services:
|
||||||
# ports:
|
# ports:
|
||||||
# - "5432:5432"
|
# - "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data/pgdata
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|
|
@ -50,6 +50,6 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ether/etherpad-lite.git"
|
"url": "https://github.com/ether/etherpad-lite.git"
|
||||||
},
|
},
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
1549
pnpm-lock.yaml
1549
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -171,6 +171,14 @@
|
||||||
*/
|
*/
|
||||||
"showSettingsInAdminPage": "${SHOW_SETTINGS_IN_ADMIN_PAGE:true}",
|
"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 authentication method used by the server.
|
||||||
The default value is sso
|
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.
|
* The type of the database.
|
||||||
*
|
*
|
||||||
|
|
|
@ -162,6 +162,14 @@
|
||||||
*/
|
*/
|
||||||
"showSettingsInAdminPage": true,
|
"showSettingsInAdminPage": true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Settings for cleanup of pads
|
||||||
|
*/
|
||||||
|
"cleanup": {
|
||||||
|
"enabled": false,
|
||||||
|
"keepRevisions": 5
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Node native SSL support
|
* Node native SSL support
|
||||||
*
|
*
|
||||||
|
@ -271,6 +279,14 @@
|
||||||
"pageDown": true
|
"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?
|
* Should we suppress errors from being visible in the default Pad Text?
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -82,6 +82,12 @@
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "restApi",
|
||||||
|
"hooks": {
|
||||||
|
"expressCreateServer": "ep_etherpad-lite/node/handler/RestAPI"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "socketio",
|
"name": "socketio",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
|
|
@ -24,10 +24,10 @@ import {MapArrayType} from "../types/MapType";
|
||||||
const api = require('../db/API');
|
const api = require('../db/API');
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require('../db/PadManager');
|
||||||
import createHTTPError from 'http-errors';
|
import createHTTPError from 'http-errors';
|
||||||
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
|
import {Http2ServerRequest} from "node:http2";
|
||||||
import {publicKeyExported} from "../security/OAuth2Provider";
|
import {publicKeyExported} from "../security/OAuth2Provider";
|
||||||
import {jwtVerify} from "jose";
|
import {jwtVerify} from "jose";
|
||||||
import {apikey} from './APIKeyHandler'
|
import {APIFields, apikey} from './APIKeyHandler'
|
||||||
// a list of all functions
|
// a list of all functions
|
||||||
const version:MapArrayType<any> = {};
|
const version:MapArrayType<any> = {};
|
||||||
|
|
||||||
|
@ -141,6 +141,7 @@ version['1.3.0'] = {
|
||||||
setText: ['padID', 'text', 'authorId'],
|
setText: ['padID', 'text', 'authorId'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// set the latest available API version here
|
// set the latest available API version here
|
||||||
exports.latestApiVersion = '1.3.0';
|
exports.latestApiVersion = '1.3.0';
|
||||||
|
|
||||||
|
@ -148,13 +149,6 @@ exports.latestApiVersion = '1.3.0';
|
||||||
exports.version = version;
|
exports.version = version;
|
||||||
|
|
||||||
|
|
||||||
type APIFields = {
|
|
||||||
apikey: string;
|
|
||||||
api_key: string;
|
|
||||||
padID: string;
|
|
||||||
padName: string;
|
|
||||||
authorization: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles an HTTP API call
|
* Handles an HTTP API call
|
||||||
|
|
|
@ -7,6 +7,16 @@ const settings = require('../utils/Settings');
|
||||||
|
|
||||||
const apiHandlerLogger = log4js.getLogger('APIHandler');
|
const apiHandlerLogger = log4js.getLogger('APIHandler');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type APIFields = {
|
||||||
|
apikey: string;
|
||||||
|
api_key: string;
|
||||||
|
padID: string;
|
||||||
|
padName: string;
|
||||||
|
authorization: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ensure we have an apikey
|
// ensure we have an apikey
|
||||||
export let apikey:string|null = null;
|
export let apikey:string|null = null;
|
||||||
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt');
|
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt');
|
||||||
|
|
|
@ -1147,13 +1147,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
||||||
getPadLines(pad, startNum - 1),
|
getPadLines(pad, startNum - 1),
|
||||||
// Get all needed composite Changesets.
|
// Get all needed composite Changesets.
|
||||||
...compositesChangesetNeeded.map(async (item) => {
|
...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;
|
composedChangesets[`${item.start}/${item.end}`] = changeset;
|
||||||
}),
|
}),
|
||||||
// Get all needed revision Dates.
|
// Get all needed revision Dates.
|
||||||
...revTimesNeeded.map(async (revNum) => {
|
...revTimesNeeded.map(async (revNum) => {
|
||||||
const revDate = await pad.getRevisionDate(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
|
* 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
|
* 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
|
// fetch all changesets we need
|
||||||
const headNum = pad.getHeadRevisionNumber();
|
const headNum = pad.getHeadRevisionNumber();
|
||||||
endNum = Math.min(endNum, headNum + 1);
|
endNum = Math.min(endNum, headNum + 1);
|
||||||
|
|
1527
src/node/handler/RestAPI.ts
Normal file
1527
src/node/handler/RestAPI.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -13,6 +13,7 @@ const settings = require('../../utils/Settings');
|
||||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||||
const padManager = require('../../db/PadManager');
|
const padManager = require('../../db/PadManager');
|
||||||
const api = require('../../db/API');
|
const api = require('../../db/API');
|
||||||
|
const cleanup = require('../../utils/Cleanup');
|
||||||
|
|
||||||
|
|
||||||
const queryPadLimit = 12;
|
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 () => {
|
socket.on('restartServer', async () => {
|
||||||
logger.info('Admin request to restart server through a socket on /admin/settings');
|
logger.info('Admin request to restart server through a socket on /admin/settings');
|
||||||
settings.reloadSettings();
|
settings.reloadSettings();
|
||||||
|
|
|
@ -12,6 +12,7 @@ const webaccess = require('./webaccess');
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||||
|
|
||||||
import {build, buildSync} from 'esbuild'
|
import {build, buildSync} from 'esbuild'
|
||||||
|
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||||
let ioI: { sockets: { sockets: any[]; }; } | null = null
|
let ioI: { sockets: { sockets: any[]; }; } | null = null
|
||||||
|
|
||||||
exports.socketio = (hookName: string, {io}: any) => {
|
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:
|
// This endpoint is intended to conform to:
|
||||||
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
||||||
app.get('/health', (req:any, res:any) => {
|
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 handleLiveReload = async (args: any, padString: string, timeSliderString: string, indexString: any) => {
|
||||||
const chokidar = await import('chokidar')
|
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 } = {};
|
let routeHandlers: { [key: string]: Function } = {};
|
||||||
|
|
||||||
const setRouteHandler = (path: string, newHandler: 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', {
|
const padString = eejs.require('ep_etherpad-lite/templates/padBootstrap.js', {
|
||||||
pluginModules: (() => {
|
pluginModules: (() => {
|
||||||
const pluginModules = new Set();
|
const pluginModules = new Set();
|
||||||
|
|
|
@ -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();
|
const formid = new IncomingForm();
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @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 {
|
try {
|
||||||
const {
|
const {
|
||||||
uid, prompt, params, session,
|
uid, prompt, params, session,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import {Express} from "express";
|
||||||
|
|
||||||
export type ArgsExpressType = {
|
export type ArgsExpressType = {
|
||||||
app:any,
|
app:Express,
|
||||||
io: any,
|
io: any,
|
||||||
server:any
|
server:any
|
||||||
}
|
}
|
||||||
|
|
9
src/node/types/Revision.ts
Normal file
9
src/node/types/Revision.ts
Normal 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
168
src/node/utils/Cleanup.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
|
@ -107,6 +107,7 @@ exports.ttl = {
|
||||||
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
|
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.updateServer = "https://static.etherpad.org"
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -379,6 +380,14 @@ exports.sso = {
|
||||||
*/
|
*/
|
||||||
exports.showSettingsInAdminPage = true;
|
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
|
* By default, when caret is moved out of viewport, it scrolls the minimum
|
||||||
* height needed to make this line visible.
|
* height needed to make this line visible.
|
||||||
|
|
|
@ -20,7 +20,7 @@ const loadEtherpadInformations = () => {
|
||||||
return infos;
|
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) => {
|
.then(async (resp: any) => {
|
||||||
infos = await resp.data;
|
infos = await resp.data;
|
||||||
if (infos === undefined || infos === null) {
|
if (infos === undefined || infos === null) {
|
||||||
|
|
|
@ -38,13 +38,13 @@
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"esbuild": "^0.23.1",
|
"esbuild": "^0.23.1",
|
||||||
"express": "4.19.2",
|
"express": "4.21.0",
|
||||||
"express-rate-limit": "^7.4.0",
|
"express-rate-limit": "^7.4.0",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"find-root": "1.1.0",
|
"find-root": "1.1.0",
|
||||||
"formidable": "^3.5.1",
|
"formidable": "^3.5.1",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"jose": "^5.8.0",
|
"jose": "^5.9.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsdom": "^25.0.0",
|
"jsdom": "^25.0.0",
|
||||||
"jsonminify": "0.4.2",
|
"jsonminify": "0.4.2",
|
||||||
|
@ -57,31 +57,32 @@
|
||||||
"measured-core": "^2.0.0",
|
"measured-core": "^2.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"oidc-provider": "^8.5.1",
|
"oidc-provider": "^8.5.1",
|
||||||
"openapi-backend": "^5.10.6",
|
"openapi-backend": "^5.11.0",
|
||||||
"proxy-addr": "^2.0.7",
|
"proxy-addr": "^2.0.7",
|
||||||
"rate-limiter-flexible": "^5.0.3",
|
"rate-limiter-flexible": "^5.0.3",
|
||||||
"rehype": "^13.0.1",
|
"rehype": "^13.0.1",
|
||||||
"rehype-minify-whitespace": "^6.0.0",
|
"rehype-minify-whitespace": "^6.0.1",
|
||||||
"resolve": "1.22.8",
|
"resolve": "1.22.8",
|
||||||
|
"rusty-store-kv": "^1.3.1",
|
||||||
"security": "1.0.0",
|
"security": "1.0.0",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"superagent": "10.1.0",
|
"superagent": "10.1.0",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"tsx": "4.19.0",
|
"tsx": "4.19.1",
|
||||||
"ueberdb2": "^4.2.103",
|
"ueberdb2": "^5.0.2",
|
||||||
"underscore": "1.13.7",
|
"underscore": "1.13.7",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"wtfnode": "^0.9.3",
|
"wtfnode": "^0.9.3"
|
||||||
"rusty-store-kv": "^1.2.0"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"etherpad-healthcheck": "../bin/etherpad-healthcheck",
|
"etherpad-healthcheck": "../bin/etherpad-healthcheck",
|
||||||
"etherpad-lite": "node/server.ts"
|
"etherpad-lite": "node/server.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.47.0",
|
"@playwright/test": "^1.47.1",
|
||||||
"@types/async": "^3.2.24",
|
"@types/async": "^3.2.24",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
|
@ -89,17 +90,19 @@
|
||||||
"@types/jquery": "^3.5.30",
|
"@types/jquery": "^3.5.30",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.8",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.5",
|
||||||
"@types/oidc-provider": "^8.5.2",
|
"@types/oidc-provider": "^8.5.2",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@types/swagger-ui-express": "^4.1.6",
|
||||||
"@types/underscore": "^1.11.15",
|
"@types/underscore": "^1.11.15",
|
||||||
"chokidar": "^3.6.0",
|
"@types/whatwg-mimetype": "^3.0.2",
|
||||||
"eslint": "^9.9.1",
|
"chokidar": "^4.0.0",
|
||||||
|
"eslint": "^9.10.0",
|
||||||
"eslint-config-etherpad": "^4.0.4",
|
"eslint-config-etherpad": "^4.0.4",
|
||||||
"etherpad-cli-client": "^3.0.2",
|
"etherpad-cli-client": "^3.0.2",
|
||||||
"mocha": "^10.7.3",
|
"mocha": "^10.7.3",
|
||||||
|
@ -107,11 +110,11 @@
|
||||||
"nodeify": "^1.0.1",
|
"nodeify": "^1.0.1",
|
||||||
"openapi-schema-validation": "^0.4.2",
|
"openapi-schema-validation": "^0.4.2",
|
||||||
"set-cookie-parser": "^2.7.0",
|
"set-cookie-parser": "^2.7.0",
|
||||||
"sinon": "^18.0.0",
|
"sinon": "^19.0.2",
|
||||||
"split-grid": "^1.0.11",
|
"split-grid": "^1.0.11",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^2.0.5"
|
"vitest": "^2.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.2",
|
"node": ">=18.18.2",
|
||||||
|
@ -138,6 +141,6 @@
|
||||||
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts",
|
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts",
|
||||||
"test:vitest": "vitest"
|
"test:vitest": "vitest"
|
||||||
},
|
},
|
||||||
"version": "2.2.4",
|
"version": "2.2.5",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,7 +167,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
for (const name of names) console[name] = noop;
|
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) => {
|
const performDocumentReplaceRange = (start, end, newText) => {
|
||||||
if (start === undefined) start = rep.selStart;
|
if (start === undefined) start = rep.selStart;
|
||||||
|
@ -240,7 +240,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
bgcolor = fadeColor(bgcolor, info.fade);
|
bgcolor = fadeColor(bgcolor, info.fade);
|
||||||
}
|
}
|
||||||
const textColor =
|
const textColor =
|
||||||
colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName);
|
colorutils.textColorFromBackgroundColor(bgcolor, window.clientVars.skinName);
|
||||||
const styles = [
|
const styles = [
|
||||||
cssManagers.inner.selectorStyle(authorSelector),
|
cssManagers.inner.selectorStyle(authorSelector),
|
||||||
cssManagers.parent.selectorStyle(authorSelector),
|
cssManagers.parent.selectorStyle(authorSelector),
|
||||||
|
@ -1270,7 +1270,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const prevLine = rep.lines.prev(thisLine);
|
const prevLine = rep.lines.prev(thisLine);
|
||||||
const prevLineText = prevLine.text;
|
const prevLineText = prevLine.text;
|
||||||
let theIndent = /^ *(?:)/.exec(prevLineText)[0];
|
let theIndent = /^ *(?:)/.exec(prevLineText)[0];
|
||||||
const shouldIndent = parent.parent.clientVars.indentationOnNewLine;
|
const shouldIndent = window.clientVars.indentationOnNewLine;
|
||||||
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
||||||
theIndent += THE_TAB;
|
theIndent += THE_TAB;
|
||||||
}
|
}
|
||||||
|
@ -2023,7 +2023,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const isPadLoading = (t) => t === 'setup' || t === 'setBaseText' || t === 'importText';
|
const isPadLoading = (t) => t === 'setup' || t === 'setBaseText' || t === 'importText';
|
||||||
|
|
||||||
const updateStyleButtonState = (attribName, hasStyleOnRepSelection) => {
|
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);
|
$formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2277,7 +2277,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideEditBarDropdowns = () => {
|
const hideEditBarDropdowns = () => {
|
||||||
window.parent.parent.padeditbar.toggleDropDown('none');
|
window.padeditbar.toggleDropDown('none');
|
||||||
};
|
};
|
||||||
|
|
||||||
const renumberList = (lineNum) => {
|
const renumberList = (lineNum) => {
|
||||||
|
@ -2582,7 +2582,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
specialHandled = specialHandledInHook.indexOf(true) !== -1;
|
specialHandled = specialHandledInHook.indexOf(true) !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled;
|
const padShortcutEnabled = window.clientVars.padShortcutEnabled;
|
||||||
if (!specialHandled && isTypeForSpecialKey &&
|
if (!specialHandled && isTypeForSpecialKey &&
|
||||||
altKey && keyCode === 120 &&
|
altKey && keyCode === 120 &&
|
||||||
padShortcutEnabled.altF9) {
|
padShortcutEnabled.altF9) {
|
||||||
|
@ -2591,7 +2591,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// As ubuntu cannot use Alt F10....
|
// As ubuntu cannot use Alt F10....
|
||||||
// Focus on the editbar.
|
// Focus on the editbar.
|
||||||
// -- TODO: Move Focus back to previous state (we know it so we can use it)
|
// -- 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('ul').first().children().first()
|
||||||
.children().first().children().first();
|
.children().first().children().first();
|
||||||
$(this).trigger('blur');
|
$(this).trigger('blur');
|
||||||
|
@ -2603,8 +2603,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
padShortcutEnabled.altC) {
|
padShortcutEnabled.altC) {
|
||||||
// Alt c focuses on the Chat window
|
// Alt c focuses on the Chat window
|
||||||
$(this).trigger('blur');
|
$(this).trigger('blur');
|
||||||
parent.parent.chat.show();
|
window.chat.show();
|
||||||
parent.parent.$('#chatinput').trigger('focus');
|
window.$('#chatinput').trigger('focus');
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
if (!specialHandled && type === 'keydown' &&
|
if (!specialHandled && type === 'keydown' &&
|
||||||
|
@ -2626,12 +2626,12 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (authorId) authorIds.add(authorId);
|
if (authorId) authorIds.add(authorId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const idToName = new Map(parent.parent.pad.userList().map((a) => [a.userId, a.name]));
|
const idToName = new Map(window.pad.userList().map((a) => [a.userId, a.name]));
|
||||||
const myId = parent.parent.clientVars.userId;
|
const myId = window.clientVars.userId;
|
||||||
const authors =
|
const authors =
|
||||||
[...authorIds].map((id) => id === myId ? 'me' : idToName.get(id) || 'unknown');
|
[...authorIds].map((id) => id === myId ? 'me' : idToName.get(id) || 'unknown');
|
||||||
|
|
||||||
parent.parent.$.gritter.add({
|
window.$.gritter.add({
|
||||||
title: 'Line Authors',
|
title: 'Line Authors',
|
||||||
text:
|
text:
|
||||||
authors.length === 0 ? 'No author information is available'
|
authors.length === 0 ? 'No author information is available'
|
||||||
|
@ -2680,7 +2680,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
specialHandled = true;
|
specialHandled = true;
|
||||||
|
|
||||||
// close all gritters when the user hits escape key
|
// close all gritters when the user hits escape key
|
||||||
parent.parent.$.gritter.removeAll();
|
window.$.gritter.removeAll();
|
||||||
}
|
}
|
||||||
if (!specialHandled && isTypeForCmdKey &&
|
if (!specialHandled && isTypeForCmdKey &&
|
||||||
/* Do a saved revision on ctrl S */
|
/* Do a saved revision on ctrl S */
|
||||||
|
@ -2688,13 +2688,13 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
!evt.altKey &&
|
!evt.altKey &&
|
||||||
padShortcutEnabled.cmdS) {
|
padShortcutEnabled.cmdS) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const originalBackground = parent.parent.$('#revisionlink').css('background');
|
const originalBackground = window.$('#revisionlink').css('background');
|
||||||
parent.parent.$('#revisionlink').css({background: 'lightyellow'});
|
window.$('#revisionlink').css({background: 'lightyellow'});
|
||||||
scheduler.setTimeout(() => {
|
scheduler.setTimeout(() => {
|
||||||
parent.parent.$('#revisionlink').css({background: originalBackground});
|
window.$('#revisionlink').css({background: originalBackground});
|
||||||
}, 1000);
|
}, 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;
|
specialHandled = true;
|
||||||
}
|
}
|
||||||
if (!specialHandled && isTypeForSpecialKey &&
|
if (!specialHandled && isTypeForSpecialKey &&
|
||||||
|
|
|
@ -186,7 +186,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
|
|
||||||
mutateTextLines(changeset, padContents);
|
mutateTextLines(changeset, padContents);
|
||||||
padContents.currentRevision = revision;
|
padContents.currentRevision = revision;
|
||||||
padContents.currentTime += timeDelta * 1000;
|
padContents.currentTime += timeDelta;
|
||||||
|
|
||||||
updateTimer();
|
updateTimer();
|
||||||
|
|
||||||
|
@ -299,7 +299,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
// Loading changeset history for new revision
|
// Loading changeset history for new revision
|
||||||
loadChangesetsForRevision(newRevision, update);
|
loadChangesetsForRevision(newRevision, update);
|
||||||
// Loading changeset history for old revision (to make diff between old and new revision)
|
// 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]);
|
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);
|
||||||
|
|
|
@ -140,7 +140,7 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
toSubmit = compose(submittedChangeset, userChangeset, apool);
|
toSubmit = compose(submittedChangeset, userChangeset, apool);
|
||||||
} else {
|
} else {
|
||||||
// Get my authorID
|
// 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
|
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
|
||||||
// text was copied from another author.
|
// text was copied from another author.
|
||||||
|
|
|
@ -171,7 +171,7 @@ export const getAvailablePlugins = (maxCacheAge: number|false) => {
|
||||||
return resolve(availablePlugins);
|
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>>) => {
|
.then((pluginsLoaded:AxiosResponse<MapArrayType<PackageInfo>>) => {
|
||||||
availablePlugins = pluginsLoaded.data;
|
availablePlugins = pluginsLoaded.data;
|
||||||
cacheTimestamp = nowTimestamp;
|
cacheTimestamp = nowTimestamp;
|
||||||
|
|
|
@ -15,7 +15,8 @@ class Scroll {
|
||||||
// DOM reference
|
// DOM reference
|
||||||
this.outerWin = outerWin;
|
this.outerWin = outerWin;
|
||||||
this.doc = this.outerWin.contentDocument!;
|
this.doc = this.outerWin.contentDocument!;
|
||||||
this.rootDocument = parent.parent.document;
|
this.rootDocument = document;
|
||||||
|
console.log(this.rootDocument)
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep: RepModel, isScrollableEvent: boolean, innerHeight: number) {
|
scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep: RepModel, isScrollableEvent: boolean, innerHeight: number) {
|
||||||
|
@ -112,7 +113,7 @@ class Scroll {
|
||||||
};
|
};
|
||||||
|
|
||||||
_getEditorPositionTop() {
|
_getEditorPositionTop() {
|
||||||
const editor = parent.document.getElementsByTagName('iframe');
|
const editor = document.getElementsByTagName('iframe');
|
||||||
const editorPositionTop = editor[0].offsetTop;
|
const editorPositionTop = editor[0].offsetTop;
|
||||||
return editorPositionTop;
|
return editorPositionTop;
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ep_etherpad-lite": "workspace:../src",
|
"ep_etherpad-lite": "workspace:../src",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.3"
|
"vite": "^5.4.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue