Fixed formatting.

This commit is contained in:
SamTV12345 2024-04-17 22:01:04 +02:00
parent 9f5ff6171a
commit f115ea9241
340 changed files with 77690 additions and 66928 deletions

View file

@ -278,6 +278,9 @@ importers:
specifier: ^0.9.2 specifier: ^0.9.2
version: 0.9.2 version: 0.9.2
devDependencies: devDependencies:
'@biomejs/biome':
specifier: 1.7.0
version: 1.7.0
'@playwright/test': '@playwright/test':
specifier: ^1.43.1 specifier: ^1.43.1
version: 1.43.1 version: 1.43.1
@ -721,6 +724,94 @@ packages:
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: true dev: true
/@biomejs/biome@1.7.0:
resolution: {integrity: sha512-mejiRhnAq6UrXtYvjWJUKdstcT58n0/FfKemFf3d2Ou0HxOdS88HQmWtQ/UgyZvOEPD572YbFTb6IheyROpqkw==}
engines: {node: '>=14.21.3'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.7.0
'@biomejs/cli-darwin-x64': 1.7.0
'@biomejs/cli-linux-arm64': 1.7.0
'@biomejs/cli-linux-arm64-musl': 1.7.0
'@biomejs/cli-linux-x64': 1.7.0
'@biomejs/cli-linux-x64-musl': 1.7.0
'@biomejs/cli-win32-arm64': 1.7.0
'@biomejs/cli-win32-x64': 1.7.0
dev: true
/@biomejs/cli-darwin-arm64@1.7.0:
resolution: {integrity: sha512-12TaeaKHU4SAZt0fQJ2bYk1jUb4foope7LmgDE5p3c0uMxd3mFkg1k7G721T+K6UHYULcSOQDsNNM8DhYi8Irg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@biomejs/cli-darwin-x64@1.7.0:
resolution: {integrity: sha512-6Qq1BSIB0cpp0cQNqO/+EiUV7FE3jMpF6w7+AgIBXp0oJxUWb2Ff0RDZdO9bfzkimXD58j0vGpNHMGnCcjDV2Q==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@biomejs/cli-linux-arm64-musl@1.7.0:
resolution: {integrity: sha512-pwIY80nU7SAxrVVZ6HD9ah1pruwh9ZqlSR0Nvbg4ZJqQa0POhiB+RJx7+/1Ml2mTZdrl8kb/YiwQpD16uwb5wg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@biomejs/cli-linux-arm64@1.7.0:
resolution: {integrity: sha512-GwSci7xBJ2j1CrdDXDUVXnUtrvypEz/xmiYPpFeVdlX5p95eXx+7FekPPbJfhGGw5WKSsKZ+V8AAlbN+kUwJWw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@biomejs/cli-linux-x64-musl@1.7.0:
resolution: {integrity: sha512-KzCA0mW4LSbCd7XZWaEJvTOTTBjfJoVEXkfq1fsXxww1HB+ww5PGMbhbIcbYCsj2CTJUifeD5hOkyuBVppU1xQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@biomejs/cli-linux-x64@1.7.0:
resolution: {integrity: sha512-1y+odKQsyHcw0JCGRuqhbx7Y6jxOVSh4lGIVDdJxW1b55yD22DY1kcMEfhUte6f95OIc2uqfkwtiI6xQAiZJdw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@biomejs/cli-win32-arm64@1.7.0:
resolution: {integrity: sha512-AvLDUYZBpOUFgS/mni4VruIoVV3uSGbKSkZQBPXsHgL0w4KttLll3NBrVanmWxOHsom6C6ocHLyfAY8HUc8TXg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@biomejs/cli-win32-x64@1.7.0:
resolution: {integrity: sha512-Pylm00BAAuLVb40IH9PC17432BTsY8K4pSUvhvgR1eaalnMaD6ug9SYJTTzKDbT6r24MPAGCTiSZERyhGkGzFQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@docsearch/css@3.6.0: /@docsearch/css@3.6.0:
resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==} resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==}
dev: true dev: true

View file

@ -1,138 +1,107 @@
'use strict'; "use strict";
// This is a workaround for https://github.com/eslint/eslint/issues/3458 // This is a workaround for https://github.com/eslint/eslint/issues/3458
require('eslint-config-etherpad/patch/modern-module-resolution'); require("eslint-config-etherpad/patch/modern-module-resolution");
module.exports = { module.exports = {
ignorePatterns: [ ignorePatterns: [
'/static/js/vendors/browser.js', "/static/js/vendors/browser.js",
'/static/js/vendors/farbtastic.js', "/static/js/vendors/farbtastic.js",
'/static/js/vendors/gritter.js', "/static/js/vendors/gritter.js",
'/static/js/vendors/html10n.js', "/static/js/vendors/html10n.js",
'/static/js/vendors/jquery.js', "/static/js/vendors/jquery.js",
'/static/js/vendors/nice-select.js', "/static/js/vendors/nice-select.js",
'/tests/frontend/lib/', "/tests/frontend/lib/",
], ],
overrides: [ overrides: [
{ {
files: [ files: ["**/.eslintrc.*"],
'**/.eslintrc.*', extends: "etherpad/node",
], },
extends: 'etherpad/node', {
files: ["**/*"],
excludedFiles: ["**/.eslintrc.*", "tests/frontend/**/*"],
extends: "etherpad/node",
}, },
{ {
files: [ files: [
'**/*', "static/**/*",
"tests/frontend/helper.js",
"tests/frontend/helper/**/*",
], ],
excludedFiles: [ excludedFiles: ["**/.eslintrc.*"],
'**/.eslintrc.*', extends: "etherpad/browser",
'tests/frontend/**/*',
],
extends: 'etherpad/node',
},
{
files: [
'static/**/*',
'tests/frontend/helper.js',
'tests/frontend/helper/**/*',
],
excludedFiles: [
'**/.eslintrc.*',
],
extends: 'etherpad/browser',
env: { env: {
'shared-node-browser': true, "shared-node-browser": true,
}, },
overrides: [ overrides: [
{ {
files: [ files: ["tests/frontend/helper/**/*"],
'tests/frontend/helper/**/*',
],
globals: { globals: {
helper: 'readonly', helper: "readonly",
}, },
}, },
], ],
}, },
{ {
files: [ files: ["tests/**/*"],
'tests/**/*',
],
excludedFiles: [ excludedFiles: [
'**/.eslintrc.*', "**/.eslintrc.*",
'tests/frontend/cypress/**/*', "tests/frontend/cypress/**/*",
'tests/frontend/helper.js', "tests/frontend/helper.js",
'tests/frontend/helper/**/*', "tests/frontend/helper/**/*",
'tests/frontend/travis/**/*', "tests/frontend/travis/**/*",
'tests/ratelimit/**/*', "tests/ratelimit/**/*",
], ],
extends: 'etherpad/tests', extends: "etherpad/tests",
rules: { rules: {
'mocha/no-exports': 'off', "mocha/no-exports": "off",
'mocha/no-top-level-hooks': 'off', "mocha/no-top-level-hooks": "off",
}, },
}, },
{ {
files: [ files: ["tests/backend/**/*"],
'tests/backend/**/*', excludedFiles: ["**/.eslintrc.*"],
], extends: "etherpad/tests/backend",
excludedFiles: [
'**/.eslintrc.*',
],
extends: 'etherpad/tests/backend',
overrides: [ overrides: [
{ {
files: [ files: ["tests/backend/**/*"],
'tests/backend/**/*', excludedFiles: ["tests/backend/specs/**/*"],
],
excludedFiles: [
'tests/backend/specs/**/*',
],
rules: { rules: {
'mocha/no-exports': 'off', "mocha/no-exports": "off",
'mocha/no-top-level-hooks': 'off', "mocha/no-top-level-hooks": "off",
}, },
}, },
], ],
}, },
{ {
files: [ files: ["tests/frontend/**/*"],
'tests/frontend/**/*',
],
excludedFiles: [ excludedFiles: [
'**/.eslintrc.*', "**/.eslintrc.*",
'tests/frontend/cypress/**/*', "tests/frontend/cypress/**/*",
'tests/frontend/helper.js', "tests/frontend/helper.js",
'tests/frontend/helper/**/*', "tests/frontend/helper/**/*",
'tests/frontend/travis/**/*', "tests/frontend/travis/**/*",
], ],
extends: 'etherpad/tests/frontend', extends: "etherpad/tests/frontend",
overrides: [ overrides: [
{ {
files: [ files: ["tests/frontend/**/*"],
'tests/frontend/**/*', excludedFiles: ["tests/frontend/specs/**/*"],
],
excludedFiles: [
'tests/frontend/specs/**/*',
],
rules: { rules: {
'mocha/no-exports': 'off', "mocha/no-exports": "off",
'mocha/no-top-level-hooks': 'off', "mocha/no-top-level-hooks": "off",
}, },
}, },
], ],
}, },
{ {
files: [ files: ["tests/frontend/cypress/**/*"],
'tests/frontend/cypress/**/*', extends: "etherpad/tests/cypress",
],
extends: 'etherpad/tests/cypress',
}, },
{ {
files: [ files: ["tests/frontend/travis/**/*"],
'tests/frontend/travis/**/*', extends: "etherpad/node",
],
extends: 'etherpad/node',
}, },
], ],
root: true, root: true,

12
src/biome.json Normal file
View file

@ -0,0 +1,12 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Fwolff", "Naudefj"]
"Fwolff",
"Naudefj"
]
}, },
"index.newPad": "Nuwe pad", "index.newPad": "Nuwe pad",
"index.createOpenPad": "of skep/open 'n pad met die naam:", "index.createOpenPad": "of skep/open 'n pad met die naam:",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Xuacu", "YoaR"]
"Xuacu",
"YoaR"
]
}, },
"index.newPad": "Nuevu bloc", "index.newPad": "Nuevu bloc",
"index.createOpenPad": "o crear/abrir un bloc col nome:", "index.createOpenPad": "o crear/abrir un bloc col nome:",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["1AnuraagPandey", "बडा काजी"]
"1AnuraagPandey",
"बडा काजी"
]
}, },
"index.newPad": "नयाँ प्याड", "index.newPad": "नयाँ प्याड",
"pad.toolbar.bold.title": "मोट (Ctrl-B)", "pad.toolbar.bold.title": "मोट (Ctrl-B)",

View file

@ -1,12 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Alp Er Tunqa", "Amir a57", "Ilğım", "Koroğlu", "Mousa"]
"Alp Er Tunqa",
"Amir a57",
"Ilğım",
"Koroğlu",
"Mousa"
]
}, },
"index.newPad": "یئنی یادداشت دفترچه سی", "index.newPad": "یئنی یادداشت دفترچه سی",
"index.createOpenPad": "یا دا ایجاد /بیر پد آدلا برابر آچماق:", "index.createOpenPad": "یا دا ایجاد /بیر پد آدلا برابر آچماق:",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Baloch Afghanistan", "Moshtank", "Sultanselim baloch"]
"Baloch Afghanistan",
"Moshtank",
"Sultanselim baloch"
]
}, },
"admin.page-title": "کارمسترءِ کُرسی - اترپَد", "admin.page-title": "کارمسترءِ کُرسی - اترپَد",
"admin_plugins": "گݔشانکانءِ کار ءُ بار", "admin_plugins": "گݔشانکانءِ کار ءُ بار",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Jim-by", "Red Winged Duck", "Renessaince", "Wizardist"]
"Jim-by",
"Red Winged Duck",
"Renessaince",
"Wizardist"
]
}, },
"admin.page-title": "Адміністрацыйная панэль — Etherpad", "admin.page-title": "Адміністрацыйная панэль — Etherpad",
"admin_plugins": "Кіраўнік плагінаў", "admin_plugins": "Кіраўнік плагінаў",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["StanProg", "Vlad5250", "Vodnokon4e"]
"StanProg",
"Vlad5250",
"Vodnokon4e"
]
}, },
"index.newPad": "Нов пад", "index.newPad": "Нов пад",
"index.createOpenPad": "или създаване/отваряне на пад с име:", "index.createOpenPad": "или създаване/отваряне на пад с име:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Baloch Afghanistan"]
"Baloch Afghanistan"
]
}, },
"index.newPad": "یاداشتی نوکین کتابچه", "index.newPad": "یاداشتی نوکین کتابچه",
"index.createOpenPad": "یا جوڑ\t کورتین/پاچ کورتین یک کتابچه ئی یاداشتی بی نام:", "index.createOpenPad": "یا جوڑ\t کورتین/پاچ کورتین یک کتابچه ئی یاداشتی بی نام:",

View file

@ -1,12 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Fohanno", "Fulup", "Gwenn-Ael", "Huñvreüs", "Y-M D"]
"Fohanno",
"Fulup",
"Gwenn-Ael",
"Huñvreüs",
"Y-M D"
]
}, },
"index.newPad": "Pad nevez", "index.newPad": "Pad nevez",
"index.createOpenPad": "pe krouiñ/digeriñ ur Pad gant an anv :", "index.createOpenPad": "pe krouiñ/digeriñ ur Pad gant an anv :",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Edinwiki", "Semina x", "Srdjan m", "Srđan"]
"Edinwiki",
"Semina x",
"Srdjan m",
"Srđan"
]
}, },
"index.newPad": "Novi Pad", "index.newPad": "Novi Pad",
"index.createOpenPad": "ili napravite/otvorite Pad sa imenom:", "index.createOpenPad": "ili napravite/otvorite Pad sa imenom:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Michawiki"]
"Michawiki"
]
}, },
"admin.page-title": "Administratorowa delka Etherpad", "admin.page-title": "Administratorowa delka Etherpad",
"admin_plugins": "Zastojnik tykacow", "admin_plugins": "Zastojnik tykacow",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Nirajan pant", "बडा काजी", "रमेश सिंह बोहरा", "राम प्रसाद जोशी"]
"Nirajan pant",
"बडा काजी",
"रमेश सिंह बोहरा",
"राम प्रसाद जोशी"
]
}, },
"index.newPad": "नौलो प्याड", "index.newPad": "नौलो प्याड",
"index.createOpenPad": "नाउँ सहितको नौलो प्याड सिर्जना गद्य्या / खोल्ल्या :", "index.createOpenPad": "नाउँ सहितको नौलो प्याड सिर्जना गद्य्या / खोल्ल्या :",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Kristian.kankainen", "Tiblu"]
"Kristian.kankainen",
"Tiblu"
]
}, },
"index.newPad": "Uus klade", "index.newPad": "Uus klade",
"index.createOpenPad": "loo või rööptoimeta kladet nimega:", "index.createOpenPad": "loo või rööptoimeta kladet nimega:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Ibrahima Malal Sarr"]
"Ibrahima Malal Sarr"
]
}, },
"admin.page-title": "Tiimtorde Jiiloowo - Etherpad", "admin.page-title": "Tiimtorde Jiiloowo - Etherpad",
"admin_plugins": "Toppitorde Ceŋe", "admin_plugins": "Toppitorde Ceŋe",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["EileenSanda"]
"EileenSanda"
]
}, },
"index.newPad": "Nýggjur teldil", "index.newPad": "Nýggjur teldil",
"pad.toolbar.bold.title": "Við feitum (Ctrl-B)", "pad.toolbar.bold.title": "Við feitum (Ctrl-B)",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Robin van der Vliet"]
"Robin van der Vliet"
]
}, },
"pad.toolbar.bold.title": "Fet (Ctrl+B)", "pad.toolbar.bold.title": "Fet (Ctrl+B)",
"pad.toolbar.italic.title": "Kursyf (Ctrl+I)", "pad.toolbar.italic.title": "Kursyf (Ctrl+I)",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Elisardojm", "Ghose", "Toliño"]
"Elisardojm",
"Ghose",
"Toliño"
]
}, },
"admin.page-title": "Panel de administración - Etherpad", "admin.page-title": "Panel de administración - Etherpad",
"admin_plugins": "Xestor de complementos", "admin_plugins": "Xestor de complementos",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Bhatakati aatma", "Dsvyas", "Harsh4101991", "KartikMistry"]
"Bhatakati aatma",
"Dsvyas",
"Harsh4101991",
"KartikMistry"
]
}, },
"index.newPad": "નવું પેડ", "index.newPad": "નવું પેડ",
"pad.toolbar.bold.title": "બોલ્ડ", "pad.toolbar.bold.title": "બોલ્ડ",

View file

@ -1,12 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Amire80", "Ofrahod", "Steeve815", "YaronSh", "תומר ט"]
"Amire80",
"Ofrahod",
"Steeve815",
"YaronSh",
"תומר ט"
]
}, },
"admin.page-title": "לוח ניהול - Etherpad", "admin.page-title": "לוח ניהול - Etherpad",
"admin_plugins": "מנהל תוספים", "admin_plugins": "מנהל תוספים",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Sfic"]
"Sfic"
]
}, },
"pad.toolbar.bold.title": "गहरा (Ctrl+B)", "pad.toolbar.bold.title": "गहरा (Ctrl+B)",
"pad.toolbar.italic.title": "तिरछा (Ctrl+I)", "pad.toolbar.italic.title": "तिरछा (Ctrl+I)",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Bugoslav", "Hmxhmx", "Ponor"]
"Bugoslav",
"Hmxhmx",
"Ponor"
]
}, },
"index.newPad": "Novi blokić", "index.newPad": "Novi blokić",
"index.createOpenPad": "ili stvori/otvori blokić s imenom:", "index.createOpenPad": "ili stvori/otvori blokić s imenom:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Paul Beppler"]
"Paul Beppler"
]
}, },
"index.newPad": "Neies Pad", "index.newPad": "Neies Pad",
"index.createOpenPad": "Pad mit follichendem Noome uffmache:", "index.createOpenPad": "Pad mit follichendem Noome uffmache:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Michawiki"]
"Michawiki"
]
}, },
"admin.page-title": "Administratorowa deska Etherpad", "admin.page-title": "Administratorowa deska Etherpad",
"admin_plugins": "Zrjadowak tykačow", "admin_plugins": "Zrjadowak tykačow",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Armenoid", "Kareyac"]
"Armenoid",
"Kareyac"
]
}, },
"admin_plugins.available_install.value": "Տեղադրել", "admin_plugins.available_install.value": "Տեղադրել",
"admin_plugins.description": "Նկարագրություն", "admin_plugins.description": "Նկարագրություն",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["McDutchie"]
"McDutchie"
]
}, },
"admin.page-title": "Pannello administrative Etherpad", "admin.page-title": "Pannello administrative Etherpad",
"admin_plugins": "Gestor de plug-ins", "admin_plugins": "Gestor de plug-ins",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Bennylin", "IvanLanin", "Marwan Mohamad", "Veracious"]
"Bennylin",
"IvanLanin",
"Marwan Mohamad",
"Veracious"
]
}, },
"admin.page-title": "Dasbor Pengurus - Etherpad", "admin.page-title": "Dasbor Pengurus - Etherpad",
"admin_plugins": "Manajer plugin", "admin_plugins": "Manajer plugin",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Sveinki", "Sveinn í Felli"]
"Sveinki",
"Sveinn í Felli"
]
}, },
"admin.page-title": "Stjórnborð fyrir stjórnendur - Etherpad", "admin.page-title": "Stjórnborð fyrir stjórnendur - Etherpad",
"admin_plugins": "Stýring viðbóta", "admin_plugins": "Stýring viðbóta",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Belkacem77"]
"Belkacem77"
]
}, },
"index.newPad": "Apad amaynut", "index.newPad": "Apad amaynut",
"index.createOpenPad": "neɣ rnu/ldi apad s yisem:", "index.createOpenPad": "neɣ rnu/ldi apad s yisem:",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Pichnat Thong", "Sovichet", "វ័ណថារិទ្ធ"]
"Pichnat Thong",
"Sovichet",
"វ័ណថារិទ្ធ"
]
}, },
"index.newPad": "ផេតថ្មី", "index.newPad": "ផេតថ្មី",
"index.createOpenPad": "ឬបង្កើត/បើកផេតដែលមានឈ្មោះ៖", "index.createOpenPad": "ឬបង្កើត/បើកផេតដែលមានឈ្មោះ៖",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Nayvik", "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"]
"Nayvik",
"ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"
]
}, },
"admin_plugins.available": "ಲಭ್ಯವಿರುವ ಪ್ಲಗಿನ್‌ಗಳು", "admin_plugins.available": "ಲಭ್ಯವಿರುವ ಪ್ಲಗಿನ್‌ಗಳು",
"admin_plugins.available_not-found": "ಯಾವುದೇ ಪ್ಲಗಿನ್‌ಗಳು ಸಿಗಲಿಲ್ಲ", "admin_plugins.available_not-found": "ಯಾವುದೇ ಪ್ಲಗಿನ್‌ಗಳು ಸಿಗಲಿಲ್ಲ",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Ernác", "Къарачайлы"]
"Ernác",
"Къарачайлы"
]
}, },
"admin.page-title": "Администраторну панели — Etherpad", "admin.page-title": "Администраторну панели — Etherpad",
"admin_plugins": "Плагин менеджер", "admin_plugins": "Плагин менеджер",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Purodha"]
"Purodha"
]
}, },
"index.newPad": "Neu Pädd", "index.newPad": "Neu Pädd",
"index.createOpenPad": "udder maach e Pädd op med däm Nahme:", "index.createOpenPad": "udder maach e Pädd op med däm Nahme:",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Gromper", "Robby", "Soued031", "Volvox"]
"Gromper",
"Robby",
"Soued031",
"Volvox"
]
}, },
"admin_plugins.available_install.value": "Installéieren", "admin_plugins.available_install.value": "Installéieren",
"admin_plugins.description": "Beschreiwung", "admin_plugins.description": "Beschreiwung",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Arash71", "Hosseinblue", "Lakzon"]
"Arash71",
"Hosseinblue",
"Lakzon"
]
}, },
"index.newPad": ازۀpad", "index.newPad": ازۀpad",
"index.createOpenPad": ":وە نۆم Pad یا سازین/واز کردن یإگلە", "index.createOpenPad": ":وە نۆم Pad یا سازین/واز کردن یإگلە",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Lorestani", "Mogoeilor"]
"Lorestani",
"Mogoeilor"
]
}, },
"index.newPad": "دٱفتٱرچٱ تازٱ", "index.newPad": "دٱفتٱرچٱ تازٱ",
"pad.toolbar.bold.title": "تۊپور", "pad.toolbar.bold.title": "تۊپور",

View file

@ -1,12 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Admresdeserv.", "Jmg.cmdi", "Oskars", "Papuass", "Silraks"]
"Admresdeserv.",
"Jmg.cmdi",
"Oskars",
"Papuass",
"Silraks"
]
}, },
"index.newPad": "Tiek izmantots kā pogas teksts. Blociņš, Etherpad kontekstā, ir piezīmju blociņš, uz kura rakstīt.", "index.newPad": "Tiek izmantots kā pogas teksts. Blociņš, Etherpad kontekstā, ir piezīmju blociņš, uz kura rakstīt.",
"index.createOpenPad": "vai izveidojiet/atveriet Blociņu ar nosaukumu:", "index.createOpenPad": "vai izveidojiet/atveriet Blociņu ar nosaukumu:",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Empu", "StefanusRA"]
"Empu",
"StefanusRA"
]
}, },
"index.newPad": "Pad Anyar", "index.newPad": "Pad Anyar",
"index.createOpenPad": "utawa gawe/bukak Pad nganggo jeneng:", "index.createOpenPad": "utawa gawe/bukak Pad nganggo jeneng:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Jagwar"]
"Jagwar"
]
}, },
"index.newPad": "Pad vaovao", "index.newPad": "Pad vaovao",
"index.createOpenPad": "na hamorona/hanokatra Pad manana anarana:", "index.createOpenPad": "na hamorona/hanokatra Pad manana anarana:",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Bjankuloski06", "Brest", "Vlad5250"]
"Bjankuloski06",
"Brest",
"Vlad5250"
]
}, },
"admin.page-title": "Администраторска управувачница — Etherpad", "admin.page-title": "Администраторска управувачница — Etherpad",
"admin_plugins": "Раководител со приклучоци", "admin_plugins": "Раководител со приклучоци",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["MongolWiki", "Munkhzaya.E", "Wisdom"]
"MongolWiki",
"Munkhzaya.E",
"Wisdom"
]
}, },
"pad.toolbar.bold.title": "Болд тескт (Ctrl-B)", "pad.toolbar.bold.title": "Болд тескт (Ctrl-B)",
"pad.toolbar.italic.title": "Налуу тескт (Ctrl-I)", "pad.toolbar.italic.title": "Налуу тескт (Ctrl-I)",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Aue Nai", "咽頭べさ"]
"Aue Nai",
"咽頭べさ"
]
}, },
"admin_plugins.description": "တၚ်ထမံက်ထ္ၜး", "admin_plugins.description": "တၚ်ထမံက်ထ္ၜး",
"index.newPad": "တၞးတၟိ", "index.newPad": "တၞးတၟိ",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Ganeshgiram", "V.narsikar", "Ydyashad"]
"Ganeshgiram",
"V.narsikar",
"Ydyashad"
]
}, },
"index.newPad": "नव पान", "index.newPad": "नव पान",
"pad.toolbar.bold.title": "ठळक (Ctrl-B)", "pad.toolbar.bold.title": "ठळक (Ctrl-B)",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Anakmalaysia", "Hakimi97", "Jeluang Terluang"]
"Anakmalaysia",
"Hakimi97",
"Jeluang Terluang"
]
}, },
"admin.page-title": "Papan muka Penyelia - Etherpad", "admin.page-title": "Papan muka Penyelia - Etherpad",
"index.newPad": "Pad baru", "index.newPad": "Pad baru",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Andibecker", "Dr Lotus Black"]
"Andibecker",
"Dr Lotus Black"
]
}, },
"admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad", "admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad",
"admin_plugins": "ပလပ်အင်မန်နေဂျာ", "admin_plugins": "ပလပ်အင်မန်နေဂျာ",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Akapochtli", "Languaeditor", "Taresi"]
"Akapochtli",
"Languaeditor",
"Taresi"
]
}, },
"index.newPad": "Yancuic Pad", "index.newPad": "Yancuic Pad",
"index.createOpenPad": "auh xicchīhua/xictlapo cē Pad in ītōcā:", "index.createOpenPad": "auh xicchīhua/xictlapo cē Pad in ītōcā:",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["C.R.", "Chelin", "Finizio", "Ruthven"]
"C.R.",
"Chelin",
"Finizio",
"Ruthven"
]
}, },
"admin_plugins.name": "Nomme", "admin_plugins.name": "Nomme",
"index.newPad": "Nuovo Pad", "index.newPad": "Nuovo Pad",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Gthoele", "Joachim Mos"]
"Gthoele",
"Joachim Mos"
]
}, },
"index.newPad": "Nee'et Pad", "index.newPad": "Nee'et Pad",
"index.createOpenPad": "oder Pad mit düssen Naam apen maken:", "index.createOpenPad": "oder Pad mit düssen Naam apen maken:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Unhammer"]
"Unhammer"
]
}, },
"index.newPad": "Ny blokk", "index.newPad": "Ny blokk",
"index.createOpenPad": "eller opprett/opna ei blokk med namnet:", "index.createOpenPad": "eller opprett/opna ei blokk med namnet:",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Cedric31", "Quentí"]
"Cedric31",
"Quentí"
]
}, },
"admin.page-title": "Panèl dadministracion - Etherpad", "admin.page-title": "Panèl dadministracion - Etherpad",
"admin_plugins": "Gestion de las extensions", "admin_plugins": "Gestion de las extensions",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Denö", "Ilja.mos", "Mashoi7"]
"Denö",
"Ilja.mos",
"Mashoi7"
]
}, },
"pad.toolbar.underline.title": "Alleviivua (Ctrl+U)", "pad.toolbar.underline.title": "Alleviivua (Ctrl+U)",
"pad.toolbar.settings.title": "Azetukset", "pad.toolbar.settings.title": "Azetukset",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Bouron"]
"Bouron"
]
}, },
"index.newPad": "Ног", "index.newPad": "Ног",
"index.createOpenPad": "кӕнӕ сараз/бакӕн ног документ ахӕм номимӕ:", "index.createOpenPad": "кӕнӕ сараз/бакӕн ног документ ахӕм номимӕ:",

View file

@ -1,12 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Aalam", "Babanwalia", "Tow", "ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ", "ਪ੍ਰਚਾਰਕ"]
"Aalam",
"Babanwalia",
"Tow",
"ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ",
"ਪ੍ਰਚਾਰਕ"
]
}, },
"index.newPad": "ਨਵਾਂ ਪੈਡ", "index.newPad": "ਨਵਾਂ ਪੈਡ",
"index.createOpenPad": "ਜਾਂ ਨਾਂ ਨਾਲ ਨਵਾਂ ਪੈਡ ਬਣਾਓ/ਖੋਲ੍ਹੋ:", "index.createOpenPad": "ਜਾਂ ਨਾਂ ਨਾਲ ਨਵਾਂ ਪੈਡ ਬਣਾਓ/ਖੋਲ੍ਹੋ:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Borichèt"]
"Borichèt"
]
}, },
"admin.page-title": "Cruscòt d'aministrator - Etherpad", "admin.page-title": "Cruscòt d'aministrator - Etherpad",
"admin_plugins": "Mansé dj'anstalassion", "admin_plugins": "Mansé dj'anstalassion",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Ahmed-Najib-Biabani-Ibrahimkhel"]
"Ahmed-Najib-Biabani-Ibrahimkhel"
]
}, },
"index.newPad": "نوې ليکچه", "index.newPad": "نوې ليکچه",
"index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانيستل:", "index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانيستل:",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Adr mm"]
"Adr mm"
]
}, },
"admin.page-title": "Pannellu de amministratzione - Etherpad", "admin.page-title": "Pannellu de amministratzione - Etherpad",
"admin_plugins": "Gestore de connetores", "admin_plugins": "Gestore de connetores",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["BaRaN6161 TURK", "Kaleem Bhatti", "Mehtab ahmed", "Tweety"]
"BaRaN6161 TURK",
"Kaleem Bhatti",
"Mehtab ahmed",
"Tweety"
]
}, },
"admin_settings": "ترتيبون", "admin_settings": "ترتيبون",
"index.newPad": "نئين پٽي", "index.newPad": "نئين پٽي",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Conquistador", "Vlad5250"]
"Conquistador",
"Vlad5250"
]
}, },
"admin_plugins.available_not-found": "Nijedan plugin nije pronađen.", "admin_plugins.available_not-found": "Nijedan plugin nije pronađen.",
"admin_plugins.description": "Opis", "admin_plugins.description": "Opis",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Ninjastrikers", "Saimawnkham", "Saosukham"]
"Ninjastrikers",
"Saimawnkham",
"Saosukham"
]
}, },
"index.newPad": "ၽႅတ်ႉမႂ်ႇ", "index.newPad": "ၽႅတ်ႉမႂ်ႇ",
"index.createOpenPad": "ဢမ်ႇၼၼ် ၶူင်မႂ်ႇ/ပိုတ်ႇၽႅတ်ႉၵိုၵ်းၸိုဝ်ႈ", "index.createOpenPad": "ဢမ်ႇၼၼ် ၶူင်မႂ်ႇ/ပိုတ်ႇၽႅတ်ႉၵိုၵ်းၸိုဝ်ႈ",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Saraiki"]
"Saraiki"
]
}, },
"admin_plugins": "پلگ ان منیجر", "admin_plugins": "پلگ ان منیجر",
"admin_plugins.available": "دستیاب پلگ ان", "admin_plugins.available": "دستیاب پلگ ان",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Yupik"]
"Yupik"
]
}, },
"admin_plugins.description": "Deskriptt", "admin_plugins.description": "Deskriptt",
"admin_plugins.name": "Nõmm", "admin_plugins.name": "Nõmm",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Besnik b", "Eraldkerciku", "Kosovastar", "Liridon"]
"Besnik b",
"Eraldkerciku",
"Kosovastar",
"Liridon"
]
}, },
"admin.page-title": "Pult Përgjegjësi - Etherpad", "admin.page-title": "Pult Përgjegjësi - Etherpad",
"admin_plugins": "Përgjegjës shtojcash", "admin_plugins": "Përgjegjës shtojcash",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Adr mm", "F Samaritani"]
"Adr mm",
"F Samaritani"
]
}, },
"admin.page-title": "Pannellu amministrativu - Etherpad", "admin.page-title": "Pannellu amministrativu - Etherpad",
"admin_plugins": "Gestore de connetores", "admin_plugins": "Gestore de connetores",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Andibecker", "Edwingudfriend", "Muddyb"]
"Andibecker",
"Edwingudfriend",
"Muddyb"
]
}, },
"admin.page-title": "Dashibodi ya Usimamizi - Etherpad", "admin.page-title": "Dashibodi ya Usimamizi - Etherpad",
"admin_plugins": "Meneja wa programu-jalizi", "admin_plugins": "Meneja wa programu-jalizi",

