Merge branch 'develop'

This commit is contained in:
SamTV12345 2024-04-29 21:32:32 +02:00
commit 8fa708a374
54 changed files with 5925 additions and 4418 deletions

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
# editorconfig-tools is unable to ignore longs strings or urls
max_line_length = off
[CHANGELOG.md]
indent_size = 4
[*.bat]
end_of_line = crlf

View file

@ -4,15 +4,15 @@
# Always ensure to load the env variables in every terminal session.
# Otherwise the env variables will not be available
DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED=9001
DOCKER_COMPOSE_APP_DEV_PORT_TARGET=9001
DOCKER_COMPOSE_APP_PORT_PUBLISHED=9001
DOCKER_COMPOSE_APP_PORT_TARGET=9001
# IMPORTANT: When the env var DEFAULT_PAD_TEXT is unset or empty, then the pad is not established (not the landing page).
# The env var DEFAULT_PAD_TEXT seems to be mandatory in the latest version of etherpad.
DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT="Welcome to etherpad"
DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD=
DOCKER_COMPOSE_APP_ADMIN_PASSWORD=
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE=db
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD=etherpad-lite-password
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER=etherpad-lite-user
DOCKER_COMPOSE_POSTGRES_DATABASE=db
DOCKER_COMPOSE_POSTGRES_PASSWORD=etherpad-lite-password
DOCKER_COMPOSE_POSTGRES_USER=etherpad-lite-user

18
.env.dev.default Normal file
View file

@ -0,0 +1,18 @@
# Please copy and rename this file.
#
# !Attention!
# Always ensure to load the env variables in every terminal session.
# Otherwise the env variables will not be available
DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED=9001
DOCKER_COMPOSE_APP_DEV_PORT_TARGET=9001
# IMPORTANT: When the env var DEFAULT_PAD_TEXT is unset or empty, then the pad is not established (not the landing page).
# The env var DEFAULT_PAD_TEXT seems to be mandatory in the latest version of etherpad.
DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT="Welcome to etherpad"
DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD=
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE=db
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD=etherpad-lite-password
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER=etherpad-lite-user

View file

@ -36,7 +36,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -93,7 +93,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -163,7 +163,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -216,7 +216,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -38,7 +38,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -47,7 +47,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -37,7 +37,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -30,7 +30,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -96,7 +96,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -163,7 +163,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -32,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -76,7 +76,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -147,7 +147,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -32,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -38,7 +38,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Only install direct dependencies
run: pnpm config set auto-install-peers false

View file

@ -38,7 +38,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash
@ -132,7 +132,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 9.0.4
run_install: false
- name: Get pnpm store directory
shell: bash

View file

