Merge branch 'develop'

This commit is contained in:
SamTV12345 2024-09-05 18:55:46 +02:00
commit dd83164b22
159 changed files with 4971 additions and 24500 deletions

View file

@ -69,6 +69,9 @@ jobs:
-
name: Run the backend tests
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
withpluginsLinux:
# run on pushes to any branch
@ -142,6 +145,9 @@ jobs:
-
name: Run the backend tests
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
withoutpluginsWindows:
# run on pushes to any branch
@ -193,7 +199,11 @@ jobs:
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
-
name: Run the backend tests
run: cd src && pnpm test
working-directory: src
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
withpluginsWindows:
# run on pushes to any branch
@ -273,4 +283,8 @@ jobs:
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
-
name: Run the backend tests
run: cd src && pnpm test
working-directory: src
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest

View file

@ -50,7 +50,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Cache playwright binaries
uses: actions/cache@v3
uses: actions/cache@v4
id: playwright-cache
with:
path: |

View file

@ -57,7 +57,7 @@ jobs:
name: Create settings.json
run: cp ./src/tests/settings.json settings.json
- name: Cache playwright binaries
uses: actions/cache@v3
uses: actions/cache@v4
id: playwright-cache
with:
path: |
@ -127,7 +127,7 @@ jobs:
- name: Create settings.json
run: cp ./src/tests/settings.json settings.json
- name: Cache playwright binaries
uses: actions/cache@v3
uses: actions/cache@v4
id: playwright-cache
with:
path: |
@ -175,7 +175,7 @@ jobs:
with:
node-version: 22
- name: Cache playwright binaries
uses: actions/cache@v3
uses: actions/cache@v4
id: playwright-cache
with:
path: |

View file

@ -1,3 +1,14 @@
# 2.2.3
### Notable enhancements and fixes
- Introduced a new in process database `rustydb` that represents a fast key value store written in Rust.
- Readded window._ as a shortcut for getting text
- Added support for migrating any ueberdb database to another. You can now switch as you please. See here: https://docs.etherpad.org/cli.html
- Further Typescript movements
- A lot of security issues fixed and reviewed in this release. Please update.
# 2.2.2
### Notable enhancements and fixes

View file

@ -49,6 +49,14 @@ ARG ETHERPAD_PLUGINS=
# ETHERPAD_LOCAL_PLUGINS="../ep_my_plugin ../ep_another_plugin"
ARG ETHERPAD_LOCAL_PLUGINS=
# github plugins to install while building the container. By default no plugins are
# installed.
# If given a value, it has to be a space-separated, quoted list of plugin names.
#
# EXAMPLE:
# ETHERPAD_GITHUB_PLUGINS="ether/ep_plugin"
ARG ETHERPAD_GITHUB_PLUGINS=
# Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats.
# By default, it is not installed.
# If given any value, abiword will be installed.
@ -114,13 +122,13 @@ COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./
FROM build AS development
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/
COPY --chown=etherpad:etherpad ./src/ ./src/
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/ templates/admin./src/templates/admin
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
RUN bin/installDeps.sh && \
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
fi
@ -134,11 +142,10 @@ COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/template
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
fi
# Copy the configuration file.
COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json

View file