View file

@ -1,10 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Balajijagadesh", "ElangoRamanujam", "Sank"]
"Balajijagadesh",
"ElangoRamanujam",
"Sank"
]
}, },
"index.newPad": "புதிய அட்டை", "index.newPad": "புதிய அட்டை",
"index.createOpenPad": "அல்லது பெயருடன் ஒரு அட்டையை உருவாக்கு/திற", "index.createOpenPad": "அல்லது பெயருடன் ஒரு அட்டையை உருவாக்கு/திற",

View file

@ -1,9 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["BHARATHESHA ALASANDEMAJALU", "VASANTH S.N."]
"BHARATHESHA ALASANDEMAJALU",
"VASANTH S.N."
]
}, },
"index.newPad": "ಪೊಸ ಪ್ಯಾಡ್", "index.newPad": "ಪೊಸ ಪ್ಯಾಡ್",
"index.createOpenPad": "ಅತಂಡ ಈ ಪುದರ್ತ ಪ್ಯಾಡನ್ನು ಉಂಡು ಮನ್ಪು/ತೋಜಾಲ:", "index.createOpenPad": "ಅತಂಡ ಈ ಪುದರ್ತ ಪ್ಯಾಡನ್ನು ಉಂಡು ಮನ್ಪು/ತೋಜಾಲ:",

View file

@ -1,11 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Aefgh39622", "Andibecker", "Patsagorn Y.", "Trisorn Triboon"]
"Aefgh39622",
"Andibecker",
"Patsagorn Y.",
"Trisorn Triboon"
]
}, },
"admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad", "admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad",
"admin_plugins": "ตัวจัดการปลั๊กอิน", "admin_plugins": "ตัวจัดการปลั๊กอิน",

View file