@ -1,3 +1,13 @@
# 2.0.3
### Notable enhancements and fixes
- Added documentation for replacing apikeys with oauth2
- Bumped live plugin manager to 0.20.0. Thanks to @fgreinacher
- Added better documentation for using docker-compose with Etherpad
# 2.0.2
### Notable enhancements and fixes
@ -25,7 +35,7 @@
- Socket io has been updated to 4.7.5. This means that the json.send function won't work anymore and needs to be changed to .emit('message', myObj)
- Deprecating npm version 6 in favor of pnpm: We have made the decision to switch to the well established pnpm (https://pnpm.io/). It works by symlinking dependencies into a global directory allowing you to have a cleaner and more reliable environment.
- Introducing Typescript to the Etherpad core: Etherpad core logic has been rewritten in Typescript allowing for compiler checking of errors.
- Rewritten Admin Panel: The Admin panel has been rewritten in React and now features a more pleasant user experience. It now also features an integrated pad searching with sorting functionality.
- Rewritten Admin Panel: The Admin panel has been rewritten in React and now features a more pleasant user experience. It now also features an integrated pad searching with sorting functionality.
### Notable enhancements and fixes
@ -35,7 +45,7 @@
* Enhancements
- pnpm Workspaces: In addition to pnpm we introduced workspaces. A clean way to manage multiple bounded contexts like the admin panel or the bin folder.
- Bin folder: The bin folder has been moved from the src folder to the root folder. This change was necessary as the contained scripts do not represent core functionality of the user.
- Starting Etherpad: Etherpad can now be started with a single command: `pnpm run prod` in the root directory.
- Starting Etherpad: Etherpad can now be started with a single command: `pnpm run prod` in the root directory.
- Installing Etherpad: Etherpad no longer symlinks itself in the root directory. This is now also taken care by pnpm, and it just creates a node_modules folder with the src directory`s ep_etherpad-lite folder
- Plugins can now be installed simply via the command: `pnpm run install-plugins first-plugin second-plugin` or if you want to install from path you can do:
`pnpm run install-plugins --path ../path-to-plugin`
@ -45,7 +55,7 @@
### Notable enhancements and fixes
* Added Live Plugin Manager: Plugins are now installed into a separate folder on the host system. This folder is called `plugin_packages`.
* Added Live Plugin Manager: Plugins are now installed into a separate folder on the host system. This folder is called `plugin_packages`.
That way the plugins are separated from the normal etherpad installation.
* Make repairPad.js more verbose
* Fixed favicon not being loaded correctly
@ -68,19 +78,19 @@ That way the plugins are separated from the normal etherpad installation.
### Notable enhancements and fixes
* The support for the tidy program to tidy up HTML files has been removed. This decision was made because it hasn't been updated for years and also caused an incompability when exporting a pad with Abiword.
* The support for the tidy program to tidy up HTML files has been removed. This decision was made because it hasn't been updated for years and also caused an incompability when exporting a pad with Abiword.
# 1.9.4
### Compatibility changes
* Log4js has been updated to the latest version. As it involved a bump of 6 major version.
* Log4js has been updated to the latest version. As it involved a bump of 6 major version.
A lot has changed since then. Most notably the console appender has been deprecated. You can find out more about it [here](https://github.com/log4js-node/log4js-node)
### Notable enhancements and fixes
* Fix for MySQL: The logger calls were incorrectly configured leading to a crash when e.g. somebody uses a different encoding than standard MySQL encoding.
* Fix for MySQL: The logger calls were incorrectly configured leading to a crash when e.g. somebody uses a different encoding than standard MySQL encoding.
# 1.9.3
@ -88,7 +98,7 @@ That way the plugins are separated from the normal etherpad installation.
* express-rate-limit has been bumped to 7.0.0: This involves the breaking change that "max: 0"
in the importExportRateLimiting is set to always trigger. So set it to your desired value.
If you haven't changed that value in the settings.json you are all set.
If you haven't changed that value in the settings.json you are all set.
### Notable enhancements and fixes
@ -107,7 +117,7 @@ If you haven't changed that value in the settings.json you are all set.
* Enable session key rotation: This setting can be enabled in the settings.json. It changes the signing key for the cookie authentication in a fixed interval.
* Bugfixes
* Fix appendRevision when creating a new pad via the API without a text.
* Fix appendRevision when creating a new pad via the API without a text.
* Enhancements
@ -116,11 +126,11 @@ If you haven't changed that value in the settings.json you are all set.
### Compatibility changes
* No compability changes as JQuery maintains excellent backwards compatibility.
* No compability changes as JQuery maintains excellent backwards compatibility.
#### For plugin authors
* Please update to JQuery 3.7. There is an excellent deprecation guide over [here](https://api.jquery.com/category/deprecated/). Version 3.1 to 3.7 are relevant for the upgrade.
* Please update to JQuery 3.7. There is an excellent deprecation guide over [here](https://api.jquery.com/category/deprecated/). Version 3.1 to 3.7 are relevant for the upgrade.
# 1.9.1
@ -128,7 +138,7 @@ If you haven't changed that value in the settings.json you are all set.
* Security
* Limit requested revisions in timeslider and export to head revision. (affects v1.9.0)
* Bugfixes
* revisions in `CHANGESET_REQ` (timeslider) and export (txt, html, custom)
are now checked to be numbers.
@ -142,7 +152,7 @@ If you haven't changed that value in the settings.json you are all set.
* tests: drop windows 7 test coverage & use chrome latest for admin tests
* Require Node 16 for Etherpad and target Node 20 for testing
# 1.9.0
### Notable enhancements and fixes

View file

@ -8,7 +8,7 @@ FROM node:alpine as adminBuild
WORKDIR /opt/etherpad-lite
COPY ./ ./
RUN cd ./admin && npm install -g pnpm && pnpm install && pnpm run build --outDir ./dist
RUN cd ./admin && npm install -g pnpm@9.0.4 && pnpm install && pnpm run build --outDir ./dist
RUN cd ./ui && pnpm install && pnpm run build --outDir ./dist
@ -91,7 +91,7 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
RUN \
mkdir -p /usr/share/man/man1 && \
npm install pnpm -g && \
npm install pnpm@9.0.4 -g && \
apk update && apk upgrade && \
apk add --no-cache \
ca-certificates \

View file

@ -43,6 +43,63 @@ We're looking for maintainers and have some funding available. Please contact J
## Installation
### Docker-Compose
```yaml
services:
app:
user: "0:0"
image: etherpad/etherpad:latest
tty: true
stdin_open: true
volumes:
- plugins:/opt/etherpad-lite/src/plugin_packages
- etherpad-var:/opt/etherpad-lite/var
depends_on:
- postgres
environment:
NODE_ENV: production
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
restart: always
ports:
- "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
PGDATA: /var/lib/postgresql/data/pgdata
restart: always
# Exposing the port is not needed unless you want to access this database instance from the host.
# Be careful when other postgres docker container are running on the same port
# ports:
# - "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/pgdata
volumes:
postgres_data:
plugins:
etherpad-var:
```
### Requirements
[Node.js](https://nodejs.org/) >= **18.18.2**.

View file

@ -1,7 +1,7 @@
{
"name": "admin",
"private": true,
"version": "2.0.2",
"version": "2.0.3",
"type": "module",
"scripts": {
"dev": "vite",
@ -9,31 +9,33 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {},
"dependencies": {
"@radix-ui/react-switch": "^1.0.3"
},
"devDependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-toast": "^1.1.5",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.1",
"lucide-react": "^0.365.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.2",
"react-i18next": "^14.1.0",
"react-router-dom": "^6.22.3",
"zustand": "^4.5.2",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"lucide-react": "^0.372.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.3",
"react-i18next": "^14.1.0",
"react-router-dom": "^6.22.3",
"socket.io-client": "^4.7.5",
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-static-copy": "^1.0.2",
"vite-plugin-svgr": "^4.2.0"
"typescript": "^5.4.5",
"vite": "^5.2.9",
"vite-plugin-static-copy": "^1.0.3",
"vite-plugin-svgr": "^4.2.0",
"zustand": "^4.5.2"
}
}

View file

@ -6,7 +6,7 @@ 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} from "lucide-react";
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall} from "lucide-react";
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
export const App = ()=> {
@ -96,8 +96,10 @@ export const App = ()=> {
<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={"/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>

View file

@ -0,0 +1,13 @@
export type ShoutType = {
type: string,
data:{
type: string,
payload: {
message: {
message: string,
sticky: boolean
},
timestamp: number
}
}
}

View file

@ -250,10 +250,18 @@ td, th {
color: #666;
}
.settings-page {
display: flex;
flex-direction: column;
gap: 20px;
height: 100%;
}
.settings {
flex-grow: max(1, 1);
outline: none;
width: 100%;
min-height: 80vh;
resize: none;
}
@ -596,6 +604,25 @@ pre {
outline: none;
}
.send-message {
position: relative;
}
.send-message input {
width: auto;
}
.send-message {
}
.send-message svg {
position: absolute;
right: 3px;
bottom: -3px;
left: auto !important;
}
.search-field svg {
position: absolute;
left: 3px;
@ -725,3 +752,52 @@ input, button, select, optgroup, textarea {
right: 10px;
color: #666;
}
.SwitchRoot {
align-self: center;
width: 60px;
height: 30px;
background-color: black;
border-radius: 9999px;
position: relative;
box-shadow: 0 2px 10px var(--black-a7);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.SwitchRoot:focus {
box-shadow: 0 0 0 2px black;
}
.SwitchRoot[data-state='checked'] {
background-color: var(--etherpad-color);
}
.SwitchThumb {
display: block;
width: 20px;
height: 20px;
background-color: white;
border-radius: 9999px;
box-shadow: 0 2px 2px var(--black-a7);
transition: transform 100ms;
transform: translateX(2px);
will-change: transform;
}
.SwitchThumb[data-state='checked'] {
transform: translateX(25px);
}
.Label {
color: white;
font-size: 15px;
line-height: 1;
}
.message {
position: relative;
padding: 10px;
border: 1px solid #e0e0e0;
margin: 10px 20px 10px 10px;
border-radius: 10px 0 10px 10px;
background-color: var(--etherpad-color);
color: white
}

View file

@ -12,6 +12,7 @@ import {I18nextProvider} from "react-i18next";
import i18n from "./localization/i18n.ts";
import {PadPage} from "./pages/PadPage.tsx";
import {ToastDialog} from "./utils/Toast.tsx";
import {ShoutPage} from "./pages/ShoutPage.tsx";
const router = createBrowserRouter(createRoutesFromElements(
<><Route element={<App/>}>
@ -20,6 +21,7 @@ const router = createBrowserRouter(createRoutesFromElements(
<Route path="/settings" element={<SettingsPage/>}/>
<Route path="/help" element={<HelpPage/>}/>
<Route path="/pads" element={<PadPage/>}/>
<Route path="/shout" element={<ShoutPage/>}/>
</Route><Route path="/login">
<Route index element={<LoginScreen/>}/>
</Route></>

View file

@ -21,7 +21,7 @@ export const HelpPage = () => {
return <div key={hookName+i}>
<h3>{hookName}</h3>
<ul>
{Object.keys(hooks[hookName]).map((hook, i) => <li>{hook}
{Object.keys(hooks[hookName]).map((hook, i) => <li key={hook+i}>{hook}
<ul key={hookName+hook+i}>
{Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)}
</ul>
@ -46,12 +46,12 @@ export const HelpPage = () => {
</div>
<h2><Trans i18nKey="admin_plugins.installed"/></h2>
<ul>
{helpData.installedPlugins.map((plugin, i) => <li key={i}>{plugin}</li>)}
{helpData.installedPlugins.map((plugin, i) => <li key={plugin+i}>{plugin}</li>)}
</ul>
<h2><Trans i18nKey="admin_plugins_info.parts"/></h2>
<ul>
{helpData.installedParts.map((part, i) => <li key={i}>{part}</li>)}
{helpData.installedParts.map((part, i) => <li key={part+i}>{part}</li>)}
</ul>
<h2><Trans i18nKey="admin_plugins_info.hooks"/></h2>

View file

@ -100,7 +100,15 @@ export const HomePage = () => {
pluginsSocket!.on('results:search', (data: {
results: PluginDef[]
}) => {
setPlugins(data.results)
if (Array.isArray(data.results) && data.results.length > 0) {
setPlugins(data.results)
} else {
useStore.getState().setToastState({
open: true,
title: "Error retrieving plugins",
success: false
})
}
})
@ -142,7 +150,7 @@ export const HomePage = () => {
<tbody style={{overflow: 'auto'}}>
{sortedInstalledPlugins.map((plugin, index) => {
return <tr key={index}>
<td>{plugin.name}</td>
<td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
<td>{plugin.version}</td>
<td>
{

View file

@ -8,7 +8,7 @@ export const SettingsPage = ()=>{
const settingsSocket = useStore(state=>state.settingsSocket)
const settings = useStore(state=>state.settings)
return <div>
return <div className="settings-page">
<h1><Trans i18nKey="admin_settings.current"/></h1>
<textarea value={settings} className="settings" onChange={v => {
useStore.getState().setSettings(v.target.value)

View file

@ -0,0 +1,76 @@
import {useEffect, useState} from "react";
import {SendHorizonal} from 'lucide-react'
import {useStore} from "../store/store.ts";
import * as Switch from '@radix-ui/react-switch';
import {ShoutType} from "../components/ShoutType.ts";
export const ShoutPage = ()=>{
const [totalUsers, setTotalUsers] = useState(0);
const [message, setMessage] = useState<string>("");
const [sticky, setSticky] = useState<boolean>(false);
const socket = useStore(state => state.settingsSocket);
const [shouts, setShouts] = useState<ShoutType[]>([]);
useEffect(() => {
fetch('/stats')
.then(response => response.json())
.then(data => setTotalUsers(data.totalUsers));
}, []);
useEffect(() => {
if(socket) {
socket.on('shout', (shout) => {
setShouts([...shouts, shout])
})
}
}, [socket, shouts])
const sendMessage = () => {
socket?.emit('shout', {
message,
sticky
});
setMessage('')
}
return (
<div>
<h1>Communication</h1>
{totalUsers > 0 && <p>There {totalUsers>1?"are":"is"} currently {totalUsers} user{totalUsers>1?"s":""} online</p>}
<div style={{height: '80vh', display: 'flex', flexDirection: 'column'}}>
<div style={{flexGrow: 1, backgroundColor: 'white', overflowY: "auto"}}>
{
shouts.map((shout) => {
return (
<div key={shout.data.payload.timestamp} className="message">
<div>{shout.data.payload.message.message}</div>
<div style={{display: 'flex'}}>
<div style={{flexGrow: 1}}></div>
<div
style={{color: "lightgray"}}>{new Date(shout.data.payload.timestamp).toLocaleTimeString()
+ " " + new Date(shout.data.payload.timestamp).toLocaleDateString()}</div>
</div>
</div>
)
})
}
</div>
<form onSubmit={(e) => {
e.preventDefault()
sendMessage()
}} className="send-message search-field" style={{display: 'flex', gap: '10px'}}>
<Switch.Root title="Change sticky message" className="SwitchRoot" checked={sticky}
onCheckedChange={() => {
setSticky(!sticky);
}}>
<Switch.Thumb className="SwitchThumb"/>
</Switch.Root>
<input required value={message} onChange={v=>setMessage(v.target.value)}
style={{width: '100%', paddingRight: '55px', backgroundColor: '#e0e0e0', flexGrow: 1}}/>
<SendHorizonal style={{bottom: '5px', right: '9px', color: '#0f775b'}} onClick={()=>sendMessage()}/>
</form>
</div>
</div>
)
}

View file

@ -28,8 +28,11 @@ export default defineConfig({
'/admin-auth/': {
target: 'http://localhost:9001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/')
},
'/stats': {
target: 'http://localhost:9001',
changeOrigin: true,
}
}
}
}
})

View file

@ -1,6 +1,6 @@
{
"name": "bin",
"version": "2.0.2",
"version": "2.0.3",
"description": "",
"main": "checkAllPads.js",
"directories": {
@ -15,9 +15,9 @@
"ueberdb2": "^4.2.63"
},
"devDependencies": {
"@types/node": "^20.12.5",
"@types/node": "^20.12.7",
"@types/semver": "^7.5.8",
"typescript": "^5.4.4"
"typescript": "^5.4.5"
},
"scripts": {
"checkPad": "node --import tsx checkPad.ts",

View file

@ -28,7 +28,7 @@ Portal maps the internal userid to an etherpad author.
#### Request
```http
GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7
GET /api/1/createAuthorIfNotExistsFor?name=Michael&authorMapper=7
```
@ -42,7 +42,7 @@ GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7
> Portal maps the internal userid to an etherpad group:
```http
GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=7
GET http://pad.domain/api/1/createGroupIfNotExistsFor?groupMapper=7
```
### Response
@ -56,7 +56,7 @@ GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=
#### Request
```http
GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad
GET http://pad.domain/api/1/createGroupPad?groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad
```
#### Response
@ -70,7 +70,7 @@ GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0
#### Request
```http
GET http://pad.domain/api/1/createSession?apikey=secret&groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246
GET http://pad.domain/api/1/createSession?groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246
```
### Response
@ -87,7 +87,7 @@ A portal (such as WordPress) wants to transform the contents of a pad that multi
Portal retrieves the contents of the pad for entry into the db as a blog post:
> Request: `http://pad.domain/api/1/getText?apikey=secret&padID=g.s8oes9dhwrvt0zif$123`
> Request: `http://pad.domain/api/1/getText?&padID=g.s8oes9dhwrvt0zif$123`
>
> Response: `{code: 0, message:"ok", data: {text:"Welcome Text"}}`
@ -108,23 +108,23 @@ The API is accessible via HTTP. Starting from **1.8**, API endpoints can be invo
The URL of the HTTP request is of the form: `/api/$APIVERSION/$FUNCTIONNAME`. $APIVERSION depends on the endpoint you want to use. Depending on the verb you use (GET or POST) **parameters** can be passed differently.
When invoking via GET (mandatory until **1.7.5** included), parameters must be included in the query string (example: `/api/$APIVERSION/$FUNCTIONNAME?apikey=<APIKEY>&param1=value1`). Please note that starting with nodejs 8.14+ the total size of HTTP request headers has been capped to 8192 bytes. This limits the quantity of data that can be sent in an API request.
When invoking via GET (mandatory until **1.7.5** included), parameters must be included in the query string (example: `/api/$APIVERSION/$FUNCTIONNAME?param1=value1`). Please note that starting with nodejs 8.14+ the total size of HTTP request headers has been capped to 8192 bytes. This limits the quantity of data that can be sent in an API request.
Starting from Etherpad **1.8** it is also possible to invoke the HTTP API via POST. In this case, querystring parameters will still be accepted, but **any parameter with the same name sent via POST will take precedence**. If you need to send large chunks of text (for example, for `setText()`) it is advisable to invoke via POST.
Example with cURL using GET (toy example, no encoding):
```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example"
curl "http://pad.domain/api/1/setText?padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example"
```
Example with cURL using GET (better example, encodes text):
```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --get --data-urlencode "text=Text sent via GET with proper encoding. For big documents, please use POST"
curl "http://pad.domain/api/1/setText?padID=padname" --get --data-urlencode "text=Text sent via GET with proper encoding. For big documents, please use POST"
```
Example with cURL using POST:
```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --data-urlencode "text=Text sent via POST with proper encoding. For big texts (>8 KB), use this method"
curl "http://pad.domain/api/1/setText?padID=padname" --data-urlencode "text=Text sent via POST with proper encoding. For big texts (>8 KB), use this method"
```
### Response Format
@ -161,7 +161,49 @@ Responses are valid JSON in the following format:
### Authentication
Authentication works via a token that is sent with each request as a post parameter. There is a single token per Etherpad deployment. This token will be random string, generated by Etherpad at the first start. It will be saved in APIKEY.txt in the root folder of Etherpad. Only Etherpad and the requesting application knows this key. Token management will not be exposed through this API.
Authentication works via an OAuth token that is sent with each request as an Authorization header, i.e. `Authorization: Bearer YOUR_TOKEN`. You can add new clients that can sign in via the API by adding new entries to the sso section in the settings.json.
#### Example for browser login clients
This example illustrates how to add a new client that can sign in via the API using the browser login method. This method is used for users trying to sign in to the API via the browser. You can log in with the users in the settings.json file. The redirect URI is the URL where the user is redirected after the login. This is normally your etherpad instance url.
```json
{
"client_id": "admin_client",
"client_secret": "admin",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"redirect_uris": ["http://my-etherpad-instance.com"],
}
```
#### Example for services
This example illustrates how to add a new client that can sign in via the API using the client credentials method. This method is used for services trying to sign in to the API where there is no browser.
E.g. a service that creates a pad for a user or a service that inserts a text into a pad. Just make sure that the secret is complex enough as anybody who knows the secret can access the API.
```json
{
"client_id": "client_credentials",
"redirect_uris": [],
"response_types": [],
"grant_types": ["code"],
"client_secret": "client_credentials",
"extraParams": [
{
"name": "admin",
"value": "true"
}
]
}
```
Obtain a Bearer token:
`curl --request POST --url 'https://your.server.tld/oidc/token' --header 'content-type: application/x-www-form-urlencoded' --data grant_type=client_credentials --data client_id=client_credentials --data client_secret=client_credentials`
### Node Interoperability

View file

@ -278,58 +278,43 @@ docker run -d \
## Ready to use Docker Compose
```yaml
version: "3.8"
# Add this file to extend the docker-compose setup, e.g.:
# docker-compose build --no-cache
# docker-compose up -d --build --force-recreate
services:
app:
build:
context: .
args:
ETHERPAD_PLUGINS:
# change from development to production if needed
target: development
user: "0:0"
image: etherpad/etherpad:latest
tty: true
stdin_open: true
volumes:
# no volume mapping of node_modules as otherwise the build-time installed plugins will be overwritten with the mount
# the same applies to package.json and pnpm-lock.yaml in root dir as these would also get overwritten and build time installed plugins will be removed
- ./src:/opt/etherpad-lite/src
- ./bin:/opt/etherpad-lite/bin
- plugins:/opt/etherpad-lite/src/plugin_packages
- etherpad-var:/opt/etherpad-lite/var
depends_on:
- postgres
environment:
# change from development to production if needed
NODE_ENV: development
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DEV_ENV_DB_CHARSET:-utf8mb4}
NODE_ENV: production
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DEV_ENV_DISABLE_IP_LOGGING:-true}
SOFFICE: ${DOCKER_COMPOSE_APP_DEV_ENV_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_DEV_ENV_TRUST_PROXY:-true}
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
restart: always
ports:
- "${DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_DEV_PORT_TARGET:-9001}"
- "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"
postgres:
image: postgres:15-alpine
# Pass config parameters to the mysql server.
# Find more information below when you need to generate the ssl-relevant file your self
environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
PGDATA: /var/lib/postgresql/data/pgdata
restart: always
# Exposing the port is not needed unless you want to access this database instance from the host.
@ -341,4 +326,6 @@ services:
volumes:
postgres_data:
plugins:
etherpad-var:
```

View file

@ -1,6 +1,6 @@
{
"devDependencies": {
"vitepress": "^1.0.2"
"vitepress": "^1.1.3"
},
"scripts": {
"docs:dev": "vitepress dev",

63
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,63 @@
version: "3.8"
# Add this file to extend the docker-compose setup, e.g.:
# docker-compose build --no-cache
# docker-compose up -d --build --force-recreate
services:
app:
build:
context: .
args:
ETHERPAD_PLUGINS:
# change from development to production if needed
target: development
tty: true
stdin_open: true
volumes:
# no volume mapping of node_modules as otherwise the build-time installed plugins will be overwritten with the mount
# the same applies to package.json and pnpm-lock.yaml in root dir as these would also get overwritten and build time installed plugins will be removed
- ./src:/opt/etherpad-lite/src
- ./bin:/opt/etherpad-lite/bin
depends_on:
- postgres
environment:
# change from development to production if needed
NODE_ENV: development
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DEV_ENV_DB_CHARSET:-utf8mb4}
DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DEV_ENV_DISABLE_IP_LOGGING:-true}
SOFFICE: ${DOCKER_COMPOSE_APP_DEV_ENV_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_DEV_ENV_TRUST_PROXY:-true}
restart: always
ports:
- "${DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_DEV_PORT_TARGET:-9001}"
postgres:
image: postgres:15-alpine
# Pass config parameters to the mysql server.
# Find more information below when you need to generate the ssl-relevant file your self
environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
PGDATA: /var/lib/postgresql/data/pgdata
restart: always
# Exposing the port is not needed unless you want to access this database instance from the host.
# Be careful when other postgres docker container are running on the same port
# ports:
# - "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/pgdata
volumes:
postgres_data:

View file

@ -1,55 +1,40 @@
version: "3.8"
# Add this file to extend the docker-compose setup, e.g.:
# docker-compose build --no-cache
# docker-compose up -d --build --force-recreate
services:
app:
build:
context: .
args:
ETHERPAD_PLUGINS:
# change from development to production if needed
target: development
user: "0:0"
image: etherpad/etherpad:latest
tty: true
stdin_open: true
volumes:
# no volume mapping of node_modules as otherwise the build-time installed plugins will be overwritten with the mount
# the same applies to package.json and pnpm-lock.yaml in root dir as these would also get overwritten and build time installed plugins will be removed
- ./src:/opt/etherpad-lite/src
- ./bin:/opt/etherpad-lite/bin
- plugins:/opt/etherpad-lite/src/plugin_packages
- etherpad-var:/opt/etherpad-lite/var
depends_on:
- postgres
environment:
# change from development to production if needed
NODE_ENV: development
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DEV_ENV_DB_CHARSET:-utf8mb4}
NODE_ENV: production
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DEV_ENV_DISABLE_IP_LOGGING:-true}
SOFFICE: ${DOCKER_COMPOSE_APP_DEV_ENV_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_DEV_ENV_TRUST_PROXY:-true}
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
restart: always
ports:
- "${DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_DEV_PORT_TARGET:-9001}"
- "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"
postgres:
image: postgres:15-alpine
# Pass config parameters to the mysql server.
# Find more information below when you need to generate the ssl-relevant file your self
environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
PGDATA: /var/lib/postgresql/data/pgdata
restart: always
# Exposing the port is not needed unless you want to access this database instance from the host.
@ -60,4 +45,6 @@ services:
- postgres_data:/var/lib/postgresql/data/pgdata
volumes:
postgres_data:
postgres_data:
plugins:
etherpad-var:

View file

@ -43,6 +43,6 @@
"type": "git",
"url": "https://github.com/ether/etherpad-lite.git"
},
"version": "2.0.2",
"version": "2.0.3",
"license": "Apache-2.0"
}

File diff suppressed because it is too large Load diff

View file

@ -669,5 +669,16 @@
"redirect_uris": ["${USER_REDIRECT:http://localhost:9001/}"]
}
]
}
},
/* Set the time to live for the tokens
This is the time of seconds a user is logged into Etherpad
"ttl": {
"AccessToken": 3600,
"AuthorizationCode": 600,
"ClientCredentials": 3600,
"IdToken": 3600,
"RefreshToken": 86400
}
*/
}

View file

@ -671,4 +671,15 @@
}
]
}
/* Set the time to live for the tokens
This is the time of seconds a user is logged into Etherpad
"ttl": {
"AccessToken": 3600,
"AuthorizationCode": 600,
"ClientCredentials": 3600,
"IdToken": 3600,
"RefreshToken": 86400
}
*/
}

View file

@ -112,6 +112,12 @@
"hooks": {
"expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
}
},
{
"name": "ep_message_all",
"client_hooks": {
"handleClientMessage_shoutMessage": "ep_etherpad-lite/static/js/messageHandler"
}
}
]
}