@ -1,7 +1,7 @@
{
"name": "admin",
"private": true,
"version": "2.2.2",
"version": "2.2.3",
"type": "module",
"scripts": {
"dev": "vite",
@ -16,27 +16,27 @@
"devDependencies": {
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-toast": "^1.2.1",
"@types/react": "^18.3.2",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.2.25",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"@typescript-eslint/eslint-plugin": "^8.4.0",
"@typescript-eslint/parser": "^8.4.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.8.0",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.9",
"i18next": "^23.12.2",
"eslint-plugin-react-refresh": "^0.4.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.426.0",
"lucide-react": "^0.438.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.2",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.0.1",
"react-router-dom": "^6.26.0",
"react-router-dom": "^6.26.1",
"socket.io-client": "^4.7.5",
"typescript": "^5.5.4",
"vite": "^5.4.0",
"vite": "^5.4.3",
"vite-plugin-static-copy": "^1.0.6",
"vite-plugin-svgr": "^4.2.0",
"zustand": "^4.5.4"
"zustand": "^4.5.5"
}
}

View file

@ -802,3 +802,12 @@ input, button, select, optgroup, textarea {
background-color: var(--etherpad-color);
color: white
}
.search-pads{
text-align: center;
}
.search-pads-body tr td:last-child {
display: flex;
justify-content: center;
}

View file

@ -6,14 +6,51 @@ import {Trans, useTranslation} from "react-i18next";
import {SearchField} from "../components/SearchField.tsx";
import {Download, Trash} from "lucide-react";
import {IconButton} from "../components/IconButton.tsx";
import {determineSorting} from "../utils/sorting.ts";
export const HomePage = () => {
const pluginsSocket = useStore(state=>state.pluginsSocket)
const [plugins,setPlugins] = useState<PluginDef[]>([])
const [installedPlugins, setInstalledPlugins] = useState<InstalledPlugin[]>([])
const [searchParams, setSearchParams] = useState<SearchParams>({
offset: 0,
limit: 99999,
sortBy: 'name',
sortDir: 'asc',
searchTerm: ''
})
const filteredInstallablePlugins = useMemo(()=>{
return plugins.sort((a, b)=>{
if(searchParams.sortBy === "version"){
if(searchParams.sortDir === "asc"){
return a.version.localeCompare(b.version)
}
return b.version.localeCompare(a.version)
}
if(searchParams.sortBy === "last-updated"){
if(searchParams.sortDir === "asc"){
return a.time.localeCompare(b.time)
}
return b.time.localeCompare(a.time)
}
if (searchParams.sortBy === "name") {
if(searchParams.sortDir === "asc"){
return a.name.localeCompare(b.name)
}
return b.name.localeCompare(a.name)
}
return 0
})
}, [plugins, searchParams])
const sortedInstalledPlugins = useMemo(()=>{
return installedPlugins.sort((a, b)=>{
if(a.name < b.name){
return -1
}
@ -23,14 +60,8 @@ export const HomePage = () => {
return 0
})
} ,[installedPlugins])
const [searchParams, setSearchParams] = useState<SearchParams>({
offset: 0,
limit: 99999,
sortBy: 'name',
sortDir: 'asc',
searchTerm: ''
})
} ,[installedPlugins, searchParams])
const [searchTerm, setSearchTerm] = useState<string>('')
const {t} = useTranslation()
@ -165,16 +196,35 @@ export const HomePage = () => {
<table id="available-plugins">
<thead>
<tr>
<th><Trans i18nKey="admin_plugins.name"/></th>
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'name')} onClick={()=>{
setSearchParams({
...searchParams,
sortBy: 'name',
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
})
}}>
<Trans i18nKey="admin_plugins.name" /></th>
<th style={{width: '30%'}}><Trans i18nKey="admin_plugins.description"/></th>
<th><Trans i18nKey="admin_plugins.version"/></th>
<th><Trans i18nKey="admin_plugins.last-update"/></th>
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'version')} onClick={()=>{
setSearchParams({
...searchParams,
sortBy: 'version',
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
})
}}><Trans i18nKey="admin_plugins.version"/></th>
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'last-updated')} onClick={()=>{
setSearchParams({
...searchParams,
sortBy: 'last-updated',
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
})
}}><Trans i18nKey="admin_plugins.last-update"/></th>
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
</tr>
</thead>
<tbody style={{overflow: 'auto'}}>
{(plugins.length > 0) ?
plugins.map((plugin) => {
{(filteredInstallablePlugins.length > 0) ?
filteredInstallablePlugins.map((plugin) => {
return <tr key={plugin.name}>
<td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
<td>{plugin.description}</td>

View file

@ -104,7 +104,7 @@ export const PadPage = ()=>{
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
<table>
<thead>
<tr>
<tr className="search-pads">
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
setSearchParams({
...searchParams,
@ -136,7 +136,7 @@ export const PadPage = ()=>{
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
</tr>
</thead>
<tbody>
<tbody className="search-pads-body">
{
pads?.results?.map((pad)=>{
return <tr key={pad.padName}>

View file

@ -20,7 +20,7 @@ export type SearchParams = {
searchTerm: string,
offset: number,
limit: number,
sortBy: 'name'|'version',
sortBy: 'name'|'version'|'last-updated',
sortDir: 'asc'|'desc'
}

63
bin/make_docs.ts Normal file
View file

@ -0,0 +1,63 @@
import {exec} from 'child_process'
import fs from 'fs'
import path from 'path'
import pjson from '../src/package.json'
const VERSION=pjson.version
console.log(`Building docs for version ${VERSION}`)
const createDirIfNotExists = (dir: fs.PathLike) => {
if (!fs.existsSync(dir)){
fs.mkdirSync(dir)
}
}
function copyFolderSync(from: fs.PathLike, to: fs.PathLike) {
if(fs.existsSync(to)){
const stat = fs.lstatSync(to)
if (stat.isDirectory()){
fs.rmSync(to, { recursive: true })
}
else{
fs.rmSync(to)
}
}
fs.mkdirSync(to);
fs.readdirSync(from).forEach(element => {
if (fs.lstatSync(path.join(<string>from, element)).isFile()) {
if (typeof from === "string") {
if (typeof to === "string") {
fs.copyFileSync(path.join(from, element), path.join(to, element))
}
}
} else {
if (typeof from === "string") {
if (typeof to === "string") {
copyFolderSync(path.join(from, element), path.join(to, element))
}
}
}
});
}
exec('asciidoctor -v', (err,stdout)=>{
if (err){
console.log('Please install asciidoctor')
console.log('https://asciidoctor.org/docs/install-toolchain/')
process.exit(1)
}
});
createDirIfNotExists('../out')
createDirIfNotExists('../out/doc')
createDirIfNotExists('../out/doc/api')
exec(`asciidoctor -D ../out/doc ../doc/index.adoc */**.adoc -a VERSION=${VERSION}`)
exec(`asciidoctor -D ../out/doc/api ../doc/api/*.adoc -a VERSION=${VERSION}`)
copyFolderSync('../doc/public/', '../out/doc/')

83
bin/migrateDB.ts Normal file
View file

@ -0,0 +1,83 @@
// DB migration
import {readFileSync} from 'node:fs'
import {Database, DatabaseType} from "ueberdb2";
import path from "node:path";
const settings = require('ep_etherpad-lite/node/utils/Settings');
// file1 = source, file2 = target
// pnpm run migrateDB --file1 <db1.json> --file2 <db2.json>
const arg = process.argv.slice(2);
if (arg.length != 4) {
console.error('Wrong number of arguments!. Call with pnpm run migrateDB --file1 source.json target.json')
process.exit(1)
}
type SettingsConfig = {
dbType: string,
dbSettings: any
}
/*
{
"dbType": "<your-db-type>",
"dbSettings": {
<your-db-settings>
}
}
*/
let firstDBSettingsFile: string
let secondDBSettingsFile: string
if (arg[0] == "--file1") {
firstDBSettingsFile = arg[1]
} else if (arg[0] === "--file2") {
secondDBSettingsFile = arg[1]
}
if (arg[2] == "--file1") {
firstDBSettingsFile = arg[3]
} else if (arg[2] === "--file2") {
secondDBSettingsFile = arg[3]
}
const settingsfile = JSON.parse(readFileSync(path.join(settings.root,firstDBSettingsFile!)).toString()) as SettingsConfig
const settingsfile2 = JSON.parse(readFileSync(path.join(settings.root,secondDBSettingsFile!)).toString()) as SettingsConfig
console.log(settingsfile2)
if ("filename" in settingsfile.dbSettings) {
settingsfile.dbSettings.filename = path.join(settings.root, settingsfile.dbSettings.filename)
console.log(settingsfile.dbType + " location is "+ settingsfile.dbSettings.filename)
}
if ("filename" in settingsfile2.dbSettings) {
settingsfile2.dbSettings.filename = path.join(settings.root, settingsfile2.dbSettings.filename)
console.log(settingsfile2.dbType + " location is "+ settingsfile2.dbSettings.filename)
}
const ueberdb1 = new Database(settingsfile.dbType as DatabaseType, settingsfile.dbSettings)
const ueberdb2 = new Database(settingsfile2.dbType as DatabaseType, settingsfile2.dbSettings)
const handleSync = async ()=>{
await ueberdb1.init()
await ueberdb2.init()
const allKeys = await ueberdb1.findKeys('*','')
for (const key of allKeys) {
const foundVal = await ueberdb1.get(key)!
await ueberdb2.set(key, foundVal)
}
}
handleSync().then(()=>{
console.log("Done syncing dbs")
}).catch(e=>{
console.log(`Error syncing db ${e}`)
})

View file

@ -1,25 +1,26 @@
{
"name": "bin",
"version": "2.2.2",
"version": "2.2.3",
"description": "",
"main": "checkAllPads.js",
"directories": {
"doc": "doc"
},
"dependencies": {
"axios": "^1.7.3",
"axios": "^1.7.7",
"ep_etherpad-lite": "workspace:../src",
"log4js": "^6.9.1",
"semver": "^7.6.3",
"tsx": "^4.17.0",
"ueberdb2": "^4.2.92"
"tsx": "^4.19.0",
"ueberdb2": "^4.2.100"
},
"devDependencies": {
"@types/node": "^22.1.0",
"@types/node": "^22.5.4",
"@types/semver": "^7.5.8",
"typescript": "^5.5.4"
},
"scripts": {
"makeDocs": "node --import tsx make_docs.ts",
"checkPad": "node --import tsx checkPad.ts",
"checkAllPads": "node --import tsx checkAllPads.ts",
"createUserSession": "node --import tsx createUserSession.ts",
@ -33,7 +34,8 @@
"stalePlugins": "node --import tsx ./plugins/stalePlugins.ts",
"checkPlugin": "node --import tsx ./plugins/checkPlugin.ts",
"plugins": "node --import tsx ./plugins.ts",
"generateChangelog": "node --import tsx generateReleaseNotes.ts"
"generateChangelog": "node --import tsx generateReleaseNotes.ts",
"migrateDB": "node --import tsx migrateDB.ts"
},
"author": "",
"license": "ISC"

View file

@ -23,17 +23,13 @@ const possibleActions = [
]
const install = ()=> {
let registryPlugins: string[] = [];
let localPlugins: string[] = [];
if (args.indexOf('--path') !== -1) {
const indexToSplit = args.indexOf('--path');
registryPlugins = args.slice(1, indexToSplit);
localPlugins = args.slice(indexToSplit + 1);
} else {
registryPlugins = args;
}
const argsAsString: string = args.join(" ");
const regexRegistryPlugins = /(?<=i\s)(.*?)(?=--github|--path|$)/;
const regexLocalPlugins = /(?<=--path\s)(.*?)(?=--github|$)/;
const regexGithubPlugins = /(?<=--github\s)(.*?)(?=--path|$)/;
const registryPlugins = argsAsString.match(regexRegistryPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
const localPlugins = argsAsString.match(regexLocalPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
const githubPlugins = argsAsString.match(regexGithubPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
async function run() {
for (const plugin of registryPlugins) {
@ -53,6 +49,11 @@ const install = ()=> {
console.log(`Installing plugin from path: ${plugin}`);
await linkInstaller.installFromPath(plugin);
}
for (const plugin of githubPlugins) {
console.log(`Installing plugin from github: ${plugin}`);
await linkInstaller.installFromGitHub(plugin);
}
}
(async () => {

View file

@ -197,7 +197,7 @@ try {
try {
console.log('Building documentation...');
run('node ./make_docs.js');
run('pnpm run makeDocs');
console.log('Updating ether.github.com master branch...');
run('git pull --ff-only', {cwd: '../ether.github.com/'});
console.log('Committing documentation...');

View file

@ -39,7 +39,7 @@
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

29
doc/cli.md Normal file
View file

@ -0,0 +1,29 @@
# CLI
You can find different tools for migrating things, checking your Etherpad health in the bin directory.
One of these is the migrateDB command. It takes two settings.json files and copies data from one source to another one.
In this example we migrate from the old dirty db to the new rustydb engine. So we copy these files to the root of the etherpad-directory.
````json
{
"dbType": "dirty",
"dbSettings": {
"filename": "./var/rusty.db"
}
}
````
````json
{
"dbType": "rustydb",
"dbSettings": {
"filename": "./var/rusty2.db"
}
}
````
After that we need to move the data from dirty to rustydb.
Therefore, we call `pnpm run migrateDB --file1 test1.json --file2 test2.json` with these two files in our root directories. After some time the data should be copied over to the new database.

View file

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

View file

@ -9,6 +9,7 @@ services:
build:
context: .
args:
# Attention: installed plugins in the node_modules folder get overwritten during volume mount in dev
ETHERPAD_PLUGINS:
# change from development to production if needed
target: development

View file

@ -1,55 +0,0 @@
import {exec} from 'child_process'
import fs from 'fs'
import path from 'path'
import pjson from './src/package.json' assert {type: "json"}
const VERSION=pjson.version
console.log(`Building docs for version ${VERSION}`)
const createDirIfNotExists = (dir) => {
if (!fs.existsSync(dir)){
fs.mkdirSync(dir)
}
}
function copyFolderSync(from, to) {
if(fs.existsSync(to)){
const stat = fs.lstatSync(to)
if (stat.isDirectory()){
fs.rmSync(to, { recursive: true })
}
else{
fs.rmSync(to)
}
}
fs.mkdirSync(to);
fs.readdirSync(from).forEach(element => {
if (fs.lstatSync(path.join(from, element)).isFile()) {
fs.copyFileSync(path.join(from, element), path.join(to, element))
} else {
copyFolderSync(path.join(from, element), path.join(to, element))
}
});
}
exec('asciidoctor -v', (err,stdout)=>{
if (err){
console.log('Please install asciidoctor')
console.log('https://asciidoctor.org/docs/install-toolchain/')
process.exit(1)
}
});
createDirIfNotExists('./out')
createDirIfNotExists('./out/doc')
createDirIfNotExists('./out/doc/api')
exec(`asciidoctor -D out/doc doc/index.adoc */**.adoc -a VERSION=${VERSION}`)
exec(`asciidoctor -D out/doc/api ./doc/api/*.adoc -a VERSION=${VERSION}`)
copyFolderSync('./doc/public/', './out/doc/')

View file

@ -30,7 +30,8 @@
"remove-plugins": "pnpm --filter bin run remove-plugins",
"list-plugins": "pnpm --filter bin run list-plugins",
"build:etherpad": "pnpm --filter admin run build-copy && pnpm --filter ui run build-copy",
"build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy"
"build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy",
"makeDocs": "pnpm --filter bin run makeDocs"
},
"dependencies": {
"ep_etherpad-lite": "workspace:./src"
@ -49,6 +50,6 @@
"type": "git",
"url": "https://github.com/ether/etherpad-lite.git"
},
"version": "2.2.2",
"version": "2.2.3",
"license": "Apache-2.0"
}

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,10 @@
"admin_plugins_info.version_number": "Нумар вэрсіі",
"admin_settings": "Налады",
"admin_settings.current": "Цяперашняя канфігурацыя",
"admin_settings.current_example-devel": "Прыклад шаблёну наладаў распрацоўкі",
"admin_settings.current_example-prod": "Прыклад шаблёну наладаў вытворчасьці",
"admin_settings.current_restart.value": "Перазапуск Etherpad",
"admin_settings.current_save.value": "Захаваць налады",
"admin_settings.page-title": "Налады — Etherpad",
"index.newPad": "Стварыць",
"index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:",
@ -72,6 +76,7 @@
"pad.settings.fontType.normal": "Звычайны",
"pad.settings.language": "Мова:",
"pad.settings.about": "Пра",
"pad.settings.poweredBy": "Працуе на",
"pad.importExport.import_export": "Імпарт/Экспарт",
"pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты",
"pad.importExport.importSuccessful": "Пасьпяхова!",
@ -106,6 +111,9 @@
"pad.modals.corruptPad.cause": "Гэта можа быць выклікана няправільнай канфігурацыяй сэрвэру або іншымі нечаканымі дзеяньнямі. Калі ласка, скантактуйцеся з адміністратарам службы.",
"pad.modals.deleted": "Выдалены.",
"pad.modals.deleted.explanation": "Гэты дакумэнт быў выдалены.",
"pad.modals.rateLimited": "Хуткасьць абмежаваная.",
"pad.modals.rateLimited.explanation": "Вы адаслалі так шмат паведамленьняў, што гэты дакумэнт вас адключыў.",
"pad.modals.rejected.explanation": "Сэрвэр адхіліў паведамленьне, адасланае вашым броўзэрам.",
"pad.modals.disconnected": "Вы былі адключаныя.",
"pad.modals.disconnected.explanation": "Злучэньне з сэрвэрам было страчанае",
"pad.modals.disconnected.cause": "Магчыма, сэрвэр недаступны. Калі ласка, паведаміце адміністратару службы, калі праблема будзе паўтарацца.",

View file

@ -3,6 +3,7 @@
"authors": [
"Aefgh39622",
"Andibecker",
"Ekminarin",
"Patsagorn Y.",
"Trisorn Triboon"
]
@ -121,7 +122,7 @@
"pad.share.readonly": "อ่านเท่านั้น",
"pad.share.link": "ลิงก์",
"pad.share.emebdcode": "URL แบบฝังตัว",
"pad.chat": "แช",
"pad.chat": "แช",
"pad.chat.title": "เปิดการแชทสำหรับแผ่นจดบันทึกนี้",
"pad.chat.loadmessages": "โหลดข้อความเพิ่มเติม",
"pad.chat.stick.title": "ปักการสนทนาไว้บนหน้าจอ",

View file

@ -19,8 +19,10 @@
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
import {deserializeOps} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {Builder} from "../../static/js/Builder";
import {Attribute} from "../../static/js/types/Attribute";
const CustomError = require('../utils/customError');
const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler');
@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
const oldText = pad.text();
atext.text += '\n';
const eachAttribRun = (attribs: string[], func:Function) => {
const eachAttribRun = (attribs: string, func:Function) => {
let textIndex = 0;
const newTextStart = 0;
const newTextEnd = atext.text.length;
for (const op of Changeset.deserializeOps(attribs)) {
for (const op of deserializeOps(attribs)) {
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
@ -577,10 +579,10 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
};
// create a new changeset with a helper builder object
const builder = Changeset.builder(oldText.length);
const builder = new Builder(oldText.length);
// assemble each line into the builder
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
eachAttribRun(atext.attribs, (start: number, end: number, attribs:Attribute[]) => {
builder.insert(atext.text.substring(start, end), attribs);
});

View file

@ -21,8 +21,8 @@
const db = require('./DB');
const CustomError = require('../utils/customError');
const hooks = require('../../static/js/pluginfw/hooks.js');
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
const hooks = require('../../static/js/pluginfw/hooks');
import padutils, {randomString} from "../../static/js/pad_utils";
exports.getColorPalette = () => [
'#ffc7c7',
@ -169,7 +169,7 @@ exports.getAuthorId = async (token: string, user: object) => {
* @param {String} token The token
*/
exports.getAuthor4Token = async (token: string) => {
warnDeprecated(
padutils.warnDeprecated(
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
return await getAuthor4Token(token);
};

View file

@ -20,7 +20,7 @@
*/
const CustomError = require('../utils/customError');
const randomString = require('../../static/js/pad_utils').randomString;
import {randomString} from "../../static/js/pad_utils";
const db = require('./DB');
const padManager = require('./PadManager');
const sessionManager = require('./SessionManager');

View file

@ -7,10 +7,10 @@ import {MapArrayType} from "../types/MapType";
* The pad object, defined with joose
*/
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
import AttributeMap from '../../static/js/AttributeMap';
import {applyToAText, checkRep, copyAText, deserializeOps, makeAText, makeSplice, opsFromAText, pack, unpack} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import AttributePool from '../../static/js/AttributePool';
const Stream = require('../utils/Stream');
const assert = require('assert').strict;
const db = require('./DB');
@ -23,8 +23,10 @@ const CustomError = require('../utils/customError');
const readOnlyManager = require('./ReadOnlyManager');
const randomString = require('../utils/randomstring');
const hooks = require('../../static/js/pluginfw/hooks');
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
const promises = require('../utils/promises');
import pad_utils from "../../static/js/pad_utils";
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
import {} from '../utils/promises';
import {timesLimit} from "async";
/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
@ -40,7 +42,7 @@ exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
class Pad {
private db: Database;
private atext: AText;
private pool: APool;
private pool: AttributePool;
private head: number;
private chatHead: number;
private publicStatus: boolean;
@ -56,7 +58,7 @@ class Pad {
*/
constructor(id:string, database = db) {
this.db = database;
this.atext = Changeset.makeAText('\n');
this.atext = makeAText('\n');
this.pool = new AttributePool();
this.head = -1;
this.chatHead = -1;
@ -93,13 +95,13 @@ class Pad {
* @param {String} authorId The id of the author
* @return {Promise<number|string>}
*/
async appendRevision(aChangeset:AChangeSet, authorId = '') {
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
async appendRevision(aChangeset:string, authorId = '') {
const newAText = applyToAText(aChangeset, this.atext, this.pool);
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
this.head !== -1) {
return this.head;
}
Changeset.copyAText(newAText, this.atext);
copyAText(newAText, this.atext);
const newRev = ++this.head;
@ -126,11 +128,11 @@ class Pad {
pad: this,
authorId,
get author() {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
pad_utils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
return this.authorId;
},
set author(authorId) {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
pad_utils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
this.authorId = authorId;
},
...this.head === 0 ? {} : {
@ -215,7 +217,7 @@ class Pad {
]);
const apool = this.apool();
let atext = keyAText;
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
for (const cs of changesets) atext = applyToAText(cs, atext, apool);
return atext;
}
@ -293,7 +295,7 @@ class Pad {
(!ins && start > 0 && orig[start - 1] === '\n');
if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
const changeset = makeSplice(orig, start, ndel, ins);
await this.appendRevision(changeset, authorId);
}
@ -330,7 +332,7 @@ class Pad {
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
* `msgOrText.time` instead.
*/
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
async appendChatMessage(msgOrText: string| ChatMessage, authorId = null, time = null) {
const msg =
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
this.chatHead++;
@ -393,7 +395,7 @@ class Pad {
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content);
}
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
const firstChangeset = makeSplice('\n', 0, 0, text);
await this.appendRevision(firstChangeset, authorId);
}
await hooks.aCallAll('padLoad', {pad: this});
@ -437,11 +439,11 @@ class Pad {
// let the plugins know the pad was copied
await hooks.aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
pad_utils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
pad_utils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -520,8 +522,8 @@ class Pad {
const oldAText = this.atext;
// based on Changeset.makeSplice
const assem = Changeset.smartOpAssembler();
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
const assem = new SmartOpAssembler();
for (const op of opsFromAText(oldAText)) assem.append(op);
assem.endDocument();
// although we have instantiated the dstPad with '\n', an additional '\n' is
@ -533,16 +535,16 @@ class Pad {
// create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
const changeset = pack(oldLength, newLength, assem.toString(), newText);
dstPad.appendRevision(changeset, authorId);
await hooks.aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
pad_utils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
pad_utils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -585,12 +587,14 @@ class Pad {
p.push(db.remove(`pad2readonly:${padID}`));
// delete all chat messages
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
// @ts-ignore
p.push(timesLimit(this.chatHead + 1, 500, async (i: string) => {
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
}));
// delete all revisions
p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => {
// @ts-ignore
p.push(timesLimit(this.head + 1, 500, async (i: string) => {
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
}));
@ -603,7 +607,7 @@ class Pad {
p.push(padManager.removePad(padID));
p.push(hooks.aCallAll('padRemove', {
get padID() {
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
pad_utils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
return this.pad.id;
},
pad: this,
@ -706,7 +710,7 @@ class Pad {
}
})
.batch(100).buffer(99);
let atext = Changeset.makeAText('\n');
let atext = makeAText('\n');
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
try {
assert(authorId != null);
@ -717,10 +721,10 @@ class Pad {
assert(timestamp > 0);
assert(changeset != null);
assert.equal(typeof changeset, 'string');
Changeset.checkRep(changeset);
const unpacked = Changeset.unpack(changeset);
checkRep(changeset);
const unpacked = unpack(changeset);
let text = atext.text;
for (const op of Changeset.deserializeOps(unpacked.ops)) {
for (const op of deserializeOps(unpacked.ops)) {
if (['=', '-'].includes(op.opcode)) {
assert(text.length >= op.chars);
const consumed = text.slice(0, op.chars);
@ -731,7 +735,7 @@ class Pad {
}
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
}
atext = Changeset.applyToAText(changeset, atext, pool);
atext = applyToAText(changeset, atext, pool);
if (isKeyRev) assert.deepEqual(keyAText, atext);
} catch (err:any) {
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;

View file

@ -22,7 +22,7 @@
import {UserSettingsObject} from "../types/UserSettingsObject";
const authorManager = require('./AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks.js');
const hooks = require('../../static/js/pluginfw/hooks');
const padManager = require('./PadManager');
const readOnlyManager = require('./ReadOnlyManager');
const sessionManager = require('./SessionManager');
@ -30,7 +30,7 @@ const settings = require('../utils/Settings');
const webaccess = require('../hooks/express/webaccess');
const log4js = require('log4js');
const authLogger = log4js.getLogger('auth');
const {padutils} = require('../../static/js/pad_utils');
import padutils from '../../static/js/pad_utils'
const DENY = Object.freeze({accessStatus: 'deny'});

View file

@ -21,7 +21,7 @@
*/
const CustomError = require('../utils/customError');
const promises = require('../utils/promises');
import {firstSatisfies} from '../utils/promises';
const randomString = require('../utils/randomstring');
const db = require('./DB');
const groupManager = require('./GroupManager');
@ -79,7 +79,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
groupID: string;
validUntil: number;
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
const sessionInfo = await firstSatisfies(sessionInfoPromises, isMatch) as any;
if (sessionInfo == null) return undefined;
return sessionInfo.authorID;
};

View file

@ -22,7 +22,7 @@
const ejs = require('ejs');
const fs = require('fs');
const hooks = require('../../static/js/pluginfw/hooks.js');
const hooks = require('../../static/js/pluginfw/hooks');
const path = require('path');
const resolve = require('resolve');
const settings = require('../utils/Settings');

View file

@ -31,7 +31,7 @@ import os from 'os';
const importHtml = require('../utils/ImportHtml');
const importEtherpad = require('../utils/ImportEtherpad');
import log4js from 'log4js';
const hooks = require('../../static/js/pluginfw/hooks.js');
const hooks = require('../../static/js/pluginfw/hooks');
const logger = log4js.getLogger('ImportHandler');

View file

@ -21,28 +21,30 @@
import {MapArrayType} from "../types/MapType";
const AttributeMap = require('../../static/js/AttributeMap');
import AttributeMap from '../../static/js/AttributeMap';
const padManager = require('../db/PadManager');
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
import {checkRep, cloneAText, compose, deserializeOps, follow, identity, inverse, makeAText, makeSplice, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, prepareForWire, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import AttributePool from '../../static/js/AttributePool';
const AttributeManager = require('../../static/js/AttributeManager');
const authorManager = require('../db/AuthorManager');
const {padutils} = require('../../static/js/pad_utils');
import padutils from '../../static/js/pad_utils';
const readOnlyManager = require('../db/ReadOnlyManager');
const settings = require('../utils/Settings');
const securityManager = require('../db/SecurityManager');
const plugins = require('../../static/js/pluginfw/plugin_defs.js');
const plugins = require('../../static/js/pluginfw/plugin_defs');
import log4js from 'log4js';
const messageLogger = log4js.getLogger('message');
const accessLogger = log4js.getLogger('access');
const hooks = require('../../static/js/pluginfw/hooks.js');
const hooks = require('../../static/js/pluginfw/hooks');
const stats = require('../stats')
const assert = require('assert').strict;
import {RateLimiterMemory} from 'rate-limiter-flexible';
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
import {ChangeSet} from "../types/ChangeSet";
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
import {Builder} from "../../static/js/Builder";
const webaccess = require('../hooks/express/webaccess');
const { checkValidRev } = require('../utils/checkValidRev');
@ -214,7 +216,7 @@ exports.handleDisconnect = async (socket:any) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
exports.handleMessage = async (socket:any, message: ClientVarMessage) => {
const env = process.env.NODE_ENV || 'development';
if (env === 'production') {
@ -348,15 +350,15 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
stats.counter('pendingEdits').inc();
await padChannels.enqueue(thisSession.padId, {socket, message});
break;
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
case 'CHAT_MESSAGE': await handleChatMessage(socket, message); break;
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message as unknown as UserNewInfoMessage); break;
case 'CHAT_MESSAGE': await handleChatMessage(socket, message as unknown as ChatMessageMessage); break;
case 'GET_CHAT_MESSAGES': await handleGetChatMessages(socket, message); break;
case 'SAVE_REVISION': await handleSaveRevisionMessage(socket, message); break;
case 'SAVE_REVISION': await handleSaveRevisionMessage(socket, message as unknown as ClientSaveRevisionMessage); break;
case 'CLIENT_MESSAGE': {
const {type} = message.data.payload;
try {
switch (type) {
case 'suggestUserName': handleSuggestUserName(socket, message); break;
case 'suggestUserName': handleSuggestUserName(socket, message as unknown as ClientSuggestUserName); break;
default: throw new Error('unknown message type');
}
} catch (err) {
@ -384,7 +386,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
const handleSaveRevisionMessage = async (socket:any, message: string) => {
const handleSaveRevisionMessage = async (socket:any, message: ClientSaveRevisionMessage) => {
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
await pad.addSavedRevision(pad.head, authorId);
@ -397,7 +399,7 @@ const handleSaveRevisionMessage = async (socket:any, message: string) => {
* @param msg {Object} the message we're sending
* @param sessionID {string} the socketIO session to which we're sending this message
*/
exports.handleCustomObjectMessage = (msg: typeof ChatMessage, sessionID: string) => {
exports.handleCustomObjectMessage = (msg: CustomMessage, sessionID: string) => {
if (msg.data.type === 'CUSTOM') {
if (sessionID) {
// a sessionID is targeted: directly to this sessionID
@ -432,7 +434,7 @@ exports.handleCustomMessage = (padID: string, msgString:string) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
const handleChatMessage = async (socket:any, message: ChatMessageMessage) => {
const chatMessage = ChatMessage.fromObject(message.data.message);
const {padId, author: authorId} = sessioninfos[socket.id];
// Don't trust the user-supplied values.
@ -452,7 +454,7 @@ const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
* @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message
* object as the first argument and the destination pad ID as the second argument instead.
*/
exports.sendChatMessageToPadClients = async (mt: typeof ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
exports.sendChatMessageToPadClients = async (mt: ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
padId = mt instanceof ChatMessage ? puId : padId;
const pad = await padManager.getPad(padId, null, message.authorId);
@ -499,7 +501,7 @@ const handleGetChatMessages = async (socket:any, {data: {start, end}}:any) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => {
const handleSuggestUserName = (socket:any, message: ClientSuggestUserName) => {
const {newName, unnamedId} = message.data.payload;
if (newName == null) throw new Error('missing newName');
if (unnamedId == null) throw new Error('missing unnamedId');
@ -519,7 +521,7 @@ const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId}}}: PadUserInfo) => {
const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId}}}: UserNewInfoMessage) => {
if (colorId == null) throw new Error('missing colorId');
if (!name) name = null;
const session = sessioninfos[socket.id];
@ -567,7 +569,9 @@ const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
const handleUserChanges = async (socket:any, message: {
data: ClientUserChangesMessage
}) => {
// This one's no longer pending, as we're gonna process it now
stats.counter('pendingEdits').dec();
@ -591,10 +595,10 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
// Verify that the changeset has valid syntax and is in canonical form
Changeset.checkRep(changeset);
checkRep(changeset);
// Validate all added 'author' attribs to be the same value as the current user
for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) {
for (const op of deserializeOps(unpack(changeset).ops)) {
// + can add text with attribs
// = can change or add attribs
// - can have attribs, but they are discarded and don't show up in the attribs -
@ -613,7 +617,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
// ex. adoptChangesetAttribs
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
let rebasedChangeset = moveOpsToNewPool(changeset, wireApool, pad.pool);
// ex. applyUserChanges
let r = baseRev;
@ -626,21 +630,21 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
if (changeset === c && thisSession.author === authorId) {
// Assume this is a retransmission of an already applied changeset.
rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen);
rebasedChangeset = identity(unpack(changeset).oldLen);
}
// At this point, both "c" (from the pad) and "changeset" (from the
// client) are relative to revision r - 1. The follow function
// rebases "changeset" so that it is relative to revision r
// and can be applied after "c".
rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool);
rebasedChangeset = follow(c, rebasedChangeset, false, pad.pool);
}
const prevText = pad.text();
if (Changeset.oldLen(rebasedChangeset) !== prevText.length) {
if (oldLen(rebasedChangeset) !== prevText.length) {
throw new Error(
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
`${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
`${oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
}
const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author);
@ -655,7 +659,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
// Make sure the pad always ends with an empty line.
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
const nlChangeset = makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
await pad.appendRevision(nlChangeset, thisSession.author);
}
@ -710,7 +714,7 @@ exports.updatePadClients = async (pad: PadType) => {
const revChangeset = revision.changeset;
const currentTime = revision.meta.timestamp;
const forWire = Changeset.prepareForWire(revChangeset, pad.pool);
const forWire = prepareForWire(revChangeset, pad.pool);
const msg = {
type: 'COLLABROOM',
data: {
@ -738,14 +742,14 @@ exports.updatePadClients = async (pad: PadType) => {
/**
* Copied from the Etherpad Source Code. Don't know what this method does excatly...
*/
const _correctMarkersInPad = (atext: AText, apool: APool) => {
const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
const text = atext.text;
// collect char positions of line markers (e.g. bullets) in new atext
// that aren't at the start of a line
const badMarkers = [];
let offset = 0;
for (const op of Changeset.deserializeOps(atext.attribs)) {
for (const op of deserializeOps(atext.attribs)) {
const attribs = AttributeMap.fromString(op.attribs, apool);
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
if (hasMarker) {
@ -767,7 +771,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => {
// create changeset that removes these bad markers
offset = 0;
const builder = Changeset.builder(text.length);
const builder = new Builder(text.length);
badMarkers.forEach((pos) => {
builder.keepText(text.substring(offset, pos));
@ -785,7 +789,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
const sessionInfo = sessioninfos[socket.id];
if (sessionInfo == null) throw new Error('client disconnected');
assert(sessionInfo.author);
@ -793,8 +797,9 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context.
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) {
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId as string)) {
messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`);
// @ts-ignore
authorColorId = null;
}
await Promise.all([
@ -872,7 +877,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
const revisionsNeeded = [];
const changesets:MapArrayType<any> = {};
let startNum = message.client_rev + 1;
let startNum = message.client_rev! + 1;
let endNum = pad.getHeadRevisionNumber() + 1;
const headNum = pad.getHeadRevisionNumber();
@ -901,7 +906,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
// return pending changesets
for (const r of revisionsNeeded) {
const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool);
const forWire = prepareForWire(changesets[r].changeset, pad.pool);
const wireMsg = {type: 'COLLABROOM',
data: {type: 'CLIENT_RECONNECT',
headRev: pad.getHeadRevisionNumber(),
@ -926,8 +931,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
let apool;
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
try {
atext = Changeset.cloneAText(pad.atext);
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
atext = cloneAText(pad.atext);
const attribsForWire = prepareForWire(atext.attribs, pad.pool);
apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated;
} catch (e:any) {
@ -1163,13 +1168,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
const backwards = inverse(forwards, lines.textlines, lines.alines, pad.apool());
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
Changeset.mutateTextLines(forwards, lines.textlines);
mutateAttributionLines(forwards, lines.alines, pad.apool());
mutateTextLines(forwards, lines.textlines);
const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool);
const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool);
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
const t2 = revisionDate[compositeEnd - 1];
@ -1195,12 +1200,12 @@ const getPadLines = async (pad: PadType, revNum: number) => {
if (revNum >= 0) {
atext = await pad.getInternalRevisionAText(revNum);
} else {
atext = Changeset.makeAText('\n');
atext = makeAText('\n');
}
return {
textlines: Changeset.splitTextLines(atext.text),
alines: Changeset.splitAttributionLines(atext.attribs, atext.text),
textlines: splitTextLines(atext.text),
alines: splitAttributionLines(atext.attribs, atext.text),
};
};
@ -1235,7 +1240,7 @@ const composePadChangesets = async (pad: PadType, startNum: number, endNum: numb
for (r = startNum + 1; r < endNum; r++) {
const cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
changeset = compose(changeset as string, cs as string, pool);
}
return changeset;
} catch (e) {

View file

@ -324,7 +324,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
// serve index.html under /
args.app.get('/', (req: any, res: any) => {
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, settings, entrypoint: "/"+fileNameIndex}));
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, settings, entrypoint: "./"+fileNameIndex}));
});
@ -342,7 +342,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
req,
toolbar,
isReadOnly,
entrypoint: "/"+fileNamePad
entrypoint: "../"+fileNamePad
})
res.send(content);
});
@ -356,7 +356,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
req,
toolbar,
entrypoint: "/"+fileNameTimeSlider
entrypoint: "../../"+fileNameTimeSlider
}));
});
} else {

View file

@ -4,11 +4,10 @@ import {MapArrayType} from "../../types/MapType";
import {PartType} from "../../types/PartType";
const fs = require('fs').promises;
const minify = require('../../utils/Minify');
const path = require('path');
import {minify} from '../../utils/Minify';
import path from 'node:path';
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const settings = require('../../utils/Settings');
import CachingMiddleware from '../../utils/caching_middleware';
// Rewrite tar to include modules with no extensions and proper rooted paths.
const getTar = async () => {
@ -32,15 +31,10 @@ const getTar = async () => {
};
exports.expressPreSession = async (hookName:string, {app}:any) => {
// Cache both minified and static.
const assetCache = new CachingMiddleware();
// Cache static assets
app.all(/\/js\/(.*)/, assetCache.handle.bind(assetCache));
app.all(/\/css\/(.*)/, assetCache.handle.bind(assetCache));
// Minify will serve static files compressed (minify enabled). It also has
// file-specific hacks for ace/require-kernel/etc.
app.all('/static/:filename(*)', minify.minify);
app.all('/static/:filename(*)', minify);
// serve plugin definitions
// not very static, but served here so that client can do

View file

@ -177,6 +177,10 @@ const checkAccess = async (req:any, res:any, next: Function) => {
res.status(401).send('Authentication Required');
return;
}
if (ctx.username === '__proto__' || ctx.username === 'constructor' || ctx.username === 'prototype') {
res.end(403);
return;
}
settings.users[ctx.username].username = ctx.username;
// Make a shallow copy so that the password property can be deleted (to prevent it from
// appearing in logs or in the database) without breaking future authentication attempts.

View file

@ -7,8 +7,8 @@ const languages = require('languages4translatewiki');
const fs = require('fs');
const path = require('path');
const _ = require('underscore');
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
const existsSync = require('../utils/path_exists');
const pluginDefs = require('../../static/js/pluginfw/plugin_defs');
import existsSync from '../utils/path_exists';
const settings = require('../utils/Settings');
// returns all existing messages merged together and grouped by langcode

View file

@ -74,7 +74,7 @@ const express = require('./hooks/express');
const hooks = require('../static/js/pluginfw/hooks');
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins');
const {Gate} = require('./utils/promises');
import {Gate} from './utils/promises';
const stats = require('./stats')
const logger = log4js.getLogger('server');
@ -100,7 +100,7 @@ const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsLi
};
let startDoneGate: { resolve: () => void; }
let startDoneGate: Gate<unknown>
exports.start = async () => {
switch (state) {
case State.INITIAL:
@ -181,12 +181,14 @@ exports.start = async () => {
} catch (err) {
logger.error('Error occurred while starting Etherpad');
state = State.STATE_TRANSITION_FAILED;
// @ts-ignore
startDoneGate.resolve();
return await exports.exit(err);
}
logger.info('Etherpad is running');
state = State.RUNNING;
// @ts-ignore
startDoneGate.resolve();
// Return the HTTP server to make it easier to write tests.
@ -228,11 +230,13 @@ exports.stop = async () => {
} catch (err) {
logger.error('Error occurred while stopping Etherpad');
state = State.STATE_TRANSITION_FAILED;
// @ts-ignore
stopDoneGate.resolve();
return await exports.exit(err);
}
logger.info('Etherpad stopped');
state = State.STOPPED;
// @ts-ignore
stopDoneGate.resolve();
};

View file

@ -1,10 +1,11 @@
import {MapArrayType} from "./MapType";
import AttributePool from "../../static/js/AttributePool";
export type PadType = {
id: string,
apool: ()=>APool,
apool: ()=>AttributePool,
atext: AText,
pool: APool,
pool: AttributePool,
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
getRevisionAuthor: (rev: number)=>Promise<string>,
@ -35,6 +36,7 @@ export type APool = {
clone: ()=>APool,
check: ()=>Promise<void>,
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
getAttrib: (key: number)=>any,
}

View file

@ -19,8 +19,9 @@
* limitations under the License.
*/
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
import AttributeMap from '../../static/js/AttributeMap';
import AttributePool from "../../static/js/AttributePool";
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
const { checkValidRev } = require('./checkValidRev');
/*
@ -30,7 +31,7 @@ exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any;
const _analyzeLine = exports._analyzeLine;
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const apool = pad.pool;
const pieces = [];
@ -51,14 +52,14 @@ type LineModel = {
[id:string]:string|number|LineModel
}
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
exports._analyzeLine = (text:string, aline: string, apool: AttributePool) => {
const line: LineModel = {};
// identify list
let lineMarker = 0;
line.listLevel = 0;
if (aline) {
const [op] = Changeset.deserializeOps(aline);
const [op] = deserializeOps(aline);
if (op != null) {
const attribs = AttributeMap.fromString(op.attribs, apool);
let listType = attribs.get('list');
@ -78,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
}
if (lineMarker) {
line.text = text.substring(1);
line.aline = Changeset.subattribution(aline, 1);
line.aline = subattribution(aline, 1);
} else {
line.text = text;
line.aline = aline;

View file

@ -18,7 +18,7 @@ import {MapArrayType} from "../types/MapType";
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager');
const _ = require('underscore');
@ -27,7 +27,9 @@ const hooks = require('../../static/js/pluginfw/hooks');
const eejs = require('../eejs');
const _analyzeLine = require('./ExportHelper')._analyzeLine;
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
const padutils = require('../../static/js/pad_utils').padutils;
import padutils from "../../static/js/pad_utils";
import {StringIterator} from "../../static/js/StringIterator";
import {StringAssembler} from "../../static/js/StringAssembler";
const getPadHTML = async (pad: PadType, revNum: string) => {
let atext = pad.atext;
@ -44,7 +46,7 @@ const getPadHTML = async (pad: PadType, revNum: string) => {
const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string[]) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
css += '<style>\n';
for (const a of Object.keys(apool.numToAttrib)) {
// @ts-ignore
const attr = apool.numToAttrib[a];
// skip non author attributes
@ -115,6 +118,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
// see hook exportHtmlAdditionalTagsWithData
attrib = propName;
}
// @ts-ignore
const propTrueNum = apool.putAttrib(attrib, true);
if (propTrueNum >= 0) {
anumMap[propTrueNum] = i;
@ -127,8 +131,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
const taker = Changeset.stringIterator(text);
const assem = Changeset.stringAssembler();
const taker = new StringIterator(text);
const assem = new StringAssembler();
const openTags:string[] = [];
const getSpanClassFor = (i: string) => {
@ -204,7 +208,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
return;
}
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
// @ts-ignore
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
idx += numChars;
// this iterates over every op string and decides which tags to open or to close

View file

@ -22,7 +22,9 @@
import {AText, PadType} from "../types/PadType";
import {MapType} from "../types/MapType";
const Changeset = require('../../static/js/Changeset');
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
import {StringIterator} from "../../static/js/StringIterator";
import {StringAssembler} from "../../static/js/StringAssembler";
const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager');
const _analyzeLine = require('./ExportHelper')._analyzeLine;
@ -45,13 +47,14 @@ const getPadTXT = async (pad: PadType, revNum: string) => {
const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
const anumMap: MapType = {};
const css = '';
props.forEach((propName, i) => {
// @ts-ignore
const propTrueNum = apool.putAttrib([propName, true], true);
if (propTrueNum >= 0) {
anumMap[propTrueNum] = i;
@ -69,8 +72,8 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
const taker = Changeset.stringIterator(text);
const assem = Changeset.stringAssembler();
const taker = new StringIterator(text);
const assem = new StringAssembler();
let idx = 0;
@ -79,7 +82,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
return;
}
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
idx += numChars;
for (const o of ops) {

View file

@ -18,7 +18,7 @@ import {APool} from "../types/PadType";
* limitations under the License.
*/
const AttributePool = require('../../static/js/AttributePool');
import AttributePool from '../../static/js/AttributePool';
const {Pad} = require('../db/Pad');
const Stream = require('./Stream');
const authorManager = require('../db/AuthorManager');
@ -61,7 +61,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
try {
const processRecord = async (key:string, value: null|{
padIDs: string|Record<string, unknown>,
pool: APool
pool: AttributePool
}) => {
if (!value) return;
const keyParts = key.split(':');

View file

@ -16,10 +16,11 @@
*/
import log4js from 'log4js';
const Changeset = require('../../static/js/Changeset');
import {deserializeOps} from '../../static/js/Changeset';
const contentcollector = require('../../static/js/contentcollector');
import jsdom from 'jsdom';
import {PadType} from "../types/PadType";
import {Builder} from "../../static/js/Builder";
const apiLogger = log4js.getLogger('ImportHtml');
let processor:any;
@ -69,13 +70,13 @@ exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
// create a new changeset with a helper builder object
const builder = Changeset.builder(1);
const builder = new Builder(1);
// assemble each line into the builder
let textIndex = 0;
const newTextStart = 0;
const newTextEnd = newText.length;
for (const op of Changeset.deserializeOps(newAttribs)) {
for (const op of deserializeOps(newAttribs)) {
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
const start = Math.max(newTextStart, textIndex);

View file

@ -21,20 +21,20 @@
* limitations under the License.
*/
const settings = require('./Settings');
const fs = require('fs').promises;
const path = require('path');
const plugins = require('../../static/js/pluginfw/plugin_defs');
const mime = require('mime-types');
const Threads = require('threads');
const log4js = require('log4js');
const sanitizePathname = require('./sanitizePathname');
import {TransformResult} from "esbuild";
import mime from 'mime-types';
import log4js from 'log4js';
import {compressCSS, compressJS} from './MinifyWorker'
const settings = require('./Settings');
import {promises as fs} from 'fs';
import path from 'node:path';
const plugins = require('../../static/js/pluginfw/plugin_defs');
import sanitizePathname from './sanitizePathname';
const logger = log4js.getLogger('Minify');
const ROOT_DIR = path.join(settings.root, 'src/static/');
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
const LIBRARY_WHITELIST = [
'async',
@ -48,10 +48,10 @@ const LIBRARY_WHITELIST = [
// What follows is a terrible hack to avoid loop-back within the server.
// TODO: Serve files from another service, or directly from the file system.
const requestURI = async (url, method, headers) => {
const requestURI = async (url: string | URL, method: any, headers: { [x: string]: any; }) => {
const parsedUrl = new URL(url);
let status = 500;
const content = [];
const content: any[] = [];
const mockRequest = {
url,
method,
@ -61,7 +61,7 @@ const requestURI = async (url, method, headers) => {
let mockResponse;
const p = new Promise((resolve) => {
mockResponse = {
writeHead: (_status, _headers) => {
writeHead: (_status: number, _headers: { [x: string]: any; }) => {
status = _status;
for (const header in _headers) {
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
@ -69,37 +69,63 @@ const requestURI = async (url, method, headers) => {
}
}
},
setHeader: (header, value) => {
setHeader: (header: string, value: { toString: () => any; }) => {
headers[header.toLowerCase()] = value.toString();
},
header: (header, value) => {
header: (header: string, value: { toString: () => any; }) => {
headers[header.toLowerCase()] = value.toString();
},
write: (_content) => {
write: (_content: any) => {
_content && content.push(_content);
},
end: (_content) => {
end: (_content: any) => {
_content && content.push(_content);
resolve([status, headers, content.join('')]);
},
};
});
await minify(mockRequest, mockResponse);
await _minify(mockRequest, mockResponse);
return await p;
};
const requestURIs = (locations, method, headers, callback) => {
const _requestURIs = (locations: any[], method: any, headers: {
[x: string]:
/**
* This Module manages all /minified/* requests. It controls the
* minification && compression of Javascript and CSS.
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
any;
}, callback: (arg0: any[], arg1: any[], arg2: any[]) => void) => {
Promise.all(locations.map(async (loc) => {
try {
return await requestURI(loc, method, headers);
} catch (err) {
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
// @ts-ignore
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
return [500, headers, ''];
}
})).then((responses) => {
// @ts-ignore
const statuss = responses.map((x) => x[0]);
// @ts-ignore
const headerss = responses.map((x) => x[1]);
// @ts-ignore
const contentss = responses.map((x) => x[2]);
callback(statuss, headerss, contentss);
});
@ -119,11 +145,12 @@ const compatPaths = {
* @param req the Express request
* @param res the Express response
*/
const minify = async (req, res) => {
const _minify = async (req:any, res:any) => {
let filename = req.params.filename;
try {
filename = sanitizePathname(filename);
} catch (err) {
// @ts-ignore
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
res.writeHead(404, {});
res.end();
@ -131,6 +158,7 @@ const minify = async (req, res) => {
}
// Backward compatibility for plugins that require() files from old paths.
// @ts-ignore
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
if (newLocation != null) {
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
@ -193,7 +221,7 @@ const minify = async (req, res) => {
res.writeHead(200, {});
res.end();
} else if (req.method === 'GET') {
const content = await getFileCompressed(filename, contentType);
const content = await getFileCompressed(filename, contentType as string);
res.header('Content-Type', contentType);
res.writeHead(200, {});
res.write(content);
@ -205,7 +233,7 @@ const minify = async (req, res) => {
};
// Check for the existance of the file and get the last modification date.
const statFile = async (filename, dirStatLimit) => {
const statFile = async (filename: string, dirStatLimit: number):Promise<(any | boolean)[]> => {
/*
* The only external call to this function provides an explicit value for
* dirStatLimit: this check could be removed.
@ -221,6 +249,7 @@ const statFile = async (filename, dirStatLimit) => {
try {
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
} catch (err) {
// @ts-ignore
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
// Stat the directory instead.
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
@ -234,69 +263,64 @@ const statFile = async (filename, dirStatLimit) => {
let contentCache = new Map();
const getFileCompressed = async (filename, contentType) => {
const getFileCompressed = async (filename: any, contentType: string) => {
if (contentCache.has(filename)) {
return contentCache.get(filename);
}
let content = await getFile(filename);
let content: Buffer|string = await getFile(filename);
if (!content || !settings.minify) {
return content;
} else if (contentType === 'application/javascript') {
return await new Promise((resolve) => {
threadsPool.queue(async ({compressJS}) => {
return await new Promise(async (resolve) => {
try {
logger.info('Compress JS file %s.', filename);
content = content.toString();
try {
logger.info('Compress JS file %s.', filename);
content = content.toString();
const compressResult = await compressJS(content);
if (compressResult.error) {
console.error(`Error compressing JS (${filename}) using terser`, compressResult.error);
} else {
content = compressResult.code.toString(); // Convert content obj code to string
}
let compressResult: TransformResult<{ minify: boolean }>
compressResult = await compressJS(content);
content = compressResult.code.toString(); // Convert content obj code to string
} catch (error) {
console.error('getFile() returned an error in ' +
`getFileCompressed(${filename}, ${contentType}): ${error}`);
console.error(`Error compressing JS (${filename}) using esbuild`, error);
}
contentCache.set(filename, content);
resolve(content);
});
} catch (error) {
console.error('getFile() returned an error in ' +
`getFileCompressed(${filename}, ${contentType}): ${error}`);
}
contentCache.set(filename, content);
resolve(content);
});
} else if (contentType === 'text/css') {
return await new Promise((resolve) => {
threadsPool.queue(async ({compressCSS}) => {
return await new Promise(async (resolve) => {
try {
logger.info('Compress CSS file %s.', filename);
try {
logger.info('Compress CSS file %s.', filename);
const compressResult = await compressCSS(path.resolve(ROOT_DIR,filename));
if (compressResult.error) {
console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error);
} else {
content = compressResult
}
content = await compressCSS(path.resolve(ROOT_DIR, filename));
} catch (error) {
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
}
contentCache.set(filename, content);
resolve(content);
});
});
} catch (e) {
console.error('getFile() returned an error in ' +
`getFileCompressed(${filename}, ${contentType}): ${e}`);
}
})
} else {
contentCache.set(filename, content);
return content;
}
};
const getFile = async (filename) => {
const getFile = async (filename: any) => {
return await fs.readFile(path.resolve(ROOT_DIR, filename));
};
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
export const minify = (req:any, res:any, next:Function) => _minify(req, res).catch((err) => next(err || new Error(err)));
exports.requestURIs = requestURIs;
export const requestURIs = _requestURIs;
exports.shutdown = async (hookName, context) => {
await threadsPool.terminate();
export const shutdown = async (hookName: string, context:any) => {
contentCache = new Map();
};

View file

@ -3,14 +3,13 @@
* Worker thread to minify JS & CSS files out of the main NodeJS thread
*/
import {expose} from 'threads'
import {build, transform} from 'esbuild';
/*
* Minify JS content
* @param {string} content - JS content to minify
*/
const compressJS = async (content) => {
export const compressJS = async (content: string) => {
return await transform(content, {minify: true});
}
@ -19,7 +18,7 @@ const compressJS = async (content) => {
* @param {string} filename - name of the file
* @param {string} ROOT_DIR - the root dir of Etherpad
*/
const compressCSS = async (content) => {
export const compressCSS = async (content: string) => {
const transformedCSS = await build(
{
entryPoints: [content],
@ -41,8 +40,3 @@ const compressCSS = async (content) => {
)
return transformedCSS.outputFiles[0].text
};
expose({
compressJS: compressJS,
compressCSS,
});

View file

@ -169,11 +169,11 @@ exports.authenticationMethod = 'sso'
/*
* The Type of the database
*/
exports.dbType = 'dirty';
exports.dbType = 'rustydb';
/**
* This setting is passed with dbType to ueberDB to set up the database
*/
exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
exports.dbSettings = {filename: path.join(exports.root, 'var/rusty.db')};
/**
* The default Text of a new pad
@ -837,7 +837,7 @@ exports.reloadSettings = () => {
exports.skinName = 'colibris';
}
if (!exports.socketTransportProtocols.includes("websocket") || exports.socketTransportProtocols.includes("polling")) {
if (!exports.socketTransportProtocols.includes("websocket") || !exports.socketTransportProtocols.includes("polling")) {
logger.warn("Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling'].");
exports.socketTransportProtocols = ['websocket', 'polling'];
}
@ -941,6 +941,11 @@ exports.reloadSettings = () => {
logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`);
}
if (exports.dbType === 'rustydb') {
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
logger.warn(`File location: ${exports.dbSettings.filename}`);
}
if (exports.ip === '') {
// using Unix socket for connectivity
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +

View file

@ -1,211 +0,0 @@
'use strict';
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Buffer} from 'node:buffer'
import fs from 'fs';
const fsp = fs.promises;
import path from 'path';
import zlib from 'zlib';
const settings = require('./Settings');
const existsSync = require('./path_exists');
import util from 'util';
/*
* The crypto module can be absent on reduced node installations.
*
* Here we copy the approach TypeScript guys used for https://github.com/microsoft/TypeScript/issues/19100
* If importing crypto fails at runtime, we replace sha256 with djb2, which is
* weaker, but works for our case.
*
* djb2 was written in 1991 by Daniel J. Bernstein.
*
*/
import _crypto from 'crypto';
let CACHE_DIR: string|undefined = path.join(settings.root, 'var/');
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
type Headers = {
[id: string]: string
}
type ResponseCache = {
[id: string]: {
statusCode: number
headers: Headers
}
}
const responseCache: ResponseCache = {};
const djb2Hash = (data: string) => {
const chars = data.split('').map((str) => str.charCodeAt(0));
return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`;
};
const generateCacheKeyWithSha256 =
(path: string) => _crypto.createHash('sha256').update(path).digest('hex');
const generateCacheKeyWithDjb2 =
(path: string) => Buffer.from(djb2Hash(path)).toString('hex');
let generateCacheKey: (path: string)=>string;
if (_crypto) {
generateCacheKey = generateCacheKeyWithSha256;
} else {
generateCacheKey = generateCacheKeyWithDjb2;
console.warn('No crypto support in this nodejs runtime. Djb2 (weaker) will be used.');
}
// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - END
/*
This caches and compresses 200 and 404 responses to GET and HEAD requests.
TODO: Caching and compressing are solved problems, a middleware configuration
should replace this.
*/
export default class CachingMiddleware {
handle(req: any, res: any, next: any) {
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
}
async _handle(req: any, res: any, next: any) {
if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
return next(undefined, req, res);
}
const oldReq:ResponseCache = {};
const oldRes:ResponseCache = {};
const supportsGzip =
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
const url = new URL(req.url, 'http://localhost');
const cacheKey = generateCacheKey(url.pathname + url.search);
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
const modifiedSince =
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
if (stats != null && stats.mtime && responseCache[cacheKey]) {
req.headers['if-modified-since'] = stats.mtime.toUTCString();
} else {
delete req.headers['if-modified-since'];
}
// Always issue get to downstream.
oldReq.method = req.method;
req.method = 'GET';
// This handles read/write synchronization as well as its predecessor,
// which is to say, not at all.
// TODO: Implement locking on write or ditch caching of gzip and use
// existing middlewares.
const respond = () => {
req.method = oldReq.method || req.method;
res.write = oldRes.write || res.write;
res.end = oldRes.end || res.end;
const headers: Headers = {};
Object.assign(headers, (responseCache[cacheKey].headers || {}));
const statusCode = responseCache[cacheKey].statusCode;
let pathStr = `${CACHE_DIR}minified_${cacheKey}`;
if (supportsGzip && /application\/javascript/.test(headers['content-type'])) {
pathStr += '.gz';
headers['content-encoding'] = 'gzip';
}
const lastModified = headers['last-modified'] && new Date(headers['last-modified']);
if (statusCode === 200 && lastModified <= modifiedSince) {
res.writeHead(304, headers);
res.end();
} else if (req.method === 'GET') {
const readStream = fs.createReadStream(pathStr);
res.writeHead(statusCode, headers);
readStream.pipe(res);
} else {
res.writeHead(statusCode, headers);
res.end();
}
};
const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires);
if (expirationDate > new Date()) {
// Our cached version is still valid.
return respond();
}
const _headers:Headers = {};
oldRes.setHeader = res.setHeader;
res.setHeader = (key: string, value: string) => {
// Don't set cookies, see issue #707
if (key.toLowerCase() === 'set-cookie') return;
_headers[key.toLowerCase()] = value;
// @ts-ignore
oldRes.setHeader.call(res, key, value);
};
oldRes.writeHead = res.writeHead;
res.writeHead = (status: number, headers: Headers) => {
res.writeHead = oldRes.writeHead;
if (status === 200) {
// Update cache
let buffer = '';
Object.keys(headers || {}).forEach((key) => {
res.setHeader(key, headers[key]);
});
headers = _headers;
oldRes.write = res.write;
oldRes.end = res.end;
res.write = (data: number, encoding: number) => {
buffer += data.toString(encoding);
};
res.end = async (data: number, encoding: number) => {
await Promise.all([
fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}),
util.promisify(zlib.gzip)(buffer)
// @ts-ignore
.then((content: string) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content))
.catch(() => {}),
]);
responseCache[cacheKey] = {statusCode: status, headers};
respond();
};
} else if (status === 304) {
// Nothing new changed from the cached version.
oldRes.write = res.write;
oldRes.end = res.end;
res.write = (data: number, encoding: number) => {};
res.end = (data: number, encoding: number) => { respond(); };
} else {
res.writeHead(status, headers);
}
};
next(undefined, req, res);
}
};

View file

@ -3,8 +3,13 @@
import {PadAuthor, PadType} from "../types/PadType";
import {MapArrayType} from "../types/MapType";
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
import AttributeMap from '../../static/js/AttributeMap';
import {applyToAText, checkRep, compose, deserializeOps, pack, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
import {Builder} from "../../static/js/Builder";
import {OpAssembler} from "../../static/js/OpAssembler";
import {numToString} from "../../static/js/ChangesetUtils";
import Op from "../../static/js/Op";
import {StringAssembler} from "../../static/js/StringAssembler";
const attributes = require('../../static/js/attributes');
const exportHtml = require('./ExportHtml');
@ -33,7 +38,7 @@ class PadDiff {
}
_isClearAuthorship(changeset: any){
// unpack
const unpacked = Changeset.unpack(changeset);
const unpacked = unpack(changeset);
// check if there is nothing in the charBank
if (unpacked.charBank !== '') {
@ -45,7 +50,7 @@ class PadDiff {
return false;
}
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
const [clearOperator, anotherOp] = deserializeOps(unpacked.ops);
// check if there is only one operator
if (anotherOp != null) return false;
@ -78,7 +83,7 @@ class PadDiff {
const atext = await this._pad.getInternalRevisionAText(rev);
// build clearAuthorship changeset
const builder = Changeset.builder(atext.text.length);
const builder = new Builder(atext.text.length);
builder.keepText(atext.text, [['author', '']], this._pad.pool);
const changeset = builder.toString();
@ -93,7 +98,7 @@ class PadDiff {
const changeset = await this._createClearAuthorship(rev);
// apply the clearAuthorship changeset
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
const newAText = applyToAText(changeset, atext, this._pad.pool);
return newAText;
}
@ -157,7 +162,7 @@ class PadDiff {
if (superChangeset == null) {
superChangeset = changeset;
} else {
superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool);
superChangeset = compose(superChangeset, changeset, this._pad.pool);
}
}
@ -171,10 +176,10 @@ class PadDiff {
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
// apply the superChangeset, which includes all addings
atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool);
atext = applyToAText(superChangeset, atext, this._pad.pool);
// apply the deletionChangeset, which adds a deletions
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
atext = applyToAText(deletionChangeset, atext, this._pad.pool);
}
return atext;
@ -209,22 +214,22 @@ class PadDiff {
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
// unpack
const unpacked = Changeset.unpack(changeset);
const unpacked = unpack(changeset);
const assem = Changeset.opAssembler();
const assem = new OpAssembler();
// create deleted attribs
const authorAttrib = apool.putAttrib(['author', author || '']);
const deletedAttrib = apool.putAttrib(['removed', true]);
const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`;
const attribs = `*${numToString(authorAttrib)}*${numToString(deletedAttrib)}`;
for (const operator of Changeset.deserializeOps(unpacked.ops)) {
for (const operator of deserializeOps(unpacked.ops)) {
if (operator.opcode === '-') {
// this is a delete operator, extend it with the author
operator.attribs = attribs;
} else if (operator.opcode === '=' && operator.attribs) {
// this is operator changes only attributes, let's mark which author did that
operator.attribs += `*${Changeset.numToString(authorAttrib)}`;
operator.attribs += `*${numToString(authorAttrib)}`;
}
// append the new operator to our assembler
@ -232,26 +237,31 @@ class PadDiff {
}
// return the modified changeset
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
return pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
}
_createDeletionChangeset(cs: any, startAText: any, apool: any){
const lines = Changeset.splitTextLines(startAText.text);
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
const lines = splitTextLines(startAText.text);
const alines = splitAttributionLines(startAText.attribs, startAText.text);
// lines and alines are what the exports is meant to apply to.
// They may be arrays or objects with .get(i) and .length methods.
// They include final newlines on lines.
const linesGet = (idx: number) => {
// @ts-ignore
if (lines.get) {
// @ts-ignore
return lines.get(idx);
} else {
// @ts-ignore
return lines[idx];
}
};
const aLinesGet = (idx: number) => {
// @ts-ignore
if (alines.get) {
// @ts-ignore
return alines.get(idx);
} else {
return alines[idx];
@ -263,14 +273,14 @@ class PadDiff {
let curLineOps: { next: () => any; } | null = null;
let curLineOpsNext: { done: any; value: any; } | null = null;
let curLineOpsLine: number;
let curLineNextOp = new Changeset.Op('+');
let curLineNextOp = new Op('+');
const unpacked = Changeset.unpack(cs);
const builder = Changeset.builder(unpacked.newLen);
const unpacked = unpack(cs);
const builder = new Builder(unpacked.newLen);
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
if (!curLineOps || curLineOpsLine !== curLine) {
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOps = deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps!.next();
curLineOpsLine = curLine;
let indexIntoLine = 0;
@ -291,13 +301,13 @@ class PadDiff {
curChar = 0;
curLineOpsLine = curLine;
curLineNextOp.chars = 0;
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOps = deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps!.next();
}
if (!curLineNextOp.chars) {
if (curLineOpsNext!.done) {
curLineNextOp = new Changeset.Op();
curLineNextOp = new Op();
} else {
curLineNextOp = curLineOpsNext!.value;
curLineOpsNext = curLineOps!.next();
@ -332,7 +342,7 @@ class PadDiff {
const nextText = (numChars: number) => {
let len = 0;
const assem = Changeset.stringAssembler();
const assem = new StringAssembler();
const firstString = linesGet(curLine).substring(curChar);
len += firstString.length;
assem.append(firstString);
@ -360,7 +370,7 @@ class PadDiff {
};
};
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
for (const csOp of deserializeOps(unpacked.ops)) {
if (csOp.opcode === '=') {
const textBank = nextText(csOp.chars);
@ -442,7 +452,7 @@ class PadDiff {
}
}
return Changeset.checkRep(builder.toString());
return checkRep(builder.toString());
}
}
@ -450,6 +460,7 @@ class PadDiff {
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
// it adds deletions and attribute changes to the atext.
// @ts-ignore
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
};

View file

@ -1,5 +1,5 @@
'use strict';
const fs = require('fs');
import fs from 'node:fs';
const check = (path:string) => {
const existsSync = fs.statSync || fs.existsSync;
@ -13,4 +13,4 @@ const check = (path:string) => {
return result;
};
module.exports = check;
export default check;

View file

@ -7,7 +7,7 @@
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
// the predicate.
exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
export const firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
if (predicate == null) {
predicate = (x: any) => x;
}
@ -44,7 +44,7 @@ exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) =
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
// function resolves once all `total` Promises have resolved.
exports.timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
export const timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
let next = 0;
const addAnother = () => promiseCreator(next++).finally(() => {
@ -61,7 +61,7 @@ exports.timesLimit = async (total: number, concurrency: number, promiseCreator:
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
* properties.
*/
class Gate<T> extends Promise<T> {
export class Gate<T> extends Promise<T> {
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
// https://stackoverflow.com/a/65669070 for the rationale.
static get [Symbol.species]() { return Promise; }
@ -75,4 +75,3 @@ class Gate<T> extends Promise<T> {
Object.assign(this, props);
}
}
exports.Gate = Gate;

View file

@ -1,10 +1,8 @@
'use strict';
const path = require('path');
import path from 'path';
// Normalizes p and ensures that it is a relative path that does not reach outside. See
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
module.exports = (p: string, pathApi = path) => {
const sanitizeRoot = (p: string, pathApi = path) => {
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
@ -21,3 +19,5 @@ module.exports = (p: string, pathApi = path) => {
if (pathApi.sep === '\\') p = p.replace(/\\/g, '/');
return p;
};
export default sanitizeRoot

View file

@ -30,23 +30,23 @@
}
],
"dependencies": {
"@etherpad/express-session": "^1.18.2",
"async": "^3.2.5",
"axios": "^1.7.3",
"@etherpad/express-session": "^1.18.4",
"async": "^3.2.6",
"axios": "^1.7.7",
"cookie-parser": "^1.4.6",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.3",
"ejs": "^3.1.10",
"esbuild": "^0.23.0",
"esbuild": "^0.23.1",
"express": "4.19.2",
"express-rate-limit": "^7.4.0",
"fast-deep-equal": "^3.1.3",
"find-root": "1.1.0",
"formidable": "^3.5.1",
"http-errors": "^2.0.0",
"jose": "^5.6.3",
"jose": "^5.8.0",
"js-cookie": "^3.0.5",
"jsdom": "^24.1.1",
"jsdom": "^25.0.0",
"jsonminify": "0.4.2",
"jsonwebtoken": "^9.0.2",
"languages4translatewiki": "0.1.3",
@ -67,11 +67,10 @@
"semver": "^7.6.3",
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"superagent": "9.0.2",
"threads": "^1.7.0",
"superagent": "10.1.0",
"tinycon": "0.6.8",
"tsx": "4.17.0",
"ueberdb2": "^4.2.92",
"tsx": "4.19.0",
"ueberdb2": "^4.2.100",
"underscore": "1.13.7",
"unorm": "1.6.0",
"wtfnode": "^0.9.3"
@ -81,26 +80,28 @@
"etherpad-lite": "node/server.ts"
},
"devDependencies": {
"@playwright/test": "^1.46.0",
"@playwright/test": "^1.46.1",
"@types/async": "^3.2.24",
"@types/express": "^4.17.21",
"@types/formidable": "^3.4.5",
"@types/http-errors": "^2.0.4",
"@types/jquery": "^3.5.30",
"@types/js-cookie": "^3.0.6",
"@types/jsdom": "^21.1.7",
"@types/jsonwebtoken": "^9.0.6",
"@types/mime-types": "^2.1.4",
"@types/mocha": "^10.0.7",
"@types/node": "^22.1.0",
"@types/oidc-provider": "^8.5.1",
"@types/node": "^22.5.4",
"@types/oidc-provider": "^8.5.2",
"@types/semver": "^7.5.8",
"@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2",
"@types/underscore": "^1.11.15",
"chokidar": "^3.6.0",
"eslint": "^9.8.0",
"eslint": "^9.9.1",
"eslint-config-etherpad": "^4.0.4",
"etherpad-cli-client": "^3.0.2",
"mocha": "^10.7.0",
"mocha": "^10.7.3",
"mocha-froth": "^0.2.10",
"nodeify": "^1.0.1",
"openapi-schema-validation": "^0.4.2",
@ -108,7 +109,9 @@
"sinon": "^18.0.0",
"split-grid": "^1.0.11",
"supertest": "^7.0.0",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"vitest": "^2.0.5",
"rusty-store-kv": "^1.1.4"
},
"engines": {
"node": ">=18.18.2",
@ -132,8 +135,9 @@
"test-ui:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs --ui",
"test-admin": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --workers 1",
"test-admin:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --ui --workers 1",
"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"
},
"version": "2.2.2",
"version": "2.2.3",
"license": "Apache-2.0"
}

View file

@ -1,10 +1,10 @@
'use strict';
const AttributeMap = require('./AttributeMap');
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const attributes = require('./attributes');
const underscore = require("underscore")
// @ts-nocheck
import AttributeMap from './AttributeMap';
import {compose, deserializeOps, isIdentity} from './Changeset';
import {Builder} from "./Builder";
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils';
import attributes from './attributes';
import underscore from "underscore";
const lineMarkerAttribute = 'lmkr';
@ -51,7 +51,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
if (!this.applyChangesetCallback) return changeset;
const cs = changeset.toString();
if (!Changeset.isIdentity(cs)) {
if (!isIdentity(cs)) {
this.applyChangesetCallback(cs);
}
@ -85,7 +85,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// as the range might not be continuous
// due to the presence of line markers on the rows
if (allChangesets) {
allChangesets = Changeset.compose(
allChangesets = compose(
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
} else {
allChangesets = rowChangeset;
@ -125,9 +125,9 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
* @param attribs an array of attributes
*/
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
const builder = Changeset.builder(this.rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
ChangesetUtils.buildKeepRange(
const builder = new Builder(this.rep.lines.totalWidth());
buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
buildKeepRange(
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
return builder;
},
@ -150,7 +150,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// get `attributeName` attribute of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return '';
const [op] = Changeset.deserializeOps(aline);
const [op] = deserializeOps(aline);
if (op == null) return '';
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
},
@ -163,7 +163,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// get attributes of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return [];
const [op] = Changeset.deserializeOps(aline);
const [op] = deserializeOps(aline);
if (op == null) return [];
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
},
@ -221,7 +221,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
let hasAttrib = true;
let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
for (const op of deserializeOps(rep.alines[lineNum])) {
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
@ -258,7 +258,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// we need to sum up how much characters each operations take until the wanted position
let currentPointer = 0;
for (const currentOperation of Changeset.deserializeOps(aline)) {
for (const currentOperation of deserializeOps(aline)) {
currentPointer += currentOperation.chars;
if (currentPointer <= column) continue;
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
@ -285,13 +285,13 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
*/
setAttributeOnLine(lineNum, attributeName, attributeValue) {
let loc = [0, 0];
const builder = Changeset.builder(this.rep.lines.totalWidth());
const builder = new Builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum);
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
if (hasMarker) {
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
[attributeName, attributeValue],
], this.rep.apool);
} else {
@ -314,7 +314,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
* @param attributeValue if given only attributes with equal value will be removed
*/
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
const builder = Changeset.builder(this.rep.lines.totalWidth());
const builder = new Builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum);
let found = false;
@ -333,16 +333,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
return;
}
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
// if we have marker and any of attributes don't need to have marker. we need delete it
if (hasMarker && !countAttribsWithMarker) {
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
} else {
ChangesetUtils.buildKeepRange(
buildKeepRange(
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
}

View file

@ -1,6 +1,9 @@
'use strict';
const attributes = require('./attributes');
import AttributePool from "./AttributePool";
import {Attribute} from "./types/Attribute";
import attributes from './attributes';
/**
* A `[key, value]` pair of strings describing a text attribute.
@ -21,6 +24,7 @@ const attributes = require('./attributes');
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
*/
class AttributeMap extends Map {
private readonly pool? : AttributePool|null
/**
* Converts an attribute string into an AttributeMap.
*
@ -28,14 +32,14 @@ class AttributeMap extends Map {
* @param {AttributePool} pool - Attribute pool.
* @returns {AttributeMap}
*/
static fromString(str, pool) {
public static fromString(str: string, pool?: AttributePool|null): AttributeMap {
return new AttributeMap(pool).updateFromString(str);
}
/**
* @param {AttributePool} pool - Attribute pool.
*/
constructor(pool) {
constructor(pool?: AttributePool|null) {
super();
/** @public */
this.pool = pool;
@ -46,15 +50,15 @@ class AttributeMap extends Map {
* @param {string} v - Attribute value.
* @returns {AttributeMap} `this` (for chaining).
*/
set(k, v) {
set(k: string, v: string):this {
k = k == null ? '' : String(k);
v = v == null ? '' : String(v);
this.pool.putAttrib([k, v]);
this.pool!.putAttrib([k, v]);
return super.set(k, v);
}
toString() {
return attributes.attribsToString(attributes.sort([...this]), this.pool);
return attributes.attribsToString(attributes.sort([...this]), this.pool!);
}
/**
@ -63,7 +67,7 @@ class AttributeMap extends Map {
* key is removed from this map (if present).
* @returns {AttributeMap} `this` (for chaining).
*/
update(entries, emptyValueIsDelete = false) {
update(entries: Iterable<Attribute>, emptyValueIsDelete: boolean = false): AttributeMap {
for (let [k, v] of entries) {
k = k == null ? '' : String(k);
v = v == null ? '' : String(v);
@ -83,9 +87,9 @@ class AttributeMap extends Map {
* key is removed from this map (if present).
* @returns {AttributeMap} `this` (for chaining).
*/
updateFromString(str, emptyValueIsDelete = false) {
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
updateFromString(str: string, emptyValueIsDelete: boolean = false): AttributeMap {
return this.update(attributes.attribsFromString(str, this.pool!), emptyValueIsDelete);
}
}
module.exports = AttributeMap;
export default AttributeMap

View file

@ -44,6 +44,8 @@
* @property {number} nextNum - The attribute ID to assign to the next new attribute.
*/
import {Attribute} from "./types/Attribute";
/**
* Represents an attribute pool, which is a collection of attributes (pairs of key and value
* strings) along with their identifiers (non-negative integers).
@ -55,6 +57,14 @@
* in the pad.
*/
class AttributePool {
numToAttrib: {
[key: number]: [string, string]
}
private attribToNum: {
[key: number]: [string, string]
}
private nextNum: number
constructor() {
/**
* Maps an attribute identifier to the attribute's `[key, value]` string pair.
@ -96,7 +106,10 @@ class AttributePool {
*/
clone() {
const c = new AttributePool();
for (const [n, a] of Object.entries(this.numToAttrib)) c.numToAttrib[n] = [a[0], a[1]];
for (const [n, a] of Object.entries(this.numToAttrib)){
// @ts-ignore
c.numToAttrib[n] = [a[0], a[1]];
}
Object.assign(c.attribToNum, this.attribToNum);
c.nextNum = this.nextNum;
return c;
@ -111,15 +124,17 @@ class AttributePool {
* membership in the pool without mutating the pool.
* @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool.
*/
putAttrib(attrib, dontAddIfAbsent = false) {
putAttrib(attrib: Attribute, dontAddIfAbsent = false) {
const str = String(attrib);
if (str in this.attribToNum) {
// @ts-ignore
return this.attribToNum[str];
}
if (dontAddIfAbsent) {
return -1;
}
const num = this.nextNum++;
// @ts-ignore
this.attribToNum[str] = num;
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
return num;
@ -130,7 +145,7 @@ class AttributePool {
* @returns {Attribute} The attribute with the given identifier, or nullish if there is no such
* attribute.
*/
getAttrib(num) {
getAttrib(num: number): Attribute {
const pair = this.numToAttrib[num];
if (!pair) {
return pair;
@ -143,7 +158,7 @@ class AttributePool {
* @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty
* string.
*/
getAttribKey(num) {
getAttribKey(num: number): string {
const pair = this.numToAttrib[num];
if (!pair) return '';
return pair[0];
@ -154,7 +169,7 @@ class AttributePool {
* @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty
* string.
*/
getAttribValue(num) {
getAttribValue(num: number) {
const pair = this.numToAttrib[num];
if (!pair) return '';
return pair[1];
@ -166,8 +181,8 @@ class AttributePool {
* @param {Function} func - Callback to call with two arguments: key and value. Its return value
* is ignored.
*/
eachAttrib(func) {
for (const n of Object.keys(this.numToAttrib)) {
eachAttrib(func: (k: string, v: string)=>void) {
for (const n in this.numToAttrib) {
const pair = this.numToAttrib[n];
func(pair[0], pair[1]);
}
@ -196,11 +211,12 @@ class AttributePool {
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
* state will lead to pool corruption.
*/
fromJsonable(obj) {
fromJsonable(obj: this) {
this.numToAttrib = obj.numToAttrib;
this.nextNum = obj.nextNum;
this.attribToNum = {};
for (const n of Object.keys(this.numToAttrib)) {
// @ts-ignore
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
}
return this;
@ -213,6 +229,7 @@ class AttributePool {
if (!Number.isInteger(this.nextNum)) throw new Error('nextNum property is not an integer');
if (this.nextNum < 0) throw new Error('nextNum property is negative');
for (const prop of ['numToAttrib', 'attribToNum']) {
// @ts-ignore
const obj = this[prop];
if (obj == null) throw new Error(`${prop} property is null`);
if (typeof obj !== 'object') throw new TypeError(`${prop} property is not an object`);
@ -231,9 +248,10 @@ class AttributePool {
if (v == null) throw new TypeError(`attrib ${i} value is null`);
if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`);
const attrStr = String(attr);
// @ts-ignore
if (this.attribToNum[attrStr] !== i) throw new Error(`attribToNum for ${attrStr} !== ${i}`);
}
}
}
module.exports = AttributePool;
export default AttributePool

108
src/static/js/Builder.ts Normal file
View file

@ -0,0 +1,108 @@
/**
* Incrementally builds a Changeset.
*
* @typedef {object} Builder
* @property {Function} insert -
* @property {Function} keep -
* @property {Function} keepText -
* @property {Function} remove -
* @property {Function} toString -
*/
import {SmartOpAssembler} from "./SmartOpAssembler";
import Op from "./Op";
import {StringAssembler} from "./StringAssembler";
import AttributeMap from "./AttributeMap";
import {Attribute} from "./types/Attribute";
import AttributePool from "./AttributePool";
import {opsFromText, pack} from "./Changeset";
/**
* @param {number} oldLen - Old length
* @returns {Builder}
*/
export class Builder {
private readonly oldLen: number;
private assem: SmartOpAssembler;
private readonly o: Op;
private charBank: StringAssembler;
constructor(oldLen: number) {
this.oldLen = oldLen
this.assem = new SmartOpAssembler()
this.o = new Op()
this.charBank = new StringAssembler()
}
/**
* @param {number} N - Number of characters to keep.
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
* character must be a newline.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
keep = (N: number, L?: number, attribs?: string|Attribute[], pool?: AttributePool): Builder => {
this.o.opcode = '=';
this.o.attribs = typeof attribs === 'string'
? attribs : new AttributeMap(pool).update(attribs || []).toString();
this.o.chars = N;
this.o.lines = (L || 0);
this.assem.append(this.o);
return this;
}
/**
* @param {string} text - Text to keep.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
keepText= (text: string, attribs?: string|Attribute[], pool?: AttributePool): Builder=> {
for (const op of opsFromText('=', text, attribs, pool)) this.assem.append(op);
return this;
}
/**
* @param {string} text - Text to insert.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
insert= (text: string, attribs: string | Attribute[] | undefined, pool?: AttributePool | null | undefined): Builder => {
for (const op of opsFromText('+', text, attribs, pool)) this.assem.append(op);
this.charBank.append(text);
return this;
}
/**
* @param {number} N - Number of characters to remove.
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
* character must be a newline.
* @returns {Builder} this
*/
remove= (N: number, L?: number): Builder => {
this.o.opcode = '-';
this.o.attribs = '';
this.o.chars = N;
this.o.lines = (L || 0);
this.assem.append(this.o);
return this;
}
toString= () => {
this.assem.endDocument();
const newLen = this.oldLen + this.assem.getLengthChange();
return pack(this.oldLen, newLen, this.assem.toString(), this.charBank.toString());
}
}

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,12 @@
* based on a SkipList
*/
import {RepModel} from "./types/RepModel";
import {ChangeSetBuilder} from "./types/ChangeSetBuilder";
import {Attribute} from "./types/Attribute";
import AttributePool from "./AttributePool";
import {Builder} from "./Builder";
/**
* Copyright 2009 Google Inc.
*
@ -20,7 +26,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.buildRemoveRange = (rep, builder, start, end) => {
export const buildRemoveRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number,number], end: [number, number]) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -32,7 +38,7 @@ exports.buildRemoveRange = (rep, builder, start, end) => {
}
};
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
export const buildKeepRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number], end:[number, number], attribs?: Attribute[], pool?: AttributePool) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -44,9 +50,25 @@ exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
}
};
exports.buildKeepToStartOfRange = (rep, builder, start) => {
export const buildKeepToStartOfRange = (rep: RepModel, builder: Builder, start: [number, number]) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]);
builder.keep(start[1]);
};
/**
* Parses a number from string base 36.
*
* @param {string} str - string of the number in base 36
* @returns {number} number
*/
export const parseNum = (str: string): number => parseInt(str, 36);
/**
* Writes a number in base 36 and puts it in a string.
*
* @param {number} num - number
* @returns {string} string
*/
export const numToString = (num: number): string => num.toString(36).toLowerCase();

View file

@ -1,6 +1,6 @@
'use strict';
const {padutils: {warnDeprecated}} = require('./pad_utils');
import padUtils from './pad_utils'
/**
* Represents a chat message stored in the database and transmitted among users. Plugins can extend
@ -8,14 +8,25 @@ const {padutils: {warnDeprecated}} = require('./pad_utils');
*
* Supports serialization to JSON.
*/
class ChatMessage {
static fromObject(obj) {
export class ChatMessage {
customMetadata: any
text: string|null
public authorId: string|null
displayName: string|null
time: number|null
static fromObject(obj: ChatMessage) {
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept
// the old names in case the db record was written by an older version of Etherpad.
obj = Object.assign({}, obj); // Don't mutate the caller's object.
if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId;
if ('userId' in obj && !('authorId' in obj)) { // @ts-ignore
obj.authorId = obj.userId;
}
// @ts-ignore
delete obj.userId;
if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName;
if ('userName' in obj && !('displayName' in obj)) { // @ts-ignore
obj.displayName = obj.userName;
}
// @ts-ignore
delete obj.userName;
return Object.assign(new ChatMessage(), obj);
}
@ -25,7 +36,7 @@ class ChatMessage {
* @param {?string} [authorId] - Initial value of the `authorId` property.
* @param {?number} [time] - Initial value of the `time` property.
*/
constructor(text = null, authorId = null, time = null) {
constructor(text: string | null = null, authorId: string | null = null, time: number | null = null) {
/**
* The raw text of the user's chat message (before any rendering or processing).
*
@ -62,11 +73,11 @@ class ChatMessage {
* @type {string}
*/
get userId() {
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
return this.authorId;
}
set userId(val) {
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
this.authorId = val;
}
@ -77,11 +88,11 @@ class ChatMessage {
* @type {string}
*/
get userName() {
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
return this.displayName;
}
set userName(val) {
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
this.displayName = val;
}
@ -89,10 +100,12 @@ class ChatMessage {
// doesn't support authorId and displayName.
toJSON() {
const {authorId, displayName, ...obj} = this;
// @ts-ignore
obj.userId = authorId;
// @ts-ignore
obj.userName = displayName;
return obj;
}
}
module.exports = ChatMessage;
export default ChatMessage

View file

@ -0,0 +1,73 @@
import {OpAssembler} from "./OpAssembler";
import Op from "./Op";
import {clearOp, copyOp} from "./Changeset";
export class MergingOpAssembler {
private assem: OpAssembler;
private readonly bufOp: Op;
private bufOpAdditionalCharsAfterNewline: number;
constructor() {
this.assem = new OpAssembler()
this.bufOp = new Op()
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
// This variable stores the length of yyy and any other newline-less
// ops immediately after it.
this.bufOpAdditionalCharsAfterNewline = 0;
}
/**
* @param {boolean} [isEndDocument]
*/
flush = (isEndDocument?: boolean) => {
if (!this.bufOp.opcode) return;
if (isEndDocument && this.bufOp.opcode === '=' && !this.bufOp.attribs) {
// final merged keep, leave it implicit
} else {
this.assem.append(this.bufOp);
if (this.bufOpAdditionalCharsAfterNewline) {
this.bufOp.chars = this.bufOpAdditionalCharsAfterNewline;
this.bufOp.lines = 0;
this.assem.append(this.bufOp);
this.bufOpAdditionalCharsAfterNewline = 0;
}
}
this.bufOp.opcode = '';
}
append = (op: Op) => {
if (op.chars <= 0) return;
if (this.bufOp.opcode === op.opcode && this.bufOp.attribs === op.attribs) {
if (op.lines > 0) {
// bufOp and additional chars are all mergeable into a multi-line op
this.bufOp.chars += this.bufOpAdditionalCharsAfterNewline + op.chars;
this.bufOp.lines += op.lines;
this.bufOpAdditionalCharsAfterNewline = 0;
} else if (this.bufOp.lines === 0) {
// both bufOp and op are in-line
this.bufOp.chars += op.chars;
} else {
// append in-line text to multi-line bufOp
this.bufOpAdditionalCharsAfterNewline += op.chars;
}
} else {
this.flush();
copyOp(op, this.bufOp);
}
}
endDocument = () => {
this.flush(true);
};
toString = () => {
this.flush();
return this.assem.toString();
};
clear = () => {
this.assem.clear();
clearOp(this.bufOp);
};
}

78
src/static/js/Op.ts Normal file
View file

@ -0,0 +1,78 @@
import {numToString} from "./ChangesetUtils";
export type OpCode = ''|'='|'+'|'-';
/**
* An operation to apply to a shared document.
*/
export default class Op {
opcode: ''|'='|'+'|'-'
chars: number
lines: number
attribs: string
/**
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
*/
constructor(opcode:''|'='|'+'|'-' = '') {
/**
* The operation's operator:
* - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
* the document. The inserted characters come from the changeset's character bank.
* - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
* operation.
*
* @type {(''|'='|'+'|'-')}
* @public
*/
this.opcode = opcode;
/**
* The number of characters to keep, insert, or delete.
*
* @type {number}
* @public
*/
this.chars = 0;
/**
* The number of characters among the `chars` characters that are newlines. If non-zero, the
* last character must be a newline.
*
* @type {number}
* @public
*/
this.lines = 0;
/**
* Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
* sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
* '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
* identifiers come from the document's attribute pool.
*
* For keep ('=') operations, the attributes are merged with the base text's existing
* attributes:
* - A keep op attribute with a non-empty value replaces an existing base text attribute that
* has the same key.
* - A keep op attribute with an empty value is interpreted as an instruction to remove an
* existing base text attribute that has the same key, if one exists.
*
* This is the empty string for remove ('-') operations.
*
* @type {string}
* @public
*/
this.attribs = '';
}
toString() {
if (!this.opcode) throw new TypeError('null op');
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
const l = this.lines ? `|${numToString(this.lines)}` : '';
return this.attribs + l + this.opcode + numToString(this.chars);
}
}

View file

@ -0,0 +1,21 @@
import Op from "./Op";
import {assert} from './Changeset'
/**
* @returns {OpAssembler}
*/
export class OpAssembler {
private serialized: string;
constructor() {
this.serialized = ''
}
append = (op: Op) => {
assert(op instanceof Op, 'argument must be an instance of Op');
this.serialized += op.toString();
}
toString = () => this.serialized
clear = () => {
this.serialized = '';
}
}

47
src/static/js/OpIter.ts Normal file
View file

@ -0,0 +1,47 @@
import Op from "./Op";
import {clearOp, copyOp, deserializeOps} from "./Changeset";
/**
* Iterator over a changeset's operations.
*
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
*
* @deprecated Use `deserializeOps` instead.
*/
export class OpIter {
private gen
private _next: IteratorResult<Op, void>
/**
* @param {string} ops - String encoding the change operations to iterate over.
*/
constructor(ops: string) {
this.gen = deserializeOps(ops);
this._next = this.gen.next();
}
/**
* @returns {boolean} Whether there are any remaining operations.
*/
hasNext(): boolean {
return !this._next.done;
}
/**
* Returns the next operation object and advances the iterator.
*
* Note: This does NOT implement the ECMAScript iterator protocol.
*
* @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value.
* @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are
* no more operations.
*/
next(opOut: Op = new Op()): Op {
if (this.hasNext()) {
copyOp(this._next.value!, opOut);
this._next = this.gen.next();
} else {
clearOp(opOut);
}
return opOut;
}
}

View file

@ -0,0 +1,115 @@
import {MergingOpAssembler} from "./MergingOpAssembler";
import {StringAssembler} from "./StringAssembler";
import padutils from "./pad_utils";
import Op from "./Op";
import { Attribute } from "./types/Attribute";
import AttributePool from "./AttributePool";
import {opsFromText} from "./Changeset";
/**
* Creates an object that allows you to append operations (type Op) and also compresses them if
* possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser
* input, at the cost of speed. Specifically:
* - merges consecutive operations that can be merged
* - strips final "="
* - ignores 0-length changes
* - reorders consecutive + and - (which MergingOpAssembler doesn't do)
*
* @typedef {object} SmartOpAssembler
* @property {Function} append -
* @property {Function} appendOpWithText -
* @property {Function} clear -
* @property {Function} endDocument -
* @property {Function} getLengthChange -
* @property {Function} toString -
*/
export class SmartOpAssembler {
private minusAssem: MergingOpAssembler;
private plusAssem: MergingOpAssembler;
private keepAssem: MergingOpAssembler;
private lastOpcode: string;
private lengthChange: number;
private assem: StringAssembler;
constructor() {
this.minusAssem = new MergingOpAssembler()
this.plusAssem = new MergingOpAssembler()
this.keepAssem = new MergingOpAssembler()
this.assem = new StringAssembler()
this.lastOpcode = ''
this.lengthChange = 0
}
flushKeeps = () => {
this.assem.append(this.keepAssem.toString());
this.keepAssem.clear();
};
flushPlusMinus = () => {
this.assem.append(this.minusAssem.toString());
this.minusAssem.clear();
this.assem.append(this.plusAssem.toString());
this.plusAssem.clear();
};
append = (op: Op) => {
if (!op.opcode) return;
if (!op.chars) return;
if (op.opcode === '-') {
if (this.lastOpcode === '=') {
this.flushKeeps();
}
this.minusAssem.append(op);
this.lengthChange -= op.chars;
} else if (op.opcode === '+') {
if (this.lastOpcode === '=') {
this.flushKeeps();
}
this.plusAssem.append(op);
this.lengthChange += op.chars;
} else if (op.opcode === '=') {
if (this.lastOpcode !== '=') {
this.flushPlusMinus();
}
this.keepAssem.append(op);
}
this.lastOpcode = op.opcode;
};
/**
* Generates operations from the given text and attributes.
*
* @deprecated Use `opsFromText` instead.
* @param {('-'|'+'|'=')} opcode - The operator to use.
* @param {string} text - The text to remove/add/keep.
* @param {(string|Iterable<Attribute>)} attribs - The attributes to apply to the operations.
* @param {?AttributePool.ts} pool - Attribute pool. Only required if `attribs` is an iterable of
* attribute key, value pairs.
*/
appendOpWithText = (opcode: '-'|'+'|'=', text: string, attribs: Attribute[]|string, pool?: AttributePool) => {
padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' +
'use opsFromText() instead.');
for (const op of opsFromText(opcode, text, attribs, pool)) this.append(op);
};
toString = () => {
this.flushPlusMinus();
this.flushKeeps();
return this.assem.toString();
};
clear = () => {
this.minusAssem.clear();
this.plusAssem.clear();
this.keepAssem.clear();
this.assem.clear();
this.lengthChange = 0;
};
endDocument = () => {
this.keepAssem.endDocument();
};
getLengthChange = () => this.lengthChange;
}

View file

@ -0,0 +1,18 @@
/**
* @returns {StringAssembler}
*/
export class StringAssembler {
private str = ''
clear = ()=> {
this.str = '';
}
/**
* @param {string} x -
*/
append(x: string) {
this.str += String(x);
}
toString() {
return this.str
}
}

View file

@ -0,0 +1,54 @@
import {assert} from "./Changeset";
/**
* A custom made String Iterator
*
* @typedef {object} StringIterator
* @property {Function} newlines -
* @property {Function} peek -
* @property {Function} remaining -
* @property {Function} skip -
* @property {Function} take -
*/
/**
* @param {string} str - String to iterate over
* @returns {StringIterator}
*/
export class StringIterator {
private curIndex: number;
private newLines: number;
private str: String
constructor(str: string) {
this.curIndex = 0;
this.str = str
this.newLines = str.split('\n').length - 1;
}
remaining = () => this.str.length - this.curIndex;
getnewLines = () => this.newLines;
assertRemaining = (n: number) => {
assert(n <= this.remaining(), `!(${n} <= ${this.remaining()})`);
}
take = (n: number) => {
this.assertRemaining(n);
const s = this.str.substring(this.curIndex, this.curIndex+n);
this.newLines -= s.split('\n').length - 1;
this.curIndex += n;
return s;
}
peek = (n: number) => {
this.assertRemaining(n);
return this.str.substring(this.curIndex, this.curIndex+n);
}
skip = (n: number) => {
this.assertRemaining(n);
this.curIndex += n;
}
}

View file

@ -0,0 +1,348 @@
import {splitTextLines} from "./Changeset";
/**
* Class to iterate and modify texts which have several lines. It is used for applying Changesets on
* arrays of lines.
*
* Mutation operations have the same constraints as exports operations with respect to newlines, but
* not the other additional constraints (i.e. ins/del ordering, forbidden no-ops, non-mergeability,
* final newline). Can be used to mutate lists of strings where the last char of each string is not
* actually a newline, but for the purposes of N and L values, the caller should pretend it is, and
* for things to work right in that case, the input to the `insert` method should be a single line
* with no newlines.
*/
class TextLinesMutator {
private _lines: string[];
private _curSplice: [number, number?];
private _inSplice: boolean;
private _curLine: number;
private _curCol: number;
/**
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
*/
constructor(lines: string[]) {
this._lines = lines;
/**
* this._curSplice holds values that will be passed as arguments to this._lines.splice() to
* insert, delete, or change lines:
* - this._curSplice[0] is an index into the this._lines array.
* - this._curSplice[1] is the number of lines that will be removed from the this._lines array
* starting at the index.
* - The other elements represent mutated (changed by ops) lines or new lines (added by ops)
* to insert at the index.
*
* @type {[number, number?, ...string[]?]}
*/
this._curSplice = [0, 0];
this._inSplice = false;
// position in lines after curSplice is applied:
this._curLine = 0;
this._curCol = 0;
// invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) &&
// curLine >= curSplice[0]
// invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then
// curCol == 0
}
/**
* Get a line from `lines` at given index.
*
* @param {number} idx - an index
* @returns {string}
*/
_linesGet(idx: number) {
if ('get' in this._lines) {
// @ts-ignore
return this._lines.get(idx) as string;
} else {
return this._lines[idx];
}
}
/**
* Return a slice from `lines`.
*
* @param {number} start - the start index
* @param {number} end - the end index
* @returns {string[]}
*/
_linesSlice(start: number | undefined, end: number | undefined) {
// can be unimplemented if removeLines's return value not needed
if (this._lines.slice) {
return this._lines.slice(start, end);
} else {
return [];
}
}
/**
* Return the length of `lines`.
*
* @returns {number}
*/
_linesLength() {
if (typeof this._lines.length === 'number') {
return this._lines.length;
} else {
// @ts-ignore
return this._lines.length();
}
}
/**
* Starts a new splice.
*/
_enterSplice() {
this._curSplice[0] = this._curLine;
this._curSplice[1] = 0;
// TODO(doc) when is this the case?
// check all enterSplice calls and changes to curCol
if (this._curCol > 0) this._putCurLineInSplice();
this._inSplice = true;
}
/**
* Changes the lines array according to the values in curSplice and resets curSplice. Called via
* close or TODO(doc).
*/
_leaveSplice() {
this._lines.splice(...this._curSplice);
this._curSplice.length = 2;
this._curSplice[0] = this._curSplice[1] = 0;
this._inSplice = false;
}
/**
* Indicates if curLine is already in the splice. This is necessary because the last element in
* curSplice is curLine when this line is currently worked on (e.g. when skipping or inserting).
*
* @returns {boolean} true if curLine is in splice
*/
_isCurLineInSplice() {
// The value of `this._curSplice[1]` does not matter when determining the return value because
// `this._curLine` refers to the line number *after* the splice is applied (so after those lines
// are deleted).
return this._curLine - this._curSplice[0] < this._curSplice.length - 2;
}
/**
* Incorporates current line into the splice and marks its old position to be deleted.
*
* @returns {number} the index of the added line in curSplice
*/
_putCurLineInSplice() {
if (!this._isCurLineInSplice()) {
// @ts-ignore
this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1]));
// @ts-ignore
this._curSplice[1]++;
}
// TODO should be the same as this._curSplice.length - 1
return 2 + this._curLine - this._curSplice[0];
}
/**
* It will skip some newlines by putting them into the splice.
*
* @param {number} L -
* @param {boolean} includeInSplice - Indicates that attributes are present.
*/
skipLines(L: number, includeInSplice?: any) {
if (!L) return;
if (includeInSplice) {
if (!this._inSplice) this._enterSplice();
// TODO(doc) should this count the number of characters that are skipped to check?
for (let i = 0; i < L; i++) {
this._curCol = 0;
this._putCurLineInSplice();
this._curLine++;
}
} else {
if (this._inSplice) {
if (L > 1) {
// TODO(doc) figure out why single lines are incorporated into splice instead of ignored
this._leaveSplice();
} else {
this._putCurLineInSplice();
}
}
this._curLine += L;
this._curCol = 0;
}
// tests case foo in remove(), which isn't otherwise covered in current impl
}
/**
* Skip some characters. Can contain newlines.
*
* @param {number} N - number of characters to skip
* @param {number} L - number of newlines to skip
* @param {boolean} includeInSplice - indicates if attributes are present
*/
skip(N: number, L: number, includeInSplice?: any) {
if (!N) return;
if (L) {
this.skipLines(L, includeInSplice);
} else {
if (includeInSplice && !this._inSplice) this._enterSplice();
if (this._inSplice) {
// although the line is put into splice curLine is not increased, because
// only some chars are skipped, not the whole line
this._putCurLineInSplice();
}
this._curCol += N;
}
}
/**
* Remove whole lines from lines array.
*
* @param {number} L - number of lines to remove
* @returns {string}
*/
removeLines(L: number) {
if (!L) return '';
if (!this._inSplice) this._enterSplice();
/**
* Gets a string of joined lines after the end of the splice.
*
* @param {number} k - number of lines
* @returns {string} joined lines
*/
const nextKLinesText = (k: number) => {
// @ts-ignore
const m = this._curSplice[0] + this._curSplice[1];
return this._linesSlice(m, m + k).join('');
};
let removed = '';
if (this._isCurLineInSplice()) {
if (this._curCol === 0) {
// @ts-ignore
removed = this._curSplice[this._curSplice.length - 1];
this._curSplice.length--;
removed += nextKLinesText(L - 1);
// @ts-ignore
this._curSplice[1] += L - 1;
} else {
removed = nextKLinesText(L - 1);
// @ts-ignore
this._curSplice[1] += L - 1;
const sline = this._curSplice.length - 1;
// @ts-ignore
removed = this._curSplice[sline].substring(this._curCol) + removed;
// @ts-ignore
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
// @ts-ignore
this._linesGet(this._curSplice[0] + this._curSplice[1]);
// @ts-ignore
this._curSplice[1] += 1;
}
} else {
removed = nextKLinesText(L);
this._curSplice[1]! += L;
}
return removed;
}
/**
* Remove text from lines array.
*
* @param {number} N - characters to delete
* @param {number} L - lines to delete
* @returns {string}
*/
remove(N: number, L: any) {
if (!N) return '';
if (L) return this.removeLines(L);
if (!this._inSplice) this._enterSplice();
// although the line is put into splice, curLine is not increased, because
// only some chars are removed not the whole line
const sline = this._putCurLineInSplice();
// @ts-ignore
const removed = this._curSplice[sline].substring(this._curCol, this._curCol + N);
// @ts-ignore
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
// @ts-ignore
this._curSplice[sline].substring(this._curCol + N);
return removed;
}
/**
* Inserts text into lines array.
*
* @param {string} text - the text to insert
* @param {number} L - number of newlines in text
*/
insert(text: string | any[], L: any) {
if (!text) return;
if (!this._inSplice) this._enterSplice();
if (L) {
// @ts-ignore
const newLines = splitTextLines(text);
if (this._isCurLineInSplice()) {
const sline = this._curSplice.length - 1;
/** @type {string} */
const theLine = this._curSplice[sline];
const lineCol = this._curCol;
// Insert the chars up to `curCol` and the first new line.
// @ts-ignore
this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
this._curLine++;
newLines!.splice(0, 1);
// insert the remaining new lines
// @ts-ignore
this._curSplice.push(...newLines);
this._curLine += newLines!.length;
// insert the remaining chars from the "old" line (e.g. the line we were in
// when we started to insert new lines)
// @ts-ignore
this._curSplice.push(theLine.substring(lineCol));
this._curCol = 0; // TODO(doc) why is this not set to the length of last line?
} else {
this._curSplice.push(...newLines);
this._curLine += newLines!.length;
}
} else {
// There are no additional lines. Although the line is put into splice, curLine is not
// increased because there may be more chars in the line (newline is not reached).
const sline = this._putCurLineInSplice();
if (!this._curSplice[sline]) {
const err = new Error(
'curSplice[sline] not populated, actual curSplice contents is ' +
`${JSON.stringify(this._curSplice)}. Possibly related to ` +
'https://github.com/ether/etherpad-lite/issues/2802');
console.error(err.stack || err.toString());
}
// @ts-ignore
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + text +
// @ts-ignore
this._curSplice[sline].substring(this._curCol);
this._curCol += text.length;
}
}
/**
* Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`.
*
* @returns {boolean} indicates if there are lines left
*/
hasMore() {
let docLines = this._linesLength();
if (this._inSplice) {
// @ts-ignore
docLines += this._curSplice.length - 2 - this._curSplice[1];
}
return this._curLine < docLines;
}
/**
* Closes the splice
*/
close() {
if (this._inSplice) this._leaveSplice();
}
}
export default TextLinesMutator

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
import {MapArrayType} from "../../node/types/MapType";
/**
* Copyright 2009 Google Inc.
*
@ -22,11 +24,13 @@
* limitations under the License.
*/
const isNodeText = (node) => (node.nodeType === 3);
export const isNodeText = (node: {
nodeType: number
}) => (node.nodeType === 3);
const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
export const getAssoc = (obj: MapArrayType<any>, name: string) => obj[`_magicdom_${name}`];
const setAssoc = (obj, name, value) => {
export const setAssoc = (obj: MapArrayType<any>, name: string, value: string) => {
// note that in IE designMode, properties of a node can get
// copied to new nodes that are spawned during editing; also,
// properties representable in HTML text can survive copy-and-paste
@ -38,7 +42,7 @@ const setAssoc = (obj, name, value) => {
// between false and true, a number between 0 and numItems inclusive.
const binarySearch = (numItems, func) => {
export const binarySearch = (numItems: number, func: (num: number)=>boolean) => {
if (numItems < 1) return 0;
if (func(0)) return 0;
if (!func(numItems - 1)) return numItems;
@ -52,17 +56,10 @@ const binarySearch = (numItems, func) => {
return high;
};
const binarySearchInfinite = (expectedLength, func) => {
export const binarySearchInfinite = (expectedLength: number, func: (num: number)=>boolean) => {
let i = 0;
while (!func(i)) i += expectedLength;
return binarySearch(i, func);
};
const noop = () => {};
exports.isNodeText = isNodeText;
exports.getAssoc = getAssoc;
exports.setAssoc = setAssoc;
exports.binarySearch = binarySearch;
exports.binarySearchInfinite = binarySearchInfinite;
exports.noop = noop;
export const noop = () => {};

View file

@ -1,4 +1,5 @@
'use strict';
// @ts-nocheck
import {Builder} from "./Builder";
/**
* Copyright 2009 Google Inc.
@ -18,30 +19,32 @@
*/
let documentAttributeManager;
const AttributeMap = require('./AttributeMap');
import AttributeMap from './AttributeMap';
const browser = require('./vendors/browser');
const padutils = require('./pad_utils').padutils;
import padutils from './pad_utils'
const Ace2Common = require('./ace2_common');
const $ = require('./rjquery').$;
import {characterRangeFollow, checkRep, cloneAText, compose, deserializeOps, filterAttribNumbers, inverse, isIdentity, makeAText, makeAttribution, mapAttribNumbers, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, opsFromAText, pack, splitAttributionLines} from './Changeset'
const isNodeText = Ace2Common.isNodeText;
const getAssoc = Ace2Common.getAssoc;
const setAssoc = Ace2Common.setAssoc;
const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks');
import SkipList from "./skiplist";
import Scroll from './scroll'
import AttribPool from './AttributePool'
import {SmartOpAssembler} from "./SmartOpAssembler";
import Op from "./Op";
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils'
function Ace2Inner(editorInfo, cssManagers) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
const makeContentCollector = require('./contentcollector').makeContentCollector;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const linestylefilter = require('./linestylefilter').linestylefilter;
const SkipList = require('./skiplist');
const undoModule = require('./undomodule').undoModule;
const AttributeManager = require('./AttributeManager');
const DEBUG = false;
@ -174,9 +177,9 @@ function Ace2Inner(editorInfo, cssManagers) {
// CCCCCCCCCCCCCCCCCCCC\n
// CCCC\n
// end[0]: <CCC end[1] CCC>-------\n
const builder = Changeset.builder(rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
const builder = new Builder(rep.lines.totalWidth());
buildKeepToStartOfRange(rep, builder, start);
buildRemoveRange(rep, builder, start, end);
builder.insert(newText, [
['author', thisAuthor],
], rep.apool);
@ -495,10 +498,10 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const importAText = (atext, apoolJsonObj, undoable) => {
atext = Changeset.cloneAText(atext);
atext = cloneAText(atext);
if (apoolJsonObj) {
const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
atext.attribs = moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
}
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
setDocAText(atext);
@ -527,18 +530,18 @@ function Ace2Inner(editorInfo, cssManagers) {
const numLines = rep.lines.length();
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
const assem = Changeset.smartOpAssembler();
const o = new Changeset.Op('-');
const assem = new SmartOpAssembler();
const o = new Op('-');
o.chars = upToLastLine;
o.lines = numLines - 1;
assem.append(o);
o.chars = lastLineLength;
o.lines = 0;
assem.append(o);
for (const op of Changeset.opsFromAText(atext)) assem.append(op);
for (const op of opsFromAText(atext)) assem.append(op);
const newLen = oldLen + assem.getLengthChange();
const changeset = Changeset.checkRep(
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
const changeset = checkRep(
pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
performDocumentApplyChangeset(changeset);
performSelectionChange(
@ -552,7 +555,7 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const setDocText = (text) => {
setDocAText(Changeset.makeAText(text));
setDocAText(makeAText(text));
};
const getDocText = () => {
@ -1271,7 +1274,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
theIndent += THE_TAB;
}
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
const cs = new Builder(rep.lines.totalWidth()).keep(
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
theIndent, [
['author', thisAuthor],
@ -1423,7 +1426,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
const result =
Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
}
@ -1435,7 +1438,7 @@ function Ace2Inner(editorInfo, cssManagers) {
length: () => rep.lines.length(),
};
Changeset.mutateTextLines(changes, linesMutatee);
mutateTextLines(changes, linesMutatee);
if (requiredSelectionSetting) {
performSelectionChange(
@ -1446,10 +1449,10 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
Changeset.checkRep(changes);
checkRep(changes);
if (Changeset.oldLen(changes) !== rep.alltext.length) {
const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`;
if (oldLen(changes) !== rep.alltext.length) {
const errMsg = `${oldLen(changes)}/${rep.alltext.length}`;
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
}
@ -1458,10 +1461,10 @@ function Ace2Inner(editorInfo, cssManagers) {
if (!editEvent.changeset) {
editEvent.changeset = changes;
} else {
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
editEvent.changeset = compose(editEvent.changeset, changes, rep.apool);
}
} else {
const inverseChangeset = Changeset.inverse(changes, {
const inverseChangeset = inverse(changes, {
get: (i) => `${rep.lines.atIndex(i).text}\n`,
length: () => rep.lines.length(),
}, rep.alines, rep.apool);
@ -1469,11 +1472,11 @@ function Ace2Inner(editorInfo, cssManagers) {
if (!editEvent.backset) {
editEvent.backset = inverseChangeset;
} else {
editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
editEvent.backset = compose(inverseChangeset, editEvent.backset, rep.apool);
}
}
Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);
mutateAttributionLines(changes, rep.alines, rep.apool);
if (changesetTracker.isTracking()) {
changesetTracker.composeUserChangeset(changes);
@ -1582,7 +1585,7 @@ function Ace2Inner(editorInfo, cssManagers) {
let hasAttrib = true;
let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
for (const op of deserializeOps(rep.alines[lineNum])) {
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
@ -1627,7 +1630,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (n === selEndLine) {
selectionEndInLine = rep.selEnd[1];
}
for (const op of Changeset.deserializeOps(rep.alines[n])) {
for (const op of deserializeOps(rep.alines[n])) {
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
@ -1745,7 +1748,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
const startBuilder = () => {
const builder = Changeset.builder(oldLen);
const builder = new Builder(oldLen);
builder.keep(spliceStartLineStart, spliceStartLine);
builder.keep(spliceStart - spliceStartLineStart);
return builder;
@ -1755,7 +1758,7 @@ function Ace2Inner(editorInfo, cssManagers) {
let textIndex = 0;
const newTextStart = commonStart;
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
for (const op of Changeset.deserializeOps(attribs)) {
for (const op of deserializeOps(attribs)) {
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
@ -1773,7 +1776,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// changeset the applies the styles found in the DOM.
// This allows us to incorporate, e.g., Safari's native "unbold".
const incorpedAttribClearer = cachedStrFunc(
(oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => {
(oldAtts) => mapAttribNumbers(oldAtts, (n) => {
const k = rep.apool.getAttribKey(n);
if (isStyleAttribute(k)) {
return rep.apool.putAttrib([k, '']);
@ -1799,7 +1802,7 @@ function Ace2Inner(editorInfo, cssManagers) {
});
const styler = builder2.toString();
theChangeset = Changeset.compose(clearer, styler, rep.apool);
theChangeset = compose(clearer, styler, rep.apool);
} else {
const builder = startBuilder();
@ -1869,7 +1872,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const attribRuns = (attribs) => {
const lengs = [];
const atts = [];
for (const op of Changeset.deserializeOps(attribs)) {
for (const op of deserializeOps(attribs)) {
lengs.push(op.chars);
atts.push(op.attribs);
}
@ -1898,8 +1901,8 @@ function Ace2Inner(editorInfo, cssManagers) {
const newLen = newText.length;
const minLen = Math.min(oldLen, newLen);
const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
const oldARuns = attribRuns(filterAttribNumbers(oldAttribs, incorpedAttribFilter));
const newARuns = attribRuns(filterAttribNumbers(newAttribs, incorpedAttribFilter));
let commonStart = 0;
const oldStartIter = attribIterator(oldARuns, false);
@ -2297,7 +2300,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// 3-renumber every list item of the same level from the beginning, level 1
// IMPORTANT: never skip a level because there imbrication may be arbitrary
const builder = Changeset.builder(rep.lines.totalWidth());
const builder = new Builder(rep.lines.totalWidth());
let loc = [0, 0];
const applyNumberList = (line, level) => {
// init
@ -2312,8 +2315,8 @@ function Ace2Inner(editorInfo, cssManagers) {
if (isNaN(curLevel) || listType[0] === 'indent') {
return line;
} else if (curLevel === level) {
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
buildKeepRange(rep, builder, loc, (loc = [line, 0]));
buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
['start', position],
], rep.apool);
@ -2330,7 +2333,7 @@ function Ace2Inner(editorInfo, cssManagers) {
applyNumberList(lineNum, 1);
const cs = builder.toString();
if (!Changeset.isIdentity(cs)) {
if (!isIdentity(cs)) {
performDocumentApplyChangeset(cs);
}
@ -2618,7 +2621,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// TODO: There appears to be a race condition or so.
const authorIds = new Set();
if (alineAttrs) {
for (const op of Changeset.deserializeOps(alineAttrs)) {
for (const op of deserializeOps(alineAttrs)) {
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
if (authorId) authorIds.add(authorId);
}
@ -3513,8 +3516,8 @@ function Ace2Inner(editorInfo, cssManagers) {
const oneEntry = createDomLineEntry('');
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo]);
rep.alines = Changeset.splitAttributionLines(
Changeset.makeAttribution('\n'), '\n');
rep.alines = splitAttributionLines(
makeAttribution('\n'), '\n');
bindTheEventHandlers();
});

View file

@ -17,6 +17,9 @@
* @typedef {string} AttributeString
*/
import AttributePool from "./AttributePool";
import {Attribute} from "./types/Attribute";
/**
* Converts an attribute string into a sequence of attribute identifier numbers.
*
@ -28,7 +31,7 @@
* appear in `str`.
* @returns {Generator<number>}
*/
exports.decodeAttribString = function* (str) {
export const decodeAttribString = function* (str: string): Generator<number> {
const re = /\*([0-9a-z]+)|./gy;
let match;
while ((match = re.exec(str)) != null) {
@ -38,7 +41,7 @@ exports.decodeAttribString = function* (str) {
}
};
const checkAttribNum = (n) => {
const checkAttribNum = (n: number|object) => {
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
@ -50,7 +53,7 @@ const checkAttribNum = (n) => {
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
* @returns {AttributeString}
*/
exports.encodeAttribString = (attribNums) => {
export const encodeAttribString = (attribNums: Iterable<number>): string => {
let str = '';
for (const n of attribNums) {
checkAttribNum(n);
@ -67,7 +70,7 @@ exports.encodeAttribString = (attribNums) => {
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
* @returns {Generator<Attribute>}
*/
exports.attribsFromNums = function* (attribNums, pool) {
export const attribsFromNums = function* (attribNums: Iterable<number>, pool: AttributePool): Generator<Attribute> {
for (const n of attribNums) {
checkAttribNum(n);
const attrib = pool.getAttrib(n);
@ -87,7 +90,7 @@ exports.attribsFromNums = function* (attribNums, pool) {
* @yields {number} The attribute number of each attribute in `attribs`, in order.
* @returns {Generator<number>}
*/
exports.attribsToNums = function* (attribs, pool) {
export const attribsToNums = function* (attribs: Iterable<Attribute>, pool: AttributePool) {
for (const attrib of attribs) yield pool.putAttrib(attrib);
};
@ -102,8 +105,8 @@ exports.attribsToNums = function* (attribs, pool) {
* @yields {Attribute} The attributes identified in `str`, in order.
* @returns {Generator<Attribute>}
*/
exports.attribsFromString = function* (str, pool) {
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
export const attribsFromString = function* (str: string, pool: AttributePool): Generator<Attribute> {
yield* attribsFromNums(decodeAttribString(str), pool);
};
/**
@ -116,8 +119,8 @@ exports.attribsFromString = function* (str, pool) {
* @param {AttributePool} pool - Attribute pool.
* @returns {AttributeString}
*/
exports.attribsToString =
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
export const attribsToString =
(attribs: Iterable<Attribute>, pool: AttributePool): string => encodeAttribString(attribsToNums(attribs, pool));
/**
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
@ -126,5 +129,14 @@ exports.attribsToString =
* @param {Attribute[]} attribs - Attributes to sort in place.
* @returns {Attribute[]} `attribs` (for chaining).
*/
exports.sort =
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
export const sort = (attribs: Attribute[]): Attribute[] => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
export default {
decodeAttribString,
encodeAttribString,
attribsFromNums,
attribsToNums,
attribsFromString,
attribsToString,
sort,
}

View file

@ -1,3 +1,4 @@
// @ts-nocheck
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
/* Copyright 2021 Richard Hansen <rhansen@rhansen.org> */

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
@ -24,8 +25,8 @@
const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset');
import AttribPool from './AttributePool';
import {compose, deserializeOps, inverse, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, splitAttributionLines, splitTextLines, unpack} from './Changeset';
const attributes = require('./attributes');
const linestylefilter = require('./linestylefilter').linestylefilter;
const colorutils = require('./colorutils').colorutils;
@ -53,11 +54,11 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
currentRevision: clientVars.collab_client_vars.rev,
currentTime: clientVars.collab_client_vars.time,
currentLines:
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
currentDivs: null,
// to be filled in once the dom loads
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
alines: Changeset.splitAttributionLines(
alines: splitAttributionLines(
clientVars.collab_client_vars.initialAttributedText.attribs,
clientVars.collab_client_vars.initialAttributedText.text),
@ -120,7 +121,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
getActiveAuthors() {
const authorIds = new Set();
for (const aline of this.alines) {
for (const op of Changeset.deserializeOps(aline)) {
for (const op of deserializeOps(aline)) {
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
if (k !== 'author') continue;
if (v) authorIds.add(v);
@ -141,7 +142,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
const oldAlines = padContents.alines.slice();
try {
// must mutate attribution lines before text lines
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
mutateAttributionLines(changeset, padContents.alines, padContents.apool);
} catch (e) {
debugLog(e);
}
@ -163,7 +164,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
// some chars are replaced (no attributes change and no length change)
// test if there are keep ops at the start of the cs
if (lineChanged === undefined) {
const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops);
const [op] = deserializeOps(unpack(changeset).ops);
lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
}
@ -183,7 +184,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
goToLineNumber(lineChanged);
}
Changeset.mutateTextLines(changeset, padContents);
mutateTextLines(changeset, padContents);
padContents.currentRevision = revision;
padContents.currentTime += timeDelta * 1000;
@ -272,7 +273,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
let changeset = cs[0];
let timeDelta = path.times[0];
for (let i = 1; i < cs.length; i++) {
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
changeset = compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i];
}
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
@ -290,7 +291,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
let changeset = cs[0];
let timeDelta = path.times[0];
for (let i = 1; i < cs.length; i++) {
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
changeset = compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i];
}
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
@ -396,9 +397,9 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
// debugLog("adding changeset:", astart, aend);
const forwardcs =
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
const backwardcs =
Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
}
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
@ -408,13 +409,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
obj = obj.data;
if (obj.type === 'NEW_CHANGES') {
const changeset = Changeset.moveOpsToNewPool(
const changeset = moveOpsToNewPool(
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
let changesetBack = Changeset.inverse(
let changesetBack = inverse(
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
changesetBack = Changeset.moveOpsToNewPool(
changesetBack = moveOpsToNewPool(
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
@ -22,17 +23,18 @@
* limitations under the License.
*/
const AttributeMap = require('./AttributeMap');
const AttributePool = require('./AttributePool');
const Changeset = require('./Changeset');
import AttributeMap from './AttributeMap';
import AttributePool from './AttributePool';
import {applyToAText, checkRep, cloneAText, compose, deserializeOps, follow, identity, isIdentity, makeAText, moveOpsToNewPool, newLen, pack, prepareForWire, unpack} from './Changeset';
import {MergingOpAssembler} from "./MergingOpAssembler";
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// latest official text from server
let baseAText = Changeset.makeAText('\n');
let baseAText = makeAText('\n');
// changes applied to baseText that have been submitted
let submittedChangeset = null;
// changes applied to submittedChangeset since it was prepared
let userChangeset = Changeset.identity(1);
let userChangeset = identity(1);
// is the changesetTracker enabled
let tracking = false;
// stack state flag so that when we change the rep we don't
@ -66,18 +68,18 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
return self = {
isTracking: () => tracking,
setBaseText: (text) => {
self.setBaseAttributedText(Changeset.makeAText(text), null);
self.setBaseAttributedText(makeAText(text), null);
},
setBaseAttributedText: (atext, apoolJsonObj) => {
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
tracking = true;
baseAText = Changeset.cloneAText(atext);
baseAText = cloneAText(atext);
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
baseAText.attribs = moveOpsToNewPool(baseAText.attribs, wireApool, apool);
}
submittedChangeset = null;
userChangeset = Changeset.identity(atext.text.length);
userChangeset = identity(atext.text.length);
applyingNonUserChanges = true;
try {
callbacks.setDocumentAttributedText(atext);
@ -89,8 +91,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
composeUserChangeset: (c) => {
if (!tracking) return;
if (applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return;
userChangeset = Changeset.compose(userChangeset, c, apool);
if (isIdentity(c)) return;
userChangeset = compose(userChangeset, c, apool);
setChangeCallbackTimeout();
},
@ -100,23 +102,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
c = moveOpsToNewPool(c, wireApool, apool);
}
baseAText = Changeset.applyToAText(c, baseAText, apool);
baseAText = applyToAText(c, baseAText, apool);
let c2 = c;
if (submittedChangeset) {
const oldSubmittedChangeset = submittedChangeset;
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
submittedChangeset = follow(c, oldSubmittedChangeset, false, apool);
c2 = follow(oldSubmittedChangeset, c, true, apool);
}
const preferInsertingAfterUserChanges = true;
const oldUserChangeset = userChangeset;
userChangeset = Changeset.follow(
userChangeset = follow(
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(
const postChange = follow(
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
@ -135,17 +137,17 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
if (submittedChangeset) {
// submission must have been canceled, prepare new changeset
// that includes old submittedChangeset
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
toSubmit = compose(submittedChangeset, userChangeset, apool);
} else {
// Get my authorID
const authorId = parent.parent.pad.myUserInfo.userId;
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
// text was copied from another author.
const cs = Changeset.unpack(userChangeset);
const assem = Changeset.mergingOpAssembler();
const cs = unpack(userChangeset);
const assem = new MergingOpAssembler();
for (const op of Changeset.deserializeOps(cs.ops)) {
for (const op of deserializeOps(cs.ops)) {
if (op.opcode === '+') {
const attribs = AttributeMap.fromString(op.attribs, apool);
const oldAuthorId = attribs.get('author');
@ -157,23 +159,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
assem.append(op);
}
assem.endDocument();
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
Changeset.checkRep(userChangeset);
userChangeset = pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
checkRep(userChangeset);
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
if (isIdentity(userChangeset)) toSubmit = null;
else toSubmit = userChangeset;
}
let cs = null;
if (toSubmit) {
submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
userChangeset = identity(newLen(toSubmit));
cs = toSubmit;
}
let wireApool = null;
if (cs) {
const forWire = Changeset.prepareForWire(cs, apool);
const forWire = prepareForWire(cs, apool);
wireApool = forWire.pool.toJsonable();
cs = forWire.translated;
}
@ -190,13 +192,13 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
}
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
baseAText = applyToAText(submittedChangeset, baseAText, apool);
submittedChangeset = null;
},
setUserChangeNotificationCallback: (callback) => {
changeCallback = callback;
},
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
hasUncommittedChanges: () => !!(submittedChangeset || (!isIdentity(userChangeset))),
};
};

5
src/static/js/chat.js → src/static/js/chat.ts Executable file → Normal file
View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
@ -15,8 +16,8 @@
* limitations under the License.
*/
const ChatMessage = require('./ChatMessage');
const padutils = require('./pad_utils').padutils;
import ChatMessage from './ChatMessage';
import padutils from './pad_utils'
const padcookie = require('./pad_cookie').padcookie;
const Tinycon = require('tinycon/tinycon');
const hooks = require('./pluginfw/hooks');

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,5 @@
// @ts-nocheck
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
@ -8,6 +10,8 @@
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins");
import Op from "./Op";
/**
* Copyright 2009 Google Inc.
*
@ -26,9 +30,10 @@
const _MAX_LIST_LEVEL = 16;
const AttributeMap = require('./AttributeMap');
const UNorm = require('unorm');
const Changeset = require('./Changeset');
import AttributeMap from './AttributeMap';
import UNorm from 'unorm';
import {subattribution} from './Changeset';
import {SmartOpAssembler} from "./SmartOpAssembler";
const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s);
@ -83,14 +88,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
const textArray = [];
const attribsArray = [];
let attribsBuilder = null;
const op = new Changeset.Op('+');
const op = new Op('+');
const self = {
length: () => textArray.length,
atColumnZero: () => textArray[textArray.length - 1] === '',
startNew: () => {
textArray.push('');
self.flush(true);
attribsBuilder = Changeset.smartOpAssembler();
attribsBuilder = new SmartOpAssembler();
},
textOfLine: (i) => textArray[i],
appendText: (txt, attrString = '') => {
@ -653,8 +658,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
const lengthToTake = lineLimit;
newStrings.push(oldString.substring(0, lengthToTake));
oldString = oldString.substring(lengthToTake);
newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake));
oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake);
newAttribStrings.push(subattribution(oldAttribString, 0, lengthToTake));
oldAttribString = subattribution(oldAttribString, lengthToTake);
}
if (oldString.length > 0) {
newStrings.push(oldString);

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
@ -30,12 +31,13 @@
// requires: plugins
// requires: undefined
const Changeset = require('./Changeset');
const attributes = require('./attributes');
import {deserializeOps} from './Changeset';
import attributes from './attributes';
const hooks = require('./pluginfw/hooks');
const linestylefilter = {};
const AttributeManager = require('./AttributeManager');
const padutils = require('./pad_utils').padutils;
import padutils from './pad_utils'
import Op from "./Op";
linestylefilter.ATTRIB_CLASSES = {
bold: 'tag:b',
@ -98,12 +100,12 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
return classes.substring(1);
};
const attrOps = Changeset.deserializeOps(aline);
const attrOps = deserializeOps(aline);
let attrOpsNext = attrOps.next();
let nextOp, nextOpClasses;
const goNextOp = () => {
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
nextOp = attrOpsNext.done ? new Op() : attrOpsNext.value;
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
};

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
@ -33,7 +34,8 @@ require('./vendors/gritter');
import html10n from './vendors/html10n'
const Cookies = require('./pad_utils').Cookies;
import {Cookies} from "./pad_utils";
const chat = require('./chat').chat;
const getCollabClient = require('./collab_client').getCollabClient;
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
@ -44,9 +46,9 @@ const padimpexp = require('./pad_impexp').padimpexp;
const padmodals = require('./pad_modals').padmodals;
const padsavedrevs = require('./pad_savedrevs');
const paduserlist = require('./pad_userlist').paduserlist;
const padutils = require('./pad_utils').padutils;
import padutils from './pad_utils'
const colorutils = require('./colorutils').colorutils;
const randomString = require('./pad_utils').randomString;
import {randomString} from "./pad_utils";
const socketio = require('./socketio');
const hooks = require('./pluginfw/hooks');

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
import html10n from './vendors/html10n';

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
@ -16,7 +17,7 @@
* limitations under the License.
*/
const Cookies = require('./pad_utils').Cookies;
import {Cookies} from "./pad_utils";
exports.padcookie = new class {
constructor() {

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
@ -24,7 +25,7 @@
const browser = require('./vendors/browser');
const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils;
import padutils from "./pad_utils";
const padeditor = require('./pad_editor').padeditor;
const padsavedrevs = require('./pad_savedrevs');
const _ = require('underscore');

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
@ -21,9 +22,8 @@
* limitations under the License.
*/
const Cookies = require('./pad_utils').Cookies;
import padutils,{Cookies} from "./pad_utils";
const padcookie = require('./pad_cookie').padcookie;
const padutils = require('./pad_utils').padutils;
const Ace2Editor = require('./ace').Ace2Editor;
import html10n from '../js/vendors/html10n'

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
/**
@ -16,7 +17,7 @@
* limitations under the License.
*/
const padutils = require('./pad_utils').padutils;
import padutils from './pad_utils'
const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
let myUserInfo = {};

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
import {binarySearch} from "./ace2_common";
/**
* Copyright 2009 Google Inc.
*
@ -22,13 +24,14 @@
* limitations under the License.
*/
const Security = require('./security');
const Security = require('security');
import jsCookie, {CookiesStatic} from 'js-cookie'
/**
* Generates a random String with the given length. Is needed to generate the Author, Group,
* readonly, session Ids
*/
const randomString = (len) => {
export const randomString = (len?: number) => {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let randomstring = '';
len = len || 20;
@ -85,13 +88,41 @@ const urlRegex = (() => {
'tel',
].join('|')}):`;
return new RegExp(
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
})();
// https://stackoverflow.com/a/68957976
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
const padutils = {
type PadEvent = {
which: number
}
type JQueryNode = JQuery<HTMLElement>
class PadUtils {
public urlRegex: RegExp
public wordCharRegex: RegExp
public warnDeprecatedFlags: {
disabledForTestingOnly: boolean,
_rl?: {
prevs: Map<string, number>,
now: () => number,
period: number
}
logger?: any
}
public globalExceptionHandler: null | any = null;
constructor() {
this.warnDeprecatedFlags = {
disabledForTestingOnly: false
}
this.wordCharRegex = wordCharRegex
this.urlRegex = urlRegex
}
/**
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
* is using the deprecated function).
@ -107,41 +138,41 @@ const padutils = {
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
* logger is set), with a stack trace appended if available.
*/
warnDeprecated: (...args) => {
if (padutils.warnDeprecated.disabledForTestingOnly) return;
warnDeprecated = (...args: any[]) => {
if (this.warnDeprecatedFlags.disabledForTestingOnly) return;
const err = new Error();
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
if (Error.captureStackTrace) Error.captureStackTrace(err, this.warnDeprecated);
err.name = '';
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
if (typeof err.stack === 'string') {
if (padutils.warnDeprecated._rl == null) {
padutils.warnDeprecated._rl =
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
if (this.warnDeprecatedFlags._rl == null) {
this.warnDeprecatedFlags._rl =
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
}
const rl = padutils.warnDeprecated._rl;
const rl = this.warnDeprecatedFlags._rl;
const now = rl.now();
const prev = rl.prevs.get(err.stack);
if (prev != null && now - prev < rl.period) return;
rl.prevs.set(err.stack, now);
}
if (err.stack) args.push(err.stack);
(padutils.warnDeprecated.logger || console).warn(...args);
},
escapeHtml: (x) => Security.escapeHTML(String(x)),
uniqueId: () => {
(this.warnDeprecatedFlags.logger || console).warn(...args);
}
escapeHtml = (x: string) => Security.escapeHTML(String(x))
uniqueId = () => {
const pad = require('./pad').pad; // Sidestep circular dependency
// returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits
const encodeNum =
(n, width) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
(n: number, width: number) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
return [
pad.getClientIp(),
encodeNum(+new Date(), 7),
encodeNum(Math.floor(Math.random() * 1e9), 4),
].join('.');
},
}
// e.g. "Thu Jun 18 2009 13:09"
simpleDateTime: (date) => {
simpleDateTime = (date: string) => {
const d = new Date(+date); // accept either number or date
const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
const month = ([
@ -162,16 +193,14 @@ const padutils = {
const year = d.getFullYear();
const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`;
return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`;
},
wordCharRegex,
urlRegex,
}
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
findURLs: (text) => {
findURLs = (text: string) => {
// Copy padutils.urlRegex so that the use of .exec() below (which mutates the RegExp object)
// does not break other concurrent uses of padutils.urlRegex.
const urlRegex = new RegExp(padutils.urlRegex, 'g');
const urlRegex = new RegExp(this.urlRegex, 'g');
urlRegex.lastIndex = 0;
let urls = null;
let urls: [number, string][] | null = null;
let execResult;
// TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped.
while ((execResult = urlRegex.exec(text))) {
@ -181,18 +210,19 @@ const padutils = {
urls.push([startIndex, url]);
}
return urls;
},
escapeHtmlWithClickableLinks: (text, target) => {
}
escapeHtmlWithClickableLinks = (text: string, target: string) => {
let idx = 0;
const pieces = [];
const urls = padutils.findURLs(text);
const urls = this.findURLs(text);
const advanceTo = (i) => {
if (i > idx) {
pieces.push(Security.escapeHTML(text.substring(idx, i)));
idx = i;
const advanceTo = (i: number) => {
if (i > idx) {
pieces.push(Security.escapeHTML(text.substring(idx, i)));
idx = i;
}
}
};
;
if (urls) {
for (let j = 0; j < urls.length; j++) {
const startIndex = urls[j][0];
@ -206,25 +236,25 @@ const padutils = {
// https://mathiasbynens.github.io/rel-noopener/
// https://github.com/ether/etherpad-lite/pull/3636
pieces.push(
'<a ',
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
'href="',
Security.escapeHTMLAttribute(href),
'" rel="noreferrer noopener">');
'<a ',
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
'href="',
Security.escapeHTMLAttribute(href),
'" rel="noreferrer noopener">');
advanceTo(startIndex + href.length);
pieces.push('</a>');
}
}
advanceTo(text.length);
return pieces.join('');
},
bindEnterAndEscape: (node, onEnter, onEscape) => {
}
bindEnterAndEscape = (node: JQueryNode, onEnter: Function, onEscape: Function) => {
// Use keypress instead of keyup in bindEnterAndEscape. Keyup event is fired on enter in IME
// (Input Method Editor), But keypress is not. So, I changed to use keypress instead of keyup.
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
// 3.6.10, Chrome 6.0.472, Safari 5.0).
if (onEnter) {
node.on('keypress', (evt) => {
node.on('keypress', (evt: { which: number; }) => {
if (evt.which === 13) {
onEnter(evt);
}
@ -238,13 +268,15 @@ const padutils = {
}
});
}
},
timediff: (d) => {
}
timediff = (d: number) => {
const pad = require('./pad').pad; // Sidestep circular dependency
const format = (n, word) => {
n = Math.round(n);
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
};
const format = (n: number, word: string) => {
n = Math.round(n);
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
}
;
d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000);
if (d < 60) {
return format(d, 'second');
@ -259,78 +291,89 @@ const padutils = {
}
d /= 24;
return format(d, 'day');
},
makeAnimationScheduler: (funcToAnimateOneStep, stepTime, stepsAtOnce) => {
if (stepsAtOnce === undefined) {
stepsAtOnce = 1;
}
makeAnimationScheduler =
(funcToAnimateOneStep: any, stepTime: number, stepsAtOnce?: number) => {
if (stepsAtOnce === undefined) {
stepsAtOnce = 1;
}
let animationTimer: any = null;
const scheduleAnimation = () => {
if (!animationTimer) {
animationTimer = window.setTimeout(() => {
animationTimer = null;
let n = stepsAtOnce;
let moreToDo = true;
while (moreToDo && n > 0) {
moreToDo = funcToAnimateOneStep();
n--;
}
if (moreToDo) {
// more to do
scheduleAnimation();
}
}, stepTime * stepsAtOnce);
}
};
return {scheduleAnimation};
}
let animationTimer = null;
makeFieldLabeledWhenEmpty
=
(field: JQueryNode, labelText: string) => {
field = $(field);
const scheduleAnimation = () => {
if (!animationTimer) {
animationTimer = window.setTimeout(() => {
animationTimer = null;
let n = stepsAtOnce;
let moreToDo = true;
while (moreToDo && n > 0) {
moreToDo = funcToAnimateOneStep();
n--;
}
if (moreToDo) {
// more to do
scheduleAnimation();
}
}, stepTime * stepsAtOnce);
}
};
return {scheduleAnimation};
},
makeFieldLabeledWhenEmpty: (field, labelText) => {
field = $(field);
const clear = () => {
field.addClass('editempty');
field.val(labelText);
};
field.focus(() => {
if (field.hasClass('editempty')) {
field.val('');
}
field.removeClass('editempty');
});
field.on('blur', () => {
if (!field.val()) {
clear();
}
});
return {
clear,
};
},
getCheckbox: (node) => $(node).is(':checked'),
setCheckbox: (node, value) => {
if (value) {
$(node).attr('checked', 'checked');
} else {
$(node).prop('checked', false);
const clear = () => {
field.addClass('editempty');
field.val(labelText);
}
;
field.focus(() => {
if (field.hasClass('editempty')) {
field.val('');
}
field.removeClass('editempty');
});
field.on('blur', () => {
if (!field.val()) {
clear();
}
});
return {
clear,
};
}
},
bindCheckboxChange: (node, func) => {
$(node).on('change', func);
},
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-';
return `z${c.charCodeAt(0)}z`;
}),
decodeUserId: (encodedUserId) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') {
return String.fromCharCode(Number(cc.slice(1, -1)));
} else {
return cc;
getCheckbox = (node: string) => $(node).is(':checked')
setCheckbox =
(node: JQueryNode, value: boolean) => {
if (value) {
$(node).attr('checked', 'checked');
} else {
$(node).prop('checked', false);
}
}
}),
bindCheckboxChange =
(node: JQueryNode, func: Function) => {
// @ts-ignore
$(node).on("change", func);
}
encodeUserId =
(userId: string) => userId.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-';
return `z${c.charCodeAt(0)}z`;
})
decodeUserId =
(encodedUserId: string) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
if (cc === '-') {
return '.';
} else if (cc.charAt(0) === 'z') {
return String.fromCharCode(Number(cc.slice(1, -1)));
} else {
return cc;
}
})
/**
* Returns whether a string has the expected format to be used as a secret token identifying an
* author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648
@ -340,109 +383,109 @@ const padutils = {
* conditional transformation of a token to a database key in a way that does not allow a
* malicious user to impersonate another user).
*/
isValidAuthorToken: (t) => {
isValidAuthorToken = (t: string | object) => {
if (typeof t !== 'string' || !t.startsWith('t.')) return false;
const v = t.slice(2);
return v.length > 0 && base64url.test(v);
},
}
/**
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
* particular author.
*/
generateAuthorToken: () => `t.${randomString()}`,
};
let globalExceptionHandler = null;
padutils.setupGlobalExceptionHandler = () => {
if (globalExceptionHandler == null) {
globalExceptionHandler = (e) => {
let type;
let err;
let msg, url, linenumber;
if (e instanceof ErrorEvent) {
type = 'Uncaught exception';
err = e.error || {};
({message: msg, filename: url, lineno: linenumber} = e);
} else if (e instanceof PromiseRejectionEvent) {
type = 'Unhandled Promise rejection';
err = e.reason || {};
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
} else {
throw new Error(`unknown event: ${e.toString()}`);
}
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
msg = `${err.name}: ${msg}`;
}
const errorId = randomString(20);
let msgAlreadyVisible = false;
$('.gritter-item .error-msg').each(function () {
if ($(this).text() === msg) {
msgAlreadyVisible = true;
generateAuthorToken = () => `t.${randomString()}`
setupGlobalExceptionHandler = () => {
if (this.globalExceptionHandler == null) {
this.globalExceptionHandler = (e: any) => {
let type;
let err;
let msg, url, linenumber;
if (e instanceof ErrorEvent) {
type = 'Uncaught exception';
err = e.error || {};
({message: msg, filename: url, lineno: linenumber} = e);
} else if (e instanceof PromiseRejectionEvent) {
type = 'Unhandled Promise rejection';
err = e.reason || {};
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
} else {
throw new Error(`unknown event: ${e.toString()}`);
}
});
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
msg = `${err.name}: ${msg}`;
}
const errorId = randomString(20);
if (!msgAlreadyVisible) {
const txt = document.createTextNode.bind(document); // Convenience shorthand.
const errorMsg = [
$('<p>')
let msgAlreadyVisible = false;
$('.gritter-item .error-msg').each(function () {
if ($(this).text() === msg) {
msgAlreadyVisible = true;
}
});
if (!msgAlreadyVisible) {
const txt = document.createTextNode.bind(document); // Convenience shorthand.
const errorMsg = [
$('<p>')
.append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')),
$('<p>')
$('<p>')
.text('If the problem persists, please send this error message to your webmaster:'),
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
.append($('<b>').addClass('error-msg').text(msg)).append($('<br>'))
.append(txt(`at ${url} at line ${linenumber}`)).append($('<br>'))
.append(txt(`ErrorId: ${errorId}`)).append($('<br>'))
.append(txt(type)).append($('<br>'))
.append(txt(`URL: ${window.location.href}`)).append($('<br>'))
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
];
];
$.gritter.add({
title: 'An error occurred',
text: errorMsg,
class_name: 'error',
position: 'bottom',
sticky: true,
// @ts-ignore
$.gritter.add({
title: 'An error occurred',
text: errorMsg,
class_name: 'error',
position: 'bottom',
sticky: true,
});
}
// send javascript errors to the server
$.post('../jserror', {
errorInfo: JSON.stringify({
errorId,
type,
msg,
url: window.location.href,
source: url,
linenumber,
userAgent: navigator.userAgent,
stack: err.stack,
}),
});
}
// send javascript errors to the server
$.post('../jserror', {
errorInfo: JSON.stringify({
errorId,
type,
msg,
url: window.location.href,
source: url,
linenumber,
userAgent: navigator.userAgent,
stack: err.stack,
}),
});
};
window.onerror = null; // Clear any pre-existing global error handler.
window.addEventListener('error', globalExceptionHandler);
window.addEventListener('unhandledrejection', globalExceptionHandler);
};
window.onerror = null; // Clear any pre-existing global error handler.
window.addEventListener('error', this.globalExceptionHandler);
window.addEventListener('unhandledrejection', this.globalExceptionHandler);
}
}
};
padutils.binarySearch = require('./ace2_common').binarySearch;
binarySearch = binarySearch
}
// https://stackoverflow.com/a/42660748
const inThirdPartyIframe = () => {
try {
return (!window.top.location.hostname);
return (!window.top!.location.hostname);
} catch (e) {
return true;
}
};
export let Cookies: CookiesStatic<string>
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
// window object.
if (typeof window !== 'undefined') {
exports.Cookies = require('js-cookie').withAttributes({
Cookies = jsCookie.withAttributes({
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
// because the cookies are third-party (not same-site). Many browsers/users block third-party
@ -455,5 +498,5 @@ if (typeof window !== 'undefined') {
secure: window.location.protocol === 'https:',
});
}
exports.randomString = randomString;
exports.padutils = padutils;
export default new PadUtils()

View file

@ -44,6 +44,12 @@ export class LinkInstaller {
await this.checkLinkedDependencies(installedPlugin)
}
public async installFromGitHub(repository: string) {
const installedPlugin = await this.livePluginManager.installFromGithub(repository)
this.linkDependency(installedPlugin.name)
await this.checkLinkedDependencies(installedPlugin)
}
public async installPlugin(pluginName: string, version?: string) {
if (version) {
const installedPlugin = await this.livePluginManager.install(pluginName, version);

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
const pluginUtils = require('./shared');

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
const pluginDefs = require('./plugin_defs');

View file

@ -1,3 +1,4 @@
// @ts-nocheck
'use strict';
const fs = require('fs').promises;

Some files were not shown because too many files have changed in this diff Show more