@ -1,8 +1,6 @@
{ {
"@metadata": { "@metadata": {
"authors": [ "authors": ["Fierodelveneto"]
"Fierodelveneto"
]
}, },
"index.newPad": "Novo Pad", "index.newPad": "Novo Pad",
"index.createOpenPad": "O creare o verxare on Pad co'l nome:", "index.createOpenPad": "O creare o verxare on Pad co'l nome:",

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* This module provides all API functions * This module provides all API functions
*/ */
@ -19,21 +18,21 @@
* limitations under the License. * limitations under the License.
*/ */
const Changeset = require('../../static/js/Changeset'); const Changeset = require("../../static/js/Changeset");
const ChatMessage = require('../../static/js/ChatMessage'); const ChatMessage = require("../../static/js/ChatMessage");
const CustomError = require('../utils/customError'); const CustomError = require("../utils/customError");
const padManager = require('./PadManager'); const padManager = require("./PadManager");
const padMessageHandler = require('../handler/PadMessageHandler'); const padMessageHandler = require("../handler/PadMessageHandler");
const readOnlyManager = require('./ReadOnlyManager'); const readOnlyManager = require("./ReadOnlyManager");
const groupManager = require('./GroupManager'); const groupManager = require("./GroupManager");
const authorManager = require('./AuthorManager'); const authorManager = require("./AuthorManager");
const sessionManager = require('./SessionManager'); const sessionManager = require("./SessionManager");
const exportHtml = require('../utils/ExportHtml'); const exportHtml = require("../utils/ExportHtml");
const exportTxt = require('../utils/ExportTxt'); const exportTxt = require("../utils/ExportTxt");
const importHtml = require('../utils/ImportHtml'); const importHtml = require("../utils/ImportHtml");
const cleanText = require('./Pad').cleanText; const cleanText = require("./Pad").cleanText;
const PadDiff = require('../utils/padDiff'); const PadDiff = require("../utils/padDiff");
const {checkValidRev, isInt} = require('../utils/checkValidRev'); const { checkValidRev, isInt } = require("../utils/checkValidRev");
/* ******************** /* ********************
* GROUP FUNCTIONS **** * GROUP FUNCTIONS ****
@ -136,7 +135,10 @@ exports.getRevisionChangeset = async (padID: string, rev: string) => {
if (rev !== undefined) { if (rev !== undefined) {
// check if this is a valid revision // check if this is a valid revision
if (rev > head) { if (rev > head) {
throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError(
"rev is higher than the head revision of the pad",
"apierror",
);
} }
// get the changeset for this revision // get the changeset for this revision
@ -169,7 +171,10 @@ exports.getText = async (padID: string, rev: string) => {
if (rev !== undefined) { if (rev !== undefined) {
// check if this is a valid revision // check if this is a valid revision
if (rev > head) { if (rev > head) {
throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError(
"rev is higher than the head revision of the pad",
"apierror",
);
} }
// get the text of this revision // get the text of this revision
@ -200,10 +205,14 @@ Example returns:
* @param {String} authorId the id of the author, defaulting to empty string * @param {String} authorId the id of the author, defaulting to empty string
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise<void> => { exports.setText = async (
padID: string,
text?: string,
authorId = "",
): Promise<void> => {
// text is required // text is required
if (typeof text !== 'string') { if (typeof text !== "string") {
throw new CustomError('text is not a string', 'apierror'); throw new CustomError("text is not a string", "apierror");
} }
// get the pad // get the pad
@ -225,10 +234,10 @@ Example returns:
@param {String} text the text of the pad @param {String} text the text of the pad
@param {String} authorId the id of the author, defaulting to empty string @param {String} authorId the id of the author, defaulting to empty string
*/ */
exports.appendText = async (padID:string, text?: string, authorId:string = '') => { exports.appendText = async (padID: string, text?: string, authorId = "") => {
// text is required // text is required
if (typeof text !== 'string') { if (typeof text !== "string") {
throw new CustomError('text is not a string', 'apierror'); throw new CustomError("text is not a string", "apierror");
} }
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
@ -247,7 +256,10 @@ Example returns:
@param {String} rev the revision number, defaulting to the latest revision @param {String} rev the revision number, defaulting to the latest revision
@return {Promise<{html: string}>} the html of the pad @return {Promise<{html: string}>} the html of the pad
*/ */
exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }> => { exports.getHTML = async (
padID: string,
rev: string,
): Promise<{ html: string }> => {
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
} }
@ -259,7 +271,10 @@ exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }>
// check if this is a valid revision // check if this is a valid revision
const head = pad.getHeadRevisionNumber(); const head = pad.getHeadRevisionNumber();
if (rev > head) { if (rev > head) {
throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError(
"rev is higher than the head revision of the pad",
"apierror",
);
} }
} }
@ -283,10 +298,14 @@ Example returns:
@param {String} html the html of the pad @param {String} html the html of the pad
@param {String} authorId the id of the author, defaulting to empty string @param {String} authorId the id of the author, defaulting to empty string
*/ */
exports.setHTML = async (padID: string, html:string|object, authorId = '') => { exports.setHTML = async (
padID: string,
html: string | object,
authorId = "",
) => {
// html string is required // html string is required
if (typeof html !== 'string') { if (typeof html !== "string") {
throw new CustomError('html is not a string', 'apierror'); throw new CustomError("html is not a string", "apierror");
} }
// get the pad // get the pad
@ -296,7 +315,7 @@ exports.setHTML = async (padID: string, html:string|object, authorId = '') => {
try { try {
await importHtml.setPadHTML(pad, cleanText(html), authorId); await importHtml.setPadHTML(pad, cleanText(html), authorId);
} catch (e) { } catch (e) {
throw new CustomError('HTML is malformed', 'apierror'); throw new CustomError("HTML is malformed", "apierror");
} }
// update the clients on the pad // update the clients on the pad
@ -327,13 +346,13 @@ Example returns:
exports.getChatHistory = async (padID: string, start: number, end: number) => { exports.getChatHistory = async (padID: string, start: number, end: number) => {
if (start && end) { if (start && end) {
if (start < 0) { if (start < 0) {
throw new CustomError('start is below zero', 'apierror'); throw new CustomError("start is below zero", "apierror");
} }
if (end < 0) { if (end < 0) {
throw new CustomError('end is below zero', 'apierror'); throw new CustomError("end is below zero", "apierror");
} }
if (start > end) { if (start > end) {
throw new CustomError('start is higher than end', 'apierror'); throw new CustomError("start is higher than end", "apierror");
} }
} }
@ -349,10 +368,16 @@ exports.getChatHistory = async (padID: string, start:number, end:number) => {
} }
if (start > chatHead) { if (start > chatHead) {
throw new CustomError('start is higher than the current chatHead', 'apierror'); throw new CustomError(
"start is higher than the current chatHead",
"apierror",
);
} }
if (end > chatHead) { if (end > chatHead) {
throw new CustomError('end is higher than the current chatHead', 'apierror'); throw new CustomError(
"end is higher than the current chatHead",
"apierror",
);
} }
// the whole message-log and return it to the client // the whole message-log and return it to the client
@ -374,10 +399,15 @@ Example returns:
@param {String} authorID the id of the author @param {String} authorID the id of the author
@param {Number} time the timestamp of the chat-message @param {Number} time the timestamp of the chat-message
*/ */
exports.appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => { exports.appendChatMessage = async (
padID: string,
text: string | object,
authorID: string,
time: number,
) => {
// text is required // text is required
if (typeof text !== 'string') { if (typeof text !== "string") {
throw new CustomError('text is not a string', 'apierror'); throw new CustomError("text is not a string", "apierror");
} }
// if time is not an integer value set time to current timestamp // if time is not an integer value set time to current timestamp
@ -388,7 +418,10 @@ exports.appendChatMessage = async (padID: string, text: string|object, authorID:
// @TODO - missing getPadSafe() call ? // @TODO - missing getPadSafe() call ?
// save chat message to database and send message to all connected clients // save chat message to database and send message to all connected clients
await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID); await padMessageHandler.sendChatMessageToPadClients(
new ChatMessage(text, authorID, time),
padID,
);
}; };
/* *************** /* ***************
@ -463,14 +496,17 @@ exports.saveRevision = async (padID: string, rev: number) => {
// the client asked for a special revision // the client asked for a special revision
if (rev !== undefined) { if (rev !== undefined) {
if (rev > head) { if (rev > head) {
throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError(
"rev is higher than the head revision of the pad",
"apierror",
);
} }
} else { } else {
rev = pad.getHeadRevisionNumber(); rev = pad.getHeadRevisionNumber();
} }
const author = await authorManager.createAuthor('API'); const author = await authorManager.createAuthor("API");
await pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); await pad.addSavedRevision(rev, author.authorID, "Saved through API call");
}; };
/** /**
@ -483,7 +519,9 @@ Example returns:
@param {String} padID the id of the pad @param {String} padID the id of the pad
@return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad @return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad
*/ */
exports.getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => { exports.getLastEdited = async (
padID: string,
): Promise<{ lastEdited: number }> => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
const lastEdited = await pad.getLastEdit(); const lastEdited = await pad.getLastEdit();
@ -501,16 +539,19 @@ Example returns:
@param {String} text the initial text of the pad @param {String} text the initial text of the pad
@param {String} authorId the id of the author, defaulting to empty string @param {String} authorId the id of the author, defaulting to empty string
*/ */
exports.createPad = async (padID: string, text: string, authorId = '') => { exports.createPad = async (padID: string, text: string, authorId = "") => {
if (padID) { if (padID) {
// ensure there is no $ in the padID // ensure there is no $ in the padID
if (padID.indexOf('$') !== -1) { if (padID.indexOf("$") !== -1) {
throw new CustomError("createPad can't create group pads", 'apierror'); throw new CustomError("createPad can't create group pads", "apierror");
} }
// check for url special characters // check for url special characters
if (padID.match(/(\/|\?|&|#)/)) { if (padID.match(/(\/|\?|&|#)/)) {
throw new CustomError('malformed padID: Remove special characters', 'apierror'); throw new CustomError(
"malformed padID: Remove special characters",
"apierror",
);
} }
} }
@ -543,10 +584,10 @@ exports.deletePad = async (padID: string) => {
@param {Number} rev the revision number, defaulting to the latest revision @param {Number} rev the revision number, defaulting to the latest revision
@param {String} authorId the id of the author, defaulting to empty string @param {String} authorId the id of the author, defaulting to empty string
*/ */
exports.restoreRevision = async (padID: string, rev: number, authorId = '') => { exports.restoreRevision = async (padID: string, rev: number, authorId = "") => {
// check if rev is a number // check if rev is a number
if (rev === undefined) { if (rev === undefined) {
throw new CustomError('rev is not defined', 'apierror'); throw new CustomError("rev is not defined", "apierror");
} }
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -555,13 +596,16 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
// check if this is a valid revision // check if this is a valid revision
if (rev > pad.getHeadRevisionNumber()) { if (rev > pad.getHeadRevisionNumber()) {
throw new CustomError('rev is higher than the head revision of the pad', 'apierror'); throw new CustomError(
"rev is higher than the head revision of the pad",
"apierror",
);
} }
const atext = await pad.getInternalRevisionAText(rev); const atext = await pad.getInternalRevisionAText(rev);
const oldText = pad.text(); const oldText = pad.text();
atext.text += '\n'; atext.text += "\n";
const eachAttribRun = (attribs: string[], func: Function) => { const eachAttribRun = (attribs: string[], func: Function) => {
let textIndex = 0; let textIndex = 0;
@ -570,7 +614,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
for (const op of Changeset.deserializeOps(attribs)) { for (const op of Changeset.deserializeOps(attribs)) {
const nextIndex = textIndex + op.chars; const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); func(
Math.max(newTextStart, textIndex),
Math.min(newTextEnd, nextIndex),
op.attribs,
);
} }
textIndex = nextIndex; textIndex = nextIndex;
} }
@ -580,11 +628,14 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
const builder = Changeset.builder(oldText.length); const builder = Changeset.builder(oldText.length);
// assemble each line into the builder // assemble each line into the builder
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => { eachAttribRun(
atext.attribs,
(start: number, end: number, attribs: string[]) => {
builder.insert(atext.text.substring(start, end), attribs); builder.insert(atext.text.substring(start, end), attribs);
}); },
);
const lastNewlinePos = oldText.lastIndexOf('\n'); const lastNewlinePos = oldText.lastIndexOf("\n");
if (lastNewlinePos < 0) { if (lastNewlinePos < 0) {
builder.remove(oldText.length - 1, 0); builder.remove(oldText.length - 1, 0);
} else { } else {
@ -610,7 +661,11 @@ Example returns:
@param {String} destinationID the id of the destination pad @param {String} destinationID the id of the destination pad
@param {Boolean} force whether to overwrite the destination pad if it exists @param {Boolean} force whether to overwrite the destination pad if it exists
*/ */
exports.copyPad = async (sourceID: string, destinationID: string, force: boolean) => { exports.copyPad = async (
sourceID: string,
destinationID: string,
force: boolean,
) => {
const pad = await getPadSafe(sourceID, true); const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force); await pad.copy(destinationID, force);
}; };
@ -628,7 +683,12 @@ Example returns:
@param {Boolean} force whether to overwrite the destination pad if it exists @param {Boolean} force whether to overwrite the destination pad if it exists
@param {String} authorId the id of the author, defaulting to empty string @param {String} authorId the id of the author, defaulting to empty string
*/ */
exports.copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId = '') => { exports.copyPadWithoutHistory = async (
sourceID: string,
destinationID: string,
force: boolean,
authorId = "",
) => {
const pad = await getPadSafe(sourceID, true); const pad = await getPadSafe(sourceID, true);
await pad.copyPadWithoutHistory(destinationID, force, authorId); await pad.copyPadWithoutHistory(destinationID, force, authorId);
}; };
@ -645,7 +705,11 @@ Example returns:
@param {String} destinationID the id of the destination pad @param {String} destinationID the id of the destination pad
@param {Boolean} force whether to overwrite the destination pad if it exists @param {Boolean} force whether to overwrite the destination pad if it exists
*/ */
exports.movePad = async (sourceID: string, destinationID: string, force:boolean) => { exports.movePad = async (
sourceID: string,
destinationID: string,
force: boolean,
) => {
const pad = await getPadSafe(sourceID, true); const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force); await pad.copy(destinationID, force);
await pad.remove(); await pad.remove();
@ -683,7 +747,7 @@ exports.getPadID = async (roID: string) => {
// get the PadId // get the PadId
const padID = await readOnlyManager.getPadId(roID); const padID = await readOnlyManager.getPadId(roID);
if (padID == null) { if (padID == null) {
throw new CustomError('padID does not exist', 'apierror'); throw new CustomError("padID does not exist", "apierror");
} }
return { padID }; return { padID };
@ -699,16 +763,19 @@ Example returns:
@param {String} padID the id of the pad @param {String} padID the id of the pad
@param {Boolean} publicStatus the public status of the pad @param {Boolean} publicStatus the public status of the pad
*/ */
exports.setPublicStatus = async (padID: string, publicStatus: boolean|string) => { exports.setPublicStatus = async (
padID: string,
publicStatus: boolean | string,
) => {
// ensure this is a group pad // ensure this is a group pad
checkGroupPad(padID, 'publicStatus'); checkGroupPad(padID, "publicStatus");
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
// convert string to boolean // convert string to boolean
if (typeof publicStatus === 'string') { if (typeof publicStatus === "string") {
publicStatus = (publicStatus.toLowerCase() === 'true'); publicStatus = publicStatus.toLowerCase() === "true";
} }
await pad.setPublicStatus(publicStatus); await pad.setPublicStatus(publicStatus);
@ -725,7 +792,7 @@ Example returns:
*/ */
exports.getPublicStatus = async (padID: string) => { exports.getPublicStatus = async (padID: string) => {
// ensure this is a group pad // ensure this is a group pad
checkGroupPad(padID, 'publicStatus'); checkGroupPad(padID, "publicStatus");
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
@ -786,8 +853,7 @@ Example returns:
{"code":0,"message":"ok","data":null} {"code":0,"message":"ok","data":null}
{"code":4,"message":"no or wrong API Key","data":null} {"code":4,"message":"no or wrong API Key","data":null}
*/ */
exports.checkToken = async () => { exports.checkToken = async () => {};
};
/** /**
getChatHead(padID) returns the chatHead (last number of the last chat-message) of the pad getChatHead(padID) returns the chatHead (last number of the last chat-message) of the pad
@ -799,7 +865,7 @@ Example returns:
@param {String} padID the id of the pad @param {String} padID the id of the pad
@return {Promise<{chatHead: number}>} the chatHead of the pad @return {Promise<{chatHead: number}>} the chatHead of the pad
*/ */
exports.getChatHead = async (padID:string): Promise<{ chatHead: number; }> => { exports.getChatHead = async (padID: string): Promise<{ chatHead: number }> => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return { chatHead: pad.chatHead }; return { chatHead: pad.chatHead };
@ -825,7 +891,11 @@ Example returns:
@param {Number} startRev the start revision number @param {Number} startRev the start revision number
@param {Number} endRev the end revision number @param {Number} endRev the end revision number
*/ */
exports.createDiffHTML = async (padID: string, startRev: number, endRev: number) => { exports.createDiffHTML = async (
padID: string,
startRev: number,
endRev: number,
) => {
// check if startRev is a number // check if startRev is a number
if (startRev !== undefined) { if (startRev !== undefined) {
startRev = checkValidRev(startRev); startRev = checkValidRev(startRev);
@ -873,7 +943,9 @@ exports.getStats = async () => {
const sessionKeys = Object.keys(sessionInfos); const sessionKeys = Object.keys(sessionInfos);
// @ts-ignore // @ts-ignore
const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId)); const activePads = new Set(
Object.entries(sessionInfos).map((k) => k[1].padId),
);
const { padIDs } = await padManager.listAllPads(); const { padIDs } = await padManager.listAllPads();
@ -889,15 +961,20 @@ exports.getStats = async () => {
**************************** */ **************************** */
// gets a pad safe // gets a pad safe
const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:string, authorId:string = '') => { const getPadSafe = async (
padID: string | object,
shouldExist: boolean,
text?: string,
authorId = "",
) => {
// check if padID is a string // check if padID is a string
if (typeof padID !== 'string') { if (typeof padID !== "string") {
throw new CustomError('padID is not a string', 'apierror'); throw new CustomError("padID is not a string", "apierror");
} }
// check if the padID maches the requirements // check if the padID maches the requirements
if (!padManager.isValidPadId(padID)) { if (!padManager.isValidPadId(padID)) {
throw new CustomError('padID did not match requirements', 'apierror'); throw new CustomError("padID did not match requirements", "apierror");
} }
// check if the pad exists // check if the pad exists
@ -905,12 +982,12 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri
if (!exists && shouldExist) { if (!exists && shouldExist) {
// does not exist, but should // does not exist, but should
throw new CustomError('padID does not exist', 'apierror'); throw new CustomError("padID does not exist", "apierror");
} }
if (exists && !shouldExist) { if (exists && !shouldExist) {
// does exist, but shouldn't // does exist, but shouldn't
throw new CustomError('padID does already exist', 'apierror'); throw new CustomError("padID does already exist", "apierror");
} }
// pad exists, let's get it // pad exists, let's get it
@ -920,8 +997,10 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri
// checks if a padID is part of a group // checks if a padID is part of a group
const checkGroupPad = (padID: string, field: string) => { const checkGroupPad = (padID: string, field: string) => {
// ensure this is a group pad // ensure this is a group pad
if (padID && padID.indexOf('$') === -1) { if (padID && padID.indexOf("$") === -1) {
throw new CustomError( throw new CustomError(
`You can only get/set the ${field} of pads that belong to a group`, 'apierror'); `You can only get/set the ${field} of pads that belong to a group`,
"apierror",
);
} }
}; };

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The AuthorManager controlls all information about the Pad authors * The AuthorManager controlls all information about the Pad authors
*/ */
@ -19,76 +18,79 @@
* limitations under the License. * limitations under the License.
*/ */
const db = require('./DB'); const db = require("./DB");
const CustomError = require('../utils/customError'); const CustomError = require("../utils/customError");
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require("../../static/js/pluginfw/hooks.js");
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); const {
randomString,
padutils: { warnDeprecated },
} = require("../../static/js/pad_utils");
exports.getColorPalette = () => [ exports.getColorPalette = () => [
'#ffc7c7', "#ffc7c7",
'#fff1c7', "#fff1c7",
'#e3ffc7', "#e3ffc7",
'#c7ffd5', "#c7ffd5",
'#c7ffff', "#c7ffff",
'#c7d5ff', "#c7d5ff",
'#e3c7ff', "#e3c7ff",
'#ffc7f1', "#ffc7f1",
'#ffa8a8', "#ffa8a8",
'#ffe699', "#ffe699",
'#cfff9e', "#cfff9e",
'#99ffb3', "#99ffb3",
'#a3ffff', "#a3ffff",
'#99b3ff', "#99b3ff",
'#cc99ff', "#cc99ff",
'#ff99e5', "#ff99e5",
'#e7b1b1', "#e7b1b1",
'#e9dcAf', "#e9dcAf",
'#cde9af', "#cde9af",
'#bfedcc', "#bfedcc",
'#b1e7e7', "#b1e7e7",
'#c3cdee', "#c3cdee",
'#d2b8ea', "#d2b8ea",
'#eec3e6', "#eec3e6",
'#e9cece', "#e9cece",
'#e7e0ca', "#e7e0ca",
'#d3e5c7', "#d3e5c7",
'#bce1c5', "#bce1c5",
'#c1e2e2', "#c1e2e2",
'#c1c9e2', "#c1c9e2",
'#cfc1e2', "#cfc1e2",
'#e0bdd9', "#e0bdd9",
'#baded3', "#baded3",
'#a0f8eb', "#a0f8eb",
'#b1e7e0', "#b1e7e0",
'#c3c8e4', "#c3c8e4",
'#cec5e2', "#cec5e2",
'#b1d5e7', "#b1d5e7",
'#cda8f0', "#cda8f0",
'#f0f0a8', "#f0f0a8",
'#f2f2a6', "#f2f2a6",
'#f5a8eb', "#f5a8eb",
'#c5f9a9', "#c5f9a9",
'#ececbb', "#ececbb",
'#e7c4bc', "#e7c4bc",
'#daf0b2', "#daf0b2",
'#b0a0fd', "#b0a0fd",
'#bce2e7', "#bce2e7",
'#cce2bb', "#cce2bb",
'#ec9afe', "#ec9afe",
'#edabbd', "#edabbd",
'#aeaeea', "#aeaeea",
'#c4e7b1', "#c4e7b1",
'#d722bb', "#d722bb",
'#f3a5e7', "#f3a5e7",
'#ffa8a8', "#ffa8a8",
'#d8c0c5', "#d8c0c5",
'#eaaedd', "#eaaedd",
'#adc6eb', "#adc6eb",
'#bedad1', "#bedad1",
'#dee9af', "#dee9af",
'#e9afc2', "#e9afc2",
'#f8d2a0', "#f8d2a0",
'#b3b3e6', "#b3b3e6",
]; ];
/** /**
@ -107,7 +109,6 @@ exports.doesAuthorExist = async (authorID: string) => {
*/ */
exports.doesAuthorExists = exports.doesAuthorExist; exports.doesAuthorExists = exports.doesAuthorExist;
/** /**
* Returns the AuthorID for a mapper. We can map using a mapperkey, * Returns the AuthorID for a mapper. We can map using a mapperkey,
* so far this is token2author and mapper2author * so far this is token2author and mapper2author
@ -131,7 +132,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
// there is an author with this mapper // there is an author with this mapper
// update the timestamp of this author // update the timestamp of this author
await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now()); await db.setSub(`globalAuthor:${author}`, ["timestamp"], Date.now());
// return the author // return the author
return { authorID: author }; return { authorID: author };
@ -143,7 +144,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
* @return {Promise<string|*|{authorID: string}|{authorID: *}>} * @return {Promise<string|*|{authorID: string}|{authorID: *}>}
*/ */
const getAuthor4Token = async (token: string) => { const getAuthor4Token = async (token: string) => {
const author = await mapAuthorWithDBKey('token2author', token); const author = await mapAuthorWithDBKey("token2author", token);
// return only the sub value authorID // return only the sub value authorID
return author ? author.authorID : author; return author ? author.authorID : author;
@ -157,7 +158,7 @@ const getAuthor4Token = async (token: string) => {
*/ */
exports.getAuthorId = async (token: string, user: object) => { exports.getAuthorId = async (token: string, user: object) => {
const context = { dbKey: token, token, user }; const context = { dbKey: token, token, user };
let [authorId] = await hooks.aCallFirst('getAuthorId', context); let [authorId] = await hooks.aCallFirst("getAuthorId", context);
if (!authorId) authorId = await getAuthor4Token(context.dbKey); if (!authorId) authorId = await getAuthor4Token(context.dbKey);
return authorId; return authorId;
}; };
@ -170,7 +171,8 @@ exports.getAuthorId = async (token: string, user: object) => {
*/ */
exports.getAuthor4Token = async (token: string) => { exports.getAuthor4Token = async (token: string) => {
warnDeprecated( warnDeprecated(
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead'); "AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead",
);
return await getAuthor4Token(token); return await getAuthor4Token(token);
}; };
@ -179,8 +181,11 @@ exports.getAuthor4Token = async (token: string) => {
* @param {String} authorMapper The mapper * @param {String} authorMapper The mapper
* @param {String} name The name of the author (optional) * @param {String} name The name of the author (optional)
*/ */
exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string) => { exports.createAuthorIfNotExistsFor = async (
const author = await mapAuthorWithDBKey('mapper2author', authorMapper); authorMapper: string,
name: string,
) => {
const author = await mapAuthorWithDBKey("mapper2author", authorMapper);
if (name) { if (name) {
// set the name of this author // set the name of this author
@ -190,7 +195,6 @@ exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string)
return author; return author;
}; };
/** /**
* Internal function that creates the database entry for an author * Internal function that creates the database entry for an author
* @param {String} name The name of the author * @param {String} name The name of the author
@ -201,7 +205,7 @@ exports.createAuthor = async (name: string) => {
// create the globalAuthors db entry // create the globalAuthors db entry
const authorObj = { const authorObj = {
colorId: Math.floor(Math.random() * (exports.getColorPalette().length)), colorId: Math.floor(Math.random() * exports.getColorPalette().length),
name, name,
timestamp: Date.now(), timestamp: Date.now(),
}; };
@ -216,35 +220,38 @@ exports.createAuthor = async (name: string) => {
* Returns the Author Obj of the author * Returns the Author Obj of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`); exports.getAuthor = async (author: string) =>
await db.get(`globalAuthor:${author}`);
/** /**
* Returns the color Id of the author * Returns the color Id of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']); exports.getAuthorColorId = async (author: string) =>
await db.getSub(`globalAuthor:${author}`, ["colorId"]);
/** /**
* Sets the color Id of the author * Sets the color Id of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} colorId The color id of the author * @param {String} colorId The color id of the author
*/ */
exports.setAuthorColorId = async (author: string, colorId: string) => await db.setSub( exports.setAuthorColorId = async (author: string, colorId: string) =>
`globalAuthor:${author}`, ['colorId'], colorId); await db.setSub(`globalAuthor:${author}`, ["colorId"], colorId);
/** /**
* Returns the name of the author * Returns the name of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']); exports.getAuthorName = async (author: string) =>
await db.getSub(`globalAuthor:${author}`, ["name"]);
/** /**
* Sets the name of the author * Sets the name of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} name The name of the author * @param {String} name The name of the author
*/ */
exports.setAuthorName = async (author: string, name: string) => await db.setSub( exports.setAuthorName = async (author: string, name: string) =>
`globalAuthor:${author}`, ['name'], name); await db.setSub(`globalAuthor:${author}`, ["name"], name);
/** /**
* Returns an array of all pads this author contributed to * Returns an array of all pads this author contributed to
@ -261,7 +268,7 @@ exports.listPadsOfAuthor = async (authorID: string) => {
if (author == null) { if (author == null) {
// author does not exist // author does not exist
throw new CustomError('authorID does not exist', 'apierror'); throw new CustomError("authorID does not exist", "apierror");
} }
// everything is fine, return the pad IDs // everything is fine, return the pad IDs

View file

@ -1,5 +1,3 @@
'use strict';
/** /**
* The DB Module provides a database initialized with the settings * The DB Module provides a database initialized with the settings
* provided by the settings module * provided by the settings module
@ -21,12 +19,12 @@
* limitations under the License. * limitations under the License.
*/ */
import ueberDB from 'ueberdb2'; import ueberDB from "ueberdb2";
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
import log4js from 'log4js'; import log4js from "log4js";
const stats = require('../stats') const stats = require("../stats");
const logger = log4js.getLogger('ueberDB'); const logger = log4js.getLogger("ueberDB");
/** /**
* The UeberDB Object that provides the database functions * The UeberDB Object that provides the database functions
@ -37,17 +35,23 @@ exports.db = null;
* Initializes the database with the settings provided by the settings module * Initializes the database with the settings provided by the settings module
*/ */
exports.init = async () => { exports.init = async () => {
exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger); exports.db = new ueberDB.Database(
settings.dbType,
settings.dbSettings,
null,
logger,
);
await exports.db.init(); await exports.db.init();
if (exports.db.metrics != null) { if (exports.db.metrics != null) {
for (const [metric, value] of Object.entries(exports.db.metrics)) { for (const [metric, value] of Object.entries(exports.db.metrics)) {
if (typeof value !== 'number') continue; if (typeof value !== "number") continue;
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]); stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
} }
} }
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) { for (const fn of ["get", "set", "findKeys", "getSub", "setSub", "remove"]) {
const f = exports.db[fn]; const f = exports.db[fn];
exports[fn] = async (...args:string[]) => await f.call(exports.db, ...args); exports[fn] = async (...args: string[]) =>
await f.call(exports.db, ...args);
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f)); Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f)); Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
} }
@ -56,5 +60,5 @@ exports.init = async () => {
exports.shutdown = async (hookName: string, context: any) => { exports.shutdown = async (hookName: string, context: any) => {
if (exports.db != null) await exports.db.close(); if (exports.db != null) await exports.db.close();
exports.db = null; exports.db = null;
logger.log('Database closed'); logger.log("Database closed");
}; };

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The Group Manager provides functions to manage groups in the database * The Group Manager provides functions to manage groups in the database
*/ */
@ -19,18 +18,18 @@
* limitations under the License. * limitations under the License.
*/ */
const CustomError = require('../utils/customError'); const CustomError = require("../utils/customError");
const randomString = require('../../static/js/pad_utils').randomString; const randomString = require("../../static/js/pad_utils").randomString;
const db = require('./DB'); const db = require("./DB");
const padManager = require('./PadManager'); const padManager = require("./PadManager");
const sessionManager = require('./SessionManager'); const sessionManager = require("./SessionManager");
/** /**
* Lists all groups * Lists all groups
* @return {Promise<{groupIDs: string[]}>} The ids of all groups * @return {Promise<{groupIDs: string[]}>} The ids of all groups
*/ */
exports.listAllGroups = async () => { exports.listAllGroups = async () => {
let groups = await db.get('groups'); let groups = await db.get("groups");
groups = groups || {}; groups = groups || {};
const groupIDs = Object.keys(groups); const groupIDs = Object.keys(groups);
@ -48,29 +47,35 @@ exports.deleteGroup = async (groupID: string): Promise<void> => {
// ensure group exists // ensure group exists
if (group == null) { if (group == null) {
// group does not exist // group does not exist
throw new CustomError('groupID does not exist', 'apierror'); throw new CustomError("groupID does not exist", "apierror");
} }
// iterate through all pads of this group and delete them (in parallel) // iterate through all pads of this group and delete them (in parallel)
await Promise.all(Object.keys(group.pads).map(async (padId) => { await Promise.all(
Object.keys(group.pads).map(async (padId) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
await pad.remove(); await pad.remove();
})); }),
);
// Delete associated sessions in parallel. This should be done before deleting the group2sessions // Delete associated sessions in parallel. This should be done before deleting the group2sessions
// record because deleting a session updates the group2sessions record. // record because deleting a session updates the group2sessions record.
const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {}; const { sessionIDs = {} } = (await db.get(`group2sessions:${groupID}`)) || {};
await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => { await Promise.all(
Object.keys(sessionIDs).map(async (sessionId) => {
await sessionManager.deleteSession(sessionId); await sessionManager.deleteSession(sessionId);
})); }),
);
await Promise.all([ await Promise.all([
db.remove(`group2sessions:${groupID}`), db.remove(`group2sessions:${groupID}`),
// UeberDB's setSub() method atomically reads the record, updates the appropriate property, and // UeberDB's setSub() method atomically reads the record, updates the appropriate property, and
// writes the result. Setting a property to `undefined` deletes that property (JSON.stringify() // writes the result. Setting a property to `undefined` deletes that property (JSON.stringify()
// ignores such properties). // ignores such properties).
db.setSub('groups', [groupID], undefined), db.setSub("groups", [groupID], undefined),
...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)), ...Object.keys(group.mappings || {}).map(
async (m) => await db.remove(`mapper2group:${m}`),
),
]); ]);
// Remove the group record after updating the `groups` record so that the state is consistent. // Remove the group record after updating the `groups` record so that the state is consistent.
@ -86,7 +91,7 @@ exports.doesGroupExist = async (groupID: string) => {
// try to get the group entry // try to get the group entry
const group = await db.get(`group:${groupID}`); const group = await db.get(`group:${groupID}`);
return (group != null); return group != null;
}; };
/** /**
@ -99,7 +104,7 @@ exports.createGroup = async () => {
// Add the group to the `groups` record after the group's individual record is created so that // Add the group to the `groups` record after the group's individual record is created so that
// the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates // the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates
// the appropriate property, and writes the result. // the appropriate property, and writes the result.
await db.setSub('groups', [groupID], 1); await db.setSub("groups", [groupID], 1);
return { groupID }; return { groupID };
}; };
@ -109,11 +114,11 @@ exports.createGroup = async () => {
* @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID * @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID
*/ */
exports.createGroupIfNotExistsFor = async (groupMapper: string | object) => { exports.createGroupIfNotExistsFor = async (groupMapper: string | object) => {
if (typeof groupMapper !== 'string') { if (typeof groupMapper !== "string") {
throw new CustomError('groupMapper is not a string', 'apierror'); throw new CustomError("groupMapper is not a string", "apierror");
} }
const groupID = await db.get(`mapper2group:${groupMapper}`); const groupID = await db.get(`mapper2group:${groupMapper}`);
if (groupID && await exports.doesGroupExist(groupID)) return {groupID}; if (groupID && (await exports.doesGroupExist(groupID))) return { groupID };
const result = await exports.createGroup(); const result = await exports.createGroup();
await Promise.all([ await Promise.all([
db.set(`mapper2group:${groupMapper}`, result.groupID), db.set(`mapper2group:${groupMapper}`, result.groupID),
@ -121,7 +126,7 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
// deleted. Although the core Etherpad API does not support multiple mappings for the same // deleted. Although the core Etherpad API does not support multiple mappings for the same
// group, the database record does support multiple mappings in case a plugin decides to extend // group, the database record does support multiple mappings in case a plugin decides to extend
// the core Etherpad functionality. (It's also easy to implement it this way.) // the core Etherpad functionality. (It's also easy to implement it this way.)
db.setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1), db.setSub(`group:${result.groupID}`, ["mappings", groupMapper], 1),
]); ]);
return result; return result;
}; };
@ -134,7 +139,12 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
* @param {String} authorId The id of the author * @param {String} authorId The id of the author
* @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad * @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad
*/ */
exports.createGroupPad = async (groupID: string, padName: string, text: string, authorId: string = ''): Promise<{ padID: string; }> => { exports.createGroupPad = async (
groupID: string,
padName: string,
text: string,
authorId = "",
): Promise<{ padID: string }> => {
// create the padID // create the padID
const padID = `${groupID}$${padName}`; const padID = `${groupID}$${padName}`;
@ -142,7 +152,7 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
const groupExists = await exports.doesGroupExist(groupID); const groupExists = await exports.doesGroupExist(groupID);
if (!groupExists) { if (!groupExists) {
throw new CustomError('groupID does not exist', 'apierror'); throw new CustomError("groupID does not exist", "apierror");
} }
// ensure pad doesn't exist already // ensure pad doesn't exist already
@ -150,14 +160,14 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
if (padExists) { if (padExists) {
// pad exists already // pad exists already
throw new CustomError('padName does already exist', 'apierror'); throw new CustomError("padName does already exist", "apierror");
} }
// create the pad // create the pad
await padManager.getPad(padID, text, authorId); await padManager.getPad(padID, text, authorId);
// create an entry in the group for this pad // create an entry in the group for this pad
await db.setSub(`group:${groupID}`, ['pads', padID], 1); await db.setSub(`group:${groupID}`, ["pads", padID], 1);
return { padID }; return { padID };
}; };
@ -167,16 +177,16 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
* @param {String} groupID The id of the group * @param {String} groupID The id of the group
* @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group * @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group
*/ */
exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => { exports.listPads = async (groupID: string): Promise<{ padIDs: string[] }> => {
const exists = await exports.doesGroupExist(groupID); const exists = await exports.doesGroupExist(groupID);
// ensure the group exists // ensure the group exists
if (!exists) { if (!exists) {
throw new CustomError('groupID does not exist', 'apierror'); throw new CustomError("groupID does not exist", "apierror");
} }
// group exists, let's get the pads // group exists, let's get the pads
const result = await db.getSub(`group:${groupID}`, ['pads']); const result = await db.getSub(`group:${groupID}`, ["pads"]);
const padIDs = Object.keys(result); const padIDs = Object.keys(result);
return { padIDs }; return { padIDs };

View file

@ -1,30 +1,31 @@
'use strict'; import type { Database } from "ueberdb2";
import {Database} from "ueberdb2"; import type { MapArrayType } from "../types/MapType";
import {AChangeSet, APool, AText} from "../types/PadType"; import type { AChangeSet, APool, AText } from "../types/PadType";
import {MapArrayType} from "../types/MapType";
/** /**
* The pad object, defined with joose * The pad object, defined with joose
*/ */
const AttributeMap = require('../../static/js/AttributeMap'); const AttributeMap = require("../../static/js/AttributeMap");
const Changeset = require('../../static/js/Changeset'); const Changeset = require("../../static/js/Changeset");
const ChatMessage = require('../../static/js/ChatMessage'); const ChatMessage = require("../../static/js/ChatMessage");
const AttributePool = require('../../static/js/AttributePool'); const AttributePool = require("../../static/js/AttributePool");
const Stream = require('../utils/Stream'); const Stream = require("../utils/Stream");
const assert = require('assert').strict; const assert = require("assert").strict;
const db = require('./DB'); const db = require("./DB");
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
const authorManager = require('./AuthorManager'); const authorManager = require("./AuthorManager");
const padManager = require('./PadManager'); const padManager = require("./PadManager");
const padMessageHandler = require('../handler/PadMessageHandler'); const padMessageHandler = require("../handler/PadMessageHandler");
const groupManager = require('./GroupManager'); const groupManager = require("./GroupManager");
const CustomError = require('../utils/customError'); const CustomError = require("../utils/customError");
const readOnlyManager = require('./ReadOnlyManager'); const readOnlyManager = require("./ReadOnlyManager");
const randomString = require('../utils/randomstring'); const randomString = require("../utils/randomstring");
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require("../../static/js/pluginfw/hooks");
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); const {
const promises = require('../utils/promises'); padutils: { warnDeprecated },
} = require("../../static/js/pad_utils");
const promises = require("../utils/promises");
/** /**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix * Copied from the Etherpad source code. It converts Windows line breaks to Unix
@ -32,10 +33,12 @@ const promises = require('../utils/promises');
* @param {String} txt The text to clean * @param {String} txt The text to clean
* @returns {String} The cleaned text * @returns {String} The cleaned text
*/ */
exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n') exports.cleanText = (txt: string): string =>
.replace(/\r/g, '\n') txt
.replace(/\t/g, ' ') .replace(/\r\n/g, "\n")
.replace(/\xa0/g, ' '); .replace(/\r/g, "\n")
.replace(/\t/g, " ")
.replace(/\xa0/g, " ");
class Pad { class Pad {
private db: Database; private db: Database;
@ -56,7 +59,7 @@ class Pad {
*/ */
constructor(id: string, database = db) { constructor(id: string, database = db) {
this.db = database; this.db = database;
this.atext = Changeset.makeAText('\n'); this.atext = Changeset.makeAText("\n");
this.pool = new AttributePool(); this.pool = new AttributePool();
this.head = -1; this.head = -1;
this.chatHead = -1; this.chatHead = -1;
@ -93,10 +96,13 @@ class Pad {
* @param {String} authorId The id of the author * @param {String} authorId The id of the author
* @return {Promise<number|string>} * @return {Promise<number|string>}
*/ */
async appendRevision(aChangeset:AChangeSet, authorId = '') { async appendRevision(aChangeset: AChangeSet, authorId = "") {
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs && if (
this.head !== -1) { newAText.text === this.atext.text &&
newAText.attribs === this.atext.attribs &&
this.head !== -1
) {
return this.head; return this.head;
} }
Changeset.copyAText(newAText, this.atext); Changeset.copyAText(newAText, this.atext);
@ -104,9 +110,9 @@ class Pad {
const newRev = ++this.head; const newRev = ++this.head;
// ex. getNumForAuthor // ex. getNumForAuthor
if (authorId !== '') this.pool.putAttrib(['author', authorId]); if (authorId !== "") this.pool.putAttrib(["author", authorId]);
const hook = this.head === 0 ? 'padCreate' : 'padUpdate'; const hook = this.head === 0 ? "padCreate" : "padUpdate";
await Promise.all([ await Promise.all([
// @ts-ignore // @ts-ignore
this.db.set(`pad:${this.id}:revs:${newRev}`, { this.db.set(`pad:${this.id}:revs:${newRev}`, {
@ -114,10 +120,12 @@ class Pad {
meta: { meta: {
author: authorId, author: authorId,
timestamp: Date.now(), timestamp: Date.now(),
...newRev === this.getKeyRevisionNumber(newRev) ? { ...(newRev === this.getKeyRevisionNumber(newRev)
? {
pool: this.pool, pool: this.pool,
atext: this.atext, atext: this.atext,
} : {}, }
: {}),
}, },
}), }),
this.saveToDatabase(), this.saveToDatabase(),
@ -126,17 +134,23 @@ class Pad {
pad: this, pad: this,
authorId, authorId,
get author() { get author() {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); warnDeprecated(
`${hook} hook author context is deprecated; use authorId instead`,
);
return this.authorId; return this.authorId;
}, },
set author(authorId) { set author(authorId) {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); warnDeprecated(
`${hook} hook author context is deprecated; use authorId instead`,
);
this.authorId = authorId; this.authorId = authorId;
}, },
...this.head === 0 ? {} : { ...(this.head === 0
? {}
: {
revs: newRev, revs: newRev,
changeset: aChangeset, changeset: aChangeset,
}, }),
}), }),
]); ]);
return newRev; return newRev;
@ -161,22 +175,31 @@ class Pad {
async getLastEdit() { async getLastEdit() {
const revNum = this.getHeadRevisionNumber(); const revNum = this.getHeadRevisionNumber();
// @ts-ignore // @ts-ignore
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
"meta",
"timestamp",
]);
} }
async getRevisionChangeset(revNum: number) { async getRevisionChangeset(revNum: number) {
// @ts-ignore // @ts-ignore
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']); return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ["changeset"]);
} }
async getRevisionAuthor(revNum: number) { async getRevisionAuthor(revNum: number) {
// @ts-ignore // @ts-ignore
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']); return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
"meta",
"author",
]);
} }
async getRevisionDate(revNum: number) { async getRevisionDate(revNum: number) {
// @ts-ignore // @ts-ignore
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
"meta",
"timestamp",
]);
} }
/** /**
@ -185,7 +208,10 @@ class Pad {
*/ */
async _getKeyRevisionAText(revNum: number) { async _getKeyRevisionAText(revNum: number) {
// @ts-ignore // @ts-ignore
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'atext']); return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
"meta",
"atext",
]);
} }
/** /**
@ -196,7 +222,10 @@ class Pad {
const authorIds = []; const authorIds = [];
for (const key in this.pool.numToAttrib) { for (const key in this.pool.numToAttrib) {
if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') { if (
this.pool.numToAttrib[key][0] === "author" &&
this.pool.numToAttrib[key][1] !== ""
) {
authorIds.push(this.pool.numToAttrib[key][1]); authorIds.push(this.pool.numToAttrib[key][1]);
} }
} }
@ -211,11 +240,15 @@ class Pad {
const [keyAText, changesets] = await Promise.all([ const [keyAText, changesets] = await Promise.all([
this._getKeyRevisionAText(keyRev), this._getKeyRevisionAText(keyRev),
Promise.all( Promise.all(
Stream.range(keyRev + 1, targetRev + 1).map(this.getRevisionChangeset.bind(this))), Stream.range(keyRev + 1, targetRev + 1).map(
this.getRevisionChangeset.bind(this),
),
),
]); ]);
const apool = this.apool(); const apool = this.apool();
let atext = keyAText; let atext = keyAText;
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool); for (const cs of changesets)
atext = Changeset.applyToAText(cs, atext, apool);
return atext; return atext;
} }
@ -229,18 +262,21 @@ class Pad {
const colorPalette = authorManager.getColorPalette(); const colorPalette = authorManager.getColorPalette();
await Promise.all( await Promise.all(
authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId:string) => { authorIds.map((authorId) =>
authorManager.getAuthorColorId(authorId).then((colorId: string) => {
// colorId might be a hex color or an number out of the palette // colorId might be a hex color or an number out of the palette
returnTable[authorId] = colorPalette[colorId] || colorId; returnTable[authorId] = colorPalette[colorId] || colorId;
}))); }),
),
);
return returnTable; return returnTable;
} }
getValidRevisionRange(startRev: any, endRev: any) { getValidRevisionRange(startRev: any, endRev: any) {
startRev = parseInt(startRev, 10); startRev = Number.parseInt(startRev, 10);
const head = this.getHeadRevisionNumber(); const head = this.getHeadRevisionNumber();
endRev = endRev ? parseInt(endRev, 10) : head; endRev = endRev ? Number.parseInt(endRev, 10) : head;
if (isNaN(startRev) || startRev < 0 || startRev > head) { if (isNaN(startRev) || startRev < 0 || startRev > head) {
startRev = null; startRev = null;
@ -280,18 +316,23 @@ class Pad {
* @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted). * @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted).
* @param {string} [authorId] - Author ID of the user making the change (if applicable). * @param {string} [authorId] - Author ID of the user making the change (if applicable).
*/ */
async spliceText(start:number, ndel:number, ins: string, authorId: string = '') { async spliceText(start: number, ndel: number, ins: string, authorId = "") {
if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`); if (start < 0)
if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`); throw new RangeError(`start index must be non-negative (is ${start})`);
if (ndel < 0)
throw new RangeError(
`characters to delete must be non-negative (is ${ndel})`,
);
const orig = this.text(); const orig = this.text();
assert(orig.endsWith('\n')); assert(orig.endsWith("\n"));
if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text'); if (start + ndel > orig.length)
throw new RangeError("start/delete past the end of the text");
ins = exports.cleanText(ins); ins = exports.cleanText(ins);
const willEndWithNewline = const willEndWithNewline =
start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline). start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
ins.endsWith('\n') || ins.endsWith("\n") ||
(!ins && start > 0 && orig[start - 1] === '\n'); (!ins && start > 0 && orig[start - 1] === "\n");
if (!willEndWithNewline) ins += '\n'; if (!willEndWithNewline) ins += "\n";
if (ndel === 0 && ins.length === 0) return; if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins); const changeset = Changeset.makeSplice(orig, start, ndel, ins);
await this.appendRevision(changeset, authorId); await this.appendRevision(changeset, authorId);
@ -305,7 +346,7 @@ class Pad {
* @param {string} [authorId] - The author ID of the user that initiated the change, if * @param {string} [authorId] - The author ID of the user that initiated the change, if
* applicable. * applicable.
*/ */
async setText(newText: string, authorId = '') { async setText(newText: string, authorId = "") {
await this.spliceText(0, this.text().length, newText, authorId); await this.spliceText(0, this.text().length, newText, authorId);
} }
@ -316,7 +357,7 @@ class Pad {
* @param {string} [authorId] - The author ID of the user that initiated the change, if * @param {string} [authorId] - The author ID of the user that initiated the change, if
* applicable. * applicable.
*/ */
async appendText(newText:string, authorId = '') { async appendText(newText: string, authorId = "") {
await this.spliceText(this.text().length - 1, 0, newText, authorId); await this.spliceText(this.text().length - 1, 0, newText, authorId);
} }
@ -330,15 +371,24 @@ class Pad {
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
* `msgOrText.time` instead. * `msgOrText.time` instead.
*/ */
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) { async appendChatMessage(
msgOrText: string | typeof ChatMessage,
authorId = null,
time = null,
) {
const msg = const msg =
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); msgOrText instanceof ChatMessage
? msgOrText
: new ChatMessage(msgOrText, authorId, time);
this.chatHead++; this.chatHead++;
await Promise.all([ await Promise.all([
// Don't save the display name in the database because the user can change it at any time. The // Don't save the display name in the database because the user can change it at any time. The
// `displayName` property will be populated with the current value when the message is read // `displayName` property will be populated with the current value when the message is read
// from the database. // from the database.
this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}), this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {
...msg,
displayName: undefined,
}),
this.saveToDatabase(), this.saveToDatabase(),
]); ]);
} }
@ -363,14 +413,15 @@ class Pad {
* interval as is typical in code. * interval as is typical in code.
*/ */
async getChatMessages(start: string, end: number) { async getChatMessages(start: string, end: number) {
const entries = const entries = await Promise.all(
await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this))); Stream.range(start, end + 1).map(this.getChatMessage.bind(this)),
);
// sort out broken chat entries // sort out broken chat entries
// it looks like in happened in the past that the chat head was // it looks like in happened in the past that the chat head was
// incremented, but the chat message wasn't added // incremented, but the chat message wasn't added
return entries.filter((entry) => { return entries.filter((entry) => {
const pass = (entry != null); const pass = entry != null;
if (!pass) { if (!pass) {
console.warn(`WARNING: Found broken chat entry in pad ${this.id}`); console.warn(`WARNING: Found broken chat entry in pad ${this.id}`);
} }
@ -378,25 +429,32 @@ class Pad {
}); });
} }
async init(text:string, authorId = '') { async init(text: string, authorId = "") {
// try to load the pad // try to load the pad
const value = await this.db.get(`pad:${this.id}`); const value = await this.db.get(`pad:${this.id}`);
// if this pad exists, load it // if this pad exists, load it
if (value != null) { if (value != null) {
Object.assign(this, value); Object.assign(this, value);
if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool); if ("pool" in value)
this.pool = new AttributePool().fromJsonable(value.pool);
} else { } else {
if (text == null) { if (text == null) {
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText}; const context = {
await hooks.aCallAll('padDefaultContent', context); pad: this,
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`); authorId,
type: "text",
content: settings.defaultPadText,
};
await hooks.aCallAll("padDefaultContent", context);
if (context.type !== "text")
throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content); text = exports.cleanText(context.content);
} }
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text); const firstChangeset = Changeset.makeSplice("\n", 0, 0, text);
await this.appendRevision(firstChangeset, authorId); await this.appendRevision(firstChangeset, authorId);
} }
await hooks.aCallAll('padLoad', {pad: this}); await hooks.aCallAll("padLoad", { pad: this });
} }
async copy(destinationID: string, force: boolean) { async copy(destinationID: string, force: boolean) {
@ -419,30 +477,38 @@ class Pad {
await db.set(`pad:${destinationID}${keySuffix}`, val); await db.set(`pad:${destinationID}${keySuffix}`, val);
}; };
const promises = (function* () { const promises = function* () {
yield copyRecord(''); yield copyRecord("");
// @ts-ignore // @ts-ignore
yield* Stream.range(0, this.head + 1).map((i) => copyRecord(`:revs:${i}`)); yield* Stream.range(0, this.head + 1).map((i) =>
copyRecord(`:revs:${i}`),
);
// @ts-ignore // @ts-ignore
yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`)); yield* Stream.range(0, this.chatHead + 1).map((i) =>
copyRecord(`:chat:${i}`),
);
// @ts-ignore // @ts-ignore
yield this.copyAuthorInfoToDestinationPad(destinationID); yield this.copyAuthorInfoToDestinationPad(destinationID);
if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); if (destGroupID)
}).call(this); yield db.setSub(`group:${destGroupID}`, ["pads", destinationID], 1);
}.call(this);
for (const p of new Stream(promises).batch(100).buffer(99)) await p; for (const p of new Stream(promises).batch(100).buffer(99)) await p;
// Initialize the new pad (will update the listAllPads cache) // Initialize the new pad (will update the listAllPads cache)
const dstPad = await padManager.getPad(destinationID, null); const dstPad = await padManager.getPad(destinationID, null);
// let the plugins know the pad was copied // let the plugins know the pad was copied
await hooks.aCallAll('padCopy', { await hooks.aCallAll("padCopy", {
get originalPad() { get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); warnDeprecated(
"padCopy originalPad context property is deprecated; use srcPad instead",
);
return this.srcPad; return this.srcPad;
}, },
get destinationID() { get destinationID() {
warnDeprecated( warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead'); "padCopy destinationID context property is deprecated; use dstPad.id instead",
);
return this.dstPad.id; return this.dstPad.id;
}, },
srcPad: this, srcPad: this,
@ -455,33 +521,39 @@ class Pad {
async checkIfGroupExistAndReturnIt(destinationID: string) { async checkIfGroupExistAndReturnIt(destinationID: string) {
let destGroupID: false | string = false; let destGroupID: false | string = false;
if (destinationID.indexOf('$') >= 0) { if (destinationID.indexOf("$") >= 0) {
destGroupID = destinationID.split('$')[0]; destGroupID = destinationID.split("$")[0];
const groupExists = await groupManager.doesGroupExist(destGroupID); const groupExists = await groupManager.doesGroupExist(destGroupID);
// group does not exist // group does not exist
if (!groupExists) { if (!groupExists) {
throw new CustomError('groupID does not exist for destinationID', 'apierror'); throw new CustomError(
"groupID does not exist for destinationID",
"apierror",
);
} }
} }
return destGroupID; return destGroupID;
} }
async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) { async removePadIfForceIsTrueAndAlreadyExist(
destinationID: string,
force: boolean | string,
) {
// if the pad exists, we should abort, unless forced. // if the pad exists, we should abort, unless forced.
const exists = await padManager.doesPadExist(destinationID); const exists = await padManager.doesPadExist(destinationID);
// allow force to be a string // allow force to be a string
if (typeof force === 'string') { if (typeof force === "string") {
force = (force.toLowerCase() === 'true'); force = force.toLowerCase() === "true";
} else { } else {
force = !!force; force = !!force;
} }
if (exists) { if (exists) {
if (!force) { if (!force) {
console.error('erroring out without force'); console.error("erroring out without force");
throw new CustomError('destinationID already exists', 'apierror'); throw new CustomError("destinationID already exists", "apierror");
} }
// exists and forcing // exists and forcing
@ -492,11 +564,18 @@ class Pad {
async copyAuthorInfoToDestinationPad(destinationID: string) { async copyAuthorInfoToDestinationPad(destinationID: string) {
// add the new sourcePad to all authors who contributed to the old one // add the new sourcePad to all authors who contributed to the old one
await Promise.all(this.getAllAuthors().map( await Promise.all(
(authorID) => authorManager.addPad(authorID, destinationID))); this.getAllAuthors().map((authorID) =>
authorManager.addPad(authorID, destinationID),
),
);
} }
async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') { async copyPadWithoutHistory(
destinationID: string,
force: string | boolean,
authorId = "",
) {
// flush the source pad // flush the source pad
this.saveToDatabase(); this.saveToDatabase();
@ -510,11 +589,11 @@ class Pad {
// Group pad? Add it to the group's list // Group pad? Add it to the group's list
if (destGroupID) { if (destGroupID) {
await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); await db.setSub(`group:${destGroupID}`, ["pads", destinationID], 1);
} }
// initialize the pad with a new line to avoid getting the defaultText // initialize the pad with a new line to avoid getting the defaultText
const dstPad = await padManager.getPad(destinationID, '\n', authorId); const dstPad = await padManager.getPad(destinationID, "\n", authorId);
dstPad.pool = this.pool.clone(); dstPad.pool = this.pool.clone();
const oldAText = this.atext; const oldAText = this.atext;
@ -533,17 +612,25 @@ class Pad {
// create a changeset that removes the previous text and add the newText with // create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad // all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); const changeset = Changeset.pack(
oldLength,
newLength,
assem.toString(),
newText,
);
dstPad.appendRevision(changeset, authorId); dstPad.appendRevision(changeset, authorId);
await hooks.aCallAll('padCopy', { await hooks.aCallAll("padCopy", {
get originalPad() { get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); warnDeprecated(
"padCopy originalPad context property is deprecated; use srcPad instead",
);
return this.srcPad; return this.srcPad;
}, },
get destinationID() { get destinationID() {
warnDeprecated( warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead'); "padCopy destinationID context property is deprecated; use dstPad.id instead",
);
return this.dstPad.id; return this.dstPad.id;
}, },
srcPad: this, srcPad: this,
@ -566,9 +653,9 @@ class Pad {
// run to completion // run to completion
// is it a group pad? -> delete the entry of this pad in the group // is it a group pad? -> delete the entry of this pad in the group
if (padID.indexOf('$') >= 0) { if (padID.indexOf("$") >= 0) {
// it is a group pad // it is a group pad
const groupID = padID.substring(0, padID.indexOf('$')); const groupID = padID.substring(0, padID.indexOf("$"));
const group = await db.get(`group:${groupID}`); const group = await db.get(`group:${groupID}`);
// remove the pad entry // remove the pad entry
@ -579,20 +666,26 @@ class Pad {
} }
// remove the readonly entries // remove the readonly entries
p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => { p.push(
readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => {
await db.remove(`readonly2pad:${readonlyID}`); await db.remove(`readonly2pad:${readonlyID}`);
})); }),
);
p.push(db.remove(`pad2readonly:${padID}`)); p.push(db.remove(`pad2readonly:${padID}`));
// delete all chat messages // delete all chat messages
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => { p.push(
promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
await this.db.remove(`pad:${this.id}:chat:${i}`, null); await this.db.remove(`pad:${this.id}:chat:${i}`, null);
})); }),
);
// delete all revisions // delete all revisions
p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => { p.push(
promises.timesLimit(this.head + 1, 500, async (i: string) => {
await this.db.remove(`pad:${this.id}:revs:${i}`, null); await this.db.remove(`pad:${this.id}:revs:${i}`, null);
})); }),
);
// remove pad from all authors who contributed // remove pad from all authors who contributed
this.getAllAuthors().forEach((authorId) => { this.getAllAuthors().forEach((authorId) => {
@ -601,13 +694,17 @@ class Pad {
// delete the pad entry and delete pad from padManager // delete the pad entry and delete pad from padManager
p.push(padManager.removePad(padID)); p.push(padManager.removePad(padID));
p.push(hooks.aCallAll('padRemove', { p.push(
hooks.aCallAll("padRemove", {
get padID() { get padID() {
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead'); warnDeprecated(
"padRemove padID context property is deprecated; use pad.id instead",
);
return this.pad.id; return this.pad.id;
}, },
pad: this, pad: this,
})); }),
);
await Promise.all(p); await Promise.all(p);
} }
@ -647,7 +744,7 @@ class Pad {
*/ */
async check() { async check() {
assert(this.id != null); assert(this.id != null);
assert.equal(typeof this.id, 'string'); assert.equal(typeof this.id, "string");
const head = this.getHeadRevisionNumber(); const head = this.getHeadRevisionNumber();
assert(head != null); assert(head != null);
@ -672,10 +769,10 @@ class Pad {
const savedRevisionsIds = new Set(); const savedRevisionsIds = new Set();
for (const savedRev of savedRevisions) { for (const savedRev of savedRevisions) {
assert(savedRev != null); assert(savedRev != null);
assert.equal(typeof savedRev, 'object'); assert.equal(typeof savedRev, "object");
assert(savedRevisionsList.includes(savedRev.revNum)); assert(savedRevisionsList.includes(savedRev.revNum));
assert(savedRev.id != null); assert(savedRev.id != null);
assert.equal(typeof savedRev.id, 'string'); assert.equal(typeof savedRev.id, "string");
assert(!savedRevisionsIds.has(savedRev.id)); assert(!savedRevisionsIds.has(savedRev.id));
savedRevisionsIds.add(savedRev.id); savedRevisionsIds.add(savedRev.id);
} }
@ -686,7 +783,7 @@ class Pad {
const authorIds = new Set(); const authorIds = new Set();
pool.eachAttrib((k, v) => { pool.eachAttrib((k, v) => {
if (k === 'author' && v) authorIds.add(v); if (k === "author" && v) authorIds.add(v);
}); });
const revs = Stream.range(0, head + 1) const revs = Stream.range(0, head + 1)
.map(async (r: number) => { .map(async (r: number) => {
@ -705,31 +802,42 @@ class Pad {
throw err; throw err;
} }
}) })
.batch(100).buffer(99); .batch(100)
let atext = Changeset.makeAText('\n'); .buffer(99);
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) { let atext = Changeset.makeAText("\n");
for await (const [
r,
changeset,
authorId,
timestamp,
isKeyRev,
keyAText,
] of revs) {
try { try {
assert(authorId != null); assert(authorId != null);
assert.equal(typeof authorId, 'string'); assert.equal(typeof authorId, "string");
if (authorId) authorIds.add(authorId); if (authorId) authorIds.add(authorId);
assert(timestamp != null); assert(timestamp != null);
assert.equal(typeof timestamp, 'number'); assert.equal(typeof timestamp, "number");
assert(timestamp > 0); assert(timestamp > 0);
assert(changeset != null); assert(changeset != null);
assert.equal(typeof changeset, 'string'); assert.equal(typeof changeset, "string");
Changeset.checkRep(changeset); Changeset.checkRep(changeset);
const unpacked = Changeset.unpack(changeset); const unpacked = Changeset.unpack(changeset);
let text = atext.text; let text = atext.text;
for (const op of Changeset.deserializeOps(unpacked.ops)) { for (const op of Changeset.deserializeOps(unpacked.ops)) {
if (['=', '-'].includes(op.opcode)) { if (["=", "-"].includes(op.opcode)) {
assert(text.length >= op.chars); assert(text.length >= op.chars);
const consumed = text.slice(0, op.chars); const consumed = text.slice(0, op.chars);
const nlines = (consumed.match(/\n/g) || []).length; const nlines = (consumed.match(/\n/g) || []).length;
assert.equal(op.lines, nlines); assert.equal(op.lines, nlines);
if (op.lines > 0) assert(consumed.endsWith('\n')); if (op.lines > 0) assert(consumed.endsWith("\n"));
text = text.slice(op.chars); text = text.slice(op.chars);
} }
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString()); assert.equal(
op.attribs,
AttributeMap.fromString(op.attribs, pool).toString(),
);
} }
atext = Changeset.applyToAText(changeset, atext, pool); atext = Changeset.applyToAText(changeset, atext, pool);
if (isKeyRev) assert.deepEqual(keyAText, atext); if (isKeyRev) assert.deepEqual(keyAText, atext);
@ -756,10 +864,11 @@ class Pad {
throw err; throw err;
} }
}) })
.batch(100).buffer(99); .batch(100)
.buffer(99);
for (const p of chats) await p; for (const p of chats) await p;
await hooks.aCallAll('padCheck', {pad: this}); await hooks.aCallAll("padCheck", { pad: this });
} }
} }
exports.Pad = Pad; exports.Pad = Pad;

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The Pad Manager is a Factory for pad Objects * The Pad Manager is a Factory for pad Objects
*/ */
@ -19,13 +18,13 @@
* limitations under the License. * limitations under the License.
*/ */
import {MapArrayType} from "../types/MapType"; import type { MapArrayType } from "../types/MapType";
import {PadType} from "../types/PadType"; import type { PadType } from "../types/PadType";
const CustomError = require('../utils/customError'); const CustomError = require("../utils/customError");
const Pad = require('../db/Pad'); const Pad = require("../db/Pad");
const db = require('./DB'); const db = require("./DB");
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
/** /**
* A cache of all loaded Pads. * A cache of all loaded Pads.
@ -39,12 +38,10 @@ const settings = require('../utils/Settings');
* that's defined somewhere more sensible. * that's defined somewhere more sensible.
*/ */
const globalPads: MapArrayType<any> = { const globalPads: MapArrayType<any> = {
get(name: string) get(name: string) {
{
return this[`:${name}`]; return this[`:${name}`];
}, },
set(name: string, value: any) set(name: string, value: any) {
{
this[`:${name}`] = value; this[`:${name}`] = value;
}, },
remove(name: string) { remove(name: string) {
@ -57,7 +54,7 @@ const globalPads:MapArrayType<any> = {
* *
* Updated without db access as new pads are created/old ones removed. * Updated without db access as new pads are created/old ones removed.
*/ */
const padList = new class { const padList = new (class {
private _cachedList: string[] | null; private _cachedList: string[] | null;
private _list: Set<string>; private _list: Set<string>;
private _loaded: Promise<void> | null; private _loaded: Promise<void> | null;
@ -74,9 +71,9 @@ const padList = new class {
async getPads() { async getPads() {
if (!this._loaded) { if (!this._loaded) {
this._loaded = (async () => { this._loaded = (async () => {
const dbData = await db.findKeys('pad:*', '*:*:*'); const dbData = await db.findKeys("pad:*", "*:*:*");
if (dbData == null) return; if (dbData == null) return;
for (const val of dbData) this.addPad(val.replace(/^pad:/, '')); for (const val of dbData) this.addPad(val.replace(/^pad:/, ""));
})(); })();
} }
await this._loaded; await this._loaded;
@ -95,7 +92,7 @@ const padList = new class {
this._list.delete(name); this._list.delete(name);
this._cachedList = null; this._cachedList = null;
} }
}(); })();
// initialises the all-knowing data structure // initialises the all-knowing data structure
@ -106,22 +103,26 @@ const padList = new class {
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if * @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
* applicable). * applicable).
*/ */
exports.getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise<PadType> => { exports.getPad = async (
id: string,
text?: string | null,
authorId: string | null = "",
): Promise<PadType> => {
// check if this is a valid padId // check if this is a valid padId
if (!exports.isValidPadId(id)) { if (!exports.isValidPadId(id)) {
throw new CustomError(`${id} is not a valid padId`, 'apierror'); throw new CustomError(`${id} is not a valid padId`, "apierror");
} }
// check if this is a valid text // check if this is a valid text
if (text != null) { if (text != null) {
// check if text is a string // check if text is a string
if (typeof text !== 'string') { if (typeof text !== "string") {
throw new CustomError('text is not a string', 'apierror'); throw new CustomError("text is not a string", "apierror");
} }
// check if text is less than 100k chars // check if text is less than 100k chars
if (text.length > 100000) { if (text.length > 100000) {
throw new CustomError('text must be less than 100k chars', 'apierror'); throw new CustomError("text must be less than 100k chars", "apierror");
} }
} }
@ -149,14 +150,11 @@ exports.listAllPads = async () => {
return { padIDs }; return { padIDs };
}; };
// checks if a pad exists // checks if a pad exists
exports.doesPadExist = async (padId: string) => { exports.doesPadExist = async (padId: string) => {
const value = await db.get(`pad:${padId}`); const value = await db.get(`pad:${padId}`);
return (value != null && value.atext); return value != null && value.atext;
}; };
// alias for backwards compatibility // alias for backwards compatibility
@ -167,8 +165,8 @@ exports.doesPadExists = exports.doesPadExist;
* time, and allow us to "play back" these changes so legacy padIds can be found. * time, and allow us to "play back" these changes so legacy padIds can be found.
*/ */
const padIdTransforms = [ const padIdTransforms = [
[/\s+/g, '_'], [/\s+/g, "_"],
[/:+/g, '_'], [/:+/g, "_"],
]; ];
// returns a sanitized padId, respecting legacy pad id formats // returns a sanitized padId, respecting legacy pad id formats
@ -192,7 +190,8 @@ exports.sanitizePadId = async (padId: string) => {
return padId; return padId;
}; };
exports.isValidPadId = (padId: string) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); exports.isValidPadId = (padId: string) =>
/^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
/** /**
* Removes the pad from database and unloads it. * Removes the pad from database and unloads it.

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The ReadOnlyManager manages the database and rendering releated to read only pads * The ReadOnlyManager manages the database and rendering releated to read only pads
*/ */
@ -19,17 +18,15 @@
* limitations under the License. * limitations under the License.
*/ */
const db = require("./DB");
const db = require('./DB'); const randomString = require("../utils/randomstring");
const randomString = require('../utils/randomstring');
/** /**
* checks if the id pattern matches a read-only pad id * checks if the id pattern matches a read-only pad id
* @param {String} id the pad's id * @param {String} id the pad's id
* @return {Boolean} true if the id is readonly * @return {Boolean} true if the id is readonly
*/ */
exports.isReadOnlyId = (id:string) => id.startsWith('r.'); exports.isReadOnlyId = (id: string) => id.startsWith("r.");
/** /**
* returns a read only id for a pad * returns a read only id for a pad
@ -57,7 +54,8 @@ exports.getReadOnlyId = async (padId:string) => {
* @param {String} readOnlyId read only id * @param {String} readOnlyId read only id
* @return {String} the padId * @return {String} the padId
*/ */
exports.getPadId = async (readOnlyId:string) => await db.get(`readonly2pad:${readOnlyId}`); exports.getPadId = async (readOnlyId: string) =>
await db.get(`readonly2pad:${readOnlyId}`);
/** /**
* returns the padId and readonlyPadId in an object for any id * returns the padId and readonlyPadId in an object for any id

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* Controls the security of pad access * Controls the security of pad access
*/ */
@ -19,20 +18,20 @@
* limitations under the License. * limitations under the License.
*/ */
import {UserSettingsObject} from "../types/UserSettingsObject"; import type { UserSettingsObject } from "../types/UserSettingsObject";
const authorManager = require('./AuthorManager'); const authorManager = require("./AuthorManager");
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require("../../static/js/pluginfw/hooks.js");
const padManager = require('./PadManager'); const padManager = require("./PadManager");
const readOnlyManager = require('./ReadOnlyManager'); const readOnlyManager = require("./ReadOnlyManager");
const sessionManager = require('./SessionManager'); const sessionManager = require("./SessionManager");
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
const webaccess = require('../hooks/express/webaccess'); const webaccess = require("../hooks/express/webaccess");
const log4js = require('log4js'); const log4js = require("log4js");
const authLogger = log4js.getLogger('auth'); const authLogger = log4js.getLogger("auth");
const {padutils} = require('../../static/js/pad_utils'); const { padutils } = require("../../static/js/pad_utils");
const DENY = Object.freeze({accessStatus: 'deny'}); const DENY = Object.freeze({ accessStatus: "deny" });
/** /**
* Determines whether the user can access a pad. * Determines whether the user can access a pad.
@ -57,9 +56,14 @@ const DENY = Object.freeze({accessStatus: 'deny'});
* @param {Object} userSettings * @param {Object} userSettings
* @return {DENY|{accessStatus: String, authorID: String}} * @return {DENY|{accessStatus: String, authorID: String}}
*/ */
exports.checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject) => { exports.checkAccess = async (
padID: string,
sessionCookie: string,
token: string,
userSettings: UserSettingsObject,
) => {
if (!padID) { if (!padID) {
authLogger.debug('access denied: missing padID'); authLogger.debug("access denied: missing padID");
return DENY; return DENY;
} }
@ -69,7 +73,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
canCreate = false; canCreate = false;
padID = await readOnlyManager.getPadId(padID); padID = await readOnlyManager.getPadId(padID);
if (padID == null) { if (padID == null) {
authLogger.debug('access denied: read-only pad ID for a pad that does not exist'); authLogger.debug(
"access denied: read-only pad ID for a pad that does not exist",
);
return DENY; return DENY;
} }
} }
@ -77,62 +83,82 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
// Authentication and authorization checks. // Authentication and authorization checks.
if (settings.loadTest) { if (settings.loadTest) {
console.warn( console.warn(
'bypassing socket.io authentication and authorization checks due to settings.loadTest'); "bypassing socket.io authentication and authorization checks due to settings.loadTest",
);
} else if (settings.requireAuthentication) { } else if (settings.requireAuthentication) {
if (userSettings == null) { if (userSettings == null) {
authLogger.debug('access denied: authentication is required'); authLogger.debug("access denied: authentication is required");
return DENY; return DENY;
} }
if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false; if (userSettings.canCreate != null && !userSettings.canCreate)
canCreate = false;
if (userSettings.readOnly) canCreate = false; if (userSettings.readOnly) canCreate = false;
// Note: userSettings.padAuthorizations should still be populated even if // Note: userSettings.padAuthorizations should still be populated even if
// settings.requireAuthorization is false. // settings.requireAuthorization is false.
const padAuthzs = userSettings.padAuthorizations || {}; const padAuthzs = userSettings.padAuthorizations || {};
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]); const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
if (!level) { if (!level) {
authLogger.debug('access denied: unauthorized'); authLogger.debug("access denied: unauthorized");
return DENY; return DENY;
} }
if (level !== 'create') canCreate = false; if (level !== "create") canCreate = false;
} }
// allow plugins to deny access // allow plugins to deny access
const isFalse = (x: boolean) => x === false; const isFalse = (x: boolean) => x === false;
if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) { if (
authLogger.debug('access denied: an onAccessCheck hook function returned false'); hooks
.callAll("onAccessCheck", { padID, token, sessionCookie })
.some(isFalse)
) {
authLogger.debug(
"access denied: an onAccessCheck hook function returned false",
);
return DENY; return DENY;
} }
const padExists = await padManager.doesPadExist(padID); const padExists = await padManager.doesPadExist(padID);
if (!padExists && !canCreate) { if (!padExists && !canCreate) {
authLogger.debug('access denied: user attempted to create a pad, which is prohibited'); authLogger.debug(
"access denied: user attempted to create a pad, which is prohibited",
);
return DENY; return DENY;
} }
const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie); const sessionAuthorID = await sessionManager.findAuthorID(
padID.split("$")[0],
sessionCookie,
);
if (settings.requireSession && !sessionAuthorID) { if (settings.requireSession && !sessionAuthorID) {
authLogger.debug('access denied: HTTP API session is required'); authLogger.debug("access denied: HTTP API session is required");
return DENY; return DENY;
} }
if (!sessionAuthorID && token != null && !padutils.isValidAuthorToken(token)) { if (
!sessionAuthorID &&
token != null &&
!padutils.isValidAuthorToken(token)
) {
// The author token should be kept secret, so do not log it. // The author token should be kept secret, so do not log it.
authLogger.debug('access denied: invalid author token'); authLogger.debug("access denied: invalid author token");
return DENY; return DENY;
} }
const grant = { const grant = {
accessStatus: 'grant', accessStatus: "grant",
authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings), authorID:
sessionAuthorID || (await authorManager.getAuthorId(token, userSettings)),
}; };
if (!padID.includes('$')) { if (!padID.includes("$")) {
// Only group pads can be private, so there is nothing more to check for this non-group pad. // Only group pads can be private, so there is nothing more to check for this non-group pad.
return grant; return grant;
} }
if (!padExists) { if (!padExists) {
if (sessionAuthorID == null) { if (sessionAuthorID == null) {
authLogger.debug('access denied: must have an HTTP API session to create a group pad'); authLogger.debug(
"access denied: must have an HTTP API session to create a group pad",
);
return DENY; return DENY;
} }
// Creating a group pad, so there is no public status to check. // Creating a group pad, so there is no public status to check.
@ -142,7 +168,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
const pad = await padManager.getPad(padID); const pad = await padManager.getPad(padID);
if (!pad.getPublicStatus() && sessionAuthorID == null) { if (!pad.getPublicStatus() && sessionAuthorID == null) {
authLogger.debug('access denied: must have an HTTP API session to access private group pads'); authLogger.debug(
"access denied: must have an HTTP API session to access private group pads",
);
return DENY; return DENY;
} }

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The Session Manager provides functions to manage session in the database, * The Session Manager provides functions to manage session in the database,
* it only provides session management for sessions created by the API * it only provides session management for sessions created by the API
@ -20,12 +19,12 @@
* limitations under the License. * limitations under the License.
*/ */
const CustomError = require('../utils/customError'); const CustomError = require("../utils/customError");
const promises = require('../utils/promises'); const promises = require("../utils/promises");
const randomString = require('../utils/randomstring'); const randomString = require("../utils/randomstring");
const db = require('./DB'); const db = require("./DB");
const groupManager = require('./GroupManager'); const groupManager = require("./GroupManager");
const authorManager = require('./AuthorManager'); const authorManager = require("./AuthorManager");
/** /**
* Finds the author ID for a session with matching ID and group. * Finds the author ID for a session with matching ID and group.
@ -61,13 +60,15 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
* Fixes #3819. * Fixes #3819.
* Also, see #3820. * Also, see #3820.
*/ */
const sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(','); const sessionIDs = sessionCookie.replace(/^"|"$/g, "").split(",");
const sessionInfoPromises = sessionIDs.map(async (id) => { const sessionInfoPromises = sessionIDs.map(async (id) => {
try { try {
return await exports.getSessionInfo(id); return await exports.getSessionInfo(id);
} catch (err: any) { } catch (err: any) {
if (err.message === 'sessionID does not exist') { if (err.message === "sessionID does not exist") {
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`); console.debug(
`SessionManager getAuthorID: no session exists with ID ${id}`,
);
} else { } else {
throw err; throw err;
} }
@ -75,11 +76,16 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
return undefined; return undefined;
}); });
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const isMatch = (si: { const isMatch = (
si: {
groupID: string; groupID: string;
validUntil: number; validUntil: number;
}|null) => (si != null && si.groupID === groupID && now < si.validUntil); } | null,
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch); ) => si != null && si.groupID === groupID && now < si.validUntil;
const sessionInfo = await promises.firstSatisfies(
sessionInfoPromises,
isMatch,
);
if (sessionInfo == null) return undefined; if (sessionInfo == null) return undefined;
return sessionInfo.authorID; return sessionInfo.authorID;
}; };
@ -92,7 +98,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
exports.doesSessionExist = async (sessionID: string) => { exports.doesSessionExist = async (sessionID: string) => {
// check if the database entry of this session exists // check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
return (session != null); return session != null;
}; };
/** /**
@ -102,42 +108,46 @@ exports.doesSessionExist = async (sessionID: string) => {
* @param {Number} validUntil The unix timestamp when the session should expire * @param {Number} validUntil The unix timestamp when the session should expire
* @return {Promise<{sessionID: string}>} the id of the new session * @return {Promise<{sessionID: string}>} the id of the new session
*/ */
exports.createSession = async (groupID: string, authorID: string, validUntil: number) => { exports.createSession = async (
groupID: string,
authorID: string,
validUntil: number,
) => {
// check if the group exists // check if the group exists
const groupExists = await groupManager.doesGroupExist(groupID); const groupExists = await groupManager.doesGroupExist(groupID);
if (!groupExists) { if (!groupExists) {
throw new CustomError('groupID does not exist', 'apierror'); throw new CustomError("groupID does not exist", "apierror");
} }
// check if the author exists // check if the author exists
const authorExists = await authorManager.doesAuthorExist(authorID); const authorExists = await authorManager.doesAuthorExist(authorID);
if (!authorExists) { if (!authorExists) {
throw new CustomError('authorID does not exist', 'apierror'); throw new CustomError("authorID does not exist", "apierror");
} }
// try to parse validUntil if it's not a number // try to parse validUntil if it's not a number
if (typeof validUntil !== 'number') { if (typeof validUntil !== "number") {
validUntil = parseInt(validUntil); validUntil = Number.parseInt(validUntil);
} }
// check it's a valid number // check it's a valid number
if (isNaN(validUntil)) { if (isNaN(validUntil)) {
throw new CustomError('validUntil is not a number', 'apierror'); throw new CustomError("validUntil is not a number", "apierror");
} }
// ensure this is not a negative number // ensure this is not a negative number
if (validUntil < 0) { if (validUntil < 0) {
throw new CustomError('validUntil is a negative number', 'apierror'); throw new CustomError("validUntil is a negative number", "apierror");
} }
// ensure this is not a float value // ensure this is not a float value
if (!isInt(validUntil)) { if (!isInt(validUntil)) {
throw new CustomError('validUntil is a float value', 'apierror'); throw new CustomError("validUntil is a float value", "apierror");
} }
// check if validUntil is in the future // check if validUntil is in the future
if (validUntil < Math.floor(Date.now() / 1000)) { if (validUntil < Math.floor(Date.now() / 1000)) {
throw new CustomError('validUntil is in the past', 'apierror'); throw new CustomError("validUntil is in the past", "apierror");
} }
// generate sessionID // generate sessionID
@ -151,8 +161,8 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu
await Promise.all([ await Promise.all([
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
// property, and writes the result. // property, and writes the result.
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1), db.setSub(`group2sessions:${groupID}`, ["sessionIDs", sessionID], 1),
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1), db.setSub(`author2sessions:${authorID}`, ["sessionIDs", sessionID], 1),
]); ]);
return { sessionID }; return { sessionID };
@ -169,7 +179,7 @@ exports.getSessionInfo = async (sessionID:string) => {
if (session == null) { if (session == null) {
// session does not exist // session does not exist
throw new CustomError('sessionID does not exist', 'apierror'); throw new CustomError("sessionID does not exist", "apierror");
} }
// everything is fine, return the sessioninfos // everything is fine, return the sessioninfos
@ -185,7 +195,7 @@ exports.deleteSession = async (sessionID:string) => {
// ensure that the session exists // ensure that the session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
if (session == null) { if (session == null) {
throw new CustomError('sessionID does not exist', 'apierror'); throw new CustomError("sessionID does not exist", "apierror");
} }
// everything is fine, use the sessioninfos // everything is fine, use the sessioninfos
@ -196,8 +206,16 @@ exports.deleteSession = async (sessionID:string) => {
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
// property, and writes the result. Setting a property to `undefined` deletes that property // property, and writes the result. Setting a property to `undefined` deletes that property
// (JSON.stringify() ignores such properties). // (JSON.stringify() ignores such properties).
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined), db.setSub(
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined), `group2sessions:${groupID}`,
["sessionIDs", sessionID],
undefined,
),
db.setSub(
`author2sessions:${authorID}`,
["sessionIDs", sessionID],
undefined,
),
]); ]);
// Delete the session record after updating group2sessions and author2sessions so that the state // Delete the session record after updating group2sessions and author2sessions so that the state
@ -214,7 +232,7 @@ exports.listSessionsOfGroup = async (groupID: string) => {
// check that the group exists // check that the group exists
const exists = await groupManager.doesGroupExist(groupID); const exists = await groupManager.doesGroupExist(groupID);
if (!exists) { if (!exists) {
throw new CustomError('groupID does not exist', 'apierror'); throw new CustomError("groupID does not exist", "apierror");
} }
const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`); const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`);
@ -230,7 +248,7 @@ exports.listSessionsOfAuthor = async (authorID: string) => {
// check that the author exists // check that the author exists
const exists = await authorManager.doesAuthorExist(authorID); const exists = await authorManager.doesAuthorExist(authorID);
if (!exists) { if (!exists) {
throw new CustomError('authorID does not exist', 'apierror'); throw new CustomError("authorID does not exist", "apierror");
} }
return await listSessionsWithDBKey(`author2sessions:${authorID}`); return await listSessionsWithDBKey(`author2sessions:${authorID}`);
@ -253,7 +271,7 @@ const listSessionsWithDBKey = async (dbkey: string) => {
try { try {
sessions[sessionID] = await exports.getSessionInfo(sessionID); sessions[sessionID] = await exports.getSessionInfo(sessionID);
} catch (err: any) { } catch (err: any) {
if (err.name === 'apierror') { if (err.name === "apierror") {
console.warn(`Found bad session ${sessionID} in ${dbkey}`); console.warn(`Found bad session ${sessionID} in ${dbkey}`);
sessions[sessionID] = null; sessions[sessionID] = null;
} else { } else {
@ -265,11 +283,11 @@ const listSessionsWithDBKey = async (dbkey: string) => {
return sessions; return sessions;
}; };
/** /**
* checks if a number is an int * checks if a number is an int
* @param {number|string} value * @param {number|string} value
* @return {boolean} If the value is an integer * @return {boolean} If the value is an integer
*/ */
// @ts-ignore // @ts-ignore
const isInt = (value:number|string): boolean => (parseFloat(value) === parseInt(value)) && !isNaN(value); const isInt = (value: number | string): boolean =>
Number.parseFloat(value) === Number.parseInt(value) && !isNaN(value);

View file

@ -1,11 +1,9 @@
'use strict'; const DB = require("./DB");
const Store = require("express-session").Store;
const log4js = require("log4js");
const util = require("util");
const DB = require('./DB'); const logger = log4js.getLogger("SessionStore");
const Store = require('express-session').Store;
const log4js = require('log4js');
const util = require('util');
const logger = log4js.getLogger('SessionStore');
class SessionStore extends Store { class SessionStore extends Store {
/** /**
@ -38,7 +36,9 @@ class SessionStore extends Store {
const exp = this._expirations.get(sid) || {}; const exp = this._expirations.get(sid) || {};
clearTimeout(exp.timeout); clearTimeout(exp.timeout);
// @ts-ignore // @ts-ignore
const {cookie: {expires} = {}} = sess || {}; const {
cookie: { expires } = {},
} = sess || {};
if (expires) { if (expires) {
const sessExp = new Date(expires).getTime(); const sessExp = new Date(expires).getTime();
if (updateDbExp) exp.db = sessExp; if (updateDbExp) exp.db = sessExp;
@ -47,7 +47,8 @@ class SessionStore extends Store {
if (exp.real <= now) return await this._destroy(sid); if (exp.real <= now) return await this._destroy(sid);
// If reading from the database, update the expiration with the latest value from touch() so // If reading from the database, update the expiration with the latest value from touch() so
// that touch() appears to write to the database every time even though it doesn't. // that touch() appears to write to the database every time even though it doesn't.
if (typeof expires === 'string') sess.cookie.expires = new Date(exp.real).toJSON(); if (typeof expires === "string")
sess.cookie.expires = new Date(exp.real).toJSON();
// Use this._get(), not this._destroy(), to destroy the DB record for the expired session. // Use this._get(), not this._destroy(), to destroy the DB record for the expired session.
// This is done in case multiple Etherpad instances are sharing the same database and users // This is done in case multiple Etherpad instances are sharing the same database and users
// are bouncing between the instances. By using this._get(), this instance will query the DB // are bouncing between the instances. By using this._get(), this instance will query the DB
@ -99,7 +100,11 @@ class SessionStore extends Store {
// database if it didn't already exist, but we have no way of knowing that without querying the // database if it didn't already exist, but we have no way of knowing that without querying the
// database. The query overhead is not worth it because set() should be called soon anyway. // database. The query overhead is not worth it because set() should be called soon anyway.
if (exp == null) return; if (exp == null) return;
if (exp.db != null && (this._refresh == null || exp.real < exp.db + this._refresh)) return; if (
exp.db != null &&
(this._refresh == null || exp.real < exp.db + this._refresh)
)
return;
await this._write(sid, sess); await this._write(sid, sess);
exp.db = new Date(sess.cookie.expires).getTime(); exp.db = new Date(sess.cookie.expires).getTime();
} }
@ -107,7 +112,7 @@ class SessionStore extends Store {
// express-session doesn't support Promise-based methods. This is where the callbackified versions // express-session doesn't support Promise-based methods. This is where the callbackified versions
// used by express-session are defined. // used by express-session are defined.
for (const m of ['get', 'set', 'destroy', 'touch']) { for (const m of ["get", "set", "destroy", "touch"]) {
SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]); SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]);
} }

View file

@ -1,4 +1,3 @@
'use strict';
/* /*
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no> * Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
* *
@ -20,13 +19,13 @@
* require("./index").require("./path/to/template.ejs") * require("./index").require("./path/to/template.ejs")
*/ */
const ejs = require('ejs'); const ejs = require("ejs");
const fs = require('fs'); const fs = require("fs");
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require("../../static/js/pluginfw/hooks.js");
const path = require('path'); const path = require("path");
const resolve = require('resolve'); const resolve = require("resolve");
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
import {pluginInstallPath} from '../../static/js/pluginfw/installer' import { pluginInstallPath } from "../../static/js/pluginfw/installer";
const templateCache = new Map(); const templateCache = new Map();
@ -37,7 +36,8 @@ exports.info = {
args: [], args: [],
}; };
const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1]; const getCurrentFile = () =>
exports.info.file_stack[exports.info.file_stack.length - 1];
exports._init = (b: any, recursive: boolean) => { exports._init = (b: any, recursive: boolean) => {
exports.info.__output_stack.push(exports.info.__output); exports.info.__output_stack.push(exports.info.__output);
@ -51,7 +51,7 @@ exports._exit = (b:any, recursive:boolean) => {
exports.begin_block = (name: string) => { exports.begin_block = (name: string) => {
exports.info.block_stack.push(name); exports.info.block_stack.push(name);
exports.info.__output_stack.push(exports.info.__output.get()); exports.info.__output_stack.push(exports.info.__output.get());
exports.info.__output.set(''); exports.info.__output.set("");
}; };
exports.end_block = () => { exports.end_block = () => {
@ -64,13 +64,17 @@ exports.end_block = () => {
exports.info.__output.set(exports.info.__output.get().concat(args.content)); exports.info.__output.set(exports.info.__output.get().concat(args.content));
}; };
exports.require = (name:string, args:{ exports.require = (
e?: Function, name: string,
require?: Function, args: {
}, mod:{ e?: Function;
filename:string, require?: Function;
paths:string[], },
}) => { mod: {
filename: string;
paths: string[];
},
) => {
if (args == null) args = {}; if (args == null) args = {};
let basedir = __dirname; let basedir = __dirname;
@ -88,19 +92,26 @@ exports.require = (name:string, args:{
* Add the plugin install path to the paths array * Add the plugin install path to the paths array
*/ */
if (!paths.includes(pluginInstallPath)) { if (!paths.includes(pluginInstallPath)) {
paths.push(pluginInstallPath) paths.push(pluginInstallPath);
} }
const ejspath = resolve.sync(name, {paths, basedir, extensions: ['.html', '.ejs']}); const ejspath = resolve.sync(name, {
paths,
basedir,
extensions: [".html", ".ejs"],
});
args.e = exports; args.e = exports;
args.require = require; args.require = require;
const cache = settings.maxAge !== 0; const cache = settings.maxAge !== 0;
const template = cache && templateCache.get(ejspath) || ejs.compile( const template =
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' + (cache && templateCache.get(ejspath)) ||
ejs.compile(
"<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>" +
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`, `${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
{filename: ejspath}); { filename: ejspath },
);
if (cache) templateCache.set(ejspath, template); if (cache) templateCache.set(ejspath, template);
exports.info.args.push(args); exports.info.args.push(args);

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The API Handler handles all API http requests * The API Handler handles all API http requests
*/ */
@ -19,140 +18,139 @@
* limitations under the License. * limitations under the License.
*/ */
import {MapArrayType} from "../types/MapType"; import type { MapArrayType } from "../types/MapType";
const api = require('../db/API'); const api = require("../db/API");
const padManager = require('../db/PadManager'); const padManager = require("../db/PadManager");
import createHTTPError from 'http-errors'; import type { Http2ServerRequest, Http2ServerResponse } from "node:http2";
import {Http2ServerRequest, Http2ServerResponse} from "node:http2"; import createHTTPError from "http-errors";
import {publicKeyExported} from "../security/OAuth2Provider";
import { jwtVerify } from "jose"; import { jwtVerify } from "jose";
import { publicKeyExported } from "../security/OAuth2Provider";
// a list of all functions // a list of all functions
const version: MapArrayType<any> = {}; const version: MapArrayType<any> = {};
version['1'] = { version["1"] = {
createGroup: [], createGroup: [],
createGroupIfNotExistsFor: ['groupMapper'], createGroupIfNotExistsFor: ["groupMapper"],
deleteGroup: ['groupID'], deleteGroup: ["groupID"],
listPads: ['groupID'], listPads: ["groupID"],
createPad: ['padID', 'text'], createPad: ["padID", "text"],
createGroupPad: ['groupID', 'padName', 'text'], createGroupPad: ["groupID", "padName", "text"],
createAuthor: ['name'], createAuthor: ["name"],
createAuthorIfNotExistsFor: ['authorMapper', 'name'], createAuthorIfNotExistsFor: ["authorMapper", "name"],
listPadsOfAuthor: ['authorID'], listPadsOfAuthor: ["authorID"],
createSession: ['groupID', 'authorID', 'validUntil'], createSession: ["groupID", "authorID", "validUntil"],
deleteSession: ['sessionID'], deleteSession: ["sessionID"],
getSessionInfo: ['sessionID'], getSessionInfo: ["sessionID"],
listSessionsOfGroup: ['groupID'], listSessionsOfGroup: ["groupID"],
listSessionsOfAuthor: ['authorID'], listSessionsOfAuthor: ["authorID"],
getText: ['padID', 'rev'], getText: ["padID", "rev"],
setText: ['padID', 'text'], setText: ["padID", "text"],
getHTML: ['padID', 'rev'], getHTML: ["padID", "rev"],
setHTML: ['padID', 'html'], setHTML: ["padID", "html"],
getRevisionsCount: ['padID'], getRevisionsCount: ["padID"],
getLastEdited: ['padID'], getLastEdited: ["padID"],
deletePad: ['padID'], deletePad: ["padID"],
getReadOnlyID: ['padID'], getReadOnlyID: ["padID"],
setPublicStatus: ['padID', 'publicStatus'], setPublicStatus: ["padID", "publicStatus"],
getPublicStatus: ['padID'], getPublicStatus: ["padID"],
listAuthorsOfPad: ['padID'], listAuthorsOfPad: ["padID"],
padUsersCount: ['padID'], padUsersCount: ["padID"],
}; };
version['1.1'] = { version["1.1"] = {
...version['1'], ...version["1"],
getAuthorName: ['authorID'], getAuthorName: ["authorID"],
padUsers: ['padID'], padUsers: ["padID"],
sendClientsMessage: ['padID', 'msg'], sendClientsMessage: ["padID", "msg"],
listAllGroups: [], listAllGroups: [],
}; };
version['1.2'] = { version["1.2"] = {
...version['1.1'], ...version["1.1"],
checkToken: [], checkToken: [],
}; };
version['1.2.1'] = { version["1.2.1"] = {
...version['1.2'], ...version["1.2"],
listAllPads: [], listAllPads: [],
}; };
version['1.2.7'] = { version["1.2.7"] = {
...version['1.2.1'], ...version["1.2.1"],
createDiffHTML: ['padID', 'startRev', 'endRev'], createDiffHTML: ["padID", "startRev", "endRev"],
getChatHistory: ['padID', 'start', 'end'], getChatHistory: ["padID", "start", "end"],
getChatHead: ['padID'], getChatHead: ["padID"],
}; };
version['1.2.8'] = { version["1.2.8"] = {
...version['1.2.7'], ...version["1.2.7"],
getAttributePool: ['padID'], getAttributePool: ["padID"],
getRevisionChangeset: ['padID', 'rev'], getRevisionChangeset: ["padID", "rev"],
}; };
version['1.2.9'] = { version["1.2.9"] = {
...version['1.2.8'], ...version["1.2.8"],
copyPad: ['sourceID', 'destinationID', 'force'], copyPad: ["sourceID", "destinationID", "force"],
movePad: ['sourceID', 'destinationID', 'force'], movePad: ["sourceID", "destinationID", "force"],
}; };
version['1.2.10'] = { version["1.2.10"] = {
...version['1.2.9'], ...version["1.2.9"],
getPadID: ['roID'], getPadID: ["roID"],
}; };
version['1.2.11'] = { version["1.2.11"] = {
...version['1.2.10'], ...version["1.2.10"],
getSavedRevisionsCount: ['padID'], getSavedRevisionsCount: ["padID"],
listSavedRevisions: ['padID'], listSavedRevisions: ["padID"],
saveRevision: ['padID', 'rev'], saveRevision: ["padID", "rev"],
restoreRevision: ['padID', 'rev'], restoreRevision: ["padID", "rev"],
}; };
version['1.2.12'] = { version["1.2.12"] = {
...version['1.2.11'], ...version["1.2.11"],
appendChatMessage: ['padID', 'text', 'authorID', 'time'], appendChatMessage: ["padID", "text", "authorID", "time"],
}; };
version['1.2.13'] = { version["1.2.13"] = {
...version['1.2.12'], ...version["1.2.12"],
appendText: ['padID', 'text'], appendText: ["padID", "text"],
}; };
version['1.2.14'] = { version["1.2.14"] = {
...version['1.2.13'], ...version["1.2.13"],
getStats: [], getStats: [],
}; };
version['1.2.15'] = { version["1.2.15"] = {
...version['1.2.14'], ...version["1.2.14"],
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force'], copyPadWithoutHistory: ["sourceID", "destinationID", "force"],
}; };
version['1.3.0'] = { version["1.3.0"] = {
...version['1.2.15'], ...version["1.2.15"],
appendText: ['padID', 'text', 'authorId'], appendText: ["padID", "text", "authorId"],
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId'], copyPadWithoutHistory: ["sourceID", "destinationID", "force", "authorId"],
createGroupPad: ['groupID', 'padName', 'text', 'authorId'], createGroupPad: ["groupID", "padName", "text", "authorId"],
createPad: ['padID', 'text', 'authorId'], createPad: ["padID", "text", "authorId"],
restoreRevision: ['padID', 'rev', 'authorId'], restoreRevision: ["padID", "rev", "authorId"],
setHTML: ['padID', 'html', 'authorId'], setHTML: ["padID", "html", "authorId"],
setText: ['padID', 'text', 'authorId'], setText: ["padID", "text", "authorId"],
}; };
// set the latest available API version here // set the latest available API version here
exports.latestApiVersion = '1.3.0'; exports.latestApiVersion = "1.3.0";
// exports the versions so it can be used by the new Swagger endpoint // exports the versions so it can be used by the new Swagger endpoint
exports.version = version; exports.version = version;
type APIFields = { type APIFields = {
api_key: string; api_key: string;
padID: string; padID: string;
padName: string; padName: string;
} };
/** /**
* Handles an HTTP API call * Handles an HTTP API call
@ -162,31 +160,37 @@ type APIFields = {
* @param req express request object * @param req express request object
* @param res express response object * @param res express response object
*/ */
exports.handle = async function (apiVersion: string, functionName: string, fields: APIFields, req: Http2ServerRequest, res: Http2ServerResponse) { exports.handle = async function (
apiVersion: string,
functionName: string,
fields: APIFields,
req: Http2ServerRequest,
res: Http2ServerResponse,
) {
// say goodbye if this is an unknown API version // say goodbye if this is an unknown API version
if (!(apiVersion in version)) { if (!(apiVersion in version)) {
throw new createHTTPError.NotFound('no such api version'); throw new createHTTPError.NotFound("no such api version");
} }
// say goodbye if this is an unknown function // say goodbye if this is an unknown function
if (!(functionName in version[apiVersion])) { if (!(functionName in version[apiVersion])) {
throw new createHTTPError.NotFound('no such function'); throw new createHTTPError.NotFound("no such function");
} }
if (!req.headers.authorization) { if (!req.headers.authorization) {
throw new createHTTPError.Unauthorized('no or wrong API Key'); throw new createHTTPError.Unauthorized("no or wrong API Key");
} }
try { try {
await jwtVerify(req.headers.authorization!.replace("Bearer ", ""), publicKeyExported!, {algorithms: ['RS256'], await jwtVerify(
requiredClaims: ["admin"]}) req.headers.authorization!.replace("Bearer ", ""),
publicKeyExported!,
{ algorithms: ["RS256"], requiredClaims: ["admin"] },
);
} catch (e) { } catch (e) {
throw new createHTTPError.Unauthorized('no or wrong API Key'); throw new createHTTPError.Unauthorized("no or wrong API Key");
} }
// sanitize any padIDs before continuing // sanitize any padIDs before continuing
if (fields.padID) { if (fields.padID) {
fields.padID = await padManager.sanitizePadId(fields.padID); fields.padID = await padManager.sanitizePadId(fields.padID);
@ -200,7 +204,9 @@ exports.handle = async function (apiVersion: string, functionName: string, field
// put the function parameters in an array // put the function parameters in an array
// @ts-ignore // @ts-ignore
const functionParams = version[apiVersion][functionName].map((field) => fields[field]); const functionParams = version[apiVersion][functionName].map(
(field) => fields[field],
);
// call the api function // call the api function
return api[functionName].apply(this, functionParams); return api[functionName].apply(this, functionParams);

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* Handles the export requests * Handles the export requests
*/ */
@ -20,15 +19,15 @@
* limitations under the License. * limitations under the License.
*/ */
const exporthtml = require('../utils/ExportHtml'); const exporthtml = require("../utils/ExportHtml");
const exporttxt = require('../utils/ExportTxt'); const exporttxt = require("../utils/ExportTxt");
const exportEtherpad = require('../utils/ExportEtherpad'); const exportEtherpad = require("../utils/ExportEtherpad");
import fs from 'fs'; import fs from "fs";
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
import os from 'os'; import os from "os";
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require("../../static/js/pluginfw/hooks");
import util from 'util'; import util from "util";
const { checkValidRev } = require('../utils/checkValidRev'); const { checkValidRev } = require("../utils/checkValidRev");
const fsp_writeFile = util.promisify(fs.writeFile); const fsp_writeFile = util.promisify(fs.writeFile);
const fsp_unlink = util.promisify(fs.unlink); const fsp_unlink = util.promisify(fs.unlink);
@ -43,12 +42,18 @@ const tempDirectory = os.tmpdir();
* @param {String} readOnlyId the read only id of the pad to export * @param {String} readOnlyId the read only id of the pad to export
* @param {String} type the type to export * @param {String} type the type to export
*/ */
exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string, type:string) => { exports.doExport = async (
req: any,
res: any,
padId: string,
readOnlyId: string,
type: string,
) => {
// avoid naming the read-only file as the original pad's id // avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId; let fileName = readOnlyId ? readOnlyId : padId;
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons // allow fileName to be overwritten by a hook, the type type is kept static for security reasons
const hookFileName = await hooks.aCallFirst('exportFileName', padId); const hookFileName = await hooks.aCallFirst("exportFileName", padId);
// if fileName is set then set it to the padId, note that fileName is returned as an array. // if fileName is set then set it to the padId, note that fileName is returned as an array.
if (hookFileName.length) { if (hookFileName.length) {
@ -66,29 +71,33 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
// if this is a plain text export, we can do this directly // if this is a plain text export, we can do this directly
// We have to over engineer this because tabs are stored as attributes and not plain text // We have to over engineer this because tabs are stored as attributes and not plain text
if (type === 'etherpad') { if (type === "etherpad") {
const pad = await exportEtherpad.getPadRaw(padId, readOnlyId); const pad = await exportEtherpad.getPadRaw(padId, readOnlyId);
res.send(pad); res.send(pad);
} else if (type === 'txt') { } else if (type === "txt") {
const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev); const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
res.send(txt); res.send(txt);
} else { } else {
// render the html document // render the html document
let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev, readOnlyId); let html = await exporthtml.getPadHTMLDocument(
padId,
req.params.rev,
readOnlyId,
);
// decide what to do with the html export // decide what to do with the html export
// if this is a html export, we can send this from here directly // if this is a html export, we can send this from here directly
if (type === 'html') { if (type === "html") {
// do any final changes the plugin might want to make // do any final changes the plugin might want to make
const newHTML = await hooks.aCallFirst('exportHTMLSend', html); const newHTML = await hooks.aCallFirst("exportHTMLSend", html);
if (newHTML.length) html = newHTML; if (newHTML.length) html = newHTML;
res.send(html); res.send(html);
return; return;
} }
// else write the html export to a file // else write the html export to a file
const randNum = Math.floor(Math.random() * 0xFFFFFFFF); const randNum = Math.floor(Math.random() * 0xffffffff);
const srcFile = `${tempDirectory}/etherpad_export_${randNum}.html`; const srcFile = `${tempDirectory}/etherpad_export_${randNum}.html`;
await fsp_writeFile(srcFile, html); await fsp_writeFile(srcFile, html);
@ -99,13 +108,20 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`; const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
// Allow plugins to overwrite the convert in export process // Allow plugins to overwrite the convert in export process
const result = await hooks.aCallAll('exportConvert', {srcFile, destFile, req, res}); const result = await hooks.aCallAll("exportConvert", {
srcFile,
destFile,
req,
res,
});
if (result.length > 0) { if (result.length > 0) {
// console.log("export handled by plugin", destFile); // console.log("export handled by plugin", destFile);
} else { } else {
const converter = const converter =
settings.soffice != null ? require('../utils/LibreOffice') settings.soffice != null
: settings.abiword != null ? require('../utils/Abiword') ? require("../utils/LibreOffice")
: settings.abiword != null
? require("../utils/Abiword")
: null; : null;
await converter.convertFile(srcFile, destFile, type); await converter.convertFile(srcFile, destFile, type);
} }
@ -117,7 +133,7 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
await fsp_unlink(srcFile); await fsp_unlink(srcFile);
// 100ms delay to accommodate for slow windows fs // 100ms delay to accommodate for slow windows fs
if (os.type().indexOf('Windows') > -1) { if (os.type().indexOf("Windows") > -1) {
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
} }

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* Handles the import requests * Handles the import requests
*/ */
@ -21,19 +20,19 @@
* limitations under the License. * limitations under the License.
*/ */
const padManager = require('../db/PadManager'); const padManager = require("../db/PadManager");
const padMessageHandler = require('./PadMessageHandler'); const padMessageHandler = require("./PadMessageHandler");
import {promises as fs} from 'fs'; import { promises as fs } from "fs";
import path from 'path'; import path from "path";
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
const {Formidable} = require('formidable'); const { Formidable } = require("formidable");
import os from 'os'; import os from "os";
const importHtml = require('../utils/ImportHtml'); const importHtml = require("../utils/ImportHtml");
const importEtherpad = require('../utils/ImportEtherpad'); const importEtherpad = require("../utils/ImportEtherpad");
import log4js from 'log4js'; import log4js from "log4js";
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require("../../static/js/pluginfw/hooks.js");
const logger = log4js.getLogger('ImportHandler'); const logger = log4js.getLogger("ImportHandler");
// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`. // `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`.
class ImportError extends Error { class ImportError extends Error {
@ -41,10 +40,10 @@ class ImportError extends Error {
constructor(status: string, ...args: any) { constructor(status: string, ...args: any) {
super(...args); super(...args);
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError); if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
this.name = 'ImportError'; this.name = "ImportError";
this.status = status; this.status = status;
const msg = this.message == null ? '' : String(this.message); const msg = this.message == null ? "" : String(this.message);
if (status !== '') this.message = msg === '' ? status : `${status}: ${msg}`; if (status !== "") this.message = msg === "" ? status : `${status}: ${msg}`;
} }
} }
@ -52,22 +51,22 @@ const rm = async (path: string) => {
try { try {
await fs.unlink(path); await fs.unlink(path);
} catch (err: any) { } catch (err: any) {
if (err.code !== 'ENOENT') throw err; if (err.code !== "ENOENT") throw err;
} }
}; };
let converter: any = null; let converter: any = null;
let exportExtension = 'htm'; let exportExtension = "htm";
// load abiword only if it is enabled and if soffice is disabled // load abiword only if it is enabled and if soffice is disabled
if (settings.abiword != null && settings.soffice == null) { if (settings.abiword != null && settings.soffice == null) {
converter = require('../utils/Abiword'); converter = require("../utils/Abiword");
} }
// load soffice only if it is enabled // load soffice only if it is enabled
if (settings.soffice != null) { if (settings.soffice != null) {
converter = require('../utils/LibreOffice'); converter = require("../utils/LibreOffice");
exportExtension = 'html'; exportExtension = "html";
} }
const tmpDirectory = os.tmpdir(); const tmpDirectory = os.tmpdir();
@ -79,14 +78,19 @@ const tmpDirectory = os.tmpdir();
* @param {String} padId the pad id to export * @param {String} padId the pad id to export
* @param {String} authorId the author id to use for the import * @param {String} authorId the author id to use for the import
*/ */
const doImport = async (req:any, res:any, padId:string, authorId:string) => { const doImport = async (
req: any,
res: any,
padId: string,
authorId: string,
) => {
// pipe to a file // pipe to a file
// convert file to html via abiword or soffice // convert file to html via abiword or soffice
// set html in the pad // set html in the pad
const randNum = Math.floor(Math.random() * 0xFFFFFFFF); const randNum = Math.floor(Math.random() * 0xffffffff);
// setting flag for whether to use converter or not // setting flag for whether to use converter or not
let useConverter = (converter != null); let useConverter = converter != null;
const form = new Formidable({ const form = new Formidable({
keepExtensions: true, keepExtensions: true,
@ -102,13 +106,13 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
} catch (err: any) { } catch (err: any) {
logger.warn(`Import failed due to form error: ${err.stack || err}`); logger.warn(`Import failed due to form error: ${err.stack || err}`);
if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) { if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) {
throw new ImportError('maxFileSize'); throw new ImportError("maxFileSize");
} }
throw new ImportError('uploadFailed'); throw new ImportError("uploadFailed");
} }
if (!files.file) { if (!files.file) {
logger.warn('Import failed because form had no file'); logger.warn("Import failed because form had no file");
throw new ImportError('uploadFailed'); throw new ImportError("uploadFailed");
} else { } else {
srcFile = files.file[0].filepath; srcFile = files.file[0].filepath;
} }
@ -116,9 +120,18 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
// ensure this is a file ending we know, else we change the file ending to .txt // ensure this is a file ending we know, else we change the file ending to .txt
// this allows us to accept source code files like .c or .java // this allows us to accept source code files like .c or .java
const fileEnding = path.extname(files.file[0].originalFilename).toLowerCase(); const fileEnding = path.extname(files.file[0].originalFilename).toLowerCase();
const knownFileEndings = const knownFileEndings = [
['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf']; ".txt",
const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0); ".doc",
".docx",
".pdf",
".odt",
".html",
".htm",
".etherpad",
".rtf",
];
const fileEndingUnknown = knownFileEndings.indexOf(fileEnding) < 0;
if (fileEndingUnknown) { if (fileEndingUnknown) {
// the file ending is not known // the file ending is not known
@ -127,31 +140,43 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
// we need to rename this file with a .txt ending // we need to rename this file with a .txt ending
const oldSrcFile = srcFile; const oldSrcFile = srcFile;
srcFile = path.join(path.dirname(srcFile), `${path.basename(srcFile, fileEnding)}.txt`); srcFile = path.join(
path.dirname(srcFile),
`${path.basename(srcFile, fileEnding)}.txt`,
);
await fs.rename(oldSrcFile, srcFile); await fs.rename(oldSrcFile, srcFile);
} else { } else {
logger.warn(`Not allowing unknown file type to be imported: ${fileEnding}`); logger.warn(
throw new ImportError('uploadFailed'); `Not allowing unknown file type to be imported: ${fileEnding}`,
);
throw new ImportError("uploadFailed");
} }
} }
const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); const destFile = path.join(
tmpDirectory,
`etherpad_import_${randNum}.${exportExtension}`,
);
const context = { srcFile, destFile, fileEnding, padId, ImportError }; const context = { srcFile, destFile, fileEnding, padId, ImportError };
const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x:string) => x); const importHandledByPlugin = (await hooks.aCallAll("import", context)).some(
const fileIsEtherpad = (fileEnding === '.etherpad'); (x: string) => x,
const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); );
const fileIsTXT = (fileEnding === '.txt'); const fileIsEtherpad = fileEnding === ".etherpad";
const fileIsHTML = fileEnding === ".html" || fileEnding === ".htm";
const fileIsTXT = fileEnding === ".txt";
let directDatabaseAccess = false; let directDatabaseAccess = false;
if (fileIsEtherpad) { if (fileIsEtherpad) {
// Use '\n' to avoid the default pad text if the pad doesn't yet exist. // Use '\n' to avoid the default pad text if the pad doesn't yet exist.
const pad = await padManager.getPad(padId, '\n', authorId); const pad = await padManager.getPad(padId, "\n", authorId);
const headCount = pad.head; const headCount = pad.head;
if (headCount >= 10) { if (headCount >= 10) {
logger.warn('Aborting direct database import attempt of a pad that already has content'); logger.warn(
throw new ImportError('padHasData'); "Aborting direct database import attempt of a pad that already has content",
);
throw new ImportError("padHasData");
} }
const text = await fs.readFile(srcFile, 'utf8'); const text = await fs.readFile(srcFile, "utf8");
directDatabaseAccess = true; directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, text, authorId); await importEtherpad.setPadRaw(padId, text, authorId);
} }
@ -172,7 +197,7 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
await converter.convertFile(srcFile, destFile, exportExtension); await converter.convertFile(srcFile, destFile, exportExtension);
} catch (err: any) { } catch (err: any) {
logger.warn(`Converting Error: ${err.stack || err}`); logger.warn(`Converting Error: ${err.stack || err}`);
throw new ImportError('convertFailed'); throw new ImportError("convertFailed");
} }
} }
} }
@ -182,26 +207,26 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
const buf = await fs.readFile(destFile); const buf = await fs.readFile(destFile);
// Check if there are only ascii chars in the uploaded file // Check if there are only ascii chars in the uploaded file
const isAscii = !Array.prototype.some.call(buf, (c) => (c > 240)); const isAscii = !Array.prototype.some.call(buf, (c) => c > 240);
if (!isAscii) { if (!isAscii) {
logger.warn('Attempt to import non-ASCII file'); logger.warn("Attempt to import non-ASCII file");
throw new ImportError('uploadFailed'); throw new ImportError("uploadFailed");
} }
} }
// Use '\n' to avoid the default pad text if the pad doesn't yet exist. // Use '\n' to avoid the default pad text if the pad doesn't yet exist.
let pad = await padManager.getPad(padId, '\n', authorId); let pad = await padManager.getPad(padId, "\n", authorId);
// read the text // read the text
let text; let text;
if (!directDatabaseAccess) { if (!directDatabaseAccess) {
text = await fs.readFile(destFile, 'utf8'); text = await fs.readFile(destFile, "utf8");
// node on windows has a delay on releasing of the file lock. // node on windows has a delay on releasing of the file lock.
// We add a 100ms delay to work around this // We add a 100ms delay to work around this
if (os.type().indexOf('Windows') > -1) { if (os.type().indexOf("Windows") > -1) {
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
} }
} }
@ -212,7 +237,11 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
try { try {
await importHtml.setPadHTML(pad, text, authorId); await importHtml.setPadHTML(pad, text, authorId);
} catch (err: any) { } catch (err: any) {
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`); logger.warn(
`Error importing, possibly caused by malformed HTML: ${
err.stack || err
}`,
);
} }
} else { } else {
await pad.setText(text, authorId); await pad.setText(text, authorId);
@ -221,7 +250,7 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
// Load the Pad into memory then broadcast updates to all clients // Load the Pad into memory then broadcast updates to all clients
padManager.unloadPad(padId); padManager.unloadPad(padId);
pad = await padManager.getPad(padId, '\n', authorId); pad = await padManager.getPad(padId, "\n", authorId);
padManager.unloadPad(padId); padManager.unloadPad(padId);
// Direct database access means a pad user should reload the pad and not attempt to receive // Direct database access means a pad user should reload the pad and not attempt to receive
@ -246,19 +275,22 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
* @param {String} authorId the author id to use for the import * @param {String} authorId the author id to use for the import
* @return {Promise<void>} a promise * @return {Promise<void>} a promise
*/ */
exports.doImport = async (req:any, res:any, padId:string, authorId:string = '') => { exports.doImport = async (req: any, res: any, padId: string, authorId = "") => {
let httpStatus = 200; let httpStatus = 200;
let code = 0; let code = 0;
let message = 'ok'; let message = "ok";
let directDatabaseAccess; let directDatabaseAccess;
try { try {
directDatabaseAccess = await doImport(req, res, padId, authorId); directDatabaseAccess = await doImport(req, res, padId, authorId);
} catch (err: any) { } catch (err: any) {
const known = err instanceof ImportError && err.status; const known = err instanceof ImportError && err.status;
if (!known) logger.error(`Internal error during import: ${err.stack || err}`); if (!known)
logger.error(`Internal error during import: ${err.stack || err}`);
httpStatus = known ? 400 : 500; httpStatus = known ? 400 : 500;
code = known ? 1 : 2; code = known ? 1 : 2;
message = known ? err.status : 'internalError'; message = known ? err.status : "internalError";
} }
res.status(httpStatus).json({code, message, data: {directDatabaseAccess}}); res
.status(httpStatus)
.json({ code, message, data: { directDatabaseAccess } });
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* This is the Socket.IO Router. It routes the Messages between the * This is the Socket.IO Router. It routes the Messages between the
* components of the Server. The components are at the moment: pad and timeslider * components of the Server. The components are at the moment: pad and timeslider
@ -20,13 +19,13 @@
* limitations under the License. * limitations under the License.
*/ */
import {MapArrayType} from "../types/MapType"; import type { MapArrayType } from "../types/MapType";
import {SocketModule} from "../types/SocketModule"; import type { SocketModule } from "../types/SocketModule";
const log4js = require('log4js'); const log4js = require("log4js");
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
const stats = require('../../node/stats') const stats = require("../../node/stats");
const logger = log4js.getLogger('socket.io'); const logger = log4js.getLogger("socket.io");
/** /**
* Saves all components * Saves all components
@ -51,7 +50,9 @@ exports.addComponent = (moduleName: string, module: SocketModule) => {
* removes a component * removes a component
* @param {Module} moduleName * @param {Module} moduleName
*/ */
exports.deleteComponent = (moduleName: string) => { delete components[moduleName]; }; exports.deleteComponent = (moduleName: string) => {
delete components[moduleName];
};
/** /**
* sets the socket.io and adds event functions for routing * sets the socket.io and adds event functions for routing
@ -60,8 +61,8 @@ exports.deleteComponent = (moduleName: string) => { delete components[moduleName
exports.setSocketIO = (_io: any) => { exports.setSocketIO = (_io: any) => {
io = _io; io = _io;
io.sockets.on('connection', (socket:any) => { io.sockets.on("connection", (socket: any) => {
const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip; const ip = settings.disableIPlogging ? "ANONYMOUS" : socket.request.ip;
logger.debug(`${socket.id} connected from IP ${ip}`); logger.debug(`${socket.id} connected from IP ${ip}`);
// wrap the original send function to log the messages // wrap the original send function to log the messages
@ -76,27 +77,36 @@ exports.setSocketIO = (_io:any) => {
components[i].handleConnect(socket); components[i].handleConnect(socket);
} }
socket.on('message', (message: any, ack: any = () => {}) => (async () => { socket.on("message", (message: any, ack: any = () => {}) =>
(async () => {
if (!message.component || !components[message.component]) { if (!message.component || !components[message.component]) {
throw new Error(`unknown message component: ${message.component}`); throw new Error(`unknown message component: ${message.component}`);
} }
logger.debug(`from ${socket.id}:`, message); logger.debug(`from ${socket.id}:`, message);
return await components[message.component].handleMessage(socket, message); return await components[message.component].handleMessage(
socket,
message,
);
})().then( })().then(
(val) => ack(null, val), (val) => ack(null, val),
(err) => { (err) => {
logger.error( logger.error(
`Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`); `Error handling ${message.component} message from ${socket.id}: ${
err.stack || err
}`,
);
ack({ name: err.name, message: err.message }); // socket.io can't handle Error objects. ack({ name: err.name, message: err.message }); // socket.io can't handle Error objects.
})); },
),
);
socket.on('disconnect', (reason: string) => { socket.on("disconnect", (reason: string) => {
logger.debug(`${socket.id} disconnected: ${reason}`); logger.debug(`${socket.id} disconnected: ${reason}`);
// store the lastDisconnect as a timestamp, this is useful if you want to know // store the lastDisconnect as a timestamp, this is useful if you want to know
// when the last user disconnected. If your activePads is 0 and totalUsers is 0 // when the last user disconnected. If your activePads is 0 and totalUsers is 0
// you can say, if there has been no active pads or active users for 10 minutes // you can say, if there has been no active pads or active users for 10 minutes
// this instance can be brought out of a scaling cluster. // this instance can be brought out of a scaling cluster.
stats.gauge('lastDisconnect', () => Date.now()); stats.gauge("lastDisconnect", () => Date.now());
// tell all components about this disconnect // tell all components about this disconnect
for (const i of Object.keys(components)) { for (const i of Object.keys(components)) {
components[i].handleDisconnect(socket); components[i].handleDisconnect(socket);

View file

@ -1,63 +1,67 @@
'use strict'; import type { Socket } from "node:net";
import {Socket} from "node:net";
import type { MapArrayType } from "../types/MapType"; import type { MapArrayType } from "../types/MapType";
import _ from 'underscore'; import events from "events";
import fs from "fs";
// @ts-ignore // @ts-ignore
import cookieParser from 'cookie-parser'; import cookieParser from "cookie-parser";
import events from 'events'; import express from "express";
import express from 'express';
// @ts-ignore // @ts-ignore
import expressSession from 'express-session'; import expressSession from "express-session";
import fs from 'fs'; import _ from "underscore";
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require("../../static/js/pluginfw/hooks");
import log4js from 'log4js'; import log4js from "log4js";
const SessionStore = require('../db/SessionStore'); const SessionStore = require("../db/SessionStore");
const settings = require('../utils/Settings'); const settings = require("../utils/Settings");
const stats = require('../stats') const stats = require("../stats");
import util from 'util'; import util from "util";
const webaccess = require('./express/webaccess'); const webaccess = require("./express/webaccess");
import SecretRotator from '../security/SecretRotator'; import SecretRotator from "../security/SecretRotator";
let secretRotator: SecretRotator | null = null; let secretRotator: SecretRotator | null = null;
const logger = log4js.getLogger('http'); const logger = log4js.getLogger("http");
let serverName: string; let serverName: string;
let sessionStore: { shutdown: () => void; } | null; let sessionStore: { shutdown: () => void } | null;
const sockets: Set<Socket> = new Set(); const sockets: Set<Socket> = new Set();
const socketsEvents = new events.EventEmitter(); const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime'); const startTime = stats.settableGauge("httpStartTime");
exports.server = null; exports.server = null;
const closeServer = async () => { const closeServer = async () => {
if (exports.server != null) { if (exports.server != null) {
logger.info('Closing HTTP server...'); logger.info("Closing HTTP server...");
// Call exports.server.close() to reject new connections but don't await just yet because the // Call exports.server.close() to reject new connections but don't await just yet because the
// Promise won't resolve until all preexisting connections are closed. // Promise won't resolve until all preexisting connections are closed.
const p = util.promisify(exports.server.close.bind(exports.server))(); const p = util.promisify(exports.server.close.bind(exports.server))();
await hooks.aCallAll('expressCloseServer'); await hooks.aCallAll("expressCloseServer");
// Give existing connections some time to close on their own before forcibly terminating. The // Give existing connections some time to close on their own before forcibly terminating. The
// time should be long enough to avoid interrupting most preexisting transmissions but short // time should be long enough to avoid interrupting most preexisting transmissions but short
// enough to avoid a noticeable outage. // enough to avoid a noticeable outage.
const timeout = setTimeout(async () => { const timeout = setTimeout(async () => {
logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`); logger.info(
for (const socket of sockets) socket.destroy(new Error('HTTP server is closing')); `Forcibly terminating remaining ${sockets.size} HTTP connections...`,
);
for (const socket of sockets)
socket.destroy(new Error("HTTP server is closing"));
}, 5000); }, 5000);
let lastLogged = 0; let lastLogged = 0;
while (sockets.size > 0 && !settings.enableAdminUITests) { while (sockets.size > 0 && !settings.enableAdminUITests) {
if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. if (Date.now() - lastLogged > 1000) {
logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`); // Rate limit to avoid filling logs.
logger.info(
`Waiting for ${sockets.size} HTTP clients to disconnect...`,
);
lastLogged = Date.now(); lastLogged = Date.now();
} }
await events.once(socketsEvents, 'updated'); await events.once(socketsEvents, "updated");
} }
await p; await p;
clearTimeout(timeout); clearTimeout(timeout);
exports.server = null; exports.server = null;
startTime.setValue(0); startTime.setValue(0);
logger.info('HTTP server closed'); logger.info("HTTP server closed");
} }
if (sessionStore) sessionStore.shutdown(); if (sessionStore) sessionStore.shutdown();
sessionStore = null; sessionStore = null;
@ -66,34 +70,46 @@ const closeServer = async () => {
}; };
exports.createServer = async () => { exports.createServer = async () => {
console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); console.log("Report bugs at https://github.com/ether/etherpad-lite/issues");
serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`;
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); console.log(
`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`,
);
await exports.restartServer(); await exports.restartServer();
if (settings.ip === '') { if (settings.ip === "") {
// using Unix socket for connectivity // using Unix socket for connectivity
console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`); console.log(
`You can access your Etherpad instance using the Unix socket at ${settings.port}`,
);
} else { } else {
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`); console.log(
`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`,
);
} }
if (!_.isEmpty(settings.users)) { if (!_.isEmpty(settings.users)) {
console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`); console.log(
`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`,
);
} else { } else {
console.warn('Admin username and password not set in settings.json. ' + console.warn(
'To access admin please uncomment and edit "users" in settings.json'); "Admin username and password not set in settings.json. " +
'To access admin please uncomment and edit "users" in settings.json',
);
} }
const env = process.env.NODE_ENV || 'development'; const env = process.env.NODE_ENV || "development";
if (env !== 'production') { if (env !== "production") {
console.warn('Etherpad is running in Development mode. This mode is slower for users and ' + console.warn(
'less secure than production mode. You should set the NODE_ENV environment ' + "Etherpad is running in Development mode. This mode is slower for users and " +
'variable to production by using: export NODE_ENV=production'); "less secure than production mode. You should set the NODE_ENV environment " +
"variable to production by using: export NODE_ENV=production",
);
} }
}; };
@ -103,9 +119,11 @@ exports.restartServer = async () => {
const app = express(); // New syntax for express v3 const app = express(); // New syntax for express v3
if (settings.ssl) { if (settings.ssl) {
console.log('SSL -- enabled'); console.log("SSL -- enabled");
console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- server key file: ${settings.ssl.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); console.log(
`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`,
);
const options: MapArrayType<any> = { const options: MapArrayType<any> = {
key: fs.readFileSync(settings.ssl.key), key: fs.readFileSync(settings.ssl.key),
@ -120,10 +138,10 @@ exports.restartServer = async () => {
} }
} }
const https = require('https'); const https = require("https");
exports.server = https.createServer(options, app); exports.server = https.createServer(options, app);
} else { } else {
const http = require('http'); const http = require("http");
exports.server = http.createServer(app); exports.server = http.createServer(app);
} }
@ -131,12 +149,15 @@ exports.restartServer = async () => {
// res.header("X-Frame-Options", "deny"); // breaks embedded pads // res.header("X-Frame-Options", "deny"); // breaks embedded pads
if (settings.ssl) { if (settings.ssl) {
// we use SSL // we use SSL
res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.header(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
);
} }
// Stop IE going into compatability mode // Stop IE going into compatability mode
// https://github.com/ether/etherpad-lite/issues/2547 // https://github.com/ether/etherpad-lite/issues/2547
res.header('X-UA-Compatible', 'IE=Edge,chrome=1'); res.header("X-UA-Compatible", "IE=Edge,chrome=1");
// Enable a strong referrer policy. Same-origin won't drop Referers when // Enable a strong referrer policy. Same-origin won't drop Referers when
// loading local resources, but it will drop them when loading foreign resources. // loading local resources, but it will drop them when loading foreign resources.
@ -145,11 +166,11 @@ exports.restartServer = async () => {
// marked with <meta name="referrer" content="no-referrer"> // marked with <meta name="referrer" content="no-referrer">
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
// https://github.com/ether/etherpad-lite/pull/3636 // https://github.com/ether/etherpad-lite/pull/3636
res.header('Referrer-Policy', 'same-origin'); res.header("Referrer-Policy", "same-origin");
// send git version in the Server response header if exposeVersion is true. // send git version in the Server response header if exposeVersion is true.
if (settings.exposeVersion) { if (settings.exposeVersion) {
res.header('Server', serverName); res.header("Server", serverName);
} }
next(); next();
@ -162,14 +183,17 @@ exports.restartServer = async () => {
* *
* Source: https://expressjs.com/en/guide/behind-proxies.html * Source: https://expressjs.com/en/guide/behind-proxies.html
*/ */
app.enable('trust proxy'); app.enable("trust proxy");
} }
// Measure response time // Measure response time
app.use((req, res, next) => { app.use((req, res, next) => {
const stopWatch = stats.timer('httpRequests').start(); const stopWatch = stats.timer("httpRequests").start();
const sendFn = res.send.bind(res); const sendFn = res.send.bind(res);
res.send = (...args) => { stopWatch.end(); return sendFn(...args); }; res.send = (...args) => {
stopWatch.end();
return sendFn(...args);
};
next(); next();
}); });
@ -177,22 +201,28 @@ exports.restartServer = async () => {
// starts listening to requests as reported in issue #158. Not installing the log4js connect // starts listening to requests as reported in issue #158. Not installing the log4js connect
// logger when the log level has a higher severity than INFO since it would not log at that level // logger when the log level has a higher severity than INFO since it would not log at that level
// anyway. // anyway.
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { if (!(settings.loglevel === "WARN" && settings.loglevel === "ERROR")) {
app.use(log4js.connectLogger(logger, { app.use(
log4js.connectLogger(logger, {
level: log4js.levels.DEBUG.levelStr, level: log4js.levels.DEBUG.levelStr,
format: ':status, :method :url', format: ":status, :method :url",
})); }),
);
} }
const { keyRotationInterval, sessionLifetime } = settings.cookie; const { keyRotationInterval, sessionLifetime } = settings.cookie;
let secret = settings.sessionKey; let secret = settings.sessionKey;
if (keyRotationInterval && sessionLifetime) { if (keyRotationInterval && sessionLifetime) {
secretRotator = new SecretRotator( secretRotator = new SecretRotator(
'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey); "expressSessionSecrets",
keyRotationInterval,
sessionLifetime,
settings.sessionKey,
);
await secretRotator.start(); await secretRotator.start();
secret = secretRotator.secrets; secret = secretRotator.secrets;
} }
if (!secret) throw new Error('missing cookie signing secret'); if (!secret) throw new Error("missing cookie signing secret");
app.use(cookieParser(secret, {})); app.use(cookieParser(secret, {}));
@ -206,7 +236,7 @@ exports.restartServer = async () => {
saveUninitialized: false, saveUninitialized: false,
// Set the cookie name to a javascript identifier compatible string. Makes code handling it // Set the cookie name to a javascript identifier compatible string. Makes code handling it
// cleaner :) // cleaner :)
name: 'express_sid', name: "express_sid",
cookie: { cookie: {
maxAge: sessionLifetime || null, // Convert 0 to null. maxAge: sessionLifetime || null, // Convert 0 to null.
sameSite: settings.cookie.sameSite, sameSite: settings.cookie.sameSite,
@ -227,33 +257,36 @@ exports.restartServer = async () => {
// https at the same time. // https at the same time.
// //
// reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure // reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure
secure: 'auto', secure: "auto",
}, },
}); });
// Give plugins an opportunity to install handlers/middleware before the express-session // Give plugins an opportunity to install handlers/middleware before the express-session
// middleware. This allows plugins to avoid creating an express-session record in the database // middleware. This allows plugins to avoid creating an express-session record in the database
// when it is not needed (e.g., public static content). // when it is not needed (e.g., public static content).
await hooks.aCallAll('expressPreSession', {app}); await hooks.aCallAll("expressPreSession", { app });
app.use(exports.sessionMiddleware); app.use(exports.sessionMiddleware);
app.use(webaccess.checkAccess); app.use(webaccess.checkAccess);
await Promise.all([ await Promise.all([
hooks.aCallAll('expressConfigure', {app}), hooks.aCallAll("expressConfigure", { app }),
hooks.aCallAll('expressCreateServer', {app, server: exports.server}), hooks.aCallAll("expressCreateServer", { app, server: exports.server }),
]); ]);
exports.server.on('connection', (socket:Socket) => { exports.server.on("connection", (socket: Socket) => {
sockets.add(socket); sockets.add(socket);
socketsEvents.emit('updated'); socketsEvents.emit("updated");
socket.on('close', () => { socket.on("close", () => {
sockets.delete(socket); sockets.delete(socket);
socketsEvents.emit('updated'); socketsEvents.emit("updated");
}); });
}); });
await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); await util.promisify(exports.server.listen).bind(exports.server)(
settings.port,
settings.ip,
);
startTime.setValue(Date.now()); startTime.setValue(Date.now());
logger.info('HTTP server listening for connections'); logger.info("HTTP server listening for connections");
}; };
exports.shutdown = async (hookName: string, context: any) => { exports.shutdown = async (hookName: string, context: any) => {

View file

@ -1,11 +1,6 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import path from "path"; import path from "path";
import fs from "fs";
import express from "express"; import express from "express";
const settings = require('ep_etherpad-lite/node/utils/Settings'); import type { ArgsExpressType } from "../../types/ArgsExpressType";
const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
/** /**
* Add the admin navigation link * Add the admin navigation link
@ -14,13 +9,24 @@ const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
* @param {Function} cb the callback function * @param {Function} cb the callback function
* @return {*} * @return {*}
*/ */
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => { exports.expressCreateServer = (
args.app.use('/admin/', express.static(path.join(__dirname, '../../../templates/admin'), {maxAge: 1000 * 60 * 60 * 24})); hookName: string,
args.app.get('/admin/*', (_request:any, response:any)=>{ args: ArgsExpressType,
response.sendFile(path.resolve(__dirname,'../../../templates/admin', 'index.html')); cb: Function,
} ) ): any => {
args.app.get('/admin', (req:any, res:any, next:Function) => { args.app.use(
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); "/admin/",
}) express.static(path.join(__dirname, "../../../templates/admin"), {
maxAge: 1000 * 60 * 60 * 24,
}),
);
args.app.get("/admin/*", (_request: any, response: any) => {
response.sendFile(
path.resolve(__dirname, "../../../templates/admin", "index.html"),
);
});
args.app.get("/admin", (req: any, res: any, next: Function) => {
if ("/" !== req.path[req.path.length - 1]) return res.redirect("./admin/");
});
return cb(); return cb();
}; };

View file

@ -1,32 +1,39 @@
'use strict'; import type { ArgsExpressType } from "../../types/ArgsExpressType";
import type { ErrorCaused } from "../../types/ErrorCaused";
import type { QueryType } from "../../types/QueryType";
import {ArgsExpressType} from "../../types/ArgsExpressType"; import {
import {ErrorCaused} from "../../types/ErrorCaused"; getAvailablePlugins,
import {QueryType} from "../../types/QueryType"; install,
search,
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer"; uninstall,
import {PackageData} from "../../types/PackageInfo"; } from "../../../static/js/pluginfw/installer";
import type { PackageData } from "../../types/PackageInfo";
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
import semver from 'semver';
const pluginDefs = require("../../../static/js/pluginfw/plugin_defs");
import semver from "semver";
exports.socketio = (hookName: string, args: ArgsExpressType, cb: Function) => { exports.socketio = (hookName: string, args: ArgsExpressType, cb: Function) => {
const io = args.io.of('/pluginfw/installer'); const io = args.io.of("/pluginfw/installer");
io.on('connection', (socket:any) => { io.on("connection", (socket: any) => {
// @ts-ignore // @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; const {
session: {
user: { is_admin: isAdmin } = {},
} = {},
} = socket.conn.request;
if (!isAdmin) return; if (!isAdmin) return;
socket.on('getInstalled', (query:string) => { socket.on("getInstalled", (query: string) => {
// send currently installed plugins // send currently installed plugins
const installed = const installed = Object.keys(pluginDefs.plugins).map(
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package); (plugin) => pluginDefs.plugins[plugin].package,
);
socket.emit('results:installed', {installed}); socket.emit("results:installed", { installed });
}); });
socket.on('checkUpdates', async () => { socket.on("checkUpdates", async () => {
// Check plugins for updates // Check plugins for updates
try { try {
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
@ -40,46 +47,51 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
return semver.gt(latestVersion, currentVersion); return semver.gt(latestVersion, currentVersion);
}); });
socket.emit('results:updatable', {updatable}); socket.emit("results:updatable", { updatable });
} catch (err) { } catch (err) {
const errc = err as ErrorCaused const errc = err as ErrorCaused;
console.warn(errc.stack || errc.toString()); console.warn(errc.stack || errc.toString());
socket.emit('results:updatable', {updatable: {}}); socket.emit("results:updatable", { updatable: {} });
} }
}); });
socket.on('getAvailable', async (query:string) => { socket.on("getAvailable", async (query: string) => {
try { try {
const results = await getAvailablePlugins(/* maxCacheAge:*/ false); const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
socket.emit('results:available', results); socket.emit("results:available", results);
} catch (er) { } catch (er) {
console.error(er); console.error(er);
socket.emit('results:available', {}); socket.emit("results:available", {});
} }
}); });
socket.on('search', async (query: QueryType) => { socket.on("search", async (query: QueryType) => {
try { try {
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); const results = await search(
query.searchTerm,
/* maxCacheAge:*/ 60 * 10,
);
let res = Object.keys(results) let res = Object.keys(results)
.map((pluginName) => results[pluginName]) .map((pluginName) => results[pluginName])
.filter((plugin) => !pluginDefs.plugins[plugin.name]); .filter((plugin) => !pluginDefs.plugins[plugin.name]);
res = sortPluginList(res, query.sortBy, query.sortDir) res = sortPluginList(res, query.sortBy, query.sortDir).slice(
.slice(query.offset, query.offset + query.limit); query.offset,
socket.emit('results:search', {results: res, query}); query.offset + query.limit,
);
socket.emit("results:search", { results: res, query });
} catch (er) { } catch (er) {
console.error(er); console.error(er);
socket.emit('results:search', {results: {}, query}); socket.emit("results:search", { results: {}, query });
} }
}); });
socket.on('install', (pluginName: string) => { socket.on("install", (pluginName: string) => {
install(pluginName, (err: ErrorCaused) => { install(pluginName, (err: ErrorCaused) => {
if (err) console.warn(err.stack || err.toString()); if (err) console.warn(err.stack || err.toString());
socket.emit('finished:install', { socket.emit("finished:install", {
plugin: pluginName, plugin: pluginName,
code: err ? err.code : null, code: err ? err.code : null,
error: err ? err.message : null, error: err ? err.message : null,
@ -87,11 +99,14 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
}); });
}); });
socket.on('uninstall', (pluginName:string) => { socket.on("uninstall", (pluginName: string) => {
uninstall(pluginName, (err: ErrorCaused) => { uninstall(pluginName, (err: ErrorCaused) => {
if (err) console.warn(err.stack || err.toString()); if (err) console.warn(err.stack || err.toString());
socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null}); socket.emit("finished:uninstall", {
plugin: pluginName,
error: err ? err.message : null,
});
}); });
}); });
}); });
@ -105,7 +120,12 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
* @param {String} dir The directory of the plugin * @param {String} dir The directory of the plugin
* @return {Object[]} * @return {Object[]}
*/ */
const sortPluginList = (plugins:PackageData[], property:string, /* ASC?*/dir:string): PackageData[] => plugins.sort((a, b) => { const sortPluginList = (
plugins: PackageData[],
property: string,
/* ASC?*/ dir: string,
): PackageData[] =>
plugins.sort((a, b) => {
// @ts-ignore // @ts-ignore
if (a[property] < b[property]) { if (a[property] < b[property]) {
return dir ? -1 : 1; return dir ? -1 : 1;

View file

@ -1,60 +1,69 @@
'use strict'; import type {
PadQueryResult,
PadSearchQuery,
import {PadQueryResult, PadSearchQuery} from "../../types/PadSearchQuery"; } from "../../types/PadSearchQuery";
import { PadType } from "../../types/PadType"; import { PadType } from "../../types/PadType";
const eejs = require('../../eejs'); const eejs = require("../../eejs");
const fsp = require('fs').promises; const fsp = require("fs").promises;
const hooks = require('../../../static/js/pluginfw/hooks'); const hooks = require("../../../static/js/pluginfw/hooks");
const plugins = require('../../../static/js/pluginfw/plugins'); const plugins = require("../../../static/js/pluginfw/plugins");
const settings = require('../../utils/Settings'); const settings = require("../../utils/Settings");
const UpdateCheck = require('../../utils/UpdateCheck'); const UpdateCheck = require("../../utils/UpdateCheck");
const padManager = require('../../db/PadManager'); const padManager = require("../../db/PadManager");
const api = require('../../db/API'); const api = require("../../db/API");
const queryPadLimit = 12; const queryPadLimit = 12;
exports.socketio = (hookName: string, { io }: any) => { exports.socketio = (hookName: string, { io }: any) => {
io.of('/settings').on('connection', (socket: any ) => { io.of("/settings").on("connection", (socket: any) => {
// @ts-ignore // @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; const {
session: {
user: { is_admin: isAdmin } = {},
} = {},
} = socket.conn.request;
if (!isAdmin) return; if (!isAdmin) return;
socket.on('load', async (query:string):Promise<any> => { socket.on("load", async (query: string): Promise<any> => {
let data; let data;
try { try {
data = await fsp.readFile(settings.settingsFilename, 'utf8'); data = await fsp.readFile(settings.settingsFilename, "utf8");
} catch (err) { } catch (err) {
return console.log(err); return console.log(err);
} }
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
if (settings.showSettingsInAdminPage === false) { if (settings.showSettingsInAdminPage === false) {
socket.emit('settings', {results: 'NOT_ALLOWED'}); socket.emit("settings", { results: "NOT_ALLOWED" });
} else { } else {
socket.emit('settings', {results: data}); socket.emit("settings", { results: data });
} }
}); });
socket.on('saveSettings', async (newSettings:string) => { socket.on("saveSettings", async (newSettings: string) => {
console.log('Admin request to save settings through a socket on /admin/settings'); console.log(
"Admin request to save settings through a socket on /admin/settings",
);
await fsp.writeFile(settings.settingsFilename, newSettings); await fsp.writeFile(settings.settingsFilename, newSettings);
socket.emit('saveprogress', 'saved'); socket.emit("saveprogress", "saved");
}); });
socket.on("help", () => {
socket.on('help', ()=> {
const gitCommit = settings.getGitCommit(); const gitCommit = settings.getGitCommit();
const epVersion = settings.getEpVersion(); const epVersion = settings.getEpVersion();
const hooks:Map<string, Map<string,string>> = plugins.getHooks('hooks', false); const hooks: Map<string, Map<string, string>> = plugins.getHooks(
const clientHooks:Map<string, Map<string,string>> = plugins.getHooks('client_hooks', false); "hooks",
false,
);
const clientHooks: Map<string, Map<string, string>> = plugins.getHooks(
"client_hooks",
false,
);
function mapToObject(map: Map<string, any>) { function mapToObject(map: Map<string, any>) {
let obj = Object.create(null); const obj = Object.create(null);
for (let [k,v] of map) { for (const [k, v] of map) {
if (v instanceof Map) { if (v instanceof Map) {
obj[k] = mapToObject(v); obj[k] = mapToObject(v);
} else { } else {
@ -64,7 +73,7 @@ exports.socketio = (hookName:string, {io}:any) => {
return obj; return obj;
} }
socket.emit('reply:help', { socket.emit("reply:help", {
gitCommit, gitCommit,
epVersion, epVersion,
installedPlugins: plugins.getPlugins(), installedPlugins: plugins.getPlugins(),
@ -72,16 +81,15 @@ exports.socketio = (hookName:string, {io}:any) => {
installedServerHooks: mapToObject(hooks), installedServerHooks: mapToObject(hooks),
installedClientHooks: mapToObject(clientHooks), installedClientHooks: mapToObject(clientHooks),
latestVersion: UpdateCheck.getLatestVersion(), latestVersion: UpdateCheck.getLatestVersion(),
}) });
}); });
socket.on("padLoad", async (query: PadSearchQuery) => {
socket.on('padLoad', async (query: PadSearchQuery) => {
const { padIDs } = await padManager.listAllPads(); const { padIDs } = await padManager.listAllPads();
const data: { const data: {
total: number, total: number;
results?: PadQueryResult[] results?: PadQueryResult[];
} = { } = {
total: padIDs.length, total: padIDs.length,
}; };
@ -90,7 +98,9 @@ exports.socketio = (hookName:string, {io}:any) => {
// Filter out matches // Filter out matches
if (query.pattern) { if (query.pattern) {
result = result.filter((padName: string) => padName.includes(query.pattern)); result = result.filter((padName: string) =>
padName.includes(query.pattern),
);
} }
data.total = result.length; data.total = result.length;
@ -112,16 +122,19 @@ exports.socketio = (hookName:string, {io}:any) => {
query.limit = queryPadLimit; query.limit = queryPadLimit;
} }
if (query.sortBy === 'padName') { if (query.sortBy === "padName") {
result = result.sort((a,b)=>{ result = result
.sort((a, b) => {
if (a < b) return query.ascending ? -1 : 1; if (a < b) return query.ascending ? -1 : 1;
if (a > b) return query.ascending ? 1 : -1; if (a > b) return query.ascending ? 1 : -1;
return 0; return 0;
}).slice(query.offset, query.offset + query.limit); })
.slice(query.offset, query.offset + query.limit);
data.results = await Promise.all(result.map(async (padName: string) => { data.results = await Promise.all(
result.map(async (padName: string) => {
const pad = await padManager.getPad(padName); const pad = await padManager.getPad(padName);
const revisionNumber = pad.getHeadRevisionNumber() const revisionNumber = pad.getHeadRevisionNumber();
const userCount = api.padUsersCount(padName).padUsersCount; const userCount = api.padUsersCount(padName).padUsersCount;
const lastEdited = await pad.getLastEdit(); const lastEdited = await pad.getLastEdit();
@ -129,83 +142,85 @@ exports.socketio = (hookName:string, {io}:any) => {
padName, padName,
lastEdited, lastEdited,
userCount, userCount,
revisionNumber revisionNumber,
}})); };
}),
);
} else { } else {
const currentWinners: PadQueryResult[] = [] const currentWinners: PadQueryResult[] = [];
let queryOffsetCounter = 0 let queryOffsetCounter = 0;
for (let res of result) { for (const res of result) {
const pad = await padManager.getPad(res); const pad = await padManager.getPad(res);
const padType = { const padType = {
padName: res, padName: res,
lastEdited: await pad.getLastEdit(), lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount, userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber() revisionNumber: pad.getHeadRevisionNumber(),
}; };
if (currentWinners.length < query.limit) { if (currentWinners.length < query.limit) {
if (queryOffsetCounter < query.offset) { if (queryOffsetCounter < query.offset) {
queryOffsetCounter++ queryOffsetCounter++;
continue continue;
} }
currentWinners.push({ currentWinners.push({
padName: res, padName: res,
lastEdited: await pad.getLastEdit(), lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount, userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber() revisionNumber: pad.getHeadRevisionNumber(),
}) });
} else { } else {
// Kick out worst pad and replace by current pad // Kick out worst pad and replace by current pad
let worstPad = currentWinners.sort((a, b) => { const worstPad = currentWinners.sort((a, b) => {
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1; if (a[query.sortBy] < b[query.sortBy])
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1; return query.ascending ? -1 : 1;
if (a[query.sortBy] > b[query.sortBy])
return query.ascending ? 1 : -1;
return 0; return 0;
}) });
if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){ if (
worstPad[0] &&
worstPad[0][query.sortBy] < padType[query.sortBy]
) {
if (queryOffsetCounter < query.offset) { if (queryOffsetCounter < query.offset) {
queryOffsetCounter++ queryOffsetCounter++;
continue continue;
} }
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1) currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1);
currentWinners.push({ currentWinners.push({
padName: res, padName: res,
lastEdited: await pad.getLastEdit(), lastEdited: await pad.getLastEdit(),
userCount: api.padUsersCount(res).padUsersCount, userCount: api.padUsersCount(res).padUsersCount,
revisionNumber: pad.getHeadRevisionNumber() revisionNumber: pad.getHeadRevisionNumber(),
}) });
} }
} }
} }
data.results = currentWinners; data.results = currentWinners;
} }
socket.emit('results:padLoad', data); socket.emit("results:padLoad", data);
}) });
socket.on("deletePad", async (padId: string) => {
socket.on('deletePad', async (padId: string) => {
const padExists = await padManager.doesPadExists(padId); const padExists = await padManager.doesPadExists(padId);
if (padExists) { if (padExists) {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
await pad.remove(); await pad.remove();
socket.emit('results:deletePad', padId); socket.emit("results:deletePad", padId);
} }
}) });
socket.on('restartServer', async () => { socket.on("restartServer", async () => {
console.log('Admin request to restart server through a socket on /admin/settings'); console.log(
"Admin request to restart server through a socket on /admin/settings",
);
settings.reloadSettings(); settings.reloadSettings();
await plugins.update(); await plugins.update();
await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll("loadSettings", { settings });
await hooks.aCallAll('restartServer'); await hooks.aCallAll("restartServer");
}); });
}); });
}; };
const searchPad = async (query: PadSearchQuery) => {};
const searchPad = async (query:PadSearchQuery) => {
}

View file

@ -1,17 +1,15 @@
'use strict'; const log4js = require("log4js");
const clientLogger = log4js.getLogger("client");
const log4js = require('log4js'); const { Formidable } = require("formidable");
const clientLogger = log4js.getLogger('client'); const apiHandler = require("../../handler/APIHandler");
const {Formidable} = require('formidable'); const util = require("util");
const apiHandler = require('../../handler/APIHandler');
const util = require('util');
exports.expressPreSession = async (hookName: string, { app }: any) => { exports.expressPreSession = async (hookName: string, { app }: any) => {
// The Etherpad client side sends information about how a disconnect happened // The Etherpad client side sends information about how a disconnect happened
app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => { app.post("/ep/pad/connection-diagnostic-info", async (req: any, res: any) => {
const [fields, files] = await (new Formidable({})).parse(req); const [fields, files] = await new Formidable({}).parse(req);
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
res.end('OK'); res.end("OK");
}); });
const parseJserrorForm = async (req: any) => { const parseJserrorForm = async (req: any) => {
@ -23,22 +21,25 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
}; };
// The Etherpad client side sends information about client side javscript errors // The Etherpad client side sends information about client side javscript errors
app.post('/jserror', (req:any, res:any, next:Function) => { app.post("/jserror", (req: any, res: any, next: Function) => {
(async () => { (async () => {
const data = JSON.parse(await parseJserrorForm(req)); const data = JSON.parse(await parseJserrorForm(req));
clientLogger.warn(`${data.msg} --`, { clientLogger.warn(`${data.msg} --`, {
[util.inspect.custom]: (depth: number, options: any) => { [util.inspect.custom]: (depth: number, options: any) => {
// Depth is forced to infinity to ensure that all of the provided data is logged. // Depth is forced to infinity to ensure that all of the provided data is logged.
options = Object.assign({}, options, {depth: Infinity, colors: true}); options = Object.assign({}, options, {
depth: Number.POSITIVE_INFINITY,
colors: true,
});
return util.inspect(data, options); return util.inspect(data, options);
}, },
}); });
res.end('OK'); res.end("OK");
})().catch((err) => next(err || new Error(err))); })().catch((err) => next(err || new Error(err)));
}); });
// Provide a possibility to query the latest available API version // Provide a possibility to query the latest available API version
app.get('/api', (req:any, res:any) => { app.get("/api", (req: any, res: any) => {
res.json({ currentVersion: apiHandler.latestApiVersion }); res.json({ currentVersion: apiHandler.latestApiVersion });
}); });
}; };

View file

@ -1,11 +1,13 @@
'use strict'; import type { ArgsExpressType } from "../../types/ArgsExpressType";
import type { ErrorCaused } from "../../types/ErrorCaused";
import {ArgsExpressType} from "../../types/ArgsExpressType"; const stats = require("../../stats");
import {ErrorCaused} from "../../types/ErrorCaused";
const stats = require('../../stats') exports.expressCreateServer = (
hook_name: string,
exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => { args: ArgsExpressType,
cb: Function,
) => {
exports.app = args.app; exports.app = args.app;
// Handle errors // Handle errors
@ -13,9 +15,9 @@ exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Funct
// if an error occurs Connect will pass it down // if an error occurs Connect will pass it down
// through these "error-handling" middleware // through these "error-handling" middleware
// allowing you to respond however you like // allowing you to respond however you like
res.status(500).send({error: 'Sorry, something bad happened!'}); res.status(500).send({ error: "Sorry, something bad happened!" });
console.error(err.stack ? err.stack : err.toString()); console.error(err.stack ? err.stack : err.toString());
stats.meter('http500').mark(); stats.meter("http500").mark();
}); });
return cb(); return cb();

View file

@ -1,53 +1,65 @@
'use strict'; import type { ArgsExpressType } from "../../types/ArgsExpressType";
import {ArgsExpressType} from "../../types/ArgsExpressType"; const hasPadAccess = require("../../padaccess");
const settings = require("../../utils/Settings");
const exportHandler = require("../../handler/ExportHandler");
const importHandler = require("../../handler/ImportHandler");
const padManager = require("../../db/PadManager");
const readOnlyManager = require("../../db/ReadOnlyManager");
const rateLimit = require("express-rate-limit");
const securityManager = require("../../db/SecurityManager");
const webaccess = require("./webaccess");
const hasPadAccess = require('../../padaccess'); exports.expressCreateServer = (
const settings = require('../../utils/Settings'); hookName: string,
const exportHandler = require('../../handler/ExportHandler'); args: ArgsExpressType,
const importHandler = require('../../handler/ImportHandler'); cb: Function,
const padManager = require('../../db/PadManager'); ) => {
const readOnlyManager = require('../../db/ReadOnlyManager');
const rateLimit = require('express-rate-limit');
const securityManager = require('../../db/SecurityManager');
const webaccess = require('./webaccess');
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
const limiter = rateLimit({ const limiter = rateLimit({
...settings.importExportRateLimiting, ...settings.importExportRateLimiting,
handler: (request: any) => { handler: (request: any) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) { if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs // when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' + console.warn(
`"${request.originalUrl}" for IP address ${request.ip}`); "Import/Export rate limiter triggered on " +
`"${request.originalUrl}" for IP address ${request.ip}`,
);
} }
}, },
}); });
// handle export requests // handle export requests
args.app.use('/p/:pad/:rev?/export/:type', limiter); args.app.use("/p/:pad/:rev?/export/:type", limiter);
args.app.get('/p/:pad/:rev?/export/:type', (req:any, res:any, next:Function) => { args.app.get(
"/p/:pad/:rev?/export/:type",
(req: any, res: any, next: Function) => {
(async () => { (async () => {
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; const types = ["pdf", "doc", "txt", "html", "odt", "etherpad"];
// send a 404 if we don't support this filetype // send a 404 if we don't support this filetype
if (types.indexOf(req.params.type) === -1) { if (types.indexOf(req.params.type) === -1) {
return next(); return next();
} }
// if abiword is disabled, and this is a format we only support with abiword, output a message // if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.exportAvailable() === 'no' && if (
['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { settings.exportAvailable() === "no" &&
console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` + ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1
' There is no converter configured'); ) {
console.error(
`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
" There is no converter configured",
);
// ACHTUNG: do not include req.params.type in res.send() because there is // ACHTUNG: do not include req.params.type in res.send() because there is
// no HTML escaping and it would lead to an XSS // no HTML escaping and it would lead to an XSS
res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' + res.send(
' or soffice (LibreOffice) in settings.json to enable this feature'); "This export is not enabled at this Etherpad instance. Set the path to Abiword" +
" or soffice (LibreOffice) in settings.json to enable this feature",
);
return; return;
} }
res.header('Access-Control-Allow-Origin', '*'); res.header("Access-Control-Allow-Origin", "*");
if (await hasPadAccess(req, res)) { if (await hasPadAccess(req, res)) {
let padId = req.params.pad; let padId = req.params.pad;
@ -60,26 +72,47 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
const exists = await padManager.doesPadExists(padId); const exists = await padManager.doesPadExists(padId);
if (!exists) { if (!exists) {
console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); console.warn(
`Someone tried to export a pad that doesn't exist (${padId})`,
);
return next(); return next();
} }
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); console.log(
await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); `Exporting pad "${req.params.pad}" in ${req.params.type} format`,
);
await exportHandler.doExport(
req,
res,
padId,
readOnlyId,
req.params.type,
);
} }
})().catch((err) => next(err || new Error(err))); })().catch((err) => next(err || new Error(err)));
}); },
);
// handle import requests // handle import requests
args.app.use('/p/:pad/import', limiter); args.app.use("/p/:pad/import", limiter);
args.app.post('/p/:pad/import', (req:any, res:any, next:Function) => { args.app.post("/p/:pad/import", (req: any, res: any, next: Function) => {
(async () => { (async () => {
// @ts-ignore // @ts-ignore
const {session: {user} = {}} = req; const {
const {accessStatus, authorID: authorId} = await securityManager.checkAccess( session: { user } = {},
req.params.pad, req.cookies.sessionID, req.cookies.token, user); } = req;
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { const { accessStatus, authorID: authorId } =
return res.status(403).send('Forbidden'); await securityManager.checkAccess(
req.params.pad,
req.cookies.sessionID,
req.cookies.token,
user,
);
if (
accessStatus !== "grant" ||
!webaccess.userCanModify(req.params.pad, req)
) {
return res.status(403).send("Forbidden");
} }
await importHandler.doImport(req, res, req.params.pad, authorId); await importHandler.doImport(req, res, req.params.pad, authorId);
})().catch((err) => next(err || new Error(err))); })().catch((err) => next(err || new Error(err)));

View file

@ -1,8 +1,10 @@
'use strict'; import type { ErrorCaused } from "../../types/ErrorCaused";
import type { MapArrayType } from "../../types/MapType";
import {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource"; import type {
import {MapArrayType} from "../../types/MapType"; OpenAPIOperations,
import {ErrorCaused} from "../../types/ErrorCaused"; OpenAPISuccessResponse,
SwaggerUIResource,
} from "../../types/SwaggerUIResource";
/** /**
* node/hooks/express/openapi.js * node/hooks/express/openapi.js
@ -18,260 +20,282 @@ import {ErrorCaused} from "../../types/ErrorCaused";
* - /rest/{version}/openapi.json * - /rest/{version}/openapi.json
*/ */
const OpenAPIBackend = require('openapi-backend').default; const OpenAPIBackend = require("openapi-backend").default;
const IncomingForm = require('formidable').IncomingForm; const IncomingForm = require("formidable").IncomingForm;
const cloneDeep = require('lodash.clonedeep'); const cloneDeep = require("lodash.clonedeep");
const createHTTPError = require('http-errors'); const createHTTPError = require("http-errors");
const apiHandler = require('../../handler/APIHandler'); const apiHandler = require("../../handler/APIHandler");
const settings = require('../../utils/Settings'); const settings = require("../../utils/Settings");
const log4js = require('log4js'); const log4js = require("log4js");
const logger = log4js.getLogger('API'); const logger = log4js.getLogger("API");
// https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0 // https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0
const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version const OPENAPI_VERSION = "3.0.2"; // Swagger/OAS version
const info = { const info = {
title: 'Etherpad API', title: "Etherpad API",
description: description:
'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous ' + "Etherpad is a real-time collaborative editor scalable to thousands of simultaneous " +
'real time users. It provides full data export capabilities, and runs on your server, ' + "real time users. It provides full data export capabilities, and runs on your server, " +
'under your control.', "under your control.",
termsOfService: 'https://etherpad.org/', termsOfService: "https://etherpad.org/",
contact: { contact: {
name: 'The Etherpad Foundation', name: "The Etherpad Foundation",
url: 'https://etherpad.org/', url: "https://etherpad.org/",
email: 'support@example.com', email: "support@example.com",
}, },
license: { license: {
name: 'Apache 2.0', name: "Apache 2.0",
url: 'https://www.apache.org/licenses/LICENSE-2.0.html', url: "https://www.apache.org/licenses/LICENSE-2.0.html",
}, },
version: apiHandler.latestApiVersion, version: apiHandler.latestApiVersion,
}; };
const APIPathStyle = { const APIPathStyle = {
FLAT: 'api', // flat paths e.g. /api/createGroup FLAT: "api", // flat paths e.g. /api/createGroup
REST: 'rest', // restful paths e.g. /rest/group/create REST: "rest", // restful paths e.g. /rest/group/create
}; };
// API resources - describe your API endpoints here // API resources - describe your API endpoints here
const resources: SwaggerUIResource = { const resources: SwaggerUIResource = {
// Group // Group
group: { group: {
create: { create: {
operationId: 'createGroup', operationId: "createGroup",
summary: 'creates a new group', summary: "creates a new group",
responseSchema: {groupID: {type: 'string'}}, responseSchema: { groupID: { type: "string" } },
}, },
createIfNotExistsFor: { createIfNotExistsFor: {
operationId: 'createGroupIfNotExistsFor', operationId: "createGroupIfNotExistsFor",
summary: 'this functions helps you to map your application group ids to Etherpad group ids', summary:
responseSchema: {groupID: {type: 'string'}}, "this functions helps you to map your application group ids to Etherpad group ids",
responseSchema: { groupID: { type: "string" } },
}, },
delete: { delete: {
operationId: 'deleteGroup', operationId: "deleteGroup",
summary: 'deletes a group', summary: "deletes a group",
}, },
listPads: { listPads: {
operationId: 'listPads', operationId: "listPads",
summary: 'returns all pads of this group', summary: "returns all pads of this group",
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
}, },
createPad: { createPad: {
operationId: 'createGroupPad', operationId: "createGroupPad",
summary: 'creates a new pad in this group', summary: "creates a new pad in this group",
}, },
listSessions: { listSessions: {
operationId: 'listSessionsOfGroup', operationId: "listSessionsOfGroup",
summary: '', summary: "",
responseSchema: { responseSchema: {
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}, sessions: {
type: "array",
items: { $ref: "#/components/schemas/SessionInfo" },
},
}, },
}, },
list: { list: {
operationId: 'listAllGroups', operationId: "listAllGroups",
summary: '', summary: "",
responseSchema: {groupIDs: {type: 'array', items: {type: 'string'}}}, responseSchema: {
groupIDs: { type: "array", items: { type: "string" } },
},
}, },
}, },
// Author // Author
author: { author: {
create: { create: {
operationId: 'createAuthor', operationId: "createAuthor",
summary: 'creates a new author', summary: "creates a new author",
responseSchema: {authorID: {type: 'string'}}, responseSchema: { authorID: { type: "string" } },
}, },
createIfNotExistsFor: { createIfNotExistsFor: {
operationId: 'createAuthorIfNotExistsFor', operationId: "createAuthorIfNotExistsFor",
summary: 'this functions helps you to map your application author ids to Etherpad author ids', summary:
responseSchema: {authorID: {type: 'string'}}, "this functions helps you to map your application author ids to Etherpad author ids",
responseSchema: { authorID: { type: "string" } },
}, },
listPads: { listPads: {
operationId: 'listPadsOfAuthor', operationId: "listPadsOfAuthor",
summary: 'returns an array of all pads this author contributed to', summary: "returns an array of all pads this author contributed to",
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
}, },
listSessions: { listSessions: {
operationId: 'listSessionsOfAuthor', operationId: "listSessionsOfAuthor",
summary: 'returns all sessions of an author', summary: "returns all sessions of an author",
responseSchema: { responseSchema: {
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}}, sessions: {
type: "array",
items: { $ref: "#/components/schemas/SessionInfo" },
},
}, },
}, },
// We need an operation that return a UserInfo so it can be picked up by the codegen :( // We need an operation that return a UserInfo so it can be picked up by the codegen :(
getName: { getName: {
operationId: 'getAuthorName', operationId: "getAuthorName",
summary: 'Returns the Author Name of the author', summary: "Returns the Author Name of the author",
responseSchema: {info: {$ref: '#/components/schemas/UserInfo'}}, responseSchema: { info: { $ref: "#/components/schemas/UserInfo" } },
}, },
}, },
// Session // Session
session: { session: {
create: { create: {
operationId: 'createSession', operationId: "createSession",
summary: 'creates a new session. validUntil is an unix timestamp in seconds', summary:
responseSchema: {sessionID: {type: 'string'}}, "creates a new session. validUntil is an unix timestamp in seconds",
responseSchema: { sessionID: { type: "string" } },
}, },
delete: { delete: {
operationId: 'deleteSession', operationId: "deleteSession",
summary: 'deletes a session', summary: "deletes a session",
}, },
// We need an operation that returns a SessionInfo so it can be picked up by the codegen :( // We need an operation that returns a SessionInfo so it can be picked up by the codegen :(
info: { info: {
operationId: 'getSessionInfo', operationId: "getSessionInfo",
summary: 'returns information about a session', summary: "returns information about a session",
responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}}, responseSchema: { info: { $ref: "#/components/schemas/SessionInfo" } },
}, },
}, },
// Pad // Pad
pad: { pad: {
listAll: { listAll: {
operationId: 'listAllPads', operationId: "listAllPads",
summary: 'list all the pads', summary: "list all the pads",
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}}, responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
}, },
createDiffHTML: { createDiffHTML: {
operationId: 'createDiffHTML', operationId: "createDiffHTML",
summary: '', summary: "",
responseSchema: {}, responseSchema: {},
}, },
create: { create: {
operationId: 'createPad', operationId: "createPad",
description: description:
'creates a new (non-group) pad. Note that if you need to create a group Pad, ' + "creates a new (non-group) pad. Note that if you need to create a group Pad, " +
'you should call createGroupPad', "you should call createGroupPad",
}, },
getText: { getText: {
operationId: 'getText', operationId: "getText",
summary: 'returns the text of a pad', summary: "returns the text of a pad",
responseSchema: {text: {type: 'string'}}, responseSchema: { text: { type: "string" } },
}, },
setText: { setText: {
operationId: 'setText', operationId: "setText",
summary: 'sets the text of a pad', summary: "sets the text of a pad",
}, },
getHTML: { getHTML: {
operationId: 'getHTML', operationId: "getHTML",
summary: 'returns the text of a pad formatted as HTML', summary: "returns the text of a pad formatted as HTML",
responseSchema: {html: {type: 'string'}}, responseSchema: { html: { type: "string" } },
}, },
setHTML: { setHTML: {
operationId: 'setHTML', operationId: "setHTML",
summary: 'sets the text of a pad with HTML', summary: "sets the text of a pad with HTML",
}, },
getRevisionsCount: { getRevisionsCount: {
operationId: 'getRevisionsCount', operationId: "getRevisionsCount",
summary: 'returns the number of revisions of this pad', summary: "returns the number of revisions of this pad",
responseSchema: {revisions: {type: 'integer'}}, responseSchema: { revisions: { type: "integer" } },
}, },
getLastEdited: { getLastEdited: {
operationId: 'getLastEdited', operationId: "getLastEdited",
summary: 'returns the timestamp of the last revision of the pad', summary: "returns the timestamp of the last revision of the pad",
responseSchema: {lastEdited: {type: 'integer'}}, responseSchema: { lastEdited: { type: "integer" } },
}, },
delete: { delete: {
operationId: 'deletePad', operationId: "deletePad",
summary: 'deletes a pad', summary: "deletes a pad",
}, },
getReadOnlyID: { getReadOnlyID: {
operationId: 'getReadOnlyID', operationId: "getReadOnlyID",
summary: 'returns the read only link of a pad', summary: "returns the read only link of a pad",
responseSchema: {readOnlyID: {type: 'string'}}, responseSchema: { readOnlyID: { type: "string" } },
}, },
setPublicStatus: { setPublicStatus: {
operationId: 'setPublicStatus', operationId: "setPublicStatus",
summary: 'sets a boolean for the public status of a pad', summary: "sets a boolean for the public status of a pad",
}, },
getPublicStatus: { getPublicStatus: {
operationId: 'getPublicStatus', operationId: "getPublicStatus",
summary: 'return true of false', summary: "return true of false",
responseSchema: {publicStatus: {type: 'boolean'}}, responseSchema: { publicStatus: { type: "boolean" } },
}, },
authors: { authors: {
operationId: 'listAuthorsOfPad', operationId: "listAuthorsOfPad",
summary: 'returns an array of authors who contributed to this pad', summary: "returns an array of authors who contributed to this pad",
responseSchema: {authorIDs: {type: 'array', items: {type: 'string'}}}, responseSchema: {
authorIDs: { type: "array", items: { type: "string" } },
},
}, },
usersCount: { usersCount: {
operationId: 'padUsersCount', operationId: "padUsersCount",
summary: 'returns the number of user that are currently editing this pad', summary: "returns the number of user that are currently editing this pad",
responseSchema: {padUsersCount: {type: 'integer'}}, responseSchema: { padUsersCount: { type: "integer" } },
}, },
users: { users: {
operationId: 'padUsers', operationId: "padUsers",
summary: 'returns the list of users that are currently editing this pad', summary: "returns the list of users that are currently editing this pad",
responseSchema: {padUsers: {type: 'array', items: {$ref: '#/components/schemas/UserInfo'}}}, responseSchema: {
padUsers: {
type: "array",
items: { $ref: "#/components/schemas/UserInfo" },
},
},
}, },
sendClientsMessage: { sendClientsMessage: {
operationId: 'sendClientsMessage', operationId: "sendClientsMessage",
summary: 'sends a custom message of type msg to the pad', summary: "sends a custom message of type msg to the pad",
}, },
checkToken: { checkToken: {
operationId: 'checkToken', operationId: "checkToken",
summary: 'returns ok when the current api token is valid', summary: "returns ok when the current api token is valid",
}, },
getChatHistory: { getChatHistory: {
operationId: 'getChatHistory', operationId: "getChatHistory",
summary: 'returns the chat history', summary: "returns the chat history",
responseSchema: {messages: {type: 'array', items: {$ref: '#/components/schemas/Message'}}}, responseSchema: {
messages: {
type: "array",
items: { $ref: "#/components/schemas/Message" },
},
},
}, },
// We need an operation that returns a Message so it can be picked up by the codegen :( // We need an operation that returns a Message so it can be picked up by the codegen :(
getChatHead: { getChatHead: {
operationId: 'getChatHead', operationId: "getChatHead",
summary: 'returns the chatHead (chat-message) of the pad', summary: "returns the chatHead (chat-message) of the pad",
responseSchema: {chatHead: {$ref: '#/components/schemas/Message'}}, responseSchema: { chatHead: { $ref: "#/components/schemas/Message" } },
}, },
appendChatMessage: { appendChatMessage: {
operationId: 'appendChatMessage', operationId: "appendChatMessage",
summary: 'appends a chat message', summary: "appends a chat message",
}, },
}, },
}; };
const defaultResponses = { const defaultResponses = {
Success: { Success: {
description: 'ok (code 0)', description: "ok (code 0)",
content: { content: {
'application/json': { "application/json": {
schema: { schema: {
type: 'object', type: "object",
properties: { properties: {
code: { code: {
type: 'integer', type: "integer",
example: 0, example: 0,
}, },
message: { message: {
type: 'string', type: "string",
example: 'ok', example: "ok",
}, },
data: { data: {
type: 'object', type: "object",
example: null, example: null,
}, },
}, },
@ -280,22 +304,22 @@ const defaultResponses = {
}, },
}, },
ApiError: { ApiError: {
description: 'generic api error (code 1)', description: "generic api error (code 1)",
content: { content: {
'application/json': { "application/json": {
schema: { schema: {
type: 'object', type: "object",
properties: { properties: {
code: { code: {
type: 'integer', type: "integer",
example: 1, example: 1,
}, },
message: { message: {
type: 'string', type: "string",
example: 'error message', example: "error message",
}, },
data: { data: {
type: 'object', type: "object",
example: null, example: null,
}, },
}, },
@ -304,22 +328,22 @@ const defaultResponses = {
}, },
}, },
InternalError: { InternalError: {
description: 'internal api error (code 2)', description: "internal api error (code 2)",
content: { content: {
'application/json': { "application/json": {
schema: { schema: {
type: 'object', type: "object",
properties: { properties: {
code: { code: {
type: 'integer', type: "integer",
example: 2, example: 2,
}, },
message: { message: {
type: 'string', type: "string",
example: 'internal error', example: "internal error",
}, },
data: { data: {
type: 'object', type: "object",
example: null, example: null,
}, },
}, },
@ -328,22 +352,22 @@ const defaultResponses = {
}, },
}, },
NotFound: { NotFound: {
description: 'no such function (code 4)', description: "no such function (code 4)",
content: { content: {
'application/json': { "application/json": {
schema: { schema: {
type: 'object', type: "object",
properties: { properties: {
code: { code: {
type: 'integer', type: "integer",
example: 3, example: 3,
}, },
message: { message: {
type: 'string', type: "string",
example: 'no such function', example: "no such function",
}, },
data: { data: {
type: 'object', type: "object",
example: null, example: null,
}, },
}, },
@ -352,22 +376,22 @@ const defaultResponses = {
}, },
}, },
Unauthorized: { Unauthorized: {
description: 'no or wrong API key (code 4)', description: "no or wrong API key (code 4)",
content: { content: {
'application/json': { "application/json": {
schema: { schema: {
type: 'object', type: "object",
properties: { properties: {
code: { code: {
type: 'integer', type: "integer",
example: 4, example: 4,
}, },
message: { message: {
type: 'string', type: "string",
example: 'no or wrong API key', example: "no or wrong API key",
}, },
data: { data: {
type: 'object', type: "object",
example: null, example: null,
}, },
}, },
@ -379,16 +403,16 @@ const defaultResponses = {
const defaultResponseRefs: OpenAPISuccessResponse = { const defaultResponseRefs: OpenAPISuccessResponse = {
200: { 200: {
$ref: '#/components/responses/Success', $ref: "#/components/responses/Success",
}, },
400: { 400: {
$ref: '#/components/responses/ApiError', $ref: "#/components/responses/ApiError",
}, },
401: { 401: {
$ref: '#/components/responses/Unauthorized', $ref: "#/components/responses/Unauthorized",
}, },
500: { 500: {
$ref: '#/components/responses/InternalError', $ref: "#/components/responses/InternalError",
}, },
}; };
@ -402,8 +426,8 @@ for (const [resource, actions] of Object.entries(resources)) {
const responses: OpenAPISuccessResponse = { ...defaultResponseRefs }; const responses: OpenAPISuccessResponse = { ...defaultResponseRefs };
if (responseSchema) { if (responseSchema) {
responses[200] = cloneDeep(defaultResponses.Success); responses[200] = cloneDeep(defaultResponses.Success);
responses[200].content!['application/json'].schema.properties.data = { responses[200].content!["application/json"].schema.properties.data = {
type: 'object', type: "object",
properties: responseSchema, properties: responseSchema,
}; };
} }
@ -419,7 +443,10 @@ for (const [resource, actions] of Object.entries(resources)) {
} }
} }
const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) => { const generateDefinitionForVersion = (
version: string,
style = APIPathStyle.FLAT,
) => {
const definition = { const definition = {
openapi: OPENAPI_VERSION, openapi: OPENAPI_VERSION,
info, info,
@ -428,53 +455,53 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
parameters: {}, parameters: {},
schemas: { schemas: {
SessionInfo: { SessionInfo: {
type: 'object', type: "object",
properties: { properties: {
id: { id: {
type: 'string', type: "string",
}, },
authorID: { authorID: {
type: 'string', type: "string",
}, },
groupID: { groupID: {
type: 'string', type: "string",
}, },
validUntil: { validUntil: {
type: 'integer', type: "integer",
}, },
}, },
}, },
UserInfo: { UserInfo: {
type: 'object', type: "object",
properties: { properties: {
id: { id: {
type: 'string', type: "string",
}, },
colorId: { colorId: {
type: 'string', type: "string",
}, },
name: { name: {
type: 'string', type: "string",
}, },
timestamp: { timestamp: {
type: 'integer', type: "integer",
}, },
}, },
}, },
Message: { Message: {
type: 'object', type: "object",
properties: { properties: {
text: { text: {
type: 'string', type: "string",
}, },
userId: { userId: {
type: 'string', type: "string",
}, },
userName: { userName: {
type: 'string', type: "string",
}, },
time: { time: {
type: 'integer', type: "integer",
}, },
}, },
}, },
@ -493,9 +520,9 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
openid: "openid", openid: "openid",
profile: "profile", profile: "profile",
email: "email", email: "email",
admin: "admin" admin: "admin",
} },
} },
}, },
}, },
}, },
@ -519,15 +546,17 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
// set parameters // set parameters
operation.parameters = operation.parameters || []; operation.parameters = operation.parameters || [];
for (const paramName of apiHandler.version[version][funcName]) { for (const paramName of apiHandler.version[version][funcName]) {
operation.parameters.push({$ref: `#/components/parameters/${paramName}`}); operation.parameters.push({
$ref: `#/components/parameters/${paramName}`,
});
// @ts-ignore // @ts-ignore
if (!definition.components.parameters[paramName]) { if (!definition.components.parameters[paramName]) {
// @ts-ignore // @ts-ignore
definition.components.parameters[paramName] = { definition.components.parameters[paramName] = {
name: paramName, name: paramName,
in: 'query', in: "query",
schema: { schema: {
type: 'string', type: "string",
}, },
}; };
} }
@ -572,16 +601,22 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
// serve version specific openapi definition // serve version specific openapi definition
app.get(`${apiRoot}/openapi.json`, (req: any, res: any) => { app.get(`${apiRoot}/openapi.json`, (req: any, res: any) => {
// For openapi definitions, wide CORS is probably fine // For openapi definitions, wide CORS is probably fine
res.header('Access-Control-Allow-Origin', '*'); res.header("Access-Control-Allow-Origin", "*");
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); res.json({
...definition,
servers: [generateServerForApiVersion(apiRoot, req)],
});
}); });
// serve latest openapi definition file under /api/openapi.json // serve latest openapi definition file under /api/openapi.json
const isLatestAPIVersion = version === apiHandler.latestApiVersion; const isLatestAPIVersion = version === apiHandler.latestApiVersion;
if (isLatestAPIVersion) { if (isLatestAPIVersion) {
app.get(`/${style}/openapi.json`, (req: any, res: any) => { app.get(`/${style}/openapi.json`, (req: any, res: any) => {
res.header('Access-Control-Allow-Origin', '*'); res.header("Access-Control-Allow-Origin", "*");
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); res.json({
...definition,
servers: [generateServerForApiVersion(apiRoot, req)],
});
}); });
} }
@ -597,10 +632,10 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
// register default handlers // register default handlers
api.register({ api.register({
notFound: () => { notFound: () => {
throw new createHTTPError.NotFound('no such function'); throw new createHTTPError.NotFound("no such function");
}, },
notImplemented: () => { notImplemented: () => {
throw new createHTTPError.NotImplemented('function not implemented'); throw new createHTTPError.NotImplemented("function not implemented");
}, },
}); });
@ -612,7 +647,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
// read form data if method was POST // read form data if method was POST
let formData: MapArrayType<any> = {}; let formData: MapArrayType<any> = {};
if (c.request.method === 'post') { if (c.request.method === "post") {
const form = new IncomingForm(); const form = new IncomingForm();
formData = (await form.parse(req))[0]; formData = (await form.parse(req))[0];
for (const k of Object.keys(formData)) { for (const k of Object.keys(formData)) {
@ -625,7 +660,9 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
const fields = Object.assign({}, header, params, query, formData); const fields = Object.assign({}, header, params, query, formData);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`); logger.debug(
`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`,
);
} }
// pass to api handler // pass to api handler
@ -633,12 +670,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
try { try {
data = await apiHandler.handle(version, funcName, fields, req, res); data = await apiHandler.handle(version, funcName, fields, req, res);
} catch (err) { } catch (err) {
const errCaused = err as ErrorCaused const errCaused = err as ErrorCaused;
// convert all errors to http errors // convert all errors to http errors
if (createHTTPError.isHttpError(err)) { if (createHTTPError.isHttpError(err)) {
// pass http errors thrown by handler forward // pass http errors thrown by handler forward
throw err; throw err;
} else if (errCaused.name === 'apierror') { } else if (errCaused.name === "apierror") {
// parameters were wrong and the api stopped execution, pass the error // parameters were wrong and the api stopped execution, pass the error
// convert to http error // convert to http error
throw new createHTTPError.BadRequest(errCaused.message); throw new createHTTPError.BadRequest(errCaused.message);
@ -646,12 +683,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
// an unknown error happened // an unknown error happened
// log it and throw internal error // log it and throw internal error
logger.error(errCaused.stack || errCaused.toString()); logger.error(errCaused.stack || errCaused.toString());
throw new createHTTPError.InternalError('internal error'); throw new createHTTPError.InternalError("internal error");
} }
} }
// return in common format // return in common format
const response = {code: 0, message: 'ok', data: data || null}; const response = { code: 0, message: "ok", data: data || null };
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`); logger.debug(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`);
@ -674,12 +711,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
if (style === APIPathStyle.REST) { if (style === APIPathStyle.REST) {
// @TODO: Don't allow CORS from everywhere // @TODO: Don't allow CORS from everywhere
// This is purely to maintain compatibility with old swagger-node-express // This is purely to maintain compatibility with old swagger-node-express
res.header('Access-Control-Allow-Origin', '*'); res.header("Access-Control-Allow-Origin", "*");
} }
// pass to openapi-backend handler // pass to openapi-backend handler
response = await api.handleRequest(req, req, res); response = await api.handleRequest(req, req, res);
} catch (err) { } catch (err) {
const errCaused = err as ErrorCaused const errCaused = err as ErrorCaused;
// handle http errors // handle http errors
// @ts-ignore // @ts-ignore
res.statusCode = errCaused.statusCode || 500; res.statusCode = errCaused.statusCode || 500;
@ -723,7 +760,10 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
* @param {APIPathStyle} style The style of the API path * @param {APIPathStyle} style The style of the API path
* @return {String} The root path for the API version * @return {String} The root path for the API version
*/ */
const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): string => `/${style}/${version}`; const getApiRootForVersion = (
version: string,
style: any = APIPathStyle.FLAT,
): string => `/${style}/${version}`;
/** /**
* Helper to generate an OpenAPI server object when serving definitions * Helper to generate an OpenAPI server object when serving definitions
@ -731,8 +771,11 @@ const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): st
* @param {Request} req The express request object * @param {Request} req The express request object
* @return {url: String} The server object for the OpenAPI definition location * @return {url: String} The server object for the OpenAPI definition location
*/ */
const generateServerForApiVersion = (apiRoot:string, req:any): { const generateServerForApiVersion = (
url:string apiRoot: string,
req: any,
): {
url: string;
} => ({ } => ({
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`, url: `${settings.ssl ? "https" : "http"}://${req.headers.host}${apiRoot}`,
}); });

View file

@ -1,16 +1,18 @@
'use strict'; import type { ArgsExpressType } from "../../types/ArgsExpressType";
import {ArgsExpressType} from "../../types/ArgsExpressType"; const padManager = require("../../db/PadManager");
const padManager = require('../../db/PadManager'); exports.expressCreateServer = (
hookName: string,
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => { args: ArgsExpressType,
cb: Function,
) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html // redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', (req:any, res:any, next:Function, padId:string) => { args.app.param("pad", (req: any, res: any, next: Function, padId: string) => {
(async () => { (async () => {
// ensure the padname is valid and the url doesn't end with a / // ensure the padname is valid and the url doesn't end with a /
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
res.status(404).send('Such a padname is forbidden'); res.status(404).send("Such a padname is forbidden");
return; return;
} }
@ -22,9 +24,14 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
} else { } else {
// the pad id was sanitized, so we redirect to the sanitized version // the pad id was sanitized, so we redirect to the sanitized version
const realURL = const realURL =
encodeURIComponent(sanitizedPadId) + new URL(req.url, 'http://invalid.invalid').search; encodeURIComponent(sanitizedPadId) +
res.header('Location', realURL); new URL(req.url, "http://invalid.invalid").search;
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`); res.header("Location", realURL);
res
.status(302)
.send(
`You should be redirected to <a href="${realURL}">${realURL}</a>`,
);
} }
})().catch((err) => next(err || new Error(err))); })().catch((err) => next(err || new Error(err)));
}); });

View file

@ -1,25 +1,23 @@
'use strict'; import type { ArgsExpressType } from "../../types/ArgsExpressType";
import {ArgsExpressType} from "../../types/ArgsExpressType"; import events from "events";
const express = require("../express");
import events from 'events'; import log4js from "log4js";
const express = require('../express'); const proxyaddr = require("proxy-addr");
import log4js from 'log4js'; const settings = require("../../utils/Settings");
const proxyaddr = require('proxy-addr'); import { Server, type Socket } from "socket.io";
const settings = require('../../utils/Settings'); const socketIORouter = require("../../handler/SocketIORouter");
import {Server, Socket} from 'socket.io' const hooks = require("../../../static/js/pluginfw/hooks");
const socketIORouter = require('../../handler/SocketIORouter'); const padMessageHandler = require("../../handler/PadMessageHandler");
const hooks = require('../../../static/js/pluginfw/hooks');
const padMessageHandler = require('../../handler/PadMessageHandler');
let io: any; let io: any;
const logger = log4js.getLogger('socket.io'); const logger = log4js.getLogger("socket.io");
const sockets = new Set(); const sockets = new Set();
const socketsEvents = new events.EventEmitter(); const socketsEvents = new events.EventEmitter();
export const expressCloseServer = async () => { export const expressCloseServer = async () => {
if (io == null) return; if (io == null) return;
logger.info('Closing socket.io engine...'); logger.info("Closing socket.io engine...");
// Close the socket.io engine to disconnect existing clients and reject new clients. Don't call // Close the socket.io engine to disconnect existing clients and reject new clients. Don't call
// io.close() because that closes the underlying HTTP server, which is already done elsewhere. // io.close() because that closes the underlying HTTP server, which is already done elsewhere.
// (Closing an HTTP server twice throws an exception.) The `engine` property of socket.io Server // (Closing an HTTP server twice throws an exception.) The `engine` property of socket.io Server
@ -39,21 +37,25 @@ export const expressCloseServer = async () => {
// ourselves, so that is what we do. // ourselves, so that is what we do.
let lastLogged = 0; let lastLogged = 0;
while (sockets.size > 0 && !settings.enableAdminUITests) { while (sockets.size > 0 && !settings.enableAdminUITests) {
if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs. if (Date.now() - lastLogged > 1000) {
logger.info(`Waiting for ${sockets.size} socket.io clients to disconnect...`); // Rate limit to avoid filling logs.
logger.info(
`Waiting for ${sockets.size} socket.io clients to disconnect...`,
);
lastLogged = Date.now(); lastLogged = Date.now();
} }
await events.once(socketsEvents, 'updated'); await events.once(socketsEvents, "updated");
} }
logger.info('All socket.io clients have disconnected'); logger.info("All socket.io clients have disconnected");
}; };
const socketSessionMiddleware = (args: any) => (socket: any, next: Function) => { const socketSessionMiddleware =
(args: any) => (socket: any, next: Function) => {
const req = socket.request; const req = socket.request;
// Express sets req.ip but socket.io does not. Replicate Express's behavior here. // Express sets req.ip but socket.io does not. Replicate Express's behavior here.
if (req.ip == null) { if (req.ip == null) {
if (settings.trustProxy) { if (settings.trustProxy) {
req.ip = proxyaddr(req, args.app.get('trust proxy fn')); req.ip = proxyaddr(req, args.app.get("trust proxy fn"));
} else { } else {
req.ip = socket.handshake.address; req.ip = socket.handshake.address;
} }
@ -65,7 +67,11 @@ const socketSessionMiddleware = (args: any) => (socket: any, next: Function) =>
express.sessionMiddleware(req, {}, next); express.sessionMiddleware(req, {}, next);
}; };
export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => { export const expressCreateServer = (
hookName: string,
args: ArgsExpressType,
cb: Function,
) => {
// init socket.io and redirect all requests to the MessageHandler // init socket.io and redirect all requests to the MessageHandler
// there shouldn't be a browser that isn't compatible to all // there shouldn't be a browser that isn't compatible to all
// transports in this list at once // transports in this list at once
@ -74,25 +80,24 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
transports: settings.socketTransportProtocols, transports: settings.socketTransportProtocols,
cookie: false, cookie: false,
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
}) });
const handleConnection = (socket: Socket) => { const handleConnection = (socket: Socket) => {
sockets.add(socket); sockets.add(socket);
socketsEvents.emit('updated'); socketsEvents.emit("updated");
// https://socket.io/docs/v3/faq/index.html // https://socket.io/docs/v3/faq/index.html
// @ts-ignore // @ts-ignore
const session = socket.request.session; const session = socket.request.session;
session.connections++; session.connections++;
session.save(); session.save();
socket.on('disconnect', () => { socket.on("disconnect", () => {
sockets.delete(socket); sockets.delete(socket);
socketsEvents.emit('updated'); socketsEvents.emit("updated");
}); });
} };
const renewSession = (socket: any, next: Function) => { const renewSession = (socket: any, next: Function) => {
socket.conn.on('packet', (packet:string) => { socket.conn.on("packet", (packet: string) => {
// Tell express-session that the session is still active. The session store can use these // Tell express-session that the session is still active. The session store can use these
// touch events to defer automatic session cleanup, and if express-session is configured with // touch events to defer automatic session cleanup, and if express-session is configured with
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not // rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not
@ -102,22 +107,21 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
if (socket.request.session != null) socket.request.session.touch(); if (socket.request.session != null) socket.request.session.touch();
}); });
next(); next();
} };
io.on("connection", handleConnection);
io.on('connection', handleConnection);
io.use(socketSessionMiddleware(args)); io.use(socketSessionMiddleware(args));
// Temporary workaround so all clients go through middleware and handle connection // Temporary workaround so all clients go through middleware and handle connection
io.of('/pluginfw/installer') io.of("/pluginfw/installer")
.on('connection',handleConnection) .on("connection", handleConnection)
.use(socketSessionMiddleware(args)) .use(socketSessionMiddleware(args))
.use(renewSession) .use(renewSession);
io.of('/settings') io.of("/settings")
.on('connection',handleConnection) .on("connection", handleConnection)
.use(socketSessionMiddleware(args)) .use(socketSessionMiddleware(args))
.use(renewSession) .use(renewSession);
io.use(renewSession); io.use(renewSession);
@ -134,9 +138,9 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
// Initialize the Socket.IO Router // Initialize the Socket.IO Router
socketIORouter.setSocketIO(io); socketIORouter.setSocketIO(io);
socketIORouter.addComponent('pad', padMessageHandler); socketIORouter.addComponent("pad", padMessageHandler);
hooks.callAll('socketio', {app: args.app, io, server: args.server}); hooks.callAll("socketio", { app: args.app, io, server: args.server });
return cb(); return cb();
}; };

View file

@ -1,62 +1,76 @@
'use strict'; const path = require("path");
const eejs = require("../../eejs");
const path = require('path'); const fs = require("fs");
const eejs = require('../../eejs');
const fs = require('fs');
const fsp = fs.promises; const fsp = fs.promises;
const toolbar = require('../../utils/toolbar'); const toolbar = require("../../utils/toolbar");
const hooks = require('../../../static/js/pluginfw/hooks'); const hooks = require("../../../static/js/pluginfw/hooks");
const settings = require('../../utils/Settings'); const settings = require("../../utils/Settings");
const util = require('util'); const util = require("util");
const webaccess = require('./webaccess'); const webaccess = require("./webaccess");
exports.expressPreSession = async (hookName: string, { app }: any) => { exports.expressPreSession = async (hookName: string, { app }: any) => {
// This endpoint is intended to conform to: // This endpoint is intended to conform to:
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
app.get('/health', (req:any, res:any) => { app.get("/health", (req: any, res: any) => {
res.set('Content-Type', 'application/health+json'); res.set("Content-Type", "application/health+json");
res.json({ res.json({
status: 'pass', status: "pass",
releaseId: settings.getEpVersion(), releaseId: settings.getEpVersion(),
}); });
}); });
app.get('/stats', (req:any, res:any) => { app.get("/stats", (req: any, res: any) => {
res.json(require('../../stats').toJSON()); res.json(require("../../stats").toJSON());
}); });
app.get('/javascript', (req:any, res:any) => { app.get("/javascript", (req: any, res: any) => {
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); res.send(
eejs.require("ep_etherpad-lite/templates/javascript.html", { req }),
);
}); });
app.get('/robots.txt', (req:any, res:any) => { app.get("/robots.txt", (req: any, res: any) => {
let filePath = let filePath = path.join(
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); settings.root,
"src",
"static",
"skins",
settings.skinName,
"robots.txt",
);
res.sendFile(filePath, (err: any) => { res.sendFile(filePath, (err: any) => {
// there is no custom robots.txt, send the default robots.txt which dissallows all // there is no custom robots.txt, send the default robots.txt which dissallows all
if (err) { if (err) {
filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); filePath = path.join(settings.root, "src", "static", "robots.txt");
res.sendFile(filePath); res.sendFile(filePath);
} }
}); });
}); });
app.get('/favicon.ico', (req:any, res:any, next:Function) => { app.get("/favicon.ico", (req: any, res: any, next: Function) => {
(async () => { (async () => {
/* /*
If this is a url we simply redirect to that one. If this is a url we simply redirect to that one.
*/ */
if (settings.favicon && settings.favicon.startsWith('http')) { if (settings.favicon && settings.favicon.startsWith("http")) {
res.redirect(settings.favicon); res.redirect(settings.favicon);
res.send(); res.send();
return; return;
} }
const fns = [ const fns = [
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), ...(settings.favicon
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), ? [path.resolve(settings.root, settings.favicon)]
path.join(settings.root, 'src', 'static', 'favicon.ico'), : []),
path.join(
settings.root,
"src",
"static",
"skins",
settings.skinName,
"favicon.ico",
),
path.join(settings.root, "src", "static", "favicon.ico"),
]; ];
for (const fn of fns) { for (const fn of fns) {
try { try {
@ -64,7 +78,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
} catch (err) { } catch (err) {
continue; continue;
} }
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
await util.promisify(res.sendFile.bind(res))(fn); await util.promisify(res.sendFile.bind(res))(fn);
return; return;
} }
@ -75,46 +89,50 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
exports.expressCreateServer = (hookName: string, args: any, cb: Function) => { exports.expressCreateServer = (hookName: string, args: any, cb: Function) => {
// serve index.html under / // serve index.html under /
args.app.get('/', (req:any, res:any) => { args.app.get("/", (req: any, res: any) => {
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); res.send(eejs.require("ep_etherpad-lite/templates/index.html", { req }));
}); });
// serve pad.html under /p // serve pad.html under /p
args.app.get('/p/:pad', (req:any, res:any, next:Function) => { args.app.get("/p/:pad", (req: any, res: any, next: Function) => {
// The below might break for pads being rewritten // The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req); const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', { hooks.callAll("padInitToolbar", {
toolbar, toolbar,
isReadOnly, isReadOnly,
}); });
// can be removed when require-kernel is dropped // can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\''); res.header("Feature-Policy", "sync-xhr 'self'");
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', { res.send(
eejs.require("ep_etherpad-lite/templates/pad.html", {
req, req,
toolbar, toolbar,
isReadOnly, isReadOnly,
})); }),
);
}); });
// serve timeslider.html under /p/$padname/timeslider // serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => { args.app.get("/p/:pad/timeslider", (req: any, res: any, next: Function) => {
hooks.callAll('padInitToolbar', { hooks.callAll("padInitToolbar", {
toolbar, toolbar,
}); });
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { res.send(
eejs.require("ep_etherpad-lite/templates/timeslider.html", {
req, req,
toolbar, toolbar,
})); }),
);
}); });
// The client occasionally polls this endpoint to get an updated expiration for the express_sid // The client occasionally polls this endpoint to get an updated expiration for the express_sid
// cookie. This handler must be installed after the express-session middleware. // cookie. This handler must be installed after the express-session middleware.
args.app.put('/_extendExpressSessionLifetime', (req:any, res:any) => { args.app.put("/_extendExpressSessionLifetime", (req: any, res: any) => {
// express-session automatically calls req.session.touch() so we don't need to do it here. // express-session automatically calls req.session.touch() so we don't need to do it here.
res.json({status: 'ok'}); res.json({ status: "ok" });
}); });
return cb(); return cb();

View file

@ -1,33 +1,37 @@
'use strict'; import type { MapArrayType } from "../../types/MapType";
import type { PartType } from "../../types/PartType";
import {MapArrayType} from "../../types/MapType"; const fs = require("fs").promises;
import {PartType} from "../../types/PartType"; const minify = require("../../utils/Minify");
const path = require("path");
const fs = require('fs').promises; const plugins = require("../../../static/js/pluginfw/plugin_defs");
const minify = require('../../utils/Minify'); const settings = require("../../utils/Settings");
const path = require('path'); import CachingMiddleware from "../../utils/caching_middleware";
const plugins = require('../../../static/js/pluginfw/plugin_defs'); const Yajsml = require("etherpad-yajsml");
const settings = require('../../utils/Settings');
import CachingMiddleware from '../../utils/caching_middleware';
const Yajsml = require('etherpad-yajsml');
// Rewrite tar to include modules with no extensions and proper rooted paths. // Rewrite tar to include modules with no extensions and proper rooted paths.
const getTar = async () => { const getTar = async () => {
const prefixLocalLibraryPath = (path: string) => { const prefixLocalLibraryPath = (path: string) => {
if (path.charAt(0) === '$') { if (path.charAt(0) === "$") {
return path.slice(1); return path.slice(1);
} else { } else {
return `ep_etherpad-lite/static/js/${path}`; return `ep_etherpad-lite/static/js/${path}`;
} }
}; };
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8'); const tarJson = await fs.readFile(
path.join(settings.root, "src/node/utils/tar.json"),
"utf8",
);
const tar: MapArrayType<string[]> = {}; const tar: MapArrayType<string[]> = {};
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [string, string[]][]) { for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [
string,
string[],
][]) {
const files = relativeFiles.map(prefixLocalLibraryPath); const files = relativeFiles.map(prefixLocalLibraryPath);
tar[prefixLocalLibraryPath(key)] = files tar[prefixLocalLibraryPath(key)] = files
.concat(files.map((p) => p.replace(/\.js$/, ''))) .concat(files.map((p) => p.replace(/\.js$/, "")))
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`)); .concat(files.map((p) => `${p.replace(/\.js$/, "")}/index.js`));
} }
return tar; return tar;
}; };
@ -41,21 +45,23 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
// Minify will serve static files compressed (minify enabled). It also has // Minify will serve static files compressed (minify enabled). It also has
// file-specific hacks for ace/require-kernel/etc. // file-specific hacks for ace/require-kernel/etc.
app.all('/static/:filename(*)', minify.minify); app.all("/static/:filename(*)", minify.minify);
// Setup middleware that will package JavaScript files served by minify for // Setup middleware that will package JavaScript files served by minify for
// CommonJS loader on the client-side. // CommonJS loader on the client-side.
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI. // Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
const jsServer = new (Yajsml.Server)({ const jsServer = new Yajsml.Server({
rootPath: 'javascripts/src/', rootPath: "javascripts/src/",
rootURI: 'http://invalid.invalid/static/js/', rootURI: "http://invalid.invalid/static/js/",
libraryPath: 'javascripts/lib/', libraryPath: "javascripts/lib/",
libraryURI: 'http://invalid.invalid/static/plugins/', libraryURI: "http://invalid.invalid/static/plugins/",
requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround. requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround.
}); });
const StaticAssociator = Yajsml.associators.StaticAssociator; const StaticAssociator = Yajsml.associators.StaticAssociator;
const associations = Yajsml.associators.associationsForSimpleMapping(await getTar()); const associations = Yajsml.associators.associationsForSimpleMapping(
await getTar(),
);
const associator = new StaticAssociator(associations); const associator = new StaticAssociator(associations);
jsServer.setAssociator(associator); jsServer.setAssociator(associator);
@ -64,18 +70,25 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
// serve plugin definitions // serve plugin definitions
// not very static, but served here so that client can do // not very static, but served here so that client can do
// require("pluginfw/static/js/plugin-definitions.js"); // require("pluginfw/static/js/plugin-definitions.js");
app.get('/pluginfw/plugin-definitions.json', (req: any, res:any, next:Function) => { app.get(
const clientParts = plugins.parts.filter((part: PartType) => part.client_hooks != null); "/pluginfw/plugin-definitions.json",
(req: any, res: any, next: Function) => {
const clientParts = plugins.parts.filter(
(part: PartType) => part.client_hooks != null,
);
const clientPlugins: MapArrayType<string> = {}; const clientPlugins: MapArrayType<string> = {};
for (const name of new Set(clientParts.map((part: PartType) => part.plugin))) { for (const name of new Set(
clientParts.map((part: PartType) => part.plugin),
)) {
// @ts-ignore // @ts-ignore
clientPlugins[name] = { ...plugins.plugins[name] }; clientPlugins[name] = { ...plugins.plugins[name] };
// @ts-ignore // @ts-ignore
delete clientPlugins[name].package; delete clientPlugins[name].package;
} }
res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader("Content-Type", "application/json; charset=utf-8");
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
res.write(JSON.stringify({ plugins: clientPlugins, parts: clientParts })); res.write(JSON.stringify({ plugins: clientPlugins, parts: clientParts }));
res.end(); res.end();
}); },
);
}; };

View file

@ -1,13 +1,11 @@
'use strict'; import type { Dirent } from "node:fs";
import type { PluginDef } from "../../types/PartType";
import {Dirent} from "node:fs"; const path = require("path");
import {PluginDef} from "../../types/PartType"; const fsp = require("fs").promises;
const plugins = require("../../../static/js/pluginfw/plugin_defs");
const path = require('path'); const sanitizePathname = require("../../utils/sanitizePathname");
const fsp = require('fs').promises; const settings = require("../../utils/Settings");
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const sanitizePathname = require('../../utils/sanitizePathname');
const settings = require('../../utils/Settings');
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/' // Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
// instead of path.sep to separate pathname components. // instead of path.sep to separate pathname components.
@ -16,68 +14,88 @@ const findSpecs = async (specDir: string) => {
try { try {
dirents = await fsp.readdir(specDir, { withFileTypes: true }); dirents = await fsp.readdir(specDir, { withFileTypes: true });
} catch (err: any) { } catch (err: any) {
if (['ENOENT', 'ENOTDIR'].includes(err.code)) return []; if (["ENOENT", "ENOTDIR"].includes(err.code)) return [];
throw err; throw err;
} }
const specs: string[] = []; const specs: string[] = [];
await Promise.all(dirents.map(async (dirent) => { await Promise.all(
dirents.map(async (dirent) => {
if (dirent.isDirectory()) { if (dirent.isDirectory()) {
const subdirSpecs = await findSpecs(path.join(specDir, dirent.name)); const subdirSpecs = await findSpecs(path.join(specDir, dirent.name));
specs.push(...subdirSpecs.map((spec) => `${dirent.name}/${spec}`)); specs.push(...subdirSpecs.map((spec) => `${dirent.name}/${spec}`));
return; return;
} }
if (!dirent.name.endsWith('.js')) return; if (!dirent.name.endsWith(".js")) return;
specs.push(dirent.name); specs.push(dirent.name);
})); }),
);
return specs; return specs;
}; };
exports.expressPreSession = async (hookName: string, { app }: any) => { exports.expressPreSession = async (hookName: string, { app }: any) => {
app.get('/tests/frontend/frontendTestSpecs.json', (req:any, res:any, next:Function) => { app.get(
"/tests/frontend/frontendTestSpecs.json",
(req: any, res: any, next: Function) => {
(async () => { (async () => {
const modules: string[] = []; const modules: string[] = [];
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { await Promise.all(
let {package: {path: pluginPath}} = def as PluginDef; Object.entries(plugins.plugins).map(async ([plugin, def]) => {
let {
package: { path: pluginPath },
} = def as PluginDef;
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep; if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; const specDir = `${
for (const spec of await findSpecs(path.join(pluginPath, specDir))) { plugin === "ep_etherpad-lite" ? "" : "static/"
if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests && }tests/frontend/specs`;
spec.startsWith('admin')) continue; for (const spec of await findSpecs(
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`); path.join(pluginPath, specDir),
)) {
if (
plugin === "ep_etherpad-lite" &&
!settings.enableAdminUITests &&
spec.startsWith("admin")
)
continue;
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, "")}`);
} }
})); }),
);
// Sort plugin tests before core tests. // Sort plugin tests before core tests.
modules.sort((a, b) => { modules.sort((a, b) => {
a = String(a); a = String(a);
b = String(b); b = String(b);
const aCore = a.startsWith('ep_etherpad-lite/'); const aCore = a.startsWith("ep_etherpad-lite/");
const bCore = b.startsWith('ep_etherpad-lite/'); const bCore = b.startsWith("ep_etherpad-lite/");
if (aCore === bCore) return a.localeCompare(b); if (aCore === bCore) return a.localeCompare(b);
return aCore ? 1 : -1; return aCore ? 1 : -1;
}); });
console.debug('Sent browser the following test spec modules:', modules); console.debug("Sent browser the following test spec modules:", modules);
res.json(modules); res.json(modules);
})().catch((err) => next(err || new Error(err))); })().catch((err) => next(err || new Error(err)));
}); },
);
const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); const rootTestFolder = path.join(settings.root, "src/tests/frontend/");
app.get('/tests/frontend/index.html', (req:any, res:any) => { app.get("/tests/frontend/index.html", (req: any, res: any) => {
res.redirect(['./', ...req.url.split('?').slice(1)].join('?')); res.redirect(["./", ...req.url.split("?").slice(1)].join("?"));
}); });
// The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here // The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here
// uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the // uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the
// version used with Express v4.x) interprets '.' and '*' differently than regexp. // version used with Express v4.x) interprets '.' and '*' differently than regexp.
app.get('/tests/frontend/:file([\\d\\D]{0,})', (req:any, res:any, next:Function) => { app.get(
"/tests/frontend/:file([\\d\\D]{0,})",
(req: any, res: any, next: Function) => {
(async () => { (async () => {
let file = sanitizePathname(req.params.file); let file = sanitizePathname(req.params.file);
if (['', '.', './'].includes(file)) file = 'index.html'; if (["", ".", "./"].includes(file)) file = "index.html";
res.sendFile(path.join(rootTestFolder, file)); res.sendFile(path.join(rootTestFolder, file));
})().catch((err) => next(err || new Error(err))); })().catch((err) => next(err || new Error(err)));
}); },
);
app.get('/tests/frontend', (req:any, res:any) => { app.get("/tests/frontend", (req: any, res: any) => {
res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?')); res.redirect(["./frontend/", ...req.url.split("?").slice(1)].join("?"));
}); });
}; };

View file

@ -1,34 +1,40 @@
'use strict';
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import log4js from 'log4js'; import log4js from "log4js";
import {SocketClientRequest} from "../../types/SocketClientRequest"; import type { SettingsUser } from "../../types/SettingsUser";
import {WebAccessTypes} from "../../types/WebAccessTypes"; import type { SocketClientRequest } from "../../types/SocketClientRequest";
import {SettingsUser} from "../../types/SettingsUser"; import type { WebAccessTypes } from "../../types/WebAccessTypes";
const httpLogger = log4js.getLogger('http'); const httpLogger = log4js.getLogger("http");
const settings = require('../../utils/Settings'); const settings = require("../../utils/Settings");
const hooks = require('../../../static/js/pluginfw/hooks'); const hooks = require("../../../static/js/pluginfw/hooks");
const readOnlyManager = require('../../db/ReadOnlyManager'); const readOnlyManager = require("../../db/ReadOnlyManager");
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; hooks.deprecationNotices.authFailure =
"use the authnFailure and authzFailure hooks instead";
// Promisified wrapper around hooks.aCallFirst. // Promisified wrapper around hooks.aCallFirst.
const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => { const aCallFirst = (hookName: string, context: any, pred = null) =>
hooks.aCallFirst(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred); new Promise((resolve, reject) => {
hooks.aCallFirst(
hookName,
context,
(err: any, r: unknown) => (err != null ? reject(err) : resolve(r)),
pred,
);
}); });
const aCallFirst0 = const aCallFirst0 =
// @ts-ignore // @ts-ignore
async (hookName: string, context:any, pred = null) => (await aCallFirst(hookName, context, pred))[0]; async (hookName: string, context: any, pred = null) =>
(await aCallFirst(hookName, context, pred))[0];
exports.normalizeAuthzLevel = (level: string | boolean) => { exports.normalizeAuthzLevel = (level: string | boolean) => {
if (!level) return false; if (!level) return false;
switch (level) { switch (level) {
case true: case true:
return 'create'; return "create";
case 'readOnly': case "readOnly":
case 'modify': case "modify":
case 'create': case "create":
return level; return level;
default: default:
httpLogger.warn(`Unknown authorization level '${level}', denying access`); httpLogger.warn(`Unknown authorization level '${level}', denying access`);
@ -39,18 +45,20 @@ exports.normalizeAuthzLevel = (level: string|boolean) => {
exports.userCanModify = (padId: string, req: SocketClientRequest) => { exports.userCanModify = (padId: string, req: SocketClientRequest) => {
if (readOnlyManager.isReadOnlyId(padId)) return false; if (readOnlyManager.isReadOnlyId(padId)) return false;
if (!settings.requireAuthentication) return true; if (!settings.requireAuthentication) return true;
const {session: {user} = {}} = req; const {
session: { user } = {},
} = req;
if (!user || user.readOnly) return false; if (!user || user.readOnly) return false;
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
return level && level !== 'readOnly'; return level && level !== "readOnly";
}; };
// Exported so that tests can set this to 0 to avoid unnecessary test slowness. // Exported so that tests can set this to 0 to avoid unnecessary test slowness.
exports.authnFailureDelayMs = 1000; exports.authnFailureDelayMs = 1000;
const checkAccess = async (req: any, res: any, next: Function) => { const checkAccess = async (req: any, res: any, next: Function) => {
const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth'); const requireAdmin = req.path.toLowerCase().startsWith("/admin-auth");
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
// Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin // Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin
@ -60,19 +68,28 @@ const checkAccess = async (req:any, res:any, next: Function) => {
let results: null | boolean[]; let results: null | boolean[];
let skip = false; let skip = false;
const preAuthorizeNext = (...args:any) => { skip = true; next(...args); }; const preAuthorizeNext = (...args: any) => {
skip = true;
next(...args);
};
try { try {
results = await aCallFirst('preAuthorize', {req, res, next: preAuthorizeNext}, results = (await aCallFirst(
"preAuthorize",
{ req, res, next: preAuthorizeNext },
// This predicate will cause aCallFirst to call the hook functions one at a time until one // This predicate will cause aCallFirst to call the hook functions one at a time until one
// of them returns a non-empty list, with an exception: If the request is for an /admin // of them returns a non-empty list, with an exception: If the request is for an /admin
// page, truthy entries are filtered out before checking to see whether the list is empty. // page, truthy entries are filtered out before checking to see whether the list is empty.
// This prevents plugin authors from accidentally granting admin privileges to the general // This prevents plugin authors from accidentally granting admin privileges to the general
// public. // public.
// @ts-ignore // @ts-ignore
(r) => (skip || (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0))) as boolean[]; (r) =>
skip || (r != null && r.filter((x) => !requireAdmin || !x).length > 0),
)) as boolean[];
} catch (err: any) { } catch (err: any) {
httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`); httpLogger.error(
if (!skip) res.status(500).send('Internal Server Error'); `Error in preAuthorize hook: ${err.stack || err.toString()}`,
);
if (!skip) res.status(500).send("Internal Server Error");
return; return;
} }
if (skip) return; if (skip) return;
@ -84,9 +101,9 @@ const checkAccess = async (req:any, res:any, next: Function) => {
if (results.length > 0) { if (results.length > 0) {
// Access was explicitly granted or denied. If any value is false then access is denied. // Access was explicitly granted or denied. If any value is false then access is denied.
if (results.every((x) => x)) return next(); if (results.every((x) => x)) return next();
if (await aCallFirst0('preAuthzFailure', {req, res})) return; if (await aCallFirst0("preAuthzFailure", { req, res })) return;
// No plugin handled the pre-authentication authorization failure. // No plugin handled the pre-authentication authorization failure.
return res.status(403).send('Forbidden'); return res.status(403).send("Forbidden");
} }
// This helper is used in steps 2 and 4 below, so it may be called twice per access: once before // This helper is used in steps 2 and 4 below, so it may be called twice per access: once before
@ -112,13 +129,16 @@ const checkAccess = async (req:any, res:any, next: Function) => {
return true; return true;
}; };
const isAuthenticated = req.session && req.session.user; const isAuthenticated = req.session && req.session.user;
if (isAuthenticated && req.session.user.is_admin) return await grant('create'); if (isAuthenticated && req.session.user.is_admin)
return await grant("create");
const requireAuthn = requireAdmin || settings.requireAuthentication; const requireAuthn = requireAdmin || settings.requireAuthentication;
if (!requireAuthn) return await grant('create'); if (!requireAuthn) return await grant("create");
if (!isAuthenticated) return await grant(false); if (!isAuthenticated) return await grant(false);
if (requireAdmin && !req.session.user.is_admin) return await grant(false); if (requireAdmin && !req.session.user.is_admin) return await grant(false);
if (!settings.requireAuthorization) return await grant('create'); if (!settings.requireAuthorization) return await grant("create");
return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); return await grant(
await aCallFirst0("authorize", { req, res, next, resource: req.path }),
);
}; };
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
@ -128,8 +148,8 @@ const checkAccess = async (req:any, res:any, next: Function) => {
if (await authorize()) { if (await authorize()) {
if (requireAdmin) { if (requireAdmin) {
res.status(200).send('Authorized') res.status(200).send("Authorized");
return return;
} }
return next(); return next();
} }
@ -146,35 +166,47 @@ const checkAccess = async (req:any, res:any, next: Function) => {
const ctx: WebAccessTypes = { req, res, users: settings.users, next }; const ctx: WebAccessTypes = { req, res, users: settings.users, next };
// If the HTTP basic auth header is present, extract the username and password so it can be given // If the HTTP basic auth header is present, extract the username and password so it can be given
// to authn plugins. // to authn plugins.
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); const httpBasicAuth =
req.headers.authorization && req.headers.authorization.startsWith("Basic ");
if (httpBasicAuth) { if (httpBasicAuth) {
const userpass = const userpass = Buffer.from(
Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':'); req.headers.authorization.split(" ")[1],
"base64",
)
.toString()
.split(":");
ctx.username = userpass.shift(); ctx.username = userpass.shift();
// Prevent prototype pollution vulnerabilities in plugins. This also silences a prototype // Prevent prototype pollution vulnerabilities in plugins. This also silences a prototype
// pollution warning below (when setting settings.users[ctx.username]) that isn't actually a // pollution warning below (when setting settings.users[ctx.username]) that isn't actually a
// problem unless the attacker can also set Object.prototype.password. // problem unless the attacker can also set Object.prototype.password.
if (ctx.username === '__proto__') ctx.username = null; if (ctx.username === "__proto__") ctx.username = null;
ctx.password = userpass.join(':'); ctx.password = userpass.join(":");
} }
if (!(await aCallFirst0('authenticate', ctx))) { if (!(await aCallFirst0("authenticate", ctx))) {
// Fall back to HTTP basic auth. // Fall back to HTTP basic auth.
// @ts-ignore // @ts-ignore
const {[ctx.username]: {password} = {}} = settings.users as SettingsUser; const {
[ctx.username]: { password } = {},
} = settings.users as SettingsUser;
if (!httpBasicAuth || if (
!httpBasicAuth ||
!ctx.username || !ctx.username ||
password == null || password.toString() !== ctx.password) { password == null ||
password.toString() !== ctx.password
) {
httpLogger.info(`Failed authentication from IP ${req.ip}`); httpLogger.info(`Failed authentication from IP ${req.ip}`);
if (await aCallFirst0('authnFailure', {req, res})) return; if (await aCallFirst0("authnFailure", { req, res })) return;
if (await aCallFirst0('authFailure', {req, res, next})) return; if (await aCallFirst0("authFailure", { req, res, next })) return;
// No plugin handled the authentication failure. Fall back to basic authentication. // No plugin handled the authentication failure. Fall back to basic authentication.
if (!requireAdmin) { if (!requireAdmin) {
res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); res.header("WWW-Authenticate", 'Basic realm="Protected Area"');
} }
// Delay the error response for 1s to slow down brute force attacks. // Delay the error response for 1s to slow down brute force attacks.
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs)); await new Promise((resolve) =>
res.status(401).send('Authentication Required'); setTimeout(resolve, exports.authnFailureDelayMs),
);
res.status(401).send("Authentication Required");
return; return;
} }
settings.users[ctx.username].username = ctx.username; settings.users[ctx.username].username = ctx.username;
@ -184,11 +216,15 @@ const checkAccess = async (req:any, res:any, next: Function) => {
delete req.session.user.password; delete req.session.user.password;
} }
if (req.session.user == null) { if (req.session.user == null) {
httpLogger.error('authenticate hook failed to add user settings to session'); httpLogger.error(
return res.status(500).send('Internal Server Error'); "authenticate hook failed to add user settings to session",
);
return res.status(500).send("Internal Server Error");
} }
const {username = '<no username>'} = req.session.user; const { username = "<no username>" } = req.session.user;
httpLogger.info(`Successful authentication from IP ${req.ip} for user ${username}`); httpLogger.info(
`Successful authentication from IP ${req.ip} for user ${username}`,
);
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
// Step 4: Try to access the thing again. If this fails, give the user a 403 error. Plugins can // Step 4: Try to access the thing again. If this fails, give the user a 403 error. Plugins can
@ -196,17 +232,17 @@ const checkAccess = async (req:any, res:any, next: Function) => {
// a login page). // a login page).
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
const auth = await authorize() const auth = await authorize();
if (auth && !requireAdmin) return next(); if (auth && !requireAdmin) return next();
if (auth && requireAdmin) { if (auth && requireAdmin) {
res.status(200).send('Authorized') res.status(200).send("Authorized");
return return;
} }
if (await aCallFirst0('authzFailure', {req, res})) return; if (await aCallFirst0("authzFailure", { req, res })) return;
if (await aCallFirst0('authFailure', {req, res, next})) return; if (await aCallFirst0("authFailure", { req, res, next })) return;
// No plugin handled the authorization failure. // No plugin handled the authorization failure.
res.status(403).send('Forbidden'); res.status(403).send("Forbidden");
}; };
/** /**

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