View file

@ -8,6 +8,7 @@
"MuratTheTurkish",
"Mushviq Abdulla",
"NMW03",
"Nemoralis",
"Neriman2003",
"Vesely35",
"Wertuose"

View file

@ -55,10 +55,10 @@
"pad.toolbar.import_export.title": "Файлланы башха форматларын (а/дан) импорт/экспорт",
"pad.toolbar.timeslider.title": "Заман шкала",
"pad.toolbar.savedRevision.title": "Версияны сакъла",
"pad.toolbar.settings.title": "Джарашдырыула",
"pad.toolbar.settings.title": "Джарашдырыўла",
"pad.toolbar.embed.title": "Бу блокнотну Джай эмда Ичине сал",
"pad.toolbar.showusers.title": "Хайырланыучуланы бу блокнотда кёргюзт",
"pad.colorpicker.save": "Сакъла",
"pad.colorpicker.save": "Сакъландыр",
"pad.colorpicker.cancel": "Ызына ал",
"pad.loading": "Джюклениу...",
"pad.noCookie": "Куки табылмадыла. Бразуеригизде кукилени бир джандырсагъыз! Сизни кириулеригизни арасында сессиягъыз эмда джарашдырыуларыгъыз сакъланныкъ тюлдюле. Буну чуруму, бир къауум браузерледе Etherpad iFrame ичинде болгъаны болургъа болур. Тилейбиз, Etherpad эмда аны башындагъы iFrame бир тюбдоменде/доменде болгъанындан ишексиз болугъуз.",
@ -90,7 +90,7 @@
"pad.modals.reconnecting": "Блокнотугъузгъа джангыдан байлана турады...",
"pad.modals.forcereconnect": "Джангыдан зор бла байланыу",
"pad.modals.reconnecttimer": "Джангыдан байланыргъа кюрешеди",
"pad.modals.cancel": "Ызына алыу",
"pad.modals.cancel": "Ызына ал",
"pad.modals.userdup": "Башха терезеде ачыкъды",
"pad.modals.userdup.explanation": "Бу блокнот, бу компьютерде бирден аслам бразуре терезеде ачылгъаннга ушайды.",
"pad.modals.userdup.advice": "Бу терезени хайырланыб джангыдан байлан",

View file

@ -73,7 +73,7 @@
"pad.settings.chatandusers": "Prikaži klepet in uporabnike",
"pad.settings.colorcheck": "Barve avtorstva",
"pad.settings.linenocheck": "Številke vrstic",
"pad.settings.rtlcheck": "Ali naj se vsebina bera od desne proti levi?",
"pad.settings.rtlcheck": "Ali naj se vsebina bere od desne proti levi?",
"pad.settings.fontType": "Vrsta pisave:",
"pad.settings.fontType.normal": "Normalno",
"pad.settings.language": "Jezik:",

View file

@ -88,7 +88,7 @@ exports.socketio = () => {
const sessioninfos:MapArrayType<any> = {};
exports.sessioninfos = sessioninfos;
stats.gauge('totalUsers', () => socketio ? socketio.sockets.size : 0);
stats.gauge('totalUsers', () => socketio ? socketio.engine.clientsCount : 0);
stats.gauge('activePads', () => {
const padIds = new Set();
for (const {padId} of Object.values(sessioninfos)) {

View file

@ -87,6 +87,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
});
});
socket.on('uninstall', (pluginName:string) => {
uninstall(pluginName, (err:ErrorCaused) => {
if (err) console.warn(err.stack || err.toString());

View file

@ -17,195 +17,217 @@ const api = require('../../db/API');
const queryPadLimit = 12;
exports.socketio = (hookName:string, {io}:any) => {
io.of('/settings').on('connection', (socket: any ) => {
// @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
if (!isAdmin) return;
exports.socketio = (hookName: string, {io}: any) => {
io.of('/settings').on('connection', (socket: any) => {
// @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
if (!isAdmin) return;
socket.on('load', async (query:string):Promise<any> => {
let data;
try {
data = await fsp.readFile(settings.settingsFilename, 'utf8');
} catch (err) {
return console.log(err);
}
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
if (settings.showSettingsInAdminPage === false) {
socket.emit('settings', {results: 'NOT_ALLOWED'});
} else {
socket.emit('settings', {results: data});
}
});
socket.on('saveSettings', async (newSettings:string) => {
console.log('Admin request to save settings through a socket on /admin/settings');
await fsp.writeFile(settings.settingsFilename, newSettings);
socket.emit('saveprogress', 'saved');
});
socket.on('help', ()=> {
const gitCommit = settings.getGitCommit();
const epVersion = settings.getEpVersion();
const hooks:Map<string, Map<string,string>> = plugins.getHooks('hooks', false);
const clientHooks:Map<string, Map<string,string>> = plugins.getHooks('client_hooks', false);
function mapToObject(map: Map<string,any>) {
let obj = Object.create(null);
for (let [k,v] of map) {
if(v instanceof Map) {
obj[k] = mapToObject(v);
} else {
obj[k] = v;
}
}
return obj;
}
socket.emit('reply:help', {
gitCommit,
epVersion,
installedPlugins: plugins.getPlugins(),
installedParts: plugins.getParts(),
installedServerHooks: mapToObject(hooks),
installedClientHooks: mapToObject(clientHooks),
latestVersion: UpdateCheck.getLatestVersion(),
})
});
socket.on('padLoad', async (query: PadSearchQuery) => {
const {padIDs} = await padManager.listAllPads();
const data:{
total: number,
results?: PadQueryResult[]
} = {
total: padIDs.length,
};
let result: string[] = padIDs;
let maxResult;
// Filter out matches
if (query.pattern) {
result = result.filter((padName: string) => padName.includes(query.pattern));
}
data.total = result.length;
maxResult = result.length - 1;
if (maxResult < 0) {
maxResult = 0;
}
if (query.offset && query.offset < 0) {
query.offset = 0;
} else if (query.offset > maxResult) {
query.offset = maxResult;
}
if (query.limit && query.limit < 0) {
query.limit = 0;
} else if (query.limit > queryPadLimit) {
query.limit = queryPadLimit;
}
if (query.sortBy === 'padName') {
result = result.sort((a,b)=>{
if(a < b) return query.ascending ? -1 : 1;
if(a > b) return query.ascending ? 1 : -1;
return 0;
}).slice(query.offset, query.offset + query.limit);
data.results = await Promise.all(result.map(async (padName: string) => {
const pad = await padManager.getPad(padName);
const revisionNumber = pad.getHeadRevisionNumber()
const userCount = api.padUsersCount(padName).padUsersCount;
const lastEdited = await pad.getLastEdit();
return {
padName,
lastEdited,
userCount,
revisionNumber
}}));
} else {
const currentWinners: PadQueryResult[] = []
let queryOffsetCounter = 0
for (let res of result) {
const pad = await padManager.getPad(res);
const padType = {
padName: res,
lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber()
};
if (currentWinners.length < query.limit) {
if(queryOffsetCounter < query.offset){
queryOffsetCounter++
continue
socket.on('load', async (query: string): Promise<any> => {
let data;
try {
data = await fsp.readFile(settings.settingsFilename, 'utf8');
} catch (err) {
return console.log(err);
}
currentWinners.push({
padName: res,
lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber()
})
} else {
// Kick out worst pad and replace by current pad
let worstPad = currentWinners.sort((a, b) => {
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
return 0;
})
if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){
if(queryOffsetCounter < query.offset){
queryOffsetCounter++
continue
}
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
currentWinners.push({
padName: res,
lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber()
})
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
if (settings.showSettingsInAdminPage === false) {
socket.emit('settings', {results: 'NOT_ALLOWED'});
} else {
socket.emit('settings', {results: data});
}
}
});
socket.on('saveSettings', async (newSettings: string) => {
console.log('Admin request to save settings through a socket on /admin/settings');
await fsp.writeFile(settings.settingsFilename, newSettings);
socket.emit('saveprogress', 'saved');
});
type ShoutMessage = {
message: string,
sticky: boolean,
}
data.results = currentWinners;
}
socket.emit('results:padLoad', data);
})
socket.on('shout', (message: ShoutMessage) => {
const messageToSend = {
type: "COLLABROOM",
data: {
type: "shoutMessage",
payload: {
message: message,
timestamp: Date.now()
}
}
}
io.of('/settings').emit('shout', messageToSend);
io.sockets.emit('shout', messageToSend);
})
socket.on('deletePad', async (padId: string) => {
const padExists = await padManager.doesPadExists(padId);
if (padExists) {
const pad = await padManager.getPad(padId);
await pad.remove();
socket.emit('results:deletePad', padId);
}
})
socket.on('help', () => {
const gitCommit = settings.getGitCommit();
const epVersion = settings.getEpVersion();
socket.on('restartServer', async () => {
console.log('Admin request to restart server through a socket on /admin/settings');
settings.reloadSettings();
await plugins.update();
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('restartServer');
const hooks: Map<string, Map<string, string>> = plugins.getHooks('hooks', false);
const clientHooks: Map<string, Map<string, string>> = plugins.getHooks('client_hooks', false);
function mapToObject(map: Map<string, any>) {
let obj = Object.create(null);
for (let [k, v] of map) {
if (v instanceof Map) {
obj[k] = mapToObject(v);
} else {
obj[k] = v;
}
}
return obj;
}
socket.emit('reply:help', {
gitCommit,
epVersion,
installedPlugins: plugins.getPlugins(),
installedParts: plugins.getParts(),
installedServerHooks: mapToObject(hooks),
installedClientHooks: mapToObject(clientHooks),
latestVersion: UpdateCheck.getLatestVersion(),
})
});
socket.on('padLoad', async (query: PadSearchQuery) => {
const {padIDs} = await padManager.listAllPads();
const data: {
total: number,
results?: PadQueryResult[]
} = {
total: padIDs.length,
};
let result: string[] = padIDs;
let maxResult;
// Filter out matches
if (query.pattern) {
result = result.filter((padName: string) => padName.includes(query.pattern));
}
data.total = result.length;
maxResult = result.length - 1;
if (maxResult < 0) {
maxResult = 0;
}
if (query.offset && query.offset < 0) {
query.offset = 0;
} else if (query.offset > maxResult) {
query.offset = maxResult;
}
if (query.limit && query.limit < 0) {
query.limit = 0;
} else if (query.limit > queryPadLimit) {
query.limit = queryPadLimit;
}
if (query.sortBy === 'padName') {
result = result.sort((a, b) => {
if (a < b) return query.ascending ? -1 : 1;
if (a > b) return query.ascending ? 1 : -1;
return 0;
}).slice(query.offset, query.offset + query.limit);
data.results = await Promise.all(result.map(async (padName: string) => {
const pad = await padManager.getPad(padName);
const revisionNumber = pad.getHeadRevisionNumber()
const userCount = api.padUsersCount(padName).padUsersCount;
const lastEdited = await pad.getLastEdit();
return {
padName,
lastEdited,
userCount,
revisionNumber
}
}));
} else {
const currentWinners: PadQueryResult[] = []
let queryOffsetCounter = 0
for (let res of result) {
const pad = await padManager.getPad(res);
const padType = {
padName: res,
lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber()
};
if (currentWinners.length < query.limit) {
if (queryOffsetCounter < query.offset) {
queryOffsetCounter++
continue
}
currentWinners.push({
padName: res,
lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber()
})
} else {
// Kick out worst pad and replace by current pad
let worstPad = currentWinners.sort((a, b) => {
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
return 0;
})
if (worstPad[0] && worstPad[0][query.sortBy] < padType[query.sortBy]) {
if (queryOffsetCounter < query.offset) {
queryOffsetCounter++
continue
}
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
currentWinners.push({
padName: res,
lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber()
})
}
}
}
data.results = currentWinners;
}
socket.emit('results:padLoad', data);
})
socket.on('deletePad', async (padId: string) => {
const padExists = await padManager.doesPadExists(padId);
if (padExists) {
const pad = await padManager.getPad(padId);
await pad.remove();
socket.emit('results:deletePad', padId);
}
})
socket.on('restartServer', async () => {
console.log('Admin request to restart server through a socket on /admin/settings');
settings.reloadSettings();
await plugins.update();
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('restartServer');
});
});
});
};
const searchPad = async (query:PadSearchQuery) => {
const searchPad = async (query: PadSearchQuery) => {
}

View file

@ -9,6 +9,7 @@ import express, {Request, Response} from 'express';
import {format} from 'url'
import {ParsedUrlQuery} from "node:querystring";
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
import {MapArrayType} from "../types/MapType";
const configuration: Configuration = {
scopes: ['openid', 'profile', 'email'],
@ -19,7 +20,6 @@ const configuration: Configuration = {
is_admin: boolean;
}
}
const usersArray1 = Object.keys(users).map((username) => ({
username,
...users[username]
@ -47,13 +47,7 @@ const configuration: Configuration = {
} as Account
}
},
ttl:{
AccessToken: 1 * 60 * 60, // 1 hour in seconds
AuthorizationCode: 10 * 60, // 10 minutes in seconds
ClientCredentials: 1 * 60 * 60, // 1 hour in seconds
IdToken: 1 * 60 * 60, // 1 hour in seconds
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
},
ttl: settings.ttl,
claims: {
openid: ['sub'],
email: ['email'],
@ -99,28 +93,29 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
features:{
userinfo: {enabled: true},
claimsParameter: {enabled: true},
clientCredentials: {enabled: true},
devInteractions: {enabled: false},
resourceIndicators: {enabled: true, defaultResource(ctx) {
return ctx.origin;
},
getResourceServerInfo(ctx, resourceIndicator, client) {
return {
scope: client.scope as string,
scope: "openid",
audience: 'account',
accessTokenFormat: 'jwt',
};
},
useGrantedResource(ctx, model) {
return true;
},},
},
},
jwtResponseModes: {enabled: true},
},
clientBasedCORS: (ctx, origin, client) => {
return true
},
extraParams: [],
extraTokenClaims: async (ctx, token) => {
if(token.kind === 'AccessToken') {
// Add your custom claims here. For example:
const users = settings.users as {
@ -139,6 +134,19 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
return {
admin: account?.is_admin
};
} else if (token.kind === "ClientCredentials") {
let extraParams: MapArrayType<string> = {}
settings.sso.clients
.filter((client:any) => client.client_id === token.clientId)
.forEach((client:any) => {
if(client.extraParams !== undefined) {
client.extraParams.forEach((param:any) => {
extraParams[param.name] = param.value
})
}
})
return extraParams
}
},
clients: settings.sso.clients
@ -252,7 +260,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24}));
/*
oidc.on('authorization.error', (ctx, error) => {
console.log('authorization.error', error);
})
@ -268,7 +276,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
})
oidc.on('revocation.error', (ctx, error) => {
console.log('revocation.error', error);
})*/
})
args.app.use("/oidc", oidc.callback());
//cb();
}

View file

@ -27,6 +27,7 @@ import {ErrorCaused} from "./types/ErrorCaused";
import log4js from 'log4js';
import pkg from '../package.json';
import {checkForMigration} from "../static/js/pluginfw/installer";
import axios from "axios";
const settings = require('./utils/Settings');
@ -37,6 +38,28 @@ if (settings.dumpOnUncleanExit) {
wtfnode = require('wtfnode');
}
const addProxyToAxios = (url: URL) => {
axios.defaults.proxy = {
host: url.hostname,
port: Number(url.port),
protocol: url.protocol,
}
}
if(process.env['http_proxy']) {
console.log("Using proxy: " + process.env['http_proxy'])
addProxyToAxios(new URL(process.env['http_proxy']));
}
if (process.env['https_proxy']) {
console.log("Using proxy: " + process.env['https_proxy'])
addProxyToAxios(new URL(process.env['https_proxy']));
}
/*
* early check for version compatibility before calling
* any modules that require newer versions of NodeJS

View file

@ -98,6 +98,16 @@ exports.title = 'Etherpad';
*/
exports.favicon = null;
exports.ttl = {
AccessToken: 1 * 60 * 60, // 1 hour in seconds
AuthorizationCode: 10 * 60, // 10 minutes in seconds
ClientCredentials: 1 * 60 * 60, // 1 hour in seconds
IdToken: 1 * 60 * 60, // 1 hour in seconds
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
}
/*
* Skin name.
*

View file

@ -1,7 +1,7 @@
'use strict';
const semver = require('semver');
const settings = require('./Settings');
const axios = require('axios');
import axios from 'axios';
const headers = {
'User-Agent': 'Etherpad/' + settings.getEpVersion(),
}
@ -17,7 +17,7 @@ let lastLoadingTime: number | null = null;
const loadEtherpadInformations = () => {
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
return Promise.resolve(infos);
return infos;
}
return axios.get('https://static.etherpad.org/info.json', {headers: headers})
@ -29,10 +29,10 @@ const loadEtherpadInformations = () => {
}
lastLoadingTime = Date.now();
return await Promise.resolve(infos);
return infos;
})
.catch(async (err: Error) => {
return await Promise.reject(err);
throw err;
});
}
@ -43,15 +43,15 @@ exports.getLatestVersion = () => {
};
exports.needsUpdate = async (cb?: Function) => {
await loadEtherpadInformations()
.then((info:Infos) => {
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
try {
const info = await loadEtherpadInformations()
if (semver.gt(info!.latestVersion, settings.getEpVersion())) {
if (cb) return cb(true);
}
}).catch((err: Error) => {
} catch (err) {
console.error(`Can not perform Etherpad update check: ${err}`);
if (cb) return cb(false);
});
}
};
exports.check = () => {

View file

@ -35,7 +35,7 @@
"clean-css": "^5.3.3",
"cookie-parser": "^1.4.6",
"cross-spawn": "^7.0.3",
"ejs": "^3.1.9",
"ejs": "^3.1.10",
"etherpad-require-kernel": "^1.0.16",
"etherpad-yajsml": "0.0.12",
"express": "4.19.2",
@ -51,7 +51,7 @@
"jsonminify": "0.4.2",
"jsonwebtoken": "^9.0.2",
"languages4translatewiki": "0.1.3",
"live-plugin-manager": "^0.19.0",
"live-plugin-manager": "^0.20.0",
"lodash.clonedeep": "4.5.0",
"log4js": "^6.9.1",
"measured-core": "^2.0.0",
@ -83,7 +83,7 @@
"etherpad-lite": "node/server.ts"
},
"devDependencies": {
"@playwright/test": "^1.43.0",
"@playwright/test": "^1.43.1",
"@types/async": "^3.2.24",
"@types/express": "^4.17.21",
"@types/formidable": "^3.4.5",
@ -91,7 +91,7 @@
"@types/jsdom": "^21.1.6",
"@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.6",
"@types/node": "^20.12.5",
"@types/node": "^20.12.7",
"@types/oidc-provider": "^8.4.4",
"@types/semver": "^7.5.8",
"@types/sinon": "^17.0.3",
@ -108,7 +108,7 @@
"sinon": "^17.0.1",
"split-grid": "^1.0.11",
"supertest": "^6.3.4",
"typescript": "^5.4.4"
"typescript": "^5.4.5"
},
"engines": {
"node": ">=18.18.2",
@ -132,6 +132,6 @@
"test-admin": "npx playwright test tests/frontend-new/admin-spec --workers 1",
"test-admin:ui": "npx playwright test tests/frontend-new/admin-spec --ui --workers 1"
},
"version": "2.0.2",
"version": "2.0.3",
"license": "Apache-2.0"
}

View file

@ -534,7 +534,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// As the concept of parent's doesn't exist when processing each domline...
}
} else {
// Below needs more testin if it's neccesary as _exitList should take care of this.
// Below needs more testin if it's necessary as _exitList should take care of this.
// delete state.start;
// delete state.listNesting;
// _recalcAttribString(state);

View file

@ -167,7 +167,7 @@ const getUrlVars = () => new URL(window.location.href).searchParams;
const sendClientReady = (isReconnect) => {
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces
// unescape necessary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId);
if (!isReconnect) {
@ -213,7 +213,7 @@ const sendClientReady = (isReconnect) => {
const handshake = async () => {
let receivedClientVars = false;
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces
// unescape necessary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId);
// padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear
@ -250,11 +250,27 @@ const handshake = async () => {
socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
if (reason !== 'io server disconnect') return;
console.log(`Socket disconnected: ${reason}`)
if (reason !== 'io server disconnect' || reason !== 'ping timeout') return;
socketReconnecting();
socket.connect();
});
socket.on('shout', (obj) => {
if(obj.type === "COLLABROOM") {
let date = new Date(obj.data.payload.timestamp);
$.gritter.add({
// (string | mandatory) the heading of the notification
title: 'Admin message',
// (string | mandatory) the text inside the notification
text: '[' + date.toLocaleTimeString() + ']: ' + obj.data.payload.message.message,
// (bool | optional) if you want it to fade out on its own or just sit there
sticky: obj.data.payload.message.sticky
});
}
})
socket.on('reconnecting', socketReconnecting);
socket.on('reconnect_failed', (error) => {

View file

@ -225,7 +225,7 @@ exports.callAll = (hookName, context) => {
// provides settles (resolves or rejects). If a hook function attempts to settle again (e.g., call
// the callback again, or return a value and also call the callback) then the second attempt has no
// effect except either an error message is logged or an Error object is thrown depending on whether
// the the subsequent attempt is a duplicate (same value or error) or different, respectively.
// the subsequent attempt is a duplicate (same value or error) or different, respectively.
//
// See the tests in src/tests/backend/specs/hooks.js for examples of supported and prohibited
// behaviors.

View file

@ -60,6 +60,7 @@ const migratePluginsFromNodeModules = async () => {
const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-production'];
const [{dependencies = {}}] = JSON.parse(await runCmd(cmd,
{stdio: [null, 'string']}));
await Promise.all(Object.entries(dependencies)
.filter(([pkg, info]) => pkg.startsWith(plugins.prefix) && pkg !== 'ep_etherpad-lite')
.map(async ([pkg, info]) => {

View file

@ -10,7 +10,7 @@ test('Shows troubleshooting page manager', async ({page}) => {
await page.goto('http://localhost:9001/admin/help')
await page.waitForSelector('.menu')
const menu = page.locator('.menu');
await expect(menu.locator('li')).toHaveCount(4);
await expect(menu.locator('li')).toHaveCount(5);
})
test('Shows a version number', async function ({page}) {

View file

@ -9,7 +9,7 @@
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.4.4",
"vite": "^5.2.8"
"typescript": "^5.4.5",
"vite": "^5.2.9"
}
}