mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 06:03:34 +01:00
Added biomejs as formatter and linter
This commit is contained in:
parent
1d3e899249
commit
c64c4a4073
339 changed files with 78646 additions and 66730 deletions
|
@ -278,6 +278,9 @@ importers:
|
|||
specifier: ^0.9.2
|
||||
version: 0.9.2
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 1.7.0
|
||||
version: 1.7.0
|
||||
'@playwright/test':
|
||||
specifier: ^1.43.1
|
||||
version: 1.43.1
|
||||
|
@ -721,6 +724,94 @@ packages:
|
|||
to-fast-properties: 2.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==}
|
||||
dev: true
|
||||
|
|
|
@ -1,138 +1,107 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
// 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 = {
|
||||
ignorePatterns: [
|
||||
'/static/js/vendors/browser.js',
|
||||
'/static/js/vendors/farbtastic.js',
|
||||
'/static/js/vendors/gritter.js',
|
||||
'/static/js/vendors/html10n.js',
|
||||
'/static/js/vendors/jquery.js',
|
||||
'/static/js/vendors/nice-select.js',
|
||||
'/tests/frontend/lib/',
|
||||
"/static/js/vendors/browser.js",
|
||||
"/static/js/vendors/farbtastic.js",
|
||||
"/static/js/vendors/gritter.js",
|
||||
"/static/js/vendors/html10n.js",
|
||||
"/static/js/vendors/jquery.js",
|
||||
"/static/js/vendors/nice-select.js",
|
||||
"/tests/frontend/lib/",
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/.eslintrc.*',
|
||||
],
|
||||
extends: 'etherpad/node',
|
||||
files: ["**/.eslintrc.*"],
|
||||
extends: "etherpad/node",
|
||||
},
|
||||
{
|
||||
files: ["**/*"],
|
||||
excludedFiles: ["**/.eslintrc.*", "tests/frontend/**/*"],
|
||||
extends: "etherpad/node",
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*',
|
||||
"static/**/*",
|
||||
"tests/frontend/helper.js",
|
||||
"tests/frontend/helper/**/*",
|
||||
],
|
||||
excludedFiles: [
|
||||
'**/.eslintrc.*',
|
||||
'tests/frontend/**/*',
|
||||
],
|
||||
extends: 'etherpad/node',
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'static/**/*',
|
||||
'tests/frontend/helper.js',
|
||||
'tests/frontend/helper/**/*',
|
||||
],
|
||||
excludedFiles: [
|
||||
'**/.eslintrc.*',
|
||||
],
|
||||
extends: 'etherpad/browser',
|
||||
excludedFiles: ["**/.eslintrc.*"],
|
||||
extends: "etherpad/browser",
|
||||
env: {
|
||||
'shared-node-browser': true,
|
||||
"shared-node-browser": true,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'tests/frontend/helper/**/*',
|
||||
],
|
||||
files: ["tests/frontend/helper/**/*"],
|
||||
globals: {
|
||||
helper: 'readonly',
|
||||
helper: "readonly",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'tests/**/*',
|
||||
],
|
||||
files: ["tests/**/*"],
|
||||
excludedFiles: [
|
||||
'**/.eslintrc.*',
|
||||
'tests/frontend/cypress/**/*',
|
||||
'tests/frontend/helper.js',
|
||||
'tests/frontend/helper/**/*',
|
||||
'tests/frontend/travis/**/*',
|
||||
'tests/ratelimit/**/*',
|
||||
"**/.eslintrc.*",
|
||||
"tests/frontend/cypress/**/*",
|
||||
"tests/frontend/helper.js",
|
||||
"tests/frontend/helper/**/*",
|
||||
"tests/frontend/travis/**/*",
|
||||
"tests/ratelimit/**/*",
|
||||
],
|
||||
extends: 'etherpad/tests',
|
||||
extends: "etherpad/tests",
|
||||
rules: {
|
||||
'mocha/no-exports': 'off',
|
||||
'mocha/no-top-level-hooks': 'off',
|
||||
"mocha/no-exports": "off",
|
||||
"mocha/no-top-level-hooks": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'tests/backend/**/*',
|
||||
],
|
||||
excludedFiles: [
|
||||
'**/.eslintrc.*',
|
||||
],
|
||||
extends: 'etherpad/tests/backend',
|
||||
files: ["tests/backend/**/*"],
|
||||
excludedFiles: ["**/.eslintrc.*"],
|
||||
extends: "etherpad/tests/backend",
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'tests/backend/**/*',
|
||||
],
|
||||
excludedFiles: [
|
||||
'tests/backend/specs/**/*',
|
||||
],
|
||||
files: ["tests/backend/**/*"],
|
||||
excludedFiles: ["tests/backend/specs/**/*"],
|
||||
rules: {
|
||||
'mocha/no-exports': 'off',
|
||||
'mocha/no-top-level-hooks': 'off',
|
||||
"mocha/no-exports": "off",
|
||||
"mocha/no-top-level-hooks": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'tests/frontend/**/*',
|
||||
],
|
||||
files: ["tests/frontend/**/*"],
|
||||
excludedFiles: [
|
||||
'**/.eslintrc.*',
|
||||
'tests/frontend/cypress/**/*',
|
||||
'tests/frontend/helper.js',
|
||||
'tests/frontend/helper/**/*',
|
||||
'tests/frontend/travis/**/*',
|
||||
"**/.eslintrc.*",
|
||||
"tests/frontend/cypress/**/*",
|
||||
"tests/frontend/helper.js",
|
||||
"tests/frontend/helper/**/*",
|
||||
"tests/frontend/travis/**/*",
|
||||
],
|
||||
extends: 'etherpad/tests/frontend',
|
||||
extends: "etherpad/tests/frontend",
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'tests/frontend/**/*',
|
||||
],
|
||||
excludedFiles: [
|
||||
'tests/frontend/specs/**/*',
|
||||
],
|
||||
files: ["tests/frontend/**/*"],
|
||||
excludedFiles: ["tests/frontend/specs/**/*"],
|
||||
rules: {
|
||||
'mocha/no-exports': 'off',
|
||||
'mocha/no-top-level-hooks': 'off',
|
||||
"mocha/no-exports": "off",
|
||||
"mocha/no-top-level-hooks": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'tests/frontend/cypress/**/*',
|
||||
],
|
||||
extends: 'etherpad/tests/cypress',
|
||||
files: ["tests/frontend/cypress/**/*"],
|
||||
extends: "etherpad/tests/cypress",
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'tests/frontend/travis/**/*',
|
||||
],
|
||||
extends: 'etherpad/node',
|
||||
files: ["tests/frontend/travis/**/*"],
|
||||
extends: "etherpad/node",
|
||||
},
|
||||
],
|
||||
root: true,
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Fwolff",
|
||||
"Naudefj"
|
||||
]
|
||||
"authors": ["Fwolff", "Naudefj"]
|
||||
},
|
||||
"index.newPad": "Nuwe pad",
|
||||
"index.createOpenPad": "of skep/open 'n pad met die naam:",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Xuacu",
|
||||
"YoaR"
|
||||
]
|
||||
"authors": ["Xuacu", "YoaR"]
|
||||
},
|
||||
"index.newPad": "Nuevu bloc",
|
||||
"index.createOpenPad": "o crear/abrir un bloc col nome:",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"1AnuraagPandey",
|
||||
"बडा काजी"
|
||||
]
|
||||
"authors": ["1AnuraagPandey", "बडा काजी"]
|
||||
},
|
||||
"index.newPad": "नयाँ प्याड",
|
||||
"pad.toolbar.bold.title": "मोट (Ctrl-B)",
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Alp Er Tunqa",
|
||||
"Amir a57",
|
||||
"Ilğım",
|
||||
"Koroğlu",
|
||||
"Mousa"
|
||||
]
|
||||
"authors": ["Alp Er Tunqa", "Amir a57", "Ilğım", "Koroğlu", "Mousa"]
|
||||
},
|
||||
"index.newPad": "یئنی یادداشت دفترچه سی",
|
||||
"index.createOpenPad": "یا دا ایجاد /بیر پد آدلا برابر آچماق:",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Baloch Afghanistan",
|
||||
"Moshtank",
|
||||
"Sultanselim baloch"
|
||||
]
|
||||
"authors": ["Baloch Afghanistan", "Moshtank", "Sultanselim baloch"]
|
||||
},
|
||||
"admin.page-title": "کارمسترءِ کُرسی - اترپَد",
|
||||
"admin_plugins": "گݔشانکانءِ کار ءُ بار",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Jim-by",
|
||||
"Red Winged Duck",
|
||||
"Renessaince",
|
||||
"Wizardist"
|
||||
]
|
||||
"authors": ["Jim-by", "Red Winged Duck", "Renessaince", "Wizardist"]
|
||||
},
|
||||
"admin.page-title": "Адміністрацыйная панэль — Etherpad",
|
||||
"admin_plugins": "Кіраўнік плагінаў",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"StanProg",
|
||||
"Vlad5250",
|
||||
"Vodnokon4e"
|
||||
]
|
||||
"authors": ["StanProg", "Vlad5250", "Vodnokon4e"]
|
||||
},
|
||||
"index.newPad": "Нов пад",
|
||||
"index.createOpenPad": "или създаване/отваряне на пад с име:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Baloch Afghanistan"
|
||||
]
|
||||
"authors": ["Baloch Afghanistan"]
|
||||
},
|
||||
"index.newPad": "یاداشتی نوکین کتابچه",
|
||||
"index.createOpenPad": "یا جوڑ\t کورتین/پاچ کورتین یک کتابچه ئی یاداشتی بی نام:",
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Fohanno",
|
||||
"Fulup",
|
||||
"Gwenn-Ael",
|
||||
"Huñvreüs",
|
||||
"Y-M D"
|
||||
]
|
||||
"authors": ["Fohanno", "Fulup", "Gwenn-Ael", "Huñvreüs", "Y-M D"]
|
||||
},
|
||||
"index.newPad": "Pad nevez",
|
||||
"index.createOpenPad": "pe krouiñ/digeriñ ur Pad gant an anv :",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Edinwiki",
|
||||
"Semina x",
|
||||
"Srdjan m",
|
||||
"Srđan"
|
||||
]
|
||||
"authors": ["Edinwiki", "Semina x", "Srdjan m", "Srđan"]
|
||||
},
|
||||
"index.newPad": "Novi Pad",
|
||||
"index.createOpenPad": "ili napravite/otvorite Pad sa imenom:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Michawiki"
|
||||
]
|
||||
"authors": ["Michawiki"]
|
||||
},
|
||||
"admin.page-title": "Administratorowa delka – Etherpad",
|
||||
"admin_plugins": "Zastojnik tykacow",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Nirajan pant",
|
||||
"बडा काजी",
|
||||
"रमेश सिंह बोहरा",
|
||||
"राम प्रसाद जोशी"
|
||||
]
|
||||
"authors": ["Nirajan pant", "बडा काजी", "रमेश सिंह बोहरा", "राम प्रसाद जोशी"]
|
||||
},
|
||||
"index.newPad": "नौलो प्याड",
|
||||
"index.createOpenPad": "नाउँ सहितको नौलो प्याड सिर्जना गद्य्या / खोल्ल्या :",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kristian.kankainen",
|
||||
"Tiblu"
|
||||
]
|
||||
"authors": ["Kristian.kankainen", "Tiblu"]
|
||||
},
|
||||
"index.newPad": "Uus klade",
|
||||
"index.createOpenPad": "loo või rööptoimeta kladet nimega:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ibrahima Malal Sarr"
|
||||
]
|
||||
"authors": ["Ibrahima Malal Sarr"]
|
||||
},
|
||||
"admin.page-title": "Tiimtorde Jiiloowo - Etherpad",
|
||||
"admin_plugins": "Toppitorde Ceŋe",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"EileenSanda"
|
||||
]
|
||||
"authors": ["EileenSanda"]
|
||||
},
|
||||
"index.newPad": "Nýggjur teldil",
|
||||
"pad.toolbar.bold.title": "Við feitum (Ctrl-B)",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Robin van der Vliet"
|
||||
]
|
||||
"authors": ["Robin van der Vliet"]
|
||||
},
|
||||
"pad.toolbar.bold.title": "Fet (Ctrl+B)",
|
||||
"pad.toolbar.italic.title": "Kursyf (Ctrl+I)",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Elisardojm",
|
||||
"Ghose",
|
||||
"Toliño"
|
||||
]
|
||||
"authors": ["Elisardojm", "Ghose", "Toliño"]
|
||||
},
|
||||
"admin.page-title": "Panel de administración - Etherpad",
|
||||
"admin_plugins": "Xestor de complementos",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bhatakati aatma",
|
||||
"Dsvyas",
|
||||
"Harsh4101991",
|
||||
"KartikMistry"
|
||||
]
|
||||
"authors": ["Bhatakati aatma", "Dsvyas", "Harsh4101991", "KartikMistry"]
|
||||
},
|
||||
"index.newPad": "નવું પેડ",
|
||||
"pad.toolbar.bold.title": "બોલ્ડ",
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Amire80",
|
||||
"Ofrahod",
|
||||
"Steeve815",
|
||||
"YaronSh",
|
||||
"תומר ט"
|
||||
]
|
||||
"authors": ["Amire80", "Ofrahod", "Steeve815", "YaronSh", "תומר ט"]
|
||||
},
|
||||
"admin.page-title": "לוח ניהול - Etherpad",
|
||||
"admin_plugins": "מנהל תוספים",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Sfic"
|
||||
]
|
||||
"authors": ["Sfic"]
|
||||
},
|
||||
"pad.toolbar.bold.title": "गहरा (Ctrl+B)",
|
||||
"pad.toolbar.italic.title": "तिरछा (Ctrl+I)",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bugoslav",
|
||||
"Hmxhmx",
|
||||
"Ponor"
|
||||
]
|
||||
"authors": ["Bugoslav", "Hmxhmx", "Ponor"]
|
||||
},
|
||||
"index.newPad": "Novi blokić",
|
||||
"index.createOpenPad": "ili stvori/otvori blokić s imenom:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Paul Beppler"
|
||||
]
|
||||
"authors": ["Paul Beppler"]
|
||||
},
|
||||
"index.newPad": "Neies Pad",
|
||||
"index.createOpenPad": "Pad mit follichendem Noome uffmache:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Michawiki"
|
||||
]
|
||||
"authors": ["Michawiki"]
|
||||
},
|
||||
"admin.page-title": "Administratorowa deska – Etherpad",
|
||||
"admin_plugins": "Zrjadowak tykačow",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Armenoid",
|
||||
"Kareyac"
|
||||
]
|
||||
"authors": ["Armenoid", "Kareyac"]
|
||||
},
|
||||
"admin_plugins.available_install.value": "Տեղադրել",
|
||||
"admin_plugins.description": "Նկարագրություն",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"McDutchie"
|
||||
]
|
||||
"authors": ["McDutchie"]
|
||||
},
|
||||
"admin.page-title": "Pannello administrative – Etherpad",
|
||||
"admin_plugins": "Gestor de plug-ins",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bennylin",
|
||||
"IvanLanin",
|
||||
"Marwan Mohamad",
|
||||
"Veracious"
|
||||
]
|
||||
"authors": ["Bennylin", "IvanLanin", "Marwan Mohamad", "Veracious"]
|
||||
},
|
||||
"admin.page-title": "Dasbor Pengurus - Etherpad",
|
||||
"admin_plugins": "Manajer plugin",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Sveinki",
|
||||
"Sveinn í Felli"
|
||||
]
|
||||
"authors": ["Sveinki", "Sveinn í Felli"]
|
||||
},
|
||||
"admin.page-title": "Stjórnborð fyrir stjórnendur - Etherpad",
|
||||
"admin_plugins": "Stýring viðbóta",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Belkacem77"
|
||||
]
|
||||
"authors": ["Belkacem77"]
|
||||
},
|
||||
"index.newPad": "Apad amaynut",
|
||||
"index.createOpenPad": "neɣ rnu/ldi apad s yisem:",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Pichnat Thong",
|
||||
"Sovichet",
|
||||
"វ័ណថារិទ្ធ"
|
||||
]
|
||||
"authors": ["Pichnat Thong", "Sovichet", "វ័ណថារិទ្ធ"]
|
||||
},
|
||||
"index.newPad": "ផេតថ្មី",
|
||||
"index.createOpenPad": "ឬបង្កើត/បើកផេតដែលមានឈ្មោះ៖",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Nayvik",
|
||||
"ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"
|
||||
]
|
||||
"authors": ["Nayvik", "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"]
|
||||
},
|
||||
"admin_plugins.available": "ಲಭ್ಯವಿರುವ ಪ್ಲಗಿನ್ಗಳು",
|
||||
"admin_plugins.available_not-found": "ಯಾವುದೇ ಪ್ಲಗಿನ್ಗಳು ಸಿಗಲಿಲ್ಲ",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ernác",
|
||||
"Къарачайлы"
|
||||
]
|
||||
"authors": ["Ernác", "Къарачайлы"]
|
||||
},
|
||||
"admin.page-title": "Администраторну панели — Etherpad",
|
||||
"admin_plugins": "Плагин менеджер",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Purodha"
|
||||
]
|
||||
"authors": ["Purodha"]
|
||||
},
|
||||
"index.newPad": "Neu Pädd",
|
||||
"index.createOpenPad": "udder maach e Pädd op med däm Nahme:",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Gromper",
|
||||
"Robby",
|
||||
"Soued031",
|
||||
"Volvox"
|
||||
]
|
||||
"authors": ["Gromper", "Robby", "Soued031", "Volvox"]
|
||||
},
|
||||
"admin_plugins.available_install.value": "Installéieren",
|
||||
"admin_plugins.description": "Beschreiwung",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Arash71",
|
||||
"Hosseinblue",
|
||||
"Lakzon"
|
||||
]
|
||||
"authors": ["Arash71", "Hosseinblue", "Lakzon"]
|
||||
},
|
||||
"index.newPad": "تازۀpad",
|
||||
"index.createOpenPad": ":وە نۆم Pad یا سازین/واز کردن یإگلە",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Lorestani",
|
||||
"Mogoeilor"
|
||||
]
|
||||
"authors": ["Lorestani", "Mogoeilor"]
|
||||
},
|
||||
"index.newPad": "دٱفتٱرچٱ تازٱ",
|
||||
"pad.toolbar.bold.title": "تۊپور",
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Admresdeserv.",
|
||||
"Jmg.cmdi",
|
||||
"Oskars",
|
||||
"Papuass",
|
||||
"Silraks"
|
||||
]
|
||||
"authors": ["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.createOpenPad": "vai izveidojiet/atveriet Blociņu ar nosaukumu:",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Empu",
|
||||
"StefanusRA"
|
||||
]
|
||||
"authors": ["Empu", "StefanusRA"]
|
||||
},
|
||||
"index.newPad": "Pad Anyar",
|
||||
"index.createOpenPad": "utawa gawe/bukak Pad nganggo jeneng:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Jagwar"
|
||||
]
|
||||
"authors": ["Jagwar"]
|
||||
},
|
||||
"index.newPad": "Pad vaovao",
|
||||
"index.createOpenPad": "na hamorona/hanokatra Pad manana anarana:",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bjankuloski06",
|
||||
"Brest",
|
||||
"Vlad5250"
|
||||
]
|
||||
"authors": ["Bjankuloski06", "Brest", "Vlad5250"]
|
||||
},
|
||||
"admin.page-title": "Администраторска управувачница — Etherpad",
|
||||
"admin_plugins": "Раководител со приклучоци",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"MongolWiki",
|
||||
"Munkhzaya.E",
|
||||
"Wisdom"
|
||||
]
|
||||
"authors": ["MongolWiki", "Munkhzaya.E", "Wisdom"]
|
||||
},
|
||||
"pad.toolbar.bold.title": "Болд тескт (Ctrl-B)",
|
||||
"pad.toolbar.italic.title": "Налуу тескт (Ctrl-I)",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Aue Nai",
|
||||
"咽頭べさ"
|
||||
]
|
||||
"authors": ["Aue Nai", "咽頭べさ"]
|
||||
},
|
||||
"admin_plugins.description": "တၚ်ထမံက်ထ္ၜး",
|
||||
"index.newPad": "တၞးတၟိ",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ganeshgiram",
|
||||
"V.narsikar",
|
||||
"Ydyashad"
|
||||
]
|
||||
"authors": ["Ganeshgiram", "V.narsikar", "Ydyashad"]
|
||||
},
|
||||
"index.newPad": "नव पान",
|
||||
"pad.toolbar.bold.title": "ठळक (Ctrl-B)",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Anakmalaysia",
|
||||
"Hakimi97",
|
||||
"Jeluang Terluang"
|
||||
]
|
||||
"authors": ["Anakmalaysia", "Hakimi97", "Jeluang Terluang"]
|
||||
},
|
||||
"admin.page-title": "Papan muka Penyelia - Etherpad",
|
||||
"index.newPad": "Pad baru",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Andibecker",
|
||||
"Dr Lotus Black"
|
||||
]
|
||||
"authors": ["Andibecker", "Dr Lotus Black"]
|
||||
},
|
||||
"admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad",
|
||||
"admin_plugins": "ပလပ်အင်မန်နေဂျာ",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Akapochtli",
|
||||
"Languaeditor",
|
||||
"Taresi"
|
||||
]
|
||||
"authors": ["Akapochtli", "Languaeditor", "Taresi"]
|
||||
},
|
||||
"index.newPad": "Yancuic Pad",
|
||||
"index.createOpenPad": "auh xicchīhua/xictlapo cē Pad in ītōcā:",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"C.R.",
|
||||
"Chelin",
|
||||
"Finizio",
|
||||
"Ruthven"
|
||||
]
|
||||
"authors": ["C.R.", "Chelin", "Finizio", "Ruthven"]
|
||||
},
|
||||
"admin_plugins.name": "Nomme",
|
||||
"index.newPad": "Nuovo Pad",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Gthoele",
|
||||
"Joachim Mos"
|
||||
]
|
||||
"authors": ["Gthoele", "Joachim Mos"]
|
||||
},
|
||||
"index.newPad": "Nee'et Pad",
|
||||
"index.createOpenPad": "oder Pad mit düssen Naam apen maken:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Unhammer"
|
||||
]
|
||||
"authors": ["Unhammer"]
|
||||
},
|
||||
"index.newPad": "Ny blokk",
|
||||
"index.createOpenPad": "eller opprett/opna ei blokk med namnet:",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Cedric31",
|
||||
"Quentí"
|
||||
]
|
||||
"authors": ["Cedric31", "Quentí"]
|
||||
},
|
||||
"admin.page-title": "Panèl d’administracion - Etherpad",
|
||||
"admin_plugins": "Gestion de las extensions",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Denö",
|
||||
"Ilja.mos",
|
||||
"Mashoi7"
|
||||
]
|
||||
"authors": ["Denö", "Ilja.mos", "Mashoi7"]
|
||||
},
|
||||
"pad.toolbar.underline.title": "Alleviivua (Ctrl+U)",
|
||||
"pad.toolbar.settings.title": "Azetukset",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Bouron"
|
||||
]
|
||||
"authors": ["Bouron"]
|
||||
},
|
||||
"index.newPad": "Ног",
|
||||
"index.createOpenPad": "кӕнӕ сараз/бакӕн ног документ ахӕм номимӕ:",
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Aalam",
|
||||
"Babanwalia",
|
||||
"Tow",
|
||||
"ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ",
|
||||
"ਪ੍ਰਚਾਰਕ"
|
||||
]
|
||||
"authors": ["Aalam", "Babanwalia", "Tow", "ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ", "ਪ੍ਰਚਾਰਕ"]
|
||||
},
|
||||
"index.newPad": "ਨਵਾਂ ਪੈਡ",
|
||||
"index.createOpenPad": "ਜਾਂ ਨਾਂ ਨਾਲ ਨਵਾਂ ਪੈਡ ਬਣਾਓ/ਖੋਲ੍ਹੋ:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Borichèt"
|
||||
]
|
||||
"authors": ["Borichèt"]
|
||||
},
|
||||
"admin.page-title": "Cruscòt d'aministrator - Etherpad",
|
||||
"admin_plugins": "Mansé dj'anstalassion",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ahmed-Najib-Biabani-Ibrahimkhel"
|
||||
]
|
||||
"authors": ["Ahmed-Najib-Biabani-Ibrahimkhel"]
|
||||
},
|
||||
"index.newPad": "نوې ليکچه",
|
||||
"index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانيستل:",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adr mm"
|
||||
]
|
||||
"authors": ["Adr mm"]
|
||||
},
|
||||
"admin.page-title": "Pannellu de amministratzione - Etherpad",
|
||||
"admin_plugins": "Gestore de connetores",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"BaRaN6161 TURK",
|
||||
"Kaleem Bhatti",
|
||||
"Mehtab ahmed",
|
||||
"Tweety"
|
||||
]
|
||||
"authors": ["BaRaN6161 TURK", "Kaleem Bhatti", "Mehtab ahmed", "Tweety"]
|
||||
},
|
||||
"admin_settings": "ترتيبون",
|
||||
"index.newPad": "نئين پٽي",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Conquistador",
|
||||
"Vlad5250"
|
||||
]
|
||||
"authors": ["Conquistador", "Vlad5250"]
|
||||
},
|
||||
"admin_plugins.available_not-found": "Nijedan plugin nije pronađen.",
|
||||
"admin_plugins.description": "Opis",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Ninjastrikers",
|
||||
"Saimawnkham",
|
||||
"Saosukham"
|
||||
]
|
||||
"authors": ["Ninjastrikers", "Saimawnkham", "Saosukham"]
|
||||
},
|
||||
"index.newPad": "ၽႅတ်ႉမႂ်ႇ",
|
||||
"index.createOpenPad": "ဢမ်ႇၼၼ် ၶူင်မႂ်ႇ/ပိုတ်ႇၽႅတ်ႉၵိုၵ်းၸိုဝ်ႈ",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Saraiki"
|
||||
]
|
||||
"authors": ["Saraiki"]
|
||||
},
|
||||
"admin_plugins": "پلگ ان منیجر",
|
||||
"admin_plugins.available": "دستیاب پلگ ان",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Yupik"
|
||||
]
|
||||
"authors": ["Yupik"]
|
||||
},
|
||||
"admin_plugins.description": "Deskriptt",
|
||||
"admin_plugins.name": "Nõmm",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Besnik b",
|
||||
"Eraldkerciku",
|
||||
"Kosovastar",
|
||||
"Liridon"
|
||||
]
|
||||
"authors": ["Besnik b", "Eraldkerciku", "Kosovastar", "Liridon"]
|
||||
},
|
||||
"admin.page-title": "Pult Përgjegjësi - Etherpad",
|
||||
"admin_plugins": "Përgjegjës shtojcash",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adr mm",
|
||||
"F Samaritani"
|
||||
]
|
||||
"authors": ["Adr mm", "F Samaritani"]
|
||||
},
|
||||
"admin.page-title": "Pannellu amministrativu - Etherpad",
|
||||
"admin_plugins": "Gestore de connetores",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Andibecker",
|
||||
"Edwingudfriend",
|
||||
"Muddyb"
|
||||
]
|
||||
"authors": ["Andibecker", "Edwingudfriend", "Muddyb"]
|
||||
},
|
||||
"admin.page-title": "Dashibodi ya Usimamizi - Etherpad",
|
||||
"admin_plugins": "Meneja wa programu-jalizi",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Balajijagadesh",
|
||||
"ElangoRamanujam",
|
||||
"Sank"
|
||||
]
|
||||
"authors": ["Balajijagadesh", "ElangoRamanujam", "Sank"]
|
||||
},
|
||||
"index.newPad": "புதிய அட்டை",
|
||||
"index.createOpenPad": "அல்லது பெயருடன் ஒரு அட்டையை உருவாக்கு/திற",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"BHARATHESHA ALASANDEMAJALU",
|
||||
"VASANTH S.N."
|
||||
]
|
||||
"authors": ["BHARATHESHA ALASANDEMAJALU", "VASANTH S.N."]
|
||||
},
|
||||
"index.newPad": "ಪೊಸ ಪ್ಯಾಡ್",
|
||||
"index.createOpenPad": "ಅತಂಡ ಈ ಪುದರ್ತ ಪ್ಯಾಡನ್ನು ಉಂಡು ಮನ್ಪು/ತೋಜಾಲ:",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Aefgh39622",
|
||||
"Andibecker",
|
||||
"Patsagorn Y.",
|
||||
"Trisorn Triboon"
|
||||
]
|
||||
"authors": ["Aefgh39622", "Andibecker", "Patsagorn Y.", "Trisorn Triboon"]
|
||||
},
|
||||
"admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad",
|
||||
"admin_plugins": "ตัวจัดการปลั๊กอิน",
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Fierodelveneto"
|
||||
]
|
||||
"authors": ["Fierodelveneto"]
|
||||
},
|
||||
"index.newPad": "Novo Pad",
|
||||
"index.createOpenPad": "O creare o verxare on Pad co'l nome:",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* This module provides all API functions
|
||||
*/
|
||||
|
@ -19,21 +19,21 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const CustomError = require('../utils/customError');
|
||||
const padManager = require('./PadManager');
|
||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const groupManager = require('./GroupManager');
|
||||
const authorManager = require('./AuthorManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
const exportHtml = require('../utils/ExportHtml');
|
||||
const exportTxt = require('../utils/ExportTxt');
|
||||
const importHtml = require('../utils/ImportHtml');
|
||||
const cleanText = require('./Pad').cleanText;
|
||||
const PadDiff = require('../utils/padDiff');
|
||||
const {checkValidRev, isInt} = require('../utils/checkValidRev');
|
||||
const Changeset = require("../../static/js/Changeset");
|
||||
const ChatMessage = require("../../static/js/ChatMessage");
|
||||
const CustomError = require("../utils/customError");
|
||||
const padManager = require("./PadManager");
|
||||
const padMessageHandler = require("../handler/PadMessageHandler");
|
||||
const readOnlyManager = require("./ReadOnlyManager");
|
||||
const groupManager = require("./GroupManager");
|
||||
const authorManager = require("./AuthorManager");
|
||||
const sessionManager = require("./SessionManager");
|
||||
const exportHtml = require("../utils/ExportHtml");
|
||||
const exportTxt = require("../utils/ExportTxt");
|
||||
const importHtml = require("../utils/ImportHtml");
|
||||
const cleanText = require("./Pad").cleanText;
|
||||
const PadDiff = require("../utils/padDiff");
|
||||
const { checkValidRev, isInt } = require("../utils/checkValidRev");
|
||||
|
||||
/* ********************
|
||||
* GROUP FUNCTIONS ****
|
||||
|
@ -136,7 +136,10 @@ exports.getRevisionChangeset = async (padID: string, rev: string) => {
|
|||
if (rev !== undefined) {
|
||||
// check if this is a valid revision
|
||||
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
|
||||
|
@ -169,7 +172,10 @@ exports.getText = async (padID: string, rev: string) => {
|
|||
if (rev !== undefined) {
|
||||
// check if this is a valid revision
|
||||
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
|
||||
|
@ -200,10 +206,14 @@ Example returns:
|
|||
* @param {String} authorId the id of the author, defaulting to empty string
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise<void> => {
|
||||
exports.setText = async (
|
||||
padID: string,
|
||||
text?: string,
|
||||
authorId: string = "",
|
||||
): Promise<void> => {
|
||||
// text is required
|
||||
if (typeof text !== 'string') {
|
||||
throw new CustomError('text is not a string', 'apierror');
|
||||
if (typeof text !== "string") {
|
||||
throw new CustomError("text is not a string", "apierror");
|
||||
}
|
||||
|
||||
// get the pad
|
||||
|
@ -225,10 +235,14 @@ Example returns:
|
|||
@param {String} text the text of the pad
|
||||
@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: string = "",
|
||||
) => {
|
||||
// text is required
|
||||
if (typeof text !== 'string') {
|
||||
throw new CustomError('text is not a string', 'apierror');
|
||||
if (typeof text !== "string") {
|
||||
throw new CustomError("text is not a string", "apierror");
|
||||
}
|
||||
|
||||
const pad = await getPadSafe(padID, true);
|
||||
|
@ -247,7 +261,10 @@ Example returns:
|
|||
@param {String} rev the revision number, defaulting to the latest revision
|
||||
@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) {
|
||||
rev = checkValidRev(rev);
|
||||
}
|
||||
|
@ -259,7 +276,10 @@ exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }>
|
|||
// check if this is a valid revision
|
||||
const head = pad.getHeadRevisionNumber();
|
||||
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 +303,14 @@ Example returns:
|
|||
@param {String} html the html of the pad
|
||||
@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
|
||||
if (typeof html !== 'string') {
|
||||
throw new CustomError('html is not a string', 'apierror');
|
||||
if (typeof html !== "string") {
|
||||
throw new CustomError("html is not a string", "apierror");
|
||||
}
|
||||
|
||||
// get the pad
|
||||
|
@ -296,7 +320,7 @@ exports.setHTML = async (padID: string, html:string|object, authorId = '') => {
|
|||
try {
|
||||
await importHtml.setPadHTML(pad, cleanText(html), authorId);
|
||||
} catch (e) {
|
||||
throw new CustomError('HTML is malformed', 'apierror');
|
||||
throw new CustomError("HTML is malformed", "apierror");
|
||||
}
|
||||
|
||||
// update the clients on the pad
|
||||
|
@ -327,13 +351,13 @@ Example returns:
|
|||
exports.getChatHistory = async (padID: string, start: number, end: number) => {
|
||||
if (start && end) {
|
||||
if (start < 0) {
|
||||
throw new CustomError('start is below zero', 'apierror');
|
||||
throw new CustomError("start is below zero", "apierror");
|
||||
}
|
||||
if (end < 0) {
|
||||
throw new CustomError('end is below zero', 'apierror');
|
||||
throw new CustomError("end is below zero", "apierror");
|
||||
}
|
||||
if (start > end) {
|
||||
throw new CustomError('start is higher than end', 'apierror');
|
||||
throw new CustomError("start is higher than end", "apierror");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,10 +373,16 @@ exports.getChatHistory = async (padID: string, start:number, end:number) => {
|
|||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
|
@ -374,10 +404,15 @@ Example returns:
|
|||
@param {String} authorID the id of the author
|
||||
@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
|
||||
if (typeof text !== 'string') {
|
||||
throw new CustomError('text is not a string', 'apierror');
|
||||
if (typeof text !== "string") {
|
||||
throw new CustomError("text is not a string", "apierror");
|
||||
}
|
||||
|
||||
// if time is not an integer value set time to current timestamp
|
||||
|
@ -388,7 +423,10 @@ exports.appendChatMessage = async (padID: string, text: string|object, authorID:
|
|||
// @TODO - missing getPadSafe() call ?
|
||||
|
||||
// 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 +501,17 @@ exports.saveRevision = async (padID: string, rev: number) => {
|
|||
// the client asked for a special revision
|
||||
if (rev !== undefined) {
|
||||
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 {
|
||||
rev = pad.getHeadRevisionNumber();
|
||||
}
|
||||
|
||||
const author = await authorManager.createAuthor('API');
|
||||
await pad.addSavedRevision(rev, author.authorID, 'Saved through API call');
|
||||
const author = await authorManager.createAuthor("API");
|
||||
await pad.addSavedRevision(rev, author.authorID, "Saved through API call");
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -483,7 +524,9 @@ Example returns:
|
|||
@param {String} padID the id 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
|
||||
const pad = await getPadSafe(padID, true);
|
||||
const lastEdited = await pad.getLastEdit();
|
||||
|
@ -501,16 +544,19 @@ Example returns:
|
|||
@param {String} text the initial text of the pad
|
||||
@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) {
|
||||
// ensure there is no $ in the padID
|
||||
if (padID.indexOf('$') !== -1) {
|
||||
throw new CustomError("createPad can't create group pads", 'apierror');
|
||||
if (padID.indexOf("$") !== -1) {
|
||||
throw new CustomError("createPad can't create group pads", "apierror");
|
||||
}
|
||||
|
||||
// check for url special characters
|
||||
if (padID.match(/(\/|\?|&|#)/)) {
|
||||
throw new CustomError('malformed padID: Remove special characters', 'apierror');
|
||||
throw new CustomError(
|
||||
"malformed padID: Remove special characters",
|
||||
"apierror",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -543,10 +589,10 @@ exports.deletePad = async (padID: string) => {
|
|||
@param {Number} rev the revision number, defaulting to the latest revision
|
||||
@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
|
||||
if (rev === undefined) {
|
||||
throw new CustomError('rev is not defined', 'apierror');
|
||||
throw new CustomError("rev is not defined", "apierror");
|
||||
}
|
||||
rev = checkValidRev(rev);
|
||||
|
||||
|
@ -555,13 +601,16 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
|||
|
||||
// check if this is a valid revision
|
||||
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 oldText = pad.text();
|
||||
atext.text += '\n';
|
||||
atext.text += "\n";
|
||||
|
||||
const eachAttribRun = (attribs: string[], func: Function) => {
|
||||
let textIndex = 0;
|
||||
|
@ -570,7 +619,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
|||
for (const op of Changeset.deserializeOps(attribs)) {
|
||||
const nextIndex = textIndex + op.chars;
|
||||
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;
|
||||
}
|
||||
|
@ -580,11 +633,14 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
|||
const builder = Changeset.builder(oldText.length);
|
||||
|
||||
// assemble each line into the builder
|
||||
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
|
||||
eachAttribRun(
|
||||
atext.attribs,
|
||||
(start: number, end: number, attribs: string[]) => {
|
||||
builder.insert(atext.text.substring(start, end), attribs);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const lastNewlinePos = oldText.lastIndexOf('\n');
|
||||
const lastNewlinePos = oldText.lastIndexOf("\n");
|
||||
if (lastNewlinePos < 0) {
|
||||
builder.remove(oldText.length - 1, 0);
|
||||
} else {
|
||||
|
@ -610,7 +666,11 @@ Example returns:
|
|||
@param {String} destinationID the id of the destination pad
|
||||
@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);
|
||||
await pad.copy(destinationID, force);
|
||||
};
|
||||
|
@ -628,7 +688,12 @@ Example returns:
|
|||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||
@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);
|
||||
await pad.copyPadWithoutHistory(destinationID, force, authorId);
|
||||
};
|
||||
|
@ -645,7 +710,11 @@ Example returns:
|
|||
@param {String} destinationID the id of the destination pad
|
||||
@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);
|
||||
await pad.copy(destinationID, force);
|
||||
await pad.remove();
|
||||
|
@ -683,7 +752,7 @@ exports.getPadID = async (roID: string) => {
|
|||
// get the PadId
|
||||
const padID = await readOnlyManager.getPadId(roID);
|
||||
if (padID == null) {
|
||||
throw new CustomError('padID does not exist', 'apierror');
|
||||
throw new CustomError("padID does not exist", "apierror");
|
||||
}
|
||||
|
||||
return { padID };
|
||||
|
@ -699,16 +768,19 @@ Example returns:
|
|||
@param {String} padID the id 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
|
||||
checkGroupPad(padID, 'publicStatus');
|
||||
checkGroupPad(padID, "publicStatus");
|
||||
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
|
||||
// convert string to boolean
|
||||
if (typeof publicStatus === 'string') {
|
||||
publicStatus = (publicStatus.toLowerCase() === 'true');
|
||||
if (typeof publicStatus === "string") {
|
||||
publicStatus = publicStatus.toLowerCase() === "true";
|
||||
}
|
||||
|
||||
await pad.setPublicStatus(publicStatus);
|
||||
|
@ -725,7 +797,7 @@ Example returns:
|
|||
*/
|
||||
exports.getPublicStatus = async (padID: string) => {
|
||||
// ensure this is a group pad
|
||||
checkGroupPad(padID, 'publicStatus');
|
||||
checkGroupPad(padID, "publicStatus");
|
||||
|
||||
// get the pad
|
||||
const pad = await getPadSafe(padID, true);
|
||||
|
@ -786,8 +858,7 @@ Example returns:
|
|||
{"code":0,"message":"ok","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
|
||||
|
@ -799,7 +870,7 @@ Example returns:
|
|||
@param {String} padID the id 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
|
||||
const pad = await getPadSafe(padID, true);
|
||||
return { chatHead: pad.chatHead };
|
||||
|
@ -825,7 +896,11 @@ Example returns:
|
|||
@param {Number} startRev the start 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
|
||||
if (startRev !== undefined) {
|
||||
startRev = checkValidRev(startRev);
|
||||
|
@ -873,7 +948,9 @@ exports.getStats = async () => {
|
|||
|
||||
const sessionKeys = Object.keys(sessionInfos);
|
||||
// @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();
|
||||
|
||||
|
@ -889,15 +966,20 @@ exports.getStats = async () => {
|
|||
**************************** */
|
||||
|
||||
// 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: string = "",
|
||||
) => {
|
||||
// check if padID is a string
|
||||
if (typeof padID !== 'string') {
|
||||
throw new CustomError('padID is not a string', 'apierror');
|
||||
if (typeof padID !== "string") {
|
||||
throw new CustomError("padID is not a string", "apierror");
|
||||
}
|
||||
|
||||
// check if the padID maches the requirements
|
||||
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
|
||||
|
@ -905,12 +987,12 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri
|
|||
|
||||
if (!exists && shouldExist) {
|
||||
// does not exist, but should
|
||||
throw new CustomError('padID does not exist', 'apierror');
|
||||
throw new CustomError("padID does not exist", "apierror");
|
||||
}
|
||||
|
||||
if (exists && !shouldExist) {
|
||||
// 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
|
||||
|
@ -920,8 +1002,10 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri
|
|||
// checks if a padID is part of a group
|
||||
const checkGroupPad = (padID: string, field: string) => {
|
||||
// ensure this is a group pad
|
||||
if (padID && padID.indexOf('$') === -1) {
|
||||
if (padID && padID.indexOf("$") === -1) {
|
||||
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",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* The AuthorManager controlls all information about the Pad authors
|
||||
*/
|
||||
|
@ -19,76 +19,79 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const db = require('./DB');
|
||||
const CustomError = require('../utils/customError');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
const db = require("./DB");
|
||||
const CustomError = require("../utils/customError");
|
||||
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||
const {
|
||||
randomString,
|
||||
padutils: { warnDeprecated },
|
||||
} = require("../../static/js/pad_utils");
|
||||
|
||||
exports.getColorPalette = () => [
|
||||
'#ffc7c7',
|
||||
'#fff1c7',
|
||||
'#e3ffc7',
|
||||
'#c7ffd5',
|
||||
'#c7ffff',
|
||||
'#c7d5ff',
|
||||
'#e3c7ff',
|
||||
'#ffc7f1',
|
||||
'#ffa8a8',
|
||||
'#ffe699',
|
||||
'#cfff9e',
|
||||
'#99ffb3',
|
||||
'#a3ffff',
|
||||
'#99b3ff',
|
||||
'#cc99ff',
|
||||
'#ff99e5',
|
||||
'#e7b1b1',
|
||||
'#e9dcAf',
|
||||
'#cde9af',
|
||||
'#bfedcc',
|
||||
'#b1e7e7',
|
||||
'#c3cdee',
|
||||
'#d2b8ea',
|
||||
'#eec3e6',
|
||||
'#e9cece',
|
||||
'#e7e0ca',
|
||||
'#d3e5c7',
|
||||
'#bce1c5',
|
||||
'#c1e2e2',
|
||||
'#c1c9e2',
|
||||
'#cfc1e2',
|
||||
'#e0bdd9',
|
||||
'#baded3',
|
||||
'#a0f8eb',
|
||||
'#b1e7e0',
|
||||
'#c3c8e4',
|
||||
'#cec5e2',
|
||||
'#b1d5e7',
|
||||
'#cda8f0',
|
||||
'#f0f0a8',
|
||||
'#f2f2a6',
|
||||
'#f5a8eb',
|
||||
'#c5f9a9',
|
||||
'#ececbb',
|
||||
'#e7c4bc',
|
||||
'#daf0b2',
|
||||
'#b0a0fd',
|
||||
'#bce2e7',
|
||||
'#cce2bb',
|
||||
'#ec9afe',
|
||||
'#edabbd',
|
||||
'#aeaeea',
|
||||
'#c4e7b1',
|
||||
'#d722bb',
|
||||
'#f3a5e7',
|
||||
'#ffa8a8',
|
||||
'#d8c0c5',
|
||||
'#eaaedd',
|
||||
'#adc6eb',
|
||||
'#bedad1',
|
||||
'#dee9af',
|
||||
'#e9afc2',
|
||||
'#f8d2a0',
|
||||
'#b3b3e6',
|
||||
"#ffc7c7",
|
||||
"#fff1c7",
|
||||
"#e3ffc7",
|
||||
"#c7ffd5",
|
||||
"#c7ffff",
|
||||
"#c7d5ff",
|
||||
"#e3c7ff",
|
||||
"#ffc7f1",
|
||||
"#ffa8a8",
|
||||
"#ffe699",
|
||||
"#cfff9e",
|
||||
"#99ffb3",
|
||||
"#a3ffff",
|
||||
"#99b3ff",
|
||||
"#cc99ff",
|
||||
"#ff99e5",
|
||||
"#e7b1b1",
|
||||
"#e9dcAf",
|
||||
"#cde9af",
|
||||
"#bfedcc",
|
||||
"#b1e7e7",
|
||||
"#c3cdee",
|
||||
"#d2b8ea",
|
||||
"#eec3e6",
|
||||
"#e9cece",
|
||||
"#e7e0ca",
|
||||
"#d3e5c7",
|
||||
"#bce1c5",
|
||||
"#c1e2e2",
|
||||
"#c1c9e2",
|
||||
"#cfc1e2",
|
||||
"#e0bdd9",
|
||||
"#baded3",
|
||||
"#a0f8eb",
|
||||
"#b1e7e0",
|
||||
"#c3c8e4",
|
||||
"#cec5e2",
|
||||
"#b1d5e7",
|
||||
"#cda8f0",
|
||||
"#f0f0a8",
|
||||
"#f2f2a6",
|
||||
"#f5a8eb",
|
||||
"#c5f9a9",
|
||||
"#ececbb",
|
||||
"#e7c4bc",
|
||||
"#daf0b2",
|
||||
"#b0a0fd",
|
||||
"#bce2e7",
|
||||
"#cce2bb",
|
||||
"#ec9afe",
|
||||
"#edabbd",
|
||||
"#aeaeea",
|
||||
"#c4e7b1",
|
||||
"#d722bb",
|
||||
"#f3a5e7",
|
||||
"#ffa8a8",
|
||||
"#d8c0c5",
|
||||
"#eaaedd",
|
||||
"#adc6eb",
|
||||
"#bedad1",
|
||||
"#dee9af",
|
||||
"#e9afc2",
|
||||
"#f8d2a0",
|
||||
"#b3b3e6",
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -107,7 +110,6 @@ exports.doesAuthorExist = async (authorID: string) => {
|
|||
*/
|
||||
exports.doesAuthorExists = exports.doesAuthorExist;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the AuthorID for a mapper. We can map using a mapperkey,
|
||||
* so far this is token2author and mapper2author
|
||||
|
@ -131,7 +133,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
|
|||
|
||||
// there is an author with this mapper
|
||||
// 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 { authorID: author };
|
||||
|
@ -143,7 +145,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
|
|||
* @return {Promise<string|*|{authorID: string}|{authorID: *}>}
|
||||
*/
|
||||
const getAuthor4Token = async (token: string) => {
|
||||
const author = await mapAuthorWithDBKey('token2author', token);
|
||||
const author = await mapAuthorWithDBKey("token2author", token);
|
||||
|
||||
// return only the sub value authorID
|
||||
return author ? author.authorID : author;
|
||||
|
@ -157,7 +159,7 @@ const getAuthor4Token = async (token: string) => {
|
|||
*/
|
||||
exports.getAuthorId = async (token: string, user: object) => {
|
||||
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);
|
||||
return authorId;
|
||||
};
|
||||
|
@ -170,7 +172,8 @@ exports.getAuthorId = async (token: string, user: object) => {
|
|||
*/
|
||||
exports.getAuthor4Token = async (token: string) => {
|
||||
warnDeprecated(
|
||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
||||
"AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead",
|
||||
);
|
||||
return await getAuthor4Token(token);
|
||||
};
|
||||
|
||||
|
@ -179,8 +182,11 @@ exports.getAuthor4Token = async (token: string) => {
|
|||
* @param {String} authorMapper The mapper
|
||||
* @param {String} name The name of the author (optional)
|
||||
*/
|
||||
exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string) => {
|
||||
const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
|
||||
exports.createAuthorIfNotExistsFor = async (
|
||||
authorMapper: string,
|
||||
name: string,
|
||||
) => {
|
||||
const author = await mapAuthorWithDBKey("mapper2author", authorMapper);
|
||||
|
||||
if (name) {
|
||||
// set the name of this author
|
||||
|
@ -190,7 +196,6 @@ exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string)
|
|||
return author;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Internal function that creates the database entry for an author
|
||||
* @param {String} name The name of the author
|
||||
|
@ -201,7 +206,7 @@ exports.createAuthor = async (name: string) => {
|
|||
|
||||
// create the globalAuthors db entry
|
||||
const authorObj = {
|
||||
colorId: Math.floor(Math.random() * (exports.getColorPalette().length)),
|
||||
colorId: Math.floor(Math.random() * exports.getColorPalette().length),
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
@ -216,35 +221,38 @@ exports.createAuthor = async (name: string) => {
|
|||
* Returns the Author Obj 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
|
||||
* @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
|
||||
* @param {String} author The id of the author
|
||||
* @param {String} colorId The color id of the author
|
||||
*/
|
||||
exports.setAuthorColorId = async (author: string, colorId: string) => await db.setSub(
|
||||
`globalAuthor:${author}`, ['colorId'], colorId);
|
||||
exports.setAuthorColorId = async (author: string, colorId: string) =>
|
||||
await db.setSub(`globalAuthor:${author}`, ["colorId"], colorId);
|
||||
|
||||
/**
|
||||
* Returns the name 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
|
||||
* @param {String} author The id of the author
|
||||
* @param {String} name The name of the author
|
||||
*/
|
||||
exports.setAuthorName = async (author: string, name: string) => await db.setSub(
|
||||
`globalAuthor:${author}`, ['name'], name);
|
||||
exports.setAuthorName = async (author: string, name: string) =>
|
||||
await db.setSub(`globalAuthor:${author}`, ["name"], name);
|
||||
|
||||
/**
|
||||
* Returns an array of all pads this author contributed to
|
||||
|
@ -261,7 +269,7 @@ exports.listPadsOfAuthor = async (authorID: string) => {
|
|||
|
||||
if (author == null) {
|
||||
// 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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* The DB Module provides a database initialized with the settings
|
||||
|
@ -21,12 +21,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ueberDB from 'ueberdb2';
|
||||
const settings = require('../utils/Settings');
|
||||
import log4js from 'log4js';
|
||||
const stats = require('../stats')
|
||||
import ueberDB from "ueberdb2";
|
||||
const settings = require("../utils/Settings");
|
||||
import log4js from "log4js";
|
||||
const stats = require("../stats");
|
||||
|
||||
const logger = log4js.getLogger('ueberDB');
|
||||
const logger = log4js.getLogger("ueberDB");
|
||||
|
||||
/**
|
||||
* The UeberDB Object that provides the database functions
|
||||
|
@ -37,17 +37,23 @@ exports.db = null;
|
|||
* Initializes the database with the settings provided by the settings module
|
||||
*/
|
||||
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();
|
||||
if (exports.db.metrics != null) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
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];
|
||||
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.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
|
||||
}
|
||||
|
@ -56,5 +62,5 @@ exports.init = async () => {
|
|||
exports.shutdown = async (hookName: string, context: any) => {
|
||||
if (exports.db != null) await exports.db.close();
|
||||
exports.db = null;
|
||||
logger.log('Database closed');
|
||||
logger.log("Database closed");
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* The Group Manager provides functions to manage groups in the database
|
||||
*/
|
||||
|
@ -19,18 +19,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const randomString = require('../../static/js/pad_utils').randomString;
|
||||
const db = require('./DB');
|
||||
const padManager = require('./PadManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
const CustomError = require("../utils/customError");
|
||||
const randomString = require("../../static/js/pad_utils").randomString;
|
||||
const db = require("./DB");
|
||||
const padManager = require("./PadManager");
|
||||
const sessionManager = require("./SessionManager");
|
||||
|
||||
/**
|
||||
* Lists all groups
|
||||
* @return {Promise<{groupIDs: string[]}>} The ids of all groups
|
||||
*/
|
||||
exports.listAllGroups = async () => {
|
||||
let groups = await db.get('groups');
|
||||
let groups = await db.get("groups");
|
||||
groups = groups || {};
|
||||
|
||||
const groupIDs = Object.keys(groups);
|
||||
|
@ -48,29 +48,35 @@ exports.deleteGroup = async (groupID: string): Promise<void> => {
|
|||
// ensure group exists
|
||||
if (group == null) {
|
||||
// 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)
|
||||
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);
|
||||
await pad.remove();
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
// Delete associated sessions in parallel. This should be done before deleting the group2sessions
|
||||
// record because deleting a session updates the group2sessions record.
|
||||
const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {};
|
||||
await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => {
|
||||
const { sessionIDs = {} } = (await db.get(`group2sessions:${groupID}`)) || {};
|
||||
await Promise.all(
|
||||
Object.keys(sessionIDs).map(async (sessionId) => {
|
||||
await sessionManager.deleteSession(sessionId);
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
db.remove(`group2sessions:${groupID}`),
|
||||
// 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()
|
||||
// ignores such properties).
|
||||
db.setSub('groups', [groupID], undefined),
|
||||
...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)),
|
||||
db.setSub("groups", [groupID], undefined),
|
||||
...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.
|
||||
|
@ -86,7 +92,7 @@ exports.doesGroupExist = async (groupID: string) => {
|
|||
// try to get the group entry
|
||||
const group = await db.get(`group:${groupID}`);
|
||||
|
||||
return (group != null);
|
||||
return group != null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -99,7 +105,7 @@ exports.createGroup = async () => {
|
|||
// 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 appropriate property, and writes the result.
|
||||
await db.setSub('groups', [groupID], 1);
|
||||
await db.setSub("groups", [groupID], 1);
|
||||
return { groupID };
|
||||
};
|
||||
|
||||
|
@ -109,11 +115,11 @@ exports.createGroup = async () => {
|
|||
* @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID
|
||||
*/
|
||||
exports.createGroupIfNotExistsFor = async (groupMapper: string | object) => {
|
||||
if (typeof groupMapper !== 'string') {
|
||||
throw new CustomError('groupMapper is not a string', 'apierror');
|
||||
if (typeof groupMapper !== "string") {
|
||||
throw new CustomError("groupMapper is not a string", "apierror");
|
||||
}
|
||||
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();
|
||||
await Promise.all([
|
||||
db.set(`mapper2group:${groupMapper}`, result.groupID),
|
||||
|
@ -121,7 +127,7 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
|
|||
// 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
|
||||
// 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;
|
||||
};
|
||||
|
@ -134,7 +140,12 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
|
|||
* @param {String} authorId The id of the author
|
||||
* @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: string = "",
|
||||
): Promise<{ padID: string }> => {
|
||||
// create the padID
|
||||
const padID = `${groupID}$${padName}`;
|
||||
|
||||
|
@ -142,7 +153,7 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
|
|||
const groupExists = await exports.doesGroupExist(groupID);
|
||||
|
||||
if (!groupExists) {
|
||||
throw new CustomError('groupID does not exist', 'apierror');
|
||||
throw new CustomError("groupID does not exist", "apierror");
|
||||
}
|
||||
|
||||
// ensure pad doesn't exist already
|
||||
|
@ -150,14 +161,14 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
|
|||
|
||||
if (padExists) {
|
||||
// pad exists already
|
||||
throw new CustomError('padName does already exist', 'apierror');
|
||||
throw new CustomError("padName does already exist", "apierror");
|
||||
}
|
||||
|
||||
// create the pad
|
||||
await padManager.getPad(padID, text, authorId);
|
||||
|
||||
// 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 };
|
||||
};
|
||||
|
@ -167,16 +178,16 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
|
|||
* @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
|
||||
*/
|
||||
exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => {
|
||||
exports.listPads = async (groupID: string): Promise<{ padIDs: string[] }> => {
|
||||
const exists = await exports.doesGroupExist(groupID);
|
||||
|
||||
// ensure the group 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
|
||||
const result = await db.getSub(`group:${groupID}`, ['pads']);
|
||||
const result = await db.getSub(`group:${groupID}`, ["pads"]);
|
||||
const padIDs = Object.keys(result);
|
||||
|
||||
return { padIDs };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
import { Database } from "ueberdb2";
|
||||
import { AChangeSet, APool, AText } from "../types/PadType";
|
||||
import { MapArrayType } from "../types/MapType";
|
||||
|
@ -7,24 +7,26 @@ import {MapArrayType} from "../types/MapType";
|
|||
* The pad object, defined with joose
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
const Stream = require('../utils/Stream');
|
||||
const assert = require('assert').strict;
|
||||
const db = require('./DB');
|
||||
const settings = require('../utils/Settings');
|
||||
const authorManager = require('./AuthorManager');
|
||||
const padManager = require('./PadManager');
|
||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||
const groupManager = require('./GroupManager');
|
||||
const CustomError = require('../utils/customError');
|
||||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const randomString = require('../utils/randomstring');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
const promises = require('../utils/promises');
|
||||
const AttributeMap = require("../../static/js/AttributeMap");
|
||||
const Changeset = require("../../static/js/Changeset");
|
||||
const ChatMessage = require("../../static/js/ChatMessage");
|
||||
const AttributePool = require("../../static/js/AttributePool");
|
||||
const Stream = require("../utils/Stream");
|
||||
const assert = require("assert").strict;
|
||||
const db = require("./DB");
|
||||
const settings = require("../utils/Settings");
|
||||
const authorManager = require("./AuthorManager");
|
||||
const padManager = require("./PadManager");
|
||||
const padMessageHandler = require("../handler/PadMessageHandler");
|
||||
const groupManager = require("./GroupManager");
|
||||
const CustomError = require("../utils/customError");
|
||||
const readOnlyManager = require("./ReadOnlyManager");
|
||||
const randomString = require("../utils/randomstring");
|
||||
const hooks = require("../../static/js/pluginfw/hooks");
|
||||
const {
|
||||
padutils: { warnDeprecated },
|
||||
} = require("../../static/js/pad_utils");
|
||||
const promises = require("../utils/promises");
|
||||
|
||||
/**
|
||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
||||
|
@ -32,10 +34,12 @@ const promises = require('../utils/promises');
|
|||
* @param {String} txt The text to clean
|
||||
* @returns {String} The cleaned text
|
||||
*/
|
||||
exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/\xa0/g, ' ');
|
||||
exports.cleanText = (txt: string): string =>
|
||||
txt
|
||||
.replace(/\r\n/g, "\n")
|
||||
.replace(/\r/g, "\n")
|
||||
.replace(/\t/g, " ")
|
||||
.replace(/\xa0/g, " ");
|
||||
|
||||
class Pad {
|
||||
private db: Database;
|
||||
|
@ -56,7 +60,7 @@ class Pad {
|
|||
*/
|
||||
constructor(id: string, database = db) {
|
||||
this.db = database;
|
||||
this.atext = Changeset.makeAText('\n');
|
||||
this.atext = Changeset.makeAText("\n");
|
||||
this.pool = new AttributePool();
|
||||
this.head = -1;
|
||||
this.chatHead = -1;
|
||||
|
@ -93,10 +97,13 @@ class Pad {
|
|||
* @param {String} authorId The id of the author
|
||||
* @return {Promise<number|string>}
|
||||
*/
|
||||
async appendRevision(aChangeset:AChangeSet, authorId = '') {
|
||||
async appendRevision(aChangeset: AChangeSet, authorId = "") {
|
||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
||||
this.head !== -1) {
|
||||
if (
|
||||
newAText.text === this.atext.text &&
|
||||
newAText.attribs === this.atext.attribs &&
|
||||
this.head !== -1
|
||||
) {
|
||||
return this.head;
|
||||
}
|
||||
Changeset.copyAText(newAText, this.atext);
|
||||
|
@ -104,9 +111,9 @@ class Pad {
|
|||
const newRev = ++this.head;
|
||||
|
||||
// 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([
|
||||
// @ts-ignore
|
||||
this.db.set(`pad:${this.id}:revs:${newRev}`, {
|
||||
|
@ -114,10 +121,12 @@ class Pad {
|
|||
meta: {
|
||||
author: authorId,
|
||||
timestamp: Date.now(),
|
||||
...newRev === this.getKeyRevisionNumber(newRev) ? {
|
||||
...(newRev === this.getKeyRevisionNumber(newRev)
|
||||
? {
|
||||
pool: this.pool,
|
||||
atext: this.atext,
|
||||
} : {},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
this.saveToDatabase(),
|
||||
|
@ -126,17 +135,23 @@ class Pad {
|
|||
pad: this,
|
||||
authorId,
|
||||
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;
|
||||
},
|
||||
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.head === 0 ? {} : {
|
||||
...(this.head === 0
|
||||
? {}
|
||||
: {
|
||||
revs: newRev,
|
||||
changeset: aChangeset,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
return newRev;
|
||||
|
@ -161,22 +176,31 @@ class Pad {
|
|||
async getLastEdit() {
|
||||
const revNum = this.getHeadRevisionNumber();
|
||||
// @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) {
|
||||
// @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) {
|
||||
// @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) {
|
||||
// @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 +209,10 @@ class Pad {
|
|||
*/
|
||||
async _getKeyRevisionAText(revNum: number) {
|
||||
// @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 +223,10 @@ class Pad {
|
|||
const authorIds = [];
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
@ -211,11 +241,15 @@ class Pad {
|
|||
const [keyAText, changesets] = await Promise.all([
|
||||
this._getKeyRevisionAText(keyRev),
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -229,10 +263,13 @@ class Pad {
|
|||
const colorPalette = authorManager.getColorPalette();
|
||||
|
||||
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
|
||||
returnTable[authorId] = colorPalette[colorId] || colorId;
|
||||
})));
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return returnTable;
|
||||
}
|
||||
|
@ -280,18 +317,28 @@ class Pad {
|
|||
* @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).
|
||||
*/
|
||||
async spliceText(start:number, ndel:number, ins: string, authorId: string = '') {
|
||||
if (start < 0) 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})`);
|
||||
async spliceText(
|
||||
start: number,
|
||||
ndel: number,
|
||||
ins: string,
|
||||
authorId: string = "",
|
||||
) {
|
||||
if (start < 0)
|
||||
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();
|
||||
assert(orig.endsWith('\n'));
|
||||
if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text');
|
||||
assert(orig.endsWith("\n"));
|
||||
if (start + ndel > orig.length)
|
||||
throw new RangeError("start/delete past the end of the text");
|
||||
ins = exports.cleanText(ins);
|
||||
const willEndWithNewline =
|
||||
start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
|
||||
ins.endsWith('\n') ||
|
||||
(!ins && start > 0 && orig[start - 1] === '\n');
|
||||
if (!willEndWithNewline) ins += '\n';
|
||||
ins.endsWith("\n") ||
|
||||
(!ins && start > 0 && orig[start - 1] === "\n");
|
||||
if (!willEndWithNewline) ins += "\n";
|
||||
if (ndel === 0 && ins.length === 0) return;
|
||||
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
|
||||
await this.appendRevision(changeset, authorId);
|
||||
|
@ -305,7 +352,7 @@ class Pad {
|
|||
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
||||
* applicable.
|
||||
*/
|
||||
async setText(newText: string, authorId = '') {
|
||||
async setText(newText: string, authorId = "") {
|
||||
await this.spliceText(0, this.text().length, newText, authorId);
|
||||
}
|
||||
|
||||
|
@ -316,7 +363,7 @@ class Pad {
|
|||
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
||||
* applicable.
|
||||
*/
|
||||
async appendText(newText:string, authorId = '') {
|
||||
async appendText(newText: string, authorId = "") {
|
||||
await this.spliceText(this.text().length - 1, 0, newText, authorId);
|
||||
}
|
||||
|
||||
|
@ -330,15 +377,24 @@ class Pad {
|
|||
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
||||
* `msgOrText.time` instead.
|
||||
*/
|
||||
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
|
||||
async appendChatMessage(
|
||||
msgOrText: string | typeof ChatMessage,
|
||||
authorId = null,
|
||||
time = null,
|
||||
) {
|
||||
const msg =
|
||||
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
||||
msgOrText instanceof ChatMessage
|
||||
? msgOrText
|
||||
: new ChatMessage(msgOrText, authorId, time);
|
||||
this.chatHead++;
|
||||
await Promise.all([
|
||||
// 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
|
||||
// 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(),
|
||||
]);
|
||||
}
|
||||
|
@ -363,14 +419,15 @@ class Pad {
|
|||
* interval as is typical in code.
|
||||
*/
|
||||
async getChatMessages(start: string, end: number) {
|
||||
const entries =
|
||||
await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this)));
|
||||
const entries = await Promise.all(
|
||||
Stream.range(start, end + 1).map(this.getChatMessage.bind(this)),
|
||||
);
|
||||
|
||||
// sort out broken chat entries
|
||||
// it looks like in happened in the past that the chat head was
|
||||
// incremented, but the chat message wasn't added
|
||||
return entries.filter((entry) => {
|
||||
const pass = (entry != null);
|
||||
const pass = entry != null;
|
||||
if (!pass) {
|
||||
console.warn(`WARNING: Found broken chat entry in pad ${this.id}`);
|
||||
}
|
||||
|
@ -378,25 +435,32 @@ class Pad {
|
|||
});
|
||||
}
|
||||
|
||||
async init(text:string, authorId = '') {
|
||||
async init(text: string, authorId = "") {
|
||||
// try to load the pad
|
||||
const value = await this.db.get(`pad:${this.id}`);
|
||||
|
||||
// if this pad exists, load it
|
||||
if (value != null) {
|
||||
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 {
|
||||
if (text == null) {
|
||||
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
|
||||
await hooks.aCallAll('padDefaultContent', context);
|
||||
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
||||
const context = {
|
||||
pad: this,
|
||||
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);
|
||||
}
|
||||
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
|
||||
const firstChangeset = Changeset.makeSplice("\n", 0, 0, text);
|
||||
await this.appendRevision(firstChangeset, authorId);
|
||||
}
|
||||
await hooks.aCallAll('padLoad', {pad: this});
|
||||
await hooks.aCallAll("padLoad", { pad: this });
|
||||
}
|
||||
|
||||
async copy(destinationID: string, force: boolean) {
|
||||
|
@ -419,30 +483,38 @@ class Pad {
|
|||
await db.set(`pad:${destinationID}${keySuffix}`, val);
|
||||
};
|
||||
|
||||
const promises = (function* () {
|
||||
yield copyRecord('');
|
||||
const promises = function* () {
|
||||
yield copyRecord("");
|
||||
// @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
|
||||
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
|
||||
yield this.copyAuthorInfoToDestinationPad(destinationID);
|
||||
if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
|
||||
}).call(this);
|
||||
if (destGroupID)
|
||||
yield db.setSub(`group:${destGroupID}`, ["pads", destinationID], 1);
|
||||
}.call(this);
|
||||
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
|
||||
|
||||
// Initialize the new pad (will update the listAllPads cache)
|
||||
const dstPad = await padManager.getPad(destinationID, null);
|
||||
|
||||
// let the plugins know the pad was copied
|
||||
await hooks.aCallAll('padCopy', {
|
||||
await hooks.aCallAll("padCopy", {
|
||||
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;
|
||||
},
|
||||
get destinationID() {
|
||||
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;
|
||||
},
|
||||
srcPad: this,
|
||||
|
@ -455,33 +527,39 @@ class Pad {
|
|||
async checkIfGroupExistAndReturnIt(destinationID: string) {
|
||||
let destGroupID: false | string = false;
|
||||
|
||||
if (destinationID.indexOf('$') >= 0) {
|
||||
destGroupID = destinationID.split('$')[0];
|
||||
if (destinationID.indexOf("$") >= 0) {
|
||||
destGroupID = destinationID.split("$")[0];
|
||||
const groupExists = await groupManager.doesGroupExist(destGroupID);
|
||||
|
||||
// group does not exist
|
||||
if (!groupExists) {
|
||||
throw new CustomError('groupID does not exist for destinationID', 'apierror');
|
||||
throw new CustomError(
|
||||
"groupID does not exist for destinationID",
|
||||
"apierror",
|
||||
);
|
||||
}
|
||||
}
|
||||
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.
|
||||
const exists = await padManager.doesPadExist(destinationID);
|
||||
|
||||
// allow force to be a string
|
||||
if (typeof force === 'string') {
|
||||
force = (force.toLowerCase() === 'true');
|
||||
if (typeof force === "string") {
|
||||
force = force.toLowerCase() === "true";
|
||||
} else {
|
||||
force = !!force;
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
if (!force) {
|
||||
console.error('erroring out without force');
|
||||
throw new CustomError('destinationID already exists', 'apierror');
|
||||
console.error("erroring out without force");
|
||||
throw new CustomError("destinationID already exists", "apierror");
|
||||
}
|
||||
|
||||
// exists and forcing
|
||||
|
@ -492,11 +570,18 @@ class Pad {
|
|||
|
||||
async copyAuthorInfoToDestinationPad(destinationID: string) {
|
||||
// add the new sourcePad to all authors who contributed to the old one
|
||||
await Promise.all(this.getAllAuthors().map(
|
||||
(authorID) => authorManager.addPad(authorID, destinationID)));
|
||||
await Promise.all(
|
||||
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
|
||||
this.saveToDatabase();
|
||||
|
||||
|
@ -510,11 +595,11 @@ class Pad {
|
|||
|
||||
// Group pad? Add it to the group's list
|
||||
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
|
||||
const dstPad = await padManager.getPad(destinationID, '\n', authorId);
|
||||
const dstPad = await padManager.getPad(destinationID, "\n", authorId);
|
||||
dstPad.pool = this.pool.clone();
|
||||
|
||||
const oldAText = this.atext;
|
||||
|
@ -533,17 +618,25 @@ class Pad {
|
|||
|
||||
// create a changeset that removes the previous text and add the newText with
|
||||
// all atributes present on the source pad
|
||||
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
|
||||
const changeset = Changeset.pack(
|
||||
oldLength,
|
||||
newLength,
|
||||
assem.toString(),
|
||||
newText,
|
||||
);
|
||||
dstPad.appendRevision(changeset, authorId);
|
||||
|
||||
await hooks.aCallAll('padCopy', {
|
||||
await hooks.aCallAll("padCopy", {
|
||||
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;
|
||||
},
|
||||
get destinationID() {
|
||||
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;
|
||||
},
|
||||
srcPad: this,
|
||||
|
@ -566,9 +659,9 @@ class Pad {
|
|||
// run to completion
|
||||
|
||||
// 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
|
||||
const groupID = padID.substring(0, padID.indexOf('$'));
|
||||
const groupID = padID.substring(0, padID.indexOf("$"));
|
||||
const group = await db.get(`group:${groupID}`);
|
||||
|
||||
// remove the pad entry
|
||||
|
@ -579,20 +672,26 @@ class Pad {
|
|||
}
|
||||
|
||||
// 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}`);
|
||||
}));
|
||||
}),
|
||||
);
|
||||
p.push(db.remove(`pad2readonly:${padID}`));
|
||||
|
||||
// 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);
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
// 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);
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
// remove pad from all authors who contributed
|
||||
this.getAllAuthors().forEach((authorId) => {
|
||||
|
@ -601,13 +700,17 @@ class Pad {
|
|||
|
||||
// delete the pad entry and delete pad from padManager
|
||||
p.push(padManager.removePad(padID));
|
||||
p.push(hooks.aCallAll('padRemove', {
|
||||
p.push(
|
||||
hooks.aCallAll("padRemove", {
|
||||
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;
|
||||
},
|
||||
pad: this,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
await Promise.all(p);
|
||||
}
|
||||
|
||||
|
@ -647,7 +750,7 @@ class Pad {
|
|||
*/
|
||||
async check() {
|
||||
assert(this.id != null);
|
||||
assert.equal(typeof this.id, 'string');
|
||||
assert.equal(typeof this.id, "string");
|
||||
|
||||
const head = this.getHeadRevisionNumber();
|
||||
assert(head != null);
|
||||
|
@ -672,10 +775,10 @@ class Pad {
|
|||
const savedRevisionsIds = new Set();
|
||||
for (const savedRev of savedRevisions) {
|
||||
assert(savedRev != null);
|
||||
assert.equal(typeof savedRev, 'object');
|
||||
assert.equal(typeof savedRev, "object");
|
||||
assert(savedRevisionsList.includes(savedRev.revNum));
|
||||
assert(savedRev.id != null);
|
||||
assert.equal(typeof savedRev.id, 'string');
|
||||
assert.equal(typeof savedRev.id, "string");
|
||||
assert(!savedRevisionsIds.has(savedRev.id));
|
||||
savedRevisionsIds.add(savedRev.id);
|
||||
}
|
||||
|
@ -686,7 +789,7 @@ class Pad {
|
|||
|
||||
const authorIds = new Set();
|
||||
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)
|
||||
.map(async (r: number) => {
|
||||
|
@ -705,31 +808,42 @@ class Pad {
|
|||
throw err;
|
||||
}
|
||||
})
|
||||
.batch(100).buffer(99);
|
||||
let atext = Changeset.makeAText('\n');
|
||||
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
||||
.batch(100)
|
||||
.buffer(99);
|
||||
let atext = Changeset.makeAText("\n");
|
||||
for await (const [
|
||||
r,
|
||||
changeset,
|
||||
authorId,
|
||||
timestamp,
|
||||
isKeyRev,
|
||||
keyAText,
|
||||
] of revs) {
|
||||
try {
|
||||
assert(authorId != null);
|
||||
assert.equal(typeof authorId, 'string');
|
||||
assert.equal(typeof authorId, "string");
|
||||
if (authorId) authorIds.add(authorId);
|
||||
assert(timestamp != null);
|
||||
assert.equal(typeof timestamp, 'number');
|
||||
assert.equal(typeof timestamp, "number");
|
||||
assert(timestamp > 0);
|
||||
assert(changeset != null);
|
||||
assert.equal(typeof changeset, 'string');
|
||||
assert.equal(typeof changeset, "string");
|
||||
Changeset.checkRep(changeset);
|
||||
const unpacked = Changeset.unpack(changeset);
|
||||
let text = atext.text;
|
||||
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
||||
if (['=', '-'].includes(op.opcode)) {
|
||||
if (["=", "-"].includes(op.opcode)) {
|
||||
assert(text.length >= op.chars);
|
||||
const consumed = text.slice(0, op.chars);
|
||||
const nlines = (consumed.match(/\n/g) || []).length;
|
||||
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);
|
||||
}
|
||||
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);
|
||||
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
||||
|
@ -756,10 +870,11 @@ class Pad {
|
|||
throw err;
|
||||
}
|
||||
})
|
||||
.batch(100).buffer(99);
|
||||
.batch(100)
|
||||
.buffer(99);
|
||||
for (const p of chats) await p;
|
||||
|
||||
await hooks.aCallAll('padCheck', {pad: this});
|
||||
await hooks.aCallAll("padCheck", { pad: this });
|
||||
}
|
||||
}
|
||||
exports.Pad = Pad;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* The Pad Manager is a Factory for pad Objects
|
||||
*/
|
||||
|
@ -22,10 +22,10 @@
|
|||
import { MapArrayType } from "../types/MapType";
|
||||
import { PadType } from "../types/PadType";
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const Pad = require('../db/Pad');
|
||||
const db = require('./DB');
|
||||
const settings = require('../utils/Settings');
|
||||
const CustomError = require("../utils/customError");
|
||||
const Pad = require("../db/Pad");
|
||||
const db = require("./DB");
|
||||
const settings = require("../utils/Settings");
|
||||
|
||||
/**
|
||||
* A cache of all loaded Pads.
|
||||
|
@ -39,12 +39,10 @@ const settings = require('../utils/Settings');
|
|||
* that's defined somewhere more sensible.
|
||||
*/
|
||||
const globalPads: MapArrayType<any> = {
|
||||
get(name: string)
|
||||
{
|
||||
get(name: string) {
|
||||
return this[`:${name}`];
|
||||
},
|
||||
set(name: string, value: any)
|
||||
{
|
||||
set(name: string, value: any) {
|
||||
this[`:${name}`] = value;
|
||||
},
|
||||
remove(name: string) {
|
||||
|
@ -57,7 +55,7 @@ const globalPads:MapArrayType<any> = {
|
|||
*
|
||||
* 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 _list: Set<string>;
|
||||
private _loaded: Promise<void> | null;
|
||||
|
@ -74,9 +72,9 @@ const padList = new class {
|
|||
async getPads() {
|
||||
if (!this._loaded) {
|
||||
this._loaded = (async () => {
|
||||
const dbData = await db.findKeys('pad:*', '*:*:*');
|
||||
const dbData = await db.findKeys("pad:*", "*:*:*");
|
||||
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;
|
||||
|
@ -95,7 +93,7 @@ const padList = new class {
|
|||
this._list.delete(name);
|
||||
this._cachedList = null;
|
||||
}
|
||||
}();
|
||||
})();
|
||||
|
||||
// initialises the all-knowing data structure
|
||||
|
||||
|
@ -106,22 +104,26 @@ const padList = new class {
|
|||
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
|
||||
* 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
|
||||
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
|
||||
if (text != null) {
|
||||
// check if text is a string
|
||||
if (typeof text !== 'string') {
|
||||
throw new CustomError('text is not a string', 'apierror');
|
||||
if (typeof text !== "string") {
|
||||
throw new CustomError("text is not a string", "apierror");
|
||||
}
|
||||
|
||||
// check if text is less than 100k chars
|
||||
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 +151,11 @@ exports.listAllPads = async () => {
|
|||
return { padIDs };
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// checks if a pad exists
|
||||
exports.doesPadExist = async (padId: string) => {
|
||||
const value = await db.get(`pad:${padId}`);
|
||||
|
||||
return (value != null && value.atext);
|
||||
return value != null && value.atext;
|
||||
};
|
||||
|
||||
// alias for backwards compatibility
|
||||
|
@ -167,8 +166,8 @@ exports.doesPadExists = exports.doesPadExist;
|
|||
* time, and allow us to "play back" these changes so legacy padIds can be found.
|
||||
*/
|
||||
const padIdTransforms = [
|
||||
[/\s+/g, '_'],
|
||||
[/:+/g, '_'],
|
||||
[/\s+/g, "_"],
|
||||
[/:+/g, "_"],
|
||||
];
|
||||
|
||||
// returns a sanitized padId, respecting legacy pad id formats
|
||||
|
@ -192,7 +191,8 @@ exports.sanitizePadId = async (padId: string) => {
|
|||
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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* The ReadOnlyManager manages the database and rendering releated to read only pads
|
||||
*/
|
||||
|
@ -19,17 +19,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
const db = require('./DB');
|
||||
const randomString = require('../utils/randomstring');
|
||||
|
||||
const db = require("./DB");
|
||||
const randomString = require("../utils/randomstring");
|
||||
|
||||
/**
|
||||
* checks if the id pattern matches a read-only pad id
|
||||
* @param {String} id the pad's id
|
||||
* @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
|
||||
|
@ -57,7 +55,8 @@ exports.getReadOnlyId = async (padId:string) => {
|
|||
* @param {String} readOnlyId read only id
|
||||
* @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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* Controls the security of pad access
|
||||
*/
|
||||
|
@ -21,18 +21,18 @@
|
|||
|
||||
import { UserSettingsObject } from "../types/UserSettingsObject";
|
||||
|
||||
const authorManager = require('./AuthorManager');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const padManager = require('./PadManager');
|
||||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
const settings = require('../utils/Settings');
|
||||
const webaccess = require('../hooks/express/webaccess');
|
||||
const log4js = require('log4js');
|
||||
const authLogger = log4js.getLogger('auth');
|
||||
const {padutils} = require('../../static/js/pad_utils');
|
||||
const authorManager = require("./AuthorManager");
|
||||
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||
const padManager = require("./PadManager");
|
||||
const readOnlyManager = require("./ReadOnlyManager");
|
||||
const sessionManager = require("./SessionManager");
|
||||
const settings = require("../utils/Settings");
|
||||
const webaccess = require("../hooks/express/webaccess");
|
||||
const log4js = require("log4js");
|
||||
const authLogger = log4js.getLogger("auth");
|
||||
const { padutils } = require("../../static/js/pad_utils");
|
||||
|
||||
const DENY = Object.freeze({accessStatus: 'deny'});
|
||||
const DENY = Object.freeze({ accessStatus: "deny" });
|
||||
|
||||
/**
|
||||
* Determines whether the user can access a pad.
|
||||
|
@ -57,9 +57,14 @@ const DENY = Object.freeze({accessStatus: 'deny'});
|
|||
* @param {Object} userSettings
|
||||
* @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) {
|
||||
authLogger.debug('access denied: missing padID');
|
||||
authLogger.debug("access denied: missing padID");
|
||||
return DENY;
|
||||
}
|
||||
|
||||
|
@ -69,7 +74,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
|||
canCreate = false;
|
||||
padID = await readOnlyManager.getPadId(padID);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -77,62 +84,82 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
|||
// Authentication and authorization checks.
|
||||
if (settings.loadTest) {
|
||||
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) {
|
||||
if (userSettings == null) {
|
||||
authLogger.debug('access denied: authentication is required');
|
||||
authLogger.debug("access denied: authentication is required");
|
||||
return DENY;
|
||||
}
|
||||
if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false;
|
||||
if (userSettings.canCreate != null && !userSettings.canCreate)
|
||||
canCreate = false;
|
||||
if (userSettings.readOnly) canCreate = false;
|
||||
// Note: userSettings.padAuthorizations should still be populated even if
|
||||
// settings.requireAuthorization is false.
|
||||
const padAuthzs = userSettings.padAuthorizations || {};
|
||||
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
|
||||
if (!level) {
|
||||
authLogger.debug('access denied: unauthorized');
|
||||
authLogger.debug("access denied: unauthorized");
|
||||
return DENY;
|
||||
}
|
||||
if (level !== 'create') canCreate = false;
|
||||
if (level !== "create") canCreate = false;
|
||||
}
|
||||
|
||||
// allow plugins to deny access
|
||||
const isFalse = (x: boolean) => x === false;
|
||||
if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
|
||||
authLogger.debug('access denied: an onAccessCheck hook function returned false');
|
||||
if (
|
||||
hooks
|
||||
.callAll("onAccessCheck", { padID, token, sessionCookie })
|
||||
.some(isFalse)
|
||||
) {
|
||||
authLogger.debug(
|
||||
"access denied: an onAccessCheck hook function returned false",
|
||||
);
|
||||
return DENY;
|
||||
}
|
||||
|
||||
const padExists = await padManager.doesPadExist(padID);
|
||||
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;
|
||||
}
|
||||
|
||||
const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie);
|
||||
const sessionAuthorID = await sessionManager.findAuthorID(
|
||||
padID.split("$")[0],
|
||||
sessionCookie,
|
||||
);
|
||||
if (settings.requireSession && !sessionAuthorID) {
|
||||
authLogger.debug('access denied: HTTP API session is required');
|
||||
authLogger.debug("access denied: HTTP API session is required");
|
||||
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.
|
||||
authLogger.debug('access denied: invalid author token');
|
||||
authLogger.debug("access denied: invalid author token");
|
||||
return DENY;
|
||||
}
|
||||
|
||||
const grant = {
|
||||
accessStatus: 'grant',
|
||||
authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings),
|
||||
accessStatus: "grant",
|
||||
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.
|
||||
return grant;
|
||||
}
|
||||
|
||||
if (!padExists) {
|
||||
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;
|
||||
}
|
||||
// Creating a group pad, so there is no public status to check.
|
||||
|
@ -142,7 +169,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
|||
const pad = await padManager.getPad(padID);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* The Session Manager provides functions to manage session in the database,
|
||||
* it only provides session management for sessions created by the API
|
||||
|
@ -20,12 +20,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const promises = require('../utils/promises');
|
||||
const randomString = require('../utils/randomstring');
|
||||
const db = require('./DB');
|
||||
const groupManager = require('./GroupManager');
|
||||
const authorManager = require('./AuthorManager');
|
||||
const CustomError = require("../utils/customError");
|
||||
const promises = require("../utils/promises");
|
||||
const randomString = require("../utils/randomstring");
|
||||
const db = require("./DB");
|
||||
const groupManager = require("./GroupManager");
|
||||
const authorManager = require("./AuthorManager");
|
||||
|
||||
/**
|
||||
* Finds the author ID for a session with matching ID and group.
|
||||
|
@ -61,13 +61,15 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
|||
* Fixes #3819.
|
||||
* Also, see #3820.
|
||||
*/
|
||||
const sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(',');
|
||||
const sessionIDs = sessionCookie.replace(/^"|"$/g, "").split(",");
|
||||
const sessionInfoPromises = sessionIDs.map(async (id) => {
|
||||
try {
|
||||
return await exports.getSessionInfo(id);
|
||||
} catch (err: any) {
|
||||
if (err.message === 'sessionID does not exist') {
|
||||
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
|
||||
if (err.message === "sessionID does not exist") {
|
||||
console.debug(
|
||||
`SessionManager getAuthorID: no session exists with ID ${id}`,
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
@ -75,11 +77,16 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
|||
return undefined;
|
||||
});
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const isMatch = (si: {
|
||||
const isMatch = (
|
||||
si: {
|
||||
groupID: string;
|
||||
validUntil: number;
|
||||
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
||||
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
|
||||
} | null,
|
||||
) => si != null && si.groupID === groupID && now < si.validUntil;
|
||||
const sessionInfo = await promises.firstSatisfies(
|
||||
sessionInfoPromises,
|
||||
isMatch,
|
||||
);
|
||||
if (sessionInfo == null) return undefined;
|
||||
return sessionInfo.authorID;
|
||||
};
|
||||
|
@ -92,7 +99,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
|||
exports.doesSessionExist = async (sessionID: string) => {
|
||||
// check if the database entry of this session exists
|
||||
const session = await db.get(`session:${sessionID}`);
|
||||
return (session != null);
|
||||
return session != null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -102,42 +109,46 @@ exports.doesSessionExist = async (sessionID: string) => {
|
|||
* @param {Number} validUntil The unix timestamp when the session should expire
|
||||
* @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
|
||||
const groupExists = await groupManager.doesGroupExist(groupID);
|
||||
if (!groupExists) {
|
||||
throw new CustomError('groupID does not exist', 'apierror');
|
||||
throw new CustomError("groupID does not exist", "apierror");
|
||||
}
|
||||
|
||||
// check if the author exists
|
||||
const authorExists = await authorManager.doesAuthorExist(authorID);
|
||||
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
|
||||
if (typeof validUntil !== 'number') {
|
||||
if (typeof validUntil !== "number") {
|
||||
validUntil = parseInt(validUntil);
|
||||
}
|
||||
|
||||
// check it's a valid number
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
@ -151,8 +162,8 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu
|
|||
await Promise.all([
|
||||
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
||||
// property, and writes the result.
|
||||
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1),
|
||||
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1),
|
||||
db.setSub(`group2sessions:${groupID}`, ["sessionIDs", sessionID], 1),
|
||||
db.setSub(`author2sessions:${authorID}`, ["sessionIDs", sessionID], 1),
|
||||
]);
|
||||
|
||||
return { sessionID };
|
||||
|
@ -169,7 +180,7 @@ exports.getSessionInfo = async (sessionID:string) => {
|
|||
|
||||
if (session == null) {
|
||||
// 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
|
||||
|
@ -185,7 +196,7 @@ exports.deleteSession = async (sessionID:string) => {
|
|||
// ensure that the session exists
|
||||
const session = await db.get(`session:${sessionID}`);
|
||||
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
|
||||
|
@ -196,8 +207,16 @@ exports.deleteSession = async (sessionID:string) => {
|
|||
// 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
|
||||
// (JSON.stringify() ignores such properties).
|
||||
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined),
|
||||
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined),
|
||||
db.setSub(
|
||||
`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
|
||||
|
@ -214,7 +233,7 @@ exports.listSessionsOfGroup = async (groupID: string) => {
|
|||
// check that the group exists
|
||||
const exists = await groupManager.doesGroupExist(groupID);
|
||||
if (!exists) {
|
||||
throw new CustomError('groupID does not exist', 'apierror');
|
||||
throw new CustomError("groupID does not exist", "apierror");
|
||||
}
|
||||
|
||||
const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`);
|
||||
|
@ -230,7 +249,7 @@ exports.listSessionsOfAuthor = async (authorID: string) => {
|
|||
// check that the author exists
|
||||
const exists = await authorManager.doesAuthorExist(authorID);
|
||||
if (!exists) {
|
||||
throw new CustomError('authorID does not exist', 'apierror');
|
||||
throw new CustomError("authorID does not exist", "apierror");
|
||||
}
|
||||
|
||||
return await listSessionsWithDBKey(`author2sessions:${authorID}`);
|
||||
|
@ -253,7 +272,7 @@ const listSessionsWithDBKey = async (dbkey: string) => {
|
|||
try {
|
||||
sessions[sessionID] = await exports.getSessionInfo(sessionID);
|
||||
} catch (err: any) {
|
||||
if (err.name === 'apierror') {
|
||||
if (err.name === "apierror") {
|
||||
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
|
||||
sessions[sessionID] = null;
|
||||
} else {
|
||||
|
@ -265,11 +284,11 @@ const listSessionsWithDBKey = async (dbkey: string) => {
|
|||
return sessions;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* checks if a number is an int
|
||||
* @param {number|string} value
|
||||
* @return {boolean} If the value is an integer
|
||||
*/
|
||||
// @ts-ignore
|
||||
const isInt = (value:number|string): boolean => (parseFloat(value) === parseInt(value)) && !isNaN(value);
|
||||
const isInt = (value: number | string): boolean =>
|
||||
parseFloat(value) === parseInt(value) && !isNaN(value);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
'use strict';
|
||||
"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 Store = require("express-session").Store;
|
||||
const log4js = require("log4js");
|
||||
const util = require("util");
|
||||
|
||||
const logger = log4js.getLogger('SessionStore');
|
||||
const logger = log4js.getLogger("SessionStore");
|
||||
|
||||
class SessionStore extends Store {
|
||||
/**
|
||||
|
@ -38,7 +38,9 @@ class SessionStore extends Store {
|
|||
const exp = this._expirations.get(sid) || {};
|
||||
clearTimeout(exp.timeout);
|
||||
// @ts-ignore
|
||||
const {cookie: {expires} = {}} = sess || {};
|
||||
const {
|
||||
cookie: { expires } = {},
|
||||
} = sess || {};
|
||||
if (expires) {
|
||||
const sessExp = new Date(expires).getTime();
|
||||
if (updateDbExp) exp.db = sessExp;
|
||||
|
@ -47,7 +49,8 @@ class SessionStore extends Store {
|
|||
if (exp.real <= now) return await this._destroy(sid);
|
||||
// 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.
|
||||
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.
|
||||
// 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
|
||||
|
@ -99,7 +102,11 @@ class SessionStore extends Store {
|
|||
// 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.
|
||||
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);
|
||||
exp.db = new Date(sess.cookie.expires).getTime();
|
||||
}
|
||||
|
@ -107,7 +114,7 @@ class SessionStore extends Store {
|
|||
|
||||
// express-session doesn't support Promise-based methods. This is where the callbackified versions
|
||||
// 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}`]);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/*
|
||||
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
|
||||
*
|
||||
|
@ -20,13 +20,13 @@
|
|||
* require("./index").require("./path/to/template.ejs")
|
||||
*/
|
||||
|
||||
const ejs = require('ejs');
|
||||
const fs = require('fs');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const path = require('path');
|
||||
const resolve = require('resolve');
|
||||
const settings = require('../utils/Settings');
|
||||
import {pluginInstallPath} from '../../static/js/pluginfw/installer'
|
||||
const ejs = require("ejs");
|
||||
const fs = require("fs");
|
||||
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||
const path = require("path");
|
||||
const resolve = require("resolve");
|
||||
const settings = require("../utils/Settings");
|
||||
import { pluginInstallPath } from "../../static/js/pluginfw/installer";
|
||||
|
||||
const templateCache = new Map();
|
||||
|
||||
|
@ -37,7 +37,8 @@ exports.info = {
|
|||
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.info.__output_stack.push(exports.info.__output);
|
||||
|
@ -51,7 +52,7 @@ exports._exit = (b:any, recursive:boolean) => {
|
|||
exports.begin_block = (name: string) => {
|
||||
exports.info.block_stack.push(name);
|
||||
exports.info.__output_stack.push(exports.info.__output.get());
|
||||
exports.info.__output.set('');
|
||||
exports.info.__output.set("");
|
||||
};
|
||||
|
||||
exports.end_block = () => {
|
||||
|
@ -64,13 +65,17 @@ exports.end_block = () => {
|
|||
exports.info.__output.set(exports.info.__output.get().concat(args.content));
|
||||
};
|
||||
|
||||
exports.require = (name:string, args:{
|
||||
e?: Function,
|
||||
require?: Function,
|
||||
}, mod:{
|
||||
filename:string,
|
||||
paths:string[],
|
||||
}) => {
|
||||
exports.require = (
|
||||
name: string,
|
||||
args: {
|
||||
e?: Function;
|
||||
require?: Function;
|
||||
},
|
||||
mod: {
|
||||
filename: string;
|
||||
paths: string[];
|
||||
},
|
||||
) => {
|
||||
if (args == null) args = {};
|
||||
|
||||
let basedir = __dirname;
|
||||
|
@ -88,19 +93,26 @@ exports.require = (name:string, args:{
|
|||
* Add the plugin install path to the paths array
|
||||
*/
|
||||
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.require = require;
|
||||
|
||||
const cache = settings.maxAge !== 0;
|
||||
const template = cache && templateCache.get(ejspath) || ejs.compile(
|
||||
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
|
||||
const template =
|
||||
(cache && templateCache.get(ejspath)) ||
|
||||
ejs.compile(
|
||||
"<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>" +
|
||||
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
|
||||
{filename: ejspath});
|
||||
{ filename: ejspath },
|
||||
);
|
||||
if (cache) templateCache.set(ejspath, template);
|
||||
|
||||
exports.info.args.push(args);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* The API Handler handles all API http requests
|
||||
*/
|
||||
|
@ -21,9 +21,9 @@
|
|||
|
||||
import { MapArrayType } from "../types/MapType";
|
||||
|
||||
const api = require('../db/API');
|
||||
const padManager = require('../db/PadManager');
|
||||
import createHTTPError from 'http-errors';
|
||||
const api = require("../db/API");
|
||||
const padManager = require("../db/PadManager");
|
||||
import createHTTPError from "http-errors";
|
||||
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
|
||||
import { publicKeyExported } from "../security/OAuth2Provider";
|
||||
import { jwtVerify } from "jose";
|
||||
|
@ -31,128 +31,127 @@ import {jwtVerify} from "jose";
|
|||
// a list of all functions
|
||||
const version: MapArrayType<any> = {};
|
||||
|
||||
version['1'] = {
|
||||
version["1"] = {
|
||||
createGroup: [],
|
||||
createGroupIfNotExistsFor: ['groupMapper'],
|
||||
deleteGroup: ['groupID'],
|
||||
listPads: ['groupID'],
|
||||
createPad: ['padID', 'text'],
|
||||
createGroupPad: ['groupID', 'padName', 'text'],
|
||||
createAuthor: ['name'],
|
||||
createAuthorIfNotExistsFor: ['authorMapper', 'name'],
|
||||
listPadsOfAuthor: ['authorID'],
|
||||
createSession: ['groupID', 'authorID', 'validUntil'],
|
||||
deleteSession: ['sessionID'],
|
||||
getSessionInfo: ['sessionID'],
|
||||
listSessionsOfGroup: ['groupID'],
|
||||
listSessionsOfAuthor: ['authorID'],
|
||||
getText: ['padID', 'rev'],
|
||||
setText: ['padID', 'text'],
|
||||
getHTML: ['padID', 'rev'],
|
||||
setHTML: ['padID', 'html'],
|
||||
getRevisionsCount: ['padID'],
|
||||
getLastEdited: ['padID'],
|
||||
deletePad: ['padID'],
|
||||
getReadOnlyID: ['padID'],
|
||||
setPublicStatus: ['padID', 'publicStatus'],
|
||||
getPublicStatus: ['padID'],
|
||||
listAuthorsOfPad: ['padID'],
|
||||
padUsersCount: ['padID'],
|
||||
createGroupIfNotExistsFor: ["groupMapper"],
|
||||
deleteGroup: ["groupID"],
|
||||
listPads: ["groupID"],
|
||||
createPad: ["padID", "text"],
|
||||
createGroupPad: ["groupID", "padName", "text"],
|
||||
createAuthor: ["name"],
|
||||
createAuthorIfNotExistsFor: ["authorMapper", "name"],
|
||||
listPadsOfAuthor: ["authorID"],
|
||||
createSession: ["groupID", "authorID", "validUntil"],
|
||||
deleteSession: ["sessionID"],
|
||||
getSessionInfo: ["sessionID"],
|
||||
listSessionsOfGroup: ["groupID"],
|
||||
listSessionsOfAuthor: ["authorID"],
|
||||
getText: ["padID", "rev"],
|
||||
setText: ["padID", "text"],
|
||||
getHTML: ["padID", "rev"],
|
||||
setHTML: ["padID", "html"],
|
||||
getRevisionsCount: ["padID"],
|
||||
getLastEdited: ["padID"],
|
||||
deletePad: ["padID"],
|
||||
getReadOnlyID: ["padID"],
|
||||
setPublicStatus: ["padID", "publicStatus"],
|
||||
getPublicStatus: ["padID"],
|
||||
listAuthorsOfPad: ["padID"],
|
||||
padUsersCount: ["padID"],
|
||||
};
|
||||
|
||||
version['1.1'] = {
|
||||
...version['1'],
|
||||
getAuthorName: ['authorID'],
|
||||
padUsers: ['padID'],
|
||||
sendClientsMessage: ['padID', 'msg'],
|
||||
version["1.1"] = {
|
||||
...version["1"],
|
||||
getAuthorName: ["authorID"],
|
||||
padUsers: ["padID"],
|
||||
sendClientsMessage: ["padID", "msg"],
|
||||
listAllGroups: [],
|
||||
};
|
||||
|
||||
version['1.2'] = {
|
||||
...version['1.1'],
|
||||
version["1.2"] = {
|
||||
...version["1.1"],
|
||||
checkToken: [],
|
||||
};
|
||||
|
||||
version['1.2.1'] = {
|
||||
...version['1.2'],
|
||||
version["1.2.1"] = {
|
||||
...version["1.2"],
|
||||
listAllPads: [],
|
||||
};
|
||||
|
||||
version['1.2.7'] = {
|
||||
...version['1.2.1'],
|
||||
createDiffHTML: ['padID', 'startRev', 'endRev'],
|
||||
getChatHistory: ['padID', 'start', 'end'],
|
||||
getChatHead: ['padID'],
|
||||
version["1.2.7"] = {
|
||||
...version["1.2.1"],
|
||||
createDiffHTML: ["padID", "startRev", "endRev"],
|
||||
getChatHistory: ["padID", "start", "end"],
|
||||
getChatHead: ["padID"],
|
||||
};
|
||||
|
||||
version['1.2.8'] = {
|
||||
...version['1.2.7'],
|
||||
getAttributePool: ['padID'],
|
||||
getRevisionChangeset: ['padID', 'rev'],
|
||||
version["1.2.8"] = {
|
||||
...version["1.2.7"],
|
||||
getAttributePool: ["padID"],
|
||||
getRevisionChangeset: ["padID", "rev"],
|
||||
};
|
||||
|
||||
version['1.2.9'] = {
|
||||
...version['1.2.8'],
|
||||
copyPad: ['sourceID', 'destinationID', 'force'],
|
||||
movePad: ['sourceID', 'destinationID', 'force'],
|
||||
version["1.2.9"] = {
|
||||
...version["1.2.8"],
|
||||
copyPad: ["sourceID", "destinationID", "force"],
|
||||
movePad: ["sourceID", "destinationID", "force"],
|
||||
};
|
||||
|
||||
version['1.2.10'] = {
|
||||
...version['1.2.9'],
|
||||
getPadID: ['roID'],
|
||||
version["1.2.10"] = {
|
||||
...version["1.2.9"],
|
||||
getPadID: ["roID"],
|
||||
};
|
||||
|
||||
version['1.2.11'] = {
|
||||
...version['1.2.10'],
|
||||
getSavedRevisionsCount: ['padID'],
|
||||
listSavedRevisions: ['padID'],
|
||||
saveRevision: ['padID', 'rev'],
|
||||
restoreRevision: ['padID', 'rev'],
|
||||
version["1.2.11"] = {
|
||||
...version["1.2.10"],
|
||||
getSavedRevisionsCount: ["padID"],
|
||||
listSavedRevisions: ["padID"],
|
||||
saveRevision: ["padID", "rev"],
|
||||
restoreRevision: ["padID", "rev"],
|
||||
};
|
||||
|
||||
version['1.2.12'] = {
|
||||
...version['1.2.11'],
|
||||
appendChatMessage: ['padID', 'text', 'authorID', 'time'],
|
||||
version["1.2.12"] = {
|
||||
...version["1.2.11"],
|
||||
appendChatMessage: ["padID", "text", "authorID", "time"],
|
||||
};
|
||||
|
||||
version['1.2.13'] = {
|
||||
...version['1.2.12'],
|
||||
appendText: ['padID', 'text'],
|
||||
version["1.2.13"] = {
|
||||
...version["1.2.12"],
|
||||
appendText: ["padID", "text"],
|
||||
};
|
||||
|
||||
version['1.2.14'] = {
|
||||
...version['1.2.13'],
|
||||
version["1.2.14"] = {
|
||||
...version["1.2.13"],
|
||||
getStats: [],
|
||||
};
|
||||
|
||||
version['1.2.15'] = {
|
||||
...version['1.2.14'],
|
||||
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force'],
|
||||
version["1.2.15"] = {
|
||||
...version["1.2.14"],
|
||||
copyPadWithoutHistory: ["sourceID", "destinationID", "force"],
|
||||
};
|
||||
|
||||
version['1.3.0'] = {
|
||||
...version['1.2.15'],
|
||||
appendText: ['padID', 'text', 'authorId'],
|
||||
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId'],
|
||||
createGroupPad: ['groupID', 'padName', 'text', 'authorId'],
|
||||
createPad: ['padID', 'text', 'authorId'],
|
||||
restoreRevision: ['padID', 'rev', 'authorId'],
|
||||
setHTML: ['padID', 'html', 'authorId'],
|
||||
setText: ['padID', 'text', 'authorId'],
|
||||
version["1.3.0"] = {
|
||||
...version["1.2.15"],
|
||||
appendText: ["padID", "text", "authorId"],
|
||||
copyPadWithoutHistory: ["sourceID", "destinationID", "force", "authorId"],
|
||||
createGroupPad: ["groupID", "padName", "text", "authorId"],
|
||||
createPad: ["padID", "text", "authorId"],
|
||||
restoreRevision: ["padID", "rev", "authorId"],
|
||||
setHTML: ["padID", "html", "authorId"],
|
||||
setText: ["padID", "text", "authorId"],
|
||||
};
|
||||
|
||||
// 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.version = version;
|
||||
|
||||
|
||||
type APIFields = {
|
||||
api_key: string;
|
||||
padID: string;
|
||||
padName: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles an HTTP API call
|
||||
|
@ -162,31 +161,37 @@ type APIFields = {
|
|||
* @param req express request 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
|
||||
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
|
||||
if (!(functionName in version[apiVersion])) {
|
||||
throw new createHTTPError.NotFound('no such function');
|
||||
throw new createHTTPError.NotFound("no such function");
|
||||
}
|
||||
|
||||
if (!req.headers.authorization) {
|
||||
throw new createHTTPError.Unauthorized('no or wrong API Key');
|
||||
throw new createHTTPError.Unauthorized("no or wrong API Key");
|
||||
}
|
||||
|
||||
try {
|
||||
await jwtVerify(req.headers.authorization!.replace("Bearer ", ""), publicKeyExported!, {algorithms: ['RS256'],
|
||||
requiredClaims: ["admin"]})
|
||||
|
||||
await jwtVerify(
|
||||
req.headers.authorization!.replace("Bearer ", ""),
|
||||
publicKeyExported!,
|
||||
{ algorithms: ["RS256"], requiredClaims: ["admin"] },
|
||||
);
|
||||
} 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
|
||||
if (fields.padID) {
|
||||
fields.padID = await padManager.sanitizePadId(fields.padID);
|
||||
|
@ -200,7 +205,9 @@ exports.handle = async function (apiVersion: string, functionName: string, field
|
|||
|
||||
// put the function parameters in an array
|
||||
// @ts-ignore
|
||||
const functionParams = version[apiVersion][functionName].map((field) => fields[field]);
|
||||
const functionParams = version[apiVersion][functionName].map(
|
||||
(field) => fields[field],
|
||||
);
|
||||
|
||||
// call the api function
|
||||
return api[functionName].apply(this, functionParams);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* Handles the export requests
|
||||
*/
|
||||
|
@ -20,15 +20,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const exporthtml = require('../utils/ExportHtml');
|
||||
const exporttxt = require('../utils/ExportTxt');
|
||||
const exportEtherpad = require('../utils/ExportEtherpad');
|
||||
import fs from 'fs';
|
||||
const settings = require('../utils/Settings');
|
||||
import os from 'os';
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
import util from 'util';
|
||||
const { checkValidRev } = require('../utils/checkValidRev');
|
||||
const exporthtml = require("../utils/ExportHtml");
|
||||
const exporttxt = require("../utils/ExportTxt");
|
||||
const exportEtherpad = require("../utils/ExportEtherpad");
|
||||
import fs from "fs";
|
||||
const settings = require("../utils/Settings");
|
||||
import os from "os";
|
||||
const hooks = require("../../static/js/pluginfw/hooks");
|
||||
import util from "util";
|
||||
const { checkValidRev } = require("../utils/checkValidRev");
|
||||
|
||||
const fsp_writeFile = util.promisify(fs.writeFile);
|
||||
const fsp_unlink = util.promisify(fs.unlink);
|
||||
|
@ -43,12 +43,18 @@ const tempDirectory = os.tmpdir();
|
|||
* @param {String} readOnlyId the read only id of the pad 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
|
||||
let fileName = readOnlyId ? readOnlyId : padId;
|
||||
|
||||
// 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 (hookFileName.length) {
|
||||
|
@ -66,29 +72,33 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
|
|||
|
||||
// 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
|
||||
if (type === 'etherpad') {
|
||||
if (type === "etherpad") {
|
||||
const pad = await exportEtherpad.getPadRaw(padId, readOnlyId);
|
||||
res.send(pad);
|
||||
} else if (type === 'txt') {
|
||||
} else if (type === "txt") {
|
||||
const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
|
||||
res.send(txt);
|
||||
} else {
|
||||
// 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
|
||||
|
||||
// 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
|
||||
const newHTML = await hooks.aCallFirst('exportHTMLSend', html);
|
||||
const newHTML = await hooks.aCallFirst("exportHTMLSend", html);
|
||||
if (newHTML.length) html = newHTML;
|
||||
res.send(html);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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`;
|
||||
await fsp_writeFile(srcFile, html);
|
||||
|
||||
|
@ -99,13 +109,20 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
|
|||
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
|
||||
|
||||
// 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) {
|
||||
// console.log("export handled by plugin", destFile);
|
||||
} else {
|
||||
const converter =
|
||||
settings.soffice != null ? require('../utils/LibreOffice')
|
||||
: settings.abiword != null ? require('../utils/Abiword')
|
||||
settings.soffice != null
|
||||
? require("../utils/LibreOffice")
|
||||
: settings.abiword != null
|
||||
? require("../utils/Abiword")
|
||||
: null;
|
||||
await converter.convertFile(srcFile, destFile, type);
|
||||
}
|
||||
|
@ -117,7 +134,7 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
|
|||
await fsp_unlink(srcFile);
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* Handles the import requests
|
||||
*/
|
||||
|
@ -21,19 +21,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const padManager = require('../db/PadManager');
|
||||
const padMessageHandler = require('./PadMessageHandler');
|
||||
import {promises as fs} from 'fs';
|
||||
import path from 'path';
|
||||
const settings = require('../utils/Settings');
|
||||
const {Formidable} = require('formidable');
|
||||
import os from 'os';
|
||||
const importHtml = require('../utils/ImportHtml');
|
||||
const importEtherpad = require('../utils/ImportEtherpad');
|
||||
import log4js from 'log4js';
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const padManager = require("../db/PadManager");
|
||||
const padMessageHandler = require("./PadMessageHandler");
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
const settings = require("../utils/Settings");
|
||||
const { Formidable } = require("formidable");
|
||||
import os from "os";
|
||||
const importHtml = require("../utils/ImportHtml");
|
||||
const importEtherpad = require("../utils/ImportEtherpad");
|
||||
import log4js from "log4js";
|
||||
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||
|
||||
const logger = log4js.getLogger('ImportHandler');
|
||||
const logger = log4js.getLogger("ImportHandler");
|
||||
|
||||
// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`.
|
||||
class ImportError extends Error {
|
||||
|
@ -41,10 +41,10 @@ class ImportError extends Error {
|
|||
constructor(status: string, ...args: any) {
|
||||
super(...args);
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
|
||||
this.name = 'ImportError';
|
||||
this.name = "ImportError";
|
||||
this.status = status;
|
||||
const msg = this.message == null ? '' : String(this.message);
|
||||
if (status !== '') this.message = msg === '' ? status : `${status}: ${msg}`;
|
||||
const msg = this.message == null ? "" : String(this.message);
|
||||
if (status !== "") this.message = msg === "" ? status : `${status}: ${msg}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,22 +52,22 @@ const rm = async (path: string) => {
|
|||
try {
|
||||
await fs.unlink(path);
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
if (err.code !== "ENOENT") throw err;
|
||||
}
|
||||
};
|
||||
|
||||
let converter: any = null;
|
||||
let exportExtension = 'htm';
|
||||
let exportExtension = "htm";
|
||||
|
||||
// load abiword only if it is enabled and if soffice is disabled
|
||||
if (settings.abiword != null && settings.soffice == null) {
|
||||
converter = require('../utils/Abiword');
|
||||
converter = require("../utils/Abiword");
|
||||
}
|
||||
|
||||
// load soffice only if it is enabled
|
||||
if (settings.soffice != null) {
|
||||
converter = require('../utils/LibreOffice');
|
||||
exportExtension = 'html';
|
||||
converter = require("../utils/LibreOffice");
|
||||
exportExtension = "html";
|
||||
}
|
||||
|
||||
const tmpDirectory = os.tmpdir();
|
||||
|
@ -79,14 +79,19 @@ const tmpDirectory = os.tmpdir();
|
|||
* @param {String} padId the pad id to export
|
||||
* @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
|
||||
// convert file to html via abiword or soffice
|
||||
// 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
|
||||
let useConverter = (converter != null);
|
||||
let useConverter = converter != null;
|
||||
|
||||
const form = new Formidable({
|
||||
keepExtensions: true,
|
||||
|
@ -102,13 +107,13 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
|||
} catch (err: any) {
|
||||
logger.warn(`Import failed due to form error: ${err.stack || err}`);
|
||||
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) {
|
||||
logger.warn('Import failed because form had no file');
|
||||
throw new ImportError('uploadFailed');
|
||||
logger.warn("Import failed because form had no file");
|
||||
throw new ImportError("uploadFailed");
|
||||
} else {
|
||||
srcFile = files.file[0].filepath;
|
||||
}
|
||||
|
@ -116,9 +121,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
|
||||
// this allows us to accept source code files like .c or .java
|
||||
const fileEnding = path.extname(files.file[0].originalFilename).toLowerCase();
|
||||
const knownFileEndings =
|
||||
['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf'];
|
||||
const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0);
|
||||
const knownFileEndings = [
|
||||
".txt",
|
||||
".doc",
|
||||
".docx",
|
||||
".pdf",
|
||||
".odt",
|
||||
".html",
|
||||
".htm",
|
||||
".etherpad",
|
||||
".rtf",
|
||||
];
|
||||
const fileEndingUnknown = knownFileEndings.indexOf(fileEnding) < 0;
|
||||
|
||||
if (fileEndingUnknown) {
|
||||
// the file ending is not known
|
||||
|
@ -127,31 +141,43 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
|||
// we need to rename this file with a .txt ending
|
||||
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);
|
||||
} else {
|
||||
logger.warn(`Not allowing unknown file type to be imported: ${fileEnding}`);
|
||||
throw new ImportError('uploadFailed');
|
||||
logger.warn(
|
||||
`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 importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x:string) => x);
|
||||
const fileIsEtherpad = (fileEnding === '.etherpad');
|
||||
const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm');
|
||||
const fileIsTXT = (fileEnding === '.txt');
|
||||
const importHandledByPlugin = (await hooks.aCallAll("import", context)).some(
|
||||
(x: string) => x,
|
||||
);
|
||||
const fileIsEtherpad = fileEnding === ".etherpad";
|
||||
const fileIsHTML = fileEnding === ".html" || fileEnding === ".htm";
|
||||
const fileIsTXT = fileEnding === ".txt";
|
||||
|
||||
let directDatabaseAccess = false;
|
||||
if (fileIsEtherpad) {
|
||||
// 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;
|
||||
if (headCount >= 10) {
|
||||
logger.warn('Aborting direct database import attempt of a pad that already has content');
|
||||
throw new ImportError('padHasData');
|
||||
logger.warn(
|
||||
"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;
|
||||
await importEtherpad.setPadRaw(padId, text, authorId);
|
||||
}
|
||||
|
@ -172,7 +198,7 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
|||
await converter.convertFile(srcFile, destFile, exportExtension);
|
||||
} catch (err: any) {
|
||||
logger.warn(`Converting Error: ${err.stack || err}`);
|
||||
throw new ImportError('convertFailed');
|
||||
throw new ImportError("convertFailed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -182,26 +208,26 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
|||
const buf = await fs.readFile(destFile);
|
||||
|
||||
// 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) {
|
||||
logger.warn('Attempt to import non-ASCII file');
|
||||
throw new ImportError('uploadFailed');
|
||||
logger.warn("Attempt to import non-ASCII file");
|
||||
throw new ImportError("uploadFailed");
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
let text;
|
||||
|
||||
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.
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +238,11 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
|||
try {
|
||||
await importHtml.setPadHTML(pad, text, authorId);
|
||||
} 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 {
|
||||
await pad.setText(text, authorId);
|
||||
|
@ -221,7 +251,7 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
|||
|
||||
// Load the Pad into memory then broadcast updates to all clients
|
||||
padManager.unloadPad(padId);
|
||||
pad = await padManager.getPad(padId, '\n', authorId);
|
||||
pad = await padManager.getPad(padId, "\n", authorId);
|
||||
padManager.unloadPad(padId);
|
||||
|
||||
// Direct database access means a pad user should reload the pad and not attempt to receive
|
||||
|
@ -246,19 +276,27 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
|||
* @param {String} authorId the author id to use for the import
|
||||
* @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: string = "",
|
||||
) => {
|
||||
let httpStatus = 200;
|
||||
let code = 0;
|
||||
let message = 'ok';
|
||||
let message = "ok";
|
||||
let directDatabaseAccess;
|
||||
try {
|
||||
directDatabaseAccess = await doImport(req, res, padId, authorId);
|
||||
} catch (err: any) {
|
||||
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;
|
||||
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
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
/**
|
||||
* 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
|
||||
|
@ -22,11 +22,11 @@
|
|||
|
||||
import { MapArrayType } from "../types/MapType";
|
||||
import { SocketModule } from "../types/SocketModule";
|
||||
const log4js = require('log4js');
|
||||
const settings = require('../utils/Settings');
|
||||
const stats = require('../../node/stats')
|
||||
const log4js = require("log4js");
|
||||
const settings = require("../utils/Settings");
|
||||
const stats = require("../../node/stats");
|
||||
|
||||
const logger = log4js.getLogger('socket.io');
|
||||
const logger = log4js.getLogger("socket.io");
|
||||
|
||||
/**
|
||||
* Saves all components
|
||||
|
@ -51,7 +51,9 @@ exports.addComponent = (moduleName: string, module: SocketModule) => {
|
|||
* removes a component
|
||||
* @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
|
||||
|
@ -60,8 +62,8 @@ exports.deleteComponent = (moduleName: string) => { delete components[moduleName
|
|||
exports.setSocketIO = (_io: any) => {
|
||||
io = _io;
|
||||
|
||||
io.sockets.on('connection', (socket:any) => {
|
||||
const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
|
||||
io.sockets.on("connection", (socket: any) => {
|
||||
const ip = settings.disableIPlogging ? "ANONYMOUS" : socket.request.ip;
|
||||
logger.debug(`${socket.id} connected from IP ${ip}`);
|
||||
|
||||
// wrap the original send function to log the messages
|
||||
|
@ -76,27 +78,36 @@ exports.setSocketIO = (_io:any) => {
|
|||
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]) {
|
||||
throw new Error(`unknown message component: ${message.component}`);
|
||||
}
|
||||
logger.debug(`from ${socket.id}:`, message);
|
||||
return await components[message.component].handleMessage(socket, message);
|
||||
return await components[message.component].handleMessage(
|
||||
socket,
|
||||
message,
|
||||
);
|
||||
})().then(
|
||||
(val) => ack(null, val),
|
||||
(err) => {
|
||||
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.
|
||||
}));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
socket.on('disconnect', (reason: string) => {
|
||||
socket.on("disconnect", (reason: string) => {
|
||||
logger.debug(`${socket.id} disconnected: ${reason}`);
|
||||
// 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
|
||||
// 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.
|
||||
stats.gauge('lastDisconnect', () => Date.now());
|
||||
stats.gauge("lastDisconnect", () => Date.now());
|
||||
// tell all components about this disconnect
|
||||
for (const i of Object.keys(components)) {
|
||||
components[i].handleDisconnect(socket);
|
||||
|
|
|
@ -1,63 +1,69 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { Socket } from "node:net";
|
||||
import type { MapArrayType } from "../types/MapType";
|
||||
|
||||
import _ from 'underscore';
|
||||
import _ from "underscore";
|
||||
// @ts-ignore
|
||||
import cookieParser from 'cookie-parser';
|
||||
import events from 'events';
|
||||
import express from 'express';
|
||||
import cookieParser from "cookie-parser";
|
||||
import events from "events";
|
||||
import express from "express";
|
||||
// @ts-ignore
|
||||
import expressSession from 'express-session';
|
||||
import fs from 'fs';
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
import log4js from 'log4js';
|
||||
const SessionStore = require('../db/SessionStore');
|
||||
const settings = require('../utils/Settings');
|
||||
const stats = require('../stats')
|
||||
import util from 'util';
|
||||
const webaccess = require('./express/webaccess');
|
||||
import expressSession from "express-session";
|
||||
import fs from "fs";
|
||||
const hooks = require("../../static/js/pluginfw/hooks");
|
||||
import log4js from "log4js";
|
||||
const SessionStore = require("../db/SessionStore");
|
||||
const settings = require("../utils/Settings");
|
||||
const stats = require("../stats");
|
||||
import util from "util";
|
||||
const webaccess = require("./express/webaccess");
|
||||
|
||||
import SecretRotator from '../security/SecretRotator';
|
||||
import SecretRotator from "../security/SecretRotator";
|
||||
|
||||
let secretRotator: SecretRotator | null = null;
|
||||
const logger = log4js.getLogger('http');
|
||||
const logger = log4js.getLogger("http");
|
||||
let serverName: string;
|
||||
let sessionStore: { shutdown: () => void; } | null;
|
||||
let sessionStore: { shutdown: () => void } | null;
|
||||
const sockets: Set<Socket> = new Set();
|
||||
const socketsEvents = new events.EventEmitter();
|
||||
const startTime = stats.settableGauge('httpStartTime');
|
||||
const startTime = stats.settableGauge("httpStartTime");
|
||||
|
||||
exports.server = null;
|
||||
|
||||
const closeServer = async () => {
|
||||
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
|
||||
// Promise won't resolve until all preexisting connections are closed.
|
||||
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
|
||||
// time should be long enough to avoid interrupting most preexisting transmissions but short
|
||||
// enough to avoid a noticeable outage.
|
||||
const timeout = setTimeout(async () => {
|
||||
logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`);
|
||||
for (const socket of sockets) socket.destroy(new Error('HTTP server is closing'));
|
||||
logger.info(
|
||||
`Forcibly terminating remaining ${sockets.size} HTTP connections...`,
|
||||
);
|
||||
for (const socket of sockets)
|
||||
socket.destroy(new Error("HTTP server is closing"));
|
||||
}, 5000);
|
||||
let lastLogged = 0;
|
||||
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
||||
if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs.
|
||||
logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`);
|
||||
if (Date.now() - lastLogged > 1000) {
|
||||
// Rate limit to avoid filling logs.
|
||||
logger.info(
|
||||
`Waiting for ${sockets.size} HTTP clients to disconnect...`,
|
||||
);
|
||||
lastLogged = Date.now();
|
||||
}
|
||||
await events.once(socketsEvents, 'updated');
|
||||
await events.once(socketsEvents, "updated");
|
||||
}
|
||||
await p;
|
||||
clearTimeout(timeout);
|
||||
exports.server = null;
|
||||
startTime.setValue(0);
|
||||
logger.info('HTTP server closed');
|
||||
logger.info("HTTP server closed");
|
||||
}
|
||||
if (sessionStore) sessionStore.shutdown();
|
||||
sessionStore = null;
|
||||
|
@ -66,34 +72,46 @@ const closeServer = 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)`;
|
||||
|
||||
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`);
|
||||
console.log(
|
||||
`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`,
|
||||
);
|
||||
|
||||
await exports.restartServer();
|
||||
|
||||
if (settings.ip === '') {
|
||||
if (settings.ip === "") {
|
||||
// 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 {
|
||||
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)) {
|
||||
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 {
|
||||
console.warn('Admin username and password not set in settings.json. ' +
|
||||
'To access admin please uncomment and edit "users" in settings.json');
|
||||
console.warn(
|
||||
"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') {
|
||||
console.warn('Etherpad is running in Development mode. This mode is slower for users and ' +
|
||||
'less secure than production mode. You should set the NODE_ENV environment ' +
|
||||
'variable to production by using: export NODE_ENV=production');
|
||||
if (env !== "production") {
|
||||
console.warn(
|
||||
"Etherpad is running in Development mode. This mode is slower for users and " +
|
||||
"less secure than production mode. You should set the NODE_ENV environment " +
|
||||
"variable to production by using: export NODE_ENV=production",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -103,9 +121,11 @@ exports.restartServer = async () => {
|
|||
const app = express(); // New syntax for express v3
|
||||
|
||||
if (settings.ssl) {
|
||||
console.log('SSL -- enabled');
|
||||
console.log("SSL -- enabled");
|
||||
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> = {
|
||||
key: fs.readFileSync(settings.ssl.key),
|
||||
|
@ -120,10 +140,10 @@ exports.restartServer = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const https = require('https');
|
||||
const https = require("https");
|
||||
exports.server = https.createServer(options, app);
|
||||
} else {
|
||||
const http = require('http');
|
||||
const http = require("http");
|
||||
exports.server = http.createServer(app);
|
||||
}
|
||||
|
||||
|
@ -131,12 +151,15 @@ exports.restartServer = async () => {
|
|||
// res.header("X-Frame-Options", "deny"); // breaks embedded pads
|
||||
if (settings.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
|
||||
// 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
|
||||
// loading local resources, but it will drop them when loading foreign resources.
|
||||
|
@ -145,11 +168,11 @@ exports.restartServer = async () => {
|
|||
// marked with <meta name="referrer" content="no-referrer">
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
|
||||
// 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.
|
||||
if (settings.exposeVersion) {
|
||||
res.header('Server', serverName);
|
||||
res.header("Server", serverName);
|
||||
}
|
||||
|
||||
next();
|
||||
|
@ -162,14 +185,17 @@ exports.restartServer = async () => {
|
|||
*
|
||||
* Source: https://expressjs.com/en/guide/behind-proxies.html
|
||||
*/
|
||||
app.enable('trust proxy');
|
||||
app.enable("trust proxy");
|
||||
}
|
||||
|
||||
// Measure response time
|
||||
app.use((req, res, next) => {
|
||||
const stopWatch = stats.timer('httpRequests').start();
|
||||
const stopWatch = stats.timer("httpRequests").start();
|
||||
const sendFn = res.send.bind(res);
|
||||
res.send = (...args) => { stopWatch.end(); return sendFn(...args); };
|
||||
res.send = (...args) => {
|
||||
stopWatch.end();
|
||||
return sendFn(...args);
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -177,22 +203,28 @@ exports.restartServer = async () => {
|
|||
// 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
|
||||
// anyway.
|
||||
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
|
||||
app.use(log4js.connectLogger(logger, {
|
||||
if (!(settings.loglevel === "WARN" && settings.loglevel === "ERROR")) {
|
||||
app.use(
|
||||
log4js.connectLogger(logger, {
|
||||
level: log4js.levels.DEBUG.levelStr,
|
||||
format: ':status, :method :url',
|
||||
}));
|
||||
format: ":status, :method :url",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const { keyRotationInterval, sessionLifetime } = settings.cookie;
|
||||
let secret = settings.sessionKey;
|
||||
if (keyRotationInterval && sessionLifetime) {
|
||||
secretRotator = new SecretRotator(
|
||||
'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey);
|
||||
"expressSessionSecrets",
|
||||
keyRotationInterval,
|
||||
sessionLifetime,
|
||||
settings.sessionKey,
|
||||
);
|
||||
await secretRotator.start();
|
||||
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, {}));
|
||||
|
||||
|
@ -206,7 +238,7 @@ exports.restartServer = async () => {
|
|||
saveUninitialized: false,
|
||||
// Set the cookie name to a javascript identifier compatible string. Makes code handling it
|
||||
// cleaner :)
|
||||
name: 'express_sid',
|
||||
name: "express_sid",
|
||||
cookie: {
|
||||
maxAge: sessionLifetime || null, // Convert 0 to null.
|
||||
sameSite: settings.cookie.sameSite,
|
||||
|
@ -227,33 +259,36 @@ exports.restartServer = async () => {
|
|||
// https at the same time.
|
||||
//
|
||||
// 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
|
||||
// middleware. This allows plugins to avoid creating an express-session record in the database
|
||||
// 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(webaccess.checkAccess);
|
||||
|
||||
await Promise.all([
|
||||
hooks.aCallAll('expressConfigure', {app}),
|
||||
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
|
||||
hooks.aCallAll("expressConfigure", { app }),
|
||||
hooks.aCallAll("expressCreateServer", { app, server: exports.server }),
|
||||
]);
|
||||
exports.server.on('connection', (socket:Socket) => {
|
||||
exports.server.on("connection", (socket: Socket) => {
|
||||
sockets.add(socket);
|
||||
socketsEvents.emit('updated');
|
||||
socket.on('close', () => {
|
||||
socketsEvents.emit("updated");
|
||||
socket.on("close", () => {
|
||||
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());
|
||||
logger.info('HTTP server listening for connections');
|
||||
logger.info("HTTP server listening for connections");
|
||||
};
|
||||
|
||||
exports.shutdown = async (hookName: string, context: any) => {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import express from "express";
|
||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||
const settings = require("ep_etherpad-lite/node/utils/Settings");
|
||||
|
||||
const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
|
||||
const ADMIN_PATH = path.join(settings.root, "src", "templates", "admin");
|
||||
|
||||
/**
|
||||
* Add the admin navigation link
|
||||
|
@ -14,13 +14,24 @@ const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
|
|||
* @param {Function} cb the callback function
|
||||
* @return {*}
|
||||
*/
|
||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
|
||||
args.app.use('/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/');
|
||||
})
|
||||
exports.expressCreateServer = (
|
||||
hookName: string,
|
||||
args: ArgsExpressType,
|
||||
cb: Function,
|
||||
): any => {
|
||||
args.app.use(
|
||||
"/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();
|
||||
};
|
||||
|
|
|
@ -1,32 +1,41 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||
import { ErrorCaused } from "../../types/ErrorCaused";
|
||||
import { QueryType } from "../../types/QueryType";
|
||||
|
||||
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer";
|
||||
import {
|
||||
getAvailablePlugins,
|
||||
install,
|
||||
search,
|
||||
uninstall,
|
||||
} from "../../../static/js/pluginfw/installer";
|
||||
import { 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) => {
|
||||
const io = args.io.of('/pluginfw/installer');
|
||||
io.on('connection', (socket:any) => {
|
||||
const io = args.io.of("/pluginfw/installer");
|
||||
io.on("connection", (socket: any) => {
|
||||
// @ts-ignore
|
||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
||||
const {
|
||||
session: {
|
||||
user: { is_admin: isAdmin } = {},
|
||||
} = {},
|
||||
} = socket.conn.request;
|
||||
if (!isAdmin) return;
|
||||
|
||||
socket.on('getInstalled', (query:string) => {
|
||||
socket.on("getInstalled", (query: string) => {
|
||||
// send currently installed plugins
|
||||
const installed =
|
||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
||||
const installed = Object.keys(pluginDefs.plugins).map(
|
||||
(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
|
||||
try {
|
||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
|
||||
|
@ -40,46 +49,51 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|||
return semver.gt(latestVersion, currentVersion);
|
||||
});
|
||||
|
||||
socket.emit('results:updatable', {updatable});
|
||||
socket.emit("results:updatable", { updatable });
|
||||
} catch (err) {
|
||||
const errc = err as ErrorCaused
|
||||
const errc = err as ErrorCaused;
|
||||
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 {
|
||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
||||
socket.emit('results:available', results);
|
||||
socket.emit("results:available", results);
|
||||
} catch (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 {
|
||||
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
|
||||
const results = await search(
|
||||
query.searchTerm,
|
||||
/* maxCacheAge:*/ 60 * 10,
|
||||
);
|
||||
let res = Object.keys(results)
|
||||
.map((pluginName) => results[pluginName])
|
||||
.filter((plugin) => !pluginDefs.plugins[plugin.name]);
|
||||
res = sortPluginList(res, query.sortBy, query.sortDir)
|
||||
.slice(query.offset, query.offset + query.limit);
|
||||
socket.emit('results:search', {results: res, query});
|
||||
res = sortPluginList(res, query.sortBy, query.sortDir).slice(
|
||||
query.offset,
|
||||
query.offset + query.limit,
|
||||
);
|
||||
socket.emit("results:search", { results: res, query });
|
||||
} catch (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) => {
|
||||
if (err) console.warn(err.stack || err.toString());
|
||||
|
||||
socket.emit('finished:install', {
|
||||
socket.emit("finished:install", {
|
||||
plugin: pluginName,
|
||||
code: err ? err.code : null,
|
||||
error: err ? err.message : null,
|
||||
|
@ -87,11 +101,14 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|||
});
|
||||
});
|
||||
|
||||
socket.on('uninstall', (pluginName:string) => {
|
||||
socket.on("uninstall", (pluginName: string) => {
|
||||
uninstall(pluginName, (err: ErrorCaused) => {
|
||||
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 +122,12 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|||
* @param {String} dir The directory of the plugin
|
||||
* @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
|
||||
if (a[property] < b[property]) {
|
||||
return dir ? -1 : 1;
|
||||
|
|
|
@ -1,56 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
"use strict";
|
||||
|
||||
import { PadQueryResult, PadSearchQuery } from "../../types/PadSearchQuery";
|
||||
import { PadType } from "../../types/PadType";
|
||||
|
||||
const eejs = require('../../eejs');
|
||||
const fsp = require('fs').promises;
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const plugins = require('../../../static/js/pluginfw/plugins');
|
||||
const settings = require('../../utils/Settings');
|
||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
const padManager = require('../../db/PadManager');
|
||||
const api = require('../../db/API');
|
||||
|
||||
const eejs = require("../../eejs");
|
||||
const fsp = require("fs").promises;
|
||||
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||
const plugins = require("../../../static/js/pluginfw/plugins");
|
||||
const settings = require("../../utils/Settings");
|
||||
const UpdateCheck = require("../../utils/UpdateCheck");
|
||||
const padManager = require("../../db/PadManager");
|
||||
const api = require("../../db/API");
|
||||
|
||||
const queryPadLimit = 12;
|
||||
|
||||
|
||||
exports.socketio = (hookName: string, { io }: any) => {
|
||||
io.of('/settings').on('connection', (socket: any ) => {
|
||||
io.of("/settings").on("connection", (socket: any) => {
|
||||
// @ts-ignore
|
||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
||||
const {
|
||||
session: {
|
||||
user: { is_admin: isAdmin } = {},
|
||||
} = {},
|
||||
} = socket.conn.request;
|
||||
if (!isAdmin) return;
|
||||
|
||||
socket.on('load', async (query:string):Promise<any> => {
|
||||
socket.on("load", async (query: string): Promise<any> => {
|
||||
let data;
|
||||
try {
|
||||
data = await fsp.readFile(settings.settingsFilename, 'utf8');
|
||||
data = await fsp.readFile(settings.settingsFilename, "utf8");
|
||||
} catch (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
|
||||
if (settings.showSettingsInAdminPage === false) {
|
||||
socket.emit('settings', {results: 'NOT_ALLOWED'});
|
||||
socket.emit("settings", { results: "NOT_ALLOWED" });
|
||||
} else {
|
||||
socket.emit('settings', {results: data});
|
||||
socket.emit("settings", { results: data });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('saveSettings', async (newSettings:string) => {
|
||||
console.log('Admin request to save settings through a socket on /admin/settings');
|
||||
socket.on("saveSettings", async (newSettings: string) => {
|
||||
console.log(
|
||||
"Admin request to save settings through a socket on /admin/settings",
|
||||
);
|
||||
await fsp.writeFile(settings.settingsFilename, newSettings);
|
||||
socket.emit('saveprogress', 'saved');
|
||||
socket.emit("saveprogress", "saved");
|
||||
});
|
||||
|
||||
|
||||
socket.on('help', ()=> {
|
||||
socket.on("help", () => {
|
||||
const gitCommit = settings.getGitCommit();
|
||||
const epVersion = settings.getEpVersion();
|
||||
|
||||
const hooks:Map<string, Map<string,string>> = plugins.getHooks('hooks', false);
|
||||
const clientHooks:Map<string, Map<string,string>> = plugins.getHooks('client_hooks', false);
|
||||
const hooks: Map<string, Map<string, string>> = plugins.getHooks(
|
||||
"hooks",
|
||||
false,
|
||||
);
|
||||
const clientHooks: Map<string, Map<string, string>> = plugins.getHooks(
|
||||
"client_hooks",
|
||||
false,
|
||||
);
|
||||
|
||||
function mapToObject(map: Map<string, any>) {
|
||||
let obj = Object.create(null);
|
||||
|
@ -64,7 +72,7 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
return obj;
|
||||
}
|
||||
|
||||
socket.emit('reply:help', {
|
||||
socket.emit("reply:help", {
|
||||
gitCommit,
|
||||
epVersion,
|
||||
installedPlugins: plugins.getPlugins(),
|
||||
|
@ -72,16 +80,15 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
installedServerHooks: mapToObject(hooks),
|
||||
installedClientHooks: mapToObject(clientHooks),
|
||||
latestVersion: UpdateCheck.getLatestVersion(),
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
socket.on('padLoad', async (query: PadSearchQuery) => {
|
||||
socket.on("padLoad", async (query: PadSearchQuery) => {
|
||||
const { padIDs } = await padManager.listAllPads();
|
||||
|
||||
const data: {
|
||||
total: number,
|
||||
results?: PadQueryResult[]
|
||||
total: number;
|
||||
results?: PadQueryResult[];
|
||||
} = {
|
||||
total: padIDs.length,
|
||||
};
|
||||
|
@ -90,7 +97,9 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
|
||||
// Filter out matches
|
||||
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;
|
||||
|
@ -112,16 +121,19 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
query.limit = queryPadLimit;
|
||||
}
|
||||
|
||||
if (query.sortBy === 'padName') {
|
||||
result = result.sort((a,b)=>{
|
||||
if (query.sortBy === "padName") {
|
||||
result = result
|
||||
.sort((a, b) => {
|
||||
if (a < b) return query.ascending ? -1 : 1;
|
||||
if (a > b) return query.ascending ? 1 : -1;
|
||||
return 0;
|
||||
}).slice(query.offset, query.offset + query.limit);
|
||||
})
|
||||
.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 revisionNumber = pad.getHeadRevisionNumber()
|
||||
const revisionNumber = pad.getHeadRevisionNumber();
|
||||
const userCount = api.padUsersCount(padName).padUsersCount;
|
||||
const lastEdited = await pad.getLastEdit();
|
||||
|
||||
|
@ -129,83 +141,85 @@ exports.socketio = (hookName:string, {io}:any) => {
|
|||
padName,
|
||||
lastEdited,
|
||||
userCount,
|
||||
revisionNumber
|
||||
}}));
|
||||
revisionNumber,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const currentWinners: PadQueryResult[] = []
|
||||
let queryOffsetCounter = 0
|
||||
const currentWinners: PadQueryResult[] = [];
|
||||
let queryOffsetCounter = 0;
|
||||
for (let res of result) {
|
||||
|
||||
const pad = await padManager.getPad(res);
|
||||
const padType = {
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
revisionNumber: pad.getHeadRevisionNumber(),
|
||||
};
|
||||
|
||||
if (currentWinners.length < query.limit) {
|
||||
if (queryOffsetCounter < query.offset) {
|
||||
queryOffsetCounter++
|
||||
continue
|
||||
queryOffsetCounter++;
|
||||
continue;
|
||||
}
|
||||
currentWinners.push({
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
})
|
||||
revisionNumber: pad.getHeadRevisionNumber(),
|
||||
});
|
||||
} else {
|
||||
// Kick out worst pad and replace by current pad
|
||||
let worstPad = currentWinners.sort((a, b) => {
|
||||
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
|
||||
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
|
||||
if (a[query.sortBy] < b[query.sortBy])
|
||||
return query.ascending ? -1 : 1;
|
||||
if (a[query.sortBy] > b[query.sortBy])
|
||||
return query.ascending ? 1 : -1;
|
||||
return 0;
|
||||
})
|
||||
if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){
|
||||
});
|
||||
if (
|
||||
worstPad[0] &&
|
||||
worstPad[0][query.sortBy] < padType[query.sortBy]
|
||||
) {
|
||||
if (queryOffsetCounter < query.offset) {
|
||||
queryOffsetCounter++
|
||||
continue
|
||||
queryOffsetCounter++;
|
||||
continue;
|
||||
}
|
||||
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
|
||||
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1);
|
||||
currentWinners.push({
|
||||
padName: res,
|
||||
lastEdited: await pad.getLastEdit(),
|
||||
userCount: api.padUsersCount(res).padUsersCount,
|
||||
revisionNumber: pad.getHeadRevisionNumber()
|
||||
})
|
||||
revisionNumber: pad.getHeadRevisionNumber(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
if (padExists) {
|
||||
const pad = await padManager.getPad(padId);
|
||||
await pad.remove();
|
||||
socket.emit('results:deletePad', padId);
|
||||
socket.emit("results:deletePad", padId);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
socket.on('restartServer', async () => {
|
||||
console.log('Admin request to restart server through a socket on /admin/settings');
|
||||
socket.on("restartServer", async () => {
|
||||
console.log(
|
||||
"Admin request to restart server through a socket on /admin/settings",
|
||||
);
|
||||
settings.reloadSettings();
|
||||
await plugins.update();
|
||||
await hooks.aCallAll('loadSettings', {settings});
|
||||
await hooks.aCallAll('restartServer');
|
||||
await hooks.aCallAll("loadSettings", { settings });
|
||||
await hooks.aCallAll("restartServer");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const searchPad = async (query:PadSearchQuery) => {
|
||||
|
||||
}
|
||||
|
||||
const searchPad = async (query: PadSearchQuery) => {};
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const log4js = require('log4js');
|
||||
const clientLogger = log4js.getLogger('client');
|
||||
const {Formidable} = require('formidable');
|
||||
const apiHandler = require('../../handler/APIHandler');
|
||||
const util = require('util');
|
||||
const log4js = require("log4js");
|
||||
const clientLogger = log4js.getLogger("client");
|
||||
const { Formidable } = require("formidable");
|
||||
const apiHandler = require("../../handler/APIHandler");
|
||||
const util = require("util");
|
||||
|
||||
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||
// The Etherpad client side sends information about how a disconnect happened
|
||||
app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => {
|
||||
const [fields, files] = await (new Formidable({})).parse(req);
|
||||
app.post("/ep/pad/connection-diagnostic-info", async (req: any, res: any) => {
|
||||
const [fields, files] = await new Formidable({}).parse(req);
|
||||
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
||||
res.end('OK');
|
||||
res.end("OK");
|
||||
});
|
||||
|
||||
const parseJserrorForm = async (req: any) => {
|
||||
|
@ -23,22 +23,25 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
};
|
||||
|
||||
// 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 () => {
|
||||
const data = JSON.parse(await parseJserrorForm(req));
|
||||
clientLogger.warn(`${data.msg} --`, {
|
||||
[util.inspect.custom]: (depth: number, options: any) => {
|
||||
// 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: Infinity,
|
||||
colors: true,
|
||||
});
|
||||
return util.inspect(data, options);
|
||||
},
|
||||
});
|
||||
res.end('OK');
|
||||
res.end("OK");
|
||||
})().catch((err) => next(err || new Error(err)));
|
||||
});
|
||||
|
||||
// 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 });
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||
import { ErrorCaused } from "../../types/ErrorCaused";
|
||||
|
||||
const stats = require('../../stats')
|
||||
const stats = require("../../stats");
|
||||
|
||||
exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => {
|
||||
exports.expressCreateServer = (
|
||||
hook_name: string,
|
||||
args: ArgsExpressType,
|
||||
cb: Function,
|
||||
) => {
|
||||
exports.app = args.app;
|
||||
|
||||
// Handle errors
|
||||
|
@ -13,9 +17,9 @@ exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Funct
|
|||
// if an error occurs Connect will pass it down
|
||||
// through these "error-handling" middleware
|
||||
// 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());
|
||||
stats.meter('http500').mark();
|
||||
stats.meter("http500").mark();
|
||||
});
|
||||
|
||||
return cb();
|
||||
|
|
|
@ -1,53 +1,67 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
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");
|
||||
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");
|
||||
|
||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
exports.expressCreateServer = (
|
||||
hookName: string,
|
||||
args: ArgsExpressType,
|
||||
cb: Function,
|
||||
) => {
|
||||
const limiter = rateLimit({
|
||||
...settings.importExportRateLimiting,
|
||||
handler: (request: any) => {
|
||||
if (request.rateLimit.current === request.rateLimit.limit + 1) {
|
||||
// when the rate limiter triggers, write a warning in the logs
|
||||
console.warn('Import/Export rate limiter triggered on ' +
|
||||
`"${request.originalUrl}" for IP address ${request.ip}`);
|
||||
console.warn(
|
||||
"Import/Export rate limiter triggered on " +
|
||||
`"${request.originalUrl}" for IP address ${request.ip}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// handle export requests
|
||||
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.use("/p/:pad/:rev?/export/:type", limiter);
|
||||
args.app.get(
|
||||
"/p/:pad/:rev?/export/:type",
|
||||
(req: any, res: any, next: Function) => {
|
||||
(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
|
||||
if (types.indexOf(req.params.type) === -1) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// if abiword is disabled, and this is a format we only support with abiword, output a message
|
||||
if (settings.exportAvailable() === 'no' &&
|
||||
['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) {
|
||||
console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
|
||||
' There is no converter configured');
|
||||
if (
|
||||
settings.exportAvailable() === "no" &&
|
||||
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1
|
||||
) {
|
||||
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
|
||||
// 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' +
|
||||
' or soffice (LibreOffice) in settings.json to enable this feature');
|
||||
res.send(
|
||||
"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;
|
||||
}
|
||||
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
|
||||
if (await hasPadAccess(req, res)) {
|
||||
let padId = req.params.pad;
|
||||
|
@ -60,26 +74,47 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
|
|||
|
||||
const exists = await padManager.doesPadExists(padId);
|
||||
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();
|
||||
}
|
||||
|
||||
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
|
||||
await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
|
||||
console.log(
|
||||
`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)));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// handle import requests
|
||||
args.app.use('/p/:pad/import', limiter);
|
||||
args.app.post('/p/:pad/import', (req:any, res:any, next:Function) => {
|
||||
args.app.use("/p/:pad/import", limiter);
|
||||
args.app.post("/p/:pad/import", (req: any, res: any, next: Function) => {
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
const {session: {user} = {}} = req;
|
||||
const {accessStatus, authorID: authorId} = 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');
|
||||
const {
|
||||
session: { user } = {},
|
||||
} = req;
|
||||
const { accessStatus, authorID: authorId } =
|
||||
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);
|
||||
})().catch((err) => next(err || new Error(err)));
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource";
|
||||
import {
|
||||
OpenAPIOperations,
|
||||
OpenAPISuccessResponse,
|
||||
SwaggerUIResource,
|
||||
} from "../../types/SwaggerUIResource";
|
||||
import { MapArrayType } from "../../types/MapType";
|
||||
import { ErrorCaused } from "../../types/ErrorCaused";
|
||||
|
||||
|
@ -18,260 +22,282 @@ import {ErrorCaused} from "../../types/ErrorCaused";
|
|||
* - /rest/{version}/openapi.json
|
||||
*/
|
||||
|
||||
const OpenAPIBackend = require('openapi-backend').default;
|
||||
const IncomingForm = require('formidable').IncomingForm;
|
||||
const cloneDeep = require('lodash.clonedeep');
|
||||
const createHTTPError = require('http-errors');
|
||||
const OpenAPIBackend = require("openapi-backend").default;
|
||||
const IncomingForm = require("formidable").IncomingForm;
|
||||
const cloneDeep = require("lodash.clonedeep");
|
||||
const createHTTPError = require("http-errors");
|
||||
|
||||
const apiHandler = require('../../handler/APIHandler');
|
||||
const settings = require('../../utils/Settings');
|
||||
const apiHandler = require("../../handler/APIHandler");
|
||||
const settings = require("../../utils/Settings");
|
||||
|
||||
const log4js = require('log4js');
|
||||
const logger = log4js.getLogger('API');
|
||||
const log4js = require("log4js");
|
||||
const logger = log4js.getLogger("API");
|
||||
|
||||
// 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 = {
|
||||
title: 'Etherpad API',
|
||||
title: "Etherpad API",
|
||||
description:
|
||||
'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, ' +
|
||||
'under your control.',
|
||||
termsOfService: 'https://etherpad.org/',
|
||||
"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, " +
|
||||
"under your control.",
|
||||
termsOfService: "https://etherpad.org/",
|
||||
contact: {
|
||||
name: 'The Etherpad Foundation',
|
||||
url: 'https://etherpad.org/',
|
||||
email: 'support@example.com',
|
||||
name: "The Etherpad Foundation",
|
||||
url: "https://etherpad.org/",
|
||||
email: "support@example.com",
|
||||
},
|
||||
license: {
|
||||
name: 'Apache 2.0',
|
||||
url: 'https://www.apache.org/licenses/LICENSE-2.0.html',
|
||||
name: "Apache 2.0",
|
||||
url: "https://www.apache.org/licenses/LICENSE-2.0.html",
|
||||
},
|
||||
version: apiHandler.latestApiVersion,
|
||||
};
|
||||
|
||||
const APIPathStyle = {
|
||||
FLAT: 'api', // flat paths e.g. /api/createGroup
|
||||
REST: 'rest', // restful paths e.g. /rest/group/create
|
||||
FLAT: "api", // flat paths e.g. /api/createGroup
|
||||
REST: "rest", // restful paths e.g. /rest/group/create
|
||||
};
|
||||
|
||||
|
||||
// API resources - describe your API endpoints here
|
||||
const resources: SwaggerUIResource = {
|
||||
// Group
|
||||
group: {
|
||||
create: {
|
||||
operationId: 'createGroup',
|
||||
summary: 'creates a new group',
|
||||
responseSchema: {groupID: {type: 'string'}},
|
||||
operationId: "createGroup",
|
||||
summary: "creates a new group",
|
||||
responseSchema: { groupID: { type: "string" } },
|
||||
},
|
||||
createIfNotExistsFor: {
|
||||
operationId: 'createGroupIfNotExistsFor',
|
||||
summary: 'this functions helps you to map your application group ids to Etherpad group ids',
|
||||
responseSchema: {groupID: {type: 'string'}},
|
||||
operationId: "createGroupIfNotExistsFor",
|
||||
summary:
|
||||
"this functions helps you to map your application group ids to Etherpad group ids",
|
||||
responseSchema: { groupID: { type: "string" } },
|
||||
},
|
||||
delete: {
|
||||
operationId: 'deleteGroup',
|
||||
summary: 'deletes a group',
|
||||
operationId: "deleteGroup",
|
||||
summary: "deletes a group",
|
||||
},
|
||||
listPads: {
|
||||
operationId: 'listPads',
|
||||
summary: 'returns all pads of this group',
|
||||
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}},
|
||||
operationId: "listPads",
|
||||
summary: "returns all pads of this group",
|
||||
responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
|
||||
},
|
||||
createPad: {
|
||||
operationId: 'createGroupPad',
|
||||
summary: 'creates a new pad in this group',
|
||||
operationId: "createGroupPad",
|
||||
summary: "creates a new pad in this group",
|
||||
},
|
||||
listSessions: {
|
||||
operationId: 'listSessionsOfGroup',
|
||||
summary: '',
|
||||
operationId: "listSessionsOfGroup",
|
||||
summary: "",
|
||||
responseSchema: {
|
||||
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}},
|
||||
sessions: {
|
||||
type: "array",
|
||||
items: { $ref: "#/components/schemas/SessionInfo" },
|
||||
},
|
||||
},
|
||||
},
|
||||
list: {
|
||||
operationId: 'listAllGroups',
|
||||
summary: '',
|
||||
responseSchema: {groupIDs: {type: 'array', items: {type: 'string'}}},
|
||||
operationId: "listAllGroups",
|
||||
summary: "",
|
||||
responseSchema: {
|
||||
groupIDs: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Author
|
||||
author: {
|
||||
create: {
|
||||
operationId: 'createAuthor',
|
||||
summary: 'creates a new author',
|
||||
responseSchema: {authorID: {type: 'string'}},
|
||||
operationId: "createAuthor",
|
||||
summary: "creates a new author",
|
||||
responseSchema: { authorID: { type: "string" } },
|
||||
},
|
||||
createIfNotExistsFor: {
|
||||
operationId: 'createAuthorIfNotExistsFor',
|
||||
summary: 'this functions helps you to map your application author ids to Etherpad author ids',
|
||||
responseSchema: {authorID: {type: 'string'}},
|
||||
operationId: "createAuthorIfNotExistsFor",
|
||||
summary:
|
||||
"this functions helps you to map your application author ids to Etherpad author ids",
|
||||
responseSchema: { authorID: { type: "string" } },
|
||||
},
|
||||
listPads: {
|
||||
operationId: 'listPadsOfAuthor',
|
||||
summary: 'returns an array of all pads this author contributed to',
|
||||
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}},
|
||||
operationId: "listPadsOfAuthor",
|
||||
summary: "returns an array of all pads this author contributed to",
|
||||
responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
|
||||
},
|
||||
listSessions: {
|
||||
operationId: 'listSessionsOfAuthor',
|
||||
summary: 'returns all sessions of an author',
|
||||
operationId: "listSessionsOfAuthor",
|
||||
summary: "returns all sessions of an author",
|
||||
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 :(
|
||||
getName: {
|
||||
operationId: 'getAuthorName',
|
||||
summary: 'Returns the Author Name of the author',
|
||||
responseSchema: {info: {$ref: '#/components/schemas/UserInfo'}},
|
||||
operationId: "getAuthorName",
|
||||
summary: "Returns the Author Name of the author",
|
||||
responseSchema: { info: { $ref: "#/components/schemas/UserInfo" } },
|
||||
},
|
||||
},
|
||||
|
||||
// Session
|
||||
session: {
|
||||
create: {
|
||||
operationId: 'createSession',
|
||||
summary: 'creates a new session. validUntil is an unix timestamp in seconds',
|
||||
responseSchema: {sessionID: {type: 'string'}},
|
||||
operationId: "createSession",
|
||||
summary:
|
||||
"creates a new session. validUntil is an unix timestamp in seconds",
|
||||
responseSchema: { sessionID: { type: "string" } },
|
||||
},
|
||||
delete: {
|
||||
operationId: 'deleteSession',
|
||||
summary: 'deletes a session',
|
||||
operationId: "deleteSession",
|
||||
summary: "deletes a session",
|
||||
},
|
||||
// We need an operation that returns a SessionInfo so it can be picked up by the codegen :(
|
||||
info: {
|
||||
operationId: 'getSessionInfo',
|
||||
summary: 'returns information about a session',
|
||||
responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}},
|
||||
operationId: "getSessionInfo",
|
||||
summary: "returns information about a session",
|
||||
responseSchema: { info: { $ref: "#/components/schemas/SessionInfo" } },
|
||||
},
|
||||
},
|
||||
|
||||
// Pad
|
||||
pad: {
|
||||
listAll: {
|
||||
operationId: 'listAllPads',
|
||||
summary: 'list all the pads',
|
||||
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}},
|
||||
operationId: "listAllPads",
|
||||
summary: "list all the pads",
|
||||
responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
|
||||
},
|
||||
createDiffHTML: {
|
||||
operationId: 'createDiffHTML',
|
||||
summary: '',
|
||||
operationId: "createDiffHTML",
|
||||
summary: "",
|
||||
responseSchema: {},
|
||||
},
|
||||
create: {
|
||||
operationId: 'createPad',
|
||||
operationId: "createPad",
|
||||
description:
|
||||
'creates a new (non-group) pad. Note that if you need to create a group Pad, ' +
|
||||
'you should call createGroupPad',
|
||||
"creates a new (non-group) pad. Note that if you need to create a group Pad, " +
|
||||
"you should call createGroupPad",
|
||||
},
|
||||
getText: {
|
||||
operationId: 'getText',
|
||||
summary: 'returns the text of a pad',
|
||||
responseSchema: {text: {type: 'string'}},
|
||||
operationId: "getText",
|
||||
summary: "returns the text of a pad",
|
||||
responseSchema: { text: { type: "string" } },
|
||||
},
|
||||
setText: {
|
||||
operationId: 'setText',
|
||||
summary: 'sets the text of a pad',
|
||||
operationId: "setText",
|
||||
summary: "sets the text of a pad",
|
||||
},
|
||||
getHTML: {
|
||||
operationId: 'getHTML',
|
||||
summary: 'returns the text of a pad formatted as HTML',
|
||||
responseSchema: {html: {type: 'string'}},
|
||||
operationId: "getHTML",
|
||||
summary: "returns the text of a pad formatted as HTML",
|
||||
responseSchema: { html: { type: "string" } },
|
||||
},
|
||||
setHTML: {
|
||||
operationId: 'setHTML',
|
||||
summary: 'sets the text of a pad with HTML',
|
||||
operationId: "setHTML",
|
||||
summary: "sets the text of a pad with HTML",
|
||||
},
|
||||
getRevisionsCount: {
|
||||
operationId: 'getRevisionsCount',
|
||||
summary: 'returns the number of revisions of this pad',
|
||||
responseSchema: {revisions: {type: 'integer'}},
|
||||
operationId: "getRevisionsCount",
|
||||
summary: "returns the number of revisions of this pad",
|
||||
responseSchema: { revisions: { type: "integer" } },
|
||||
},
|
||||
getLastEdited: {
|
||||
operationId: 'getLastEdited',
|
||||
summary: 'returns the timestamp of the last revision of the pad',
|
||||
responseSchema: {lastEdited: {type: 'integer'}},
|
||||
operationId: "getLastEdited",
|
||||
summary: "returns the timestamp of the last revision of the pad",
|
||||
responseSchema: { lastEdited: { type: "integer" } },
|
||||
},
|
||||
delete: {
|
||||
operationId: 'deletePad',
|
||||
summary: 'deletes a pad',
|
||||
operationId: "deletePad",
|
||||
summary: "deletes a pad",
|
||||
},
|
||||
getReadOnlyID: {
|
||||
operationId: 'getReadOnlyID',
|
||||
summary: 'returns the read only link of a pad',
|
||||
responseSchema: {readOnlyID: {type: 'string'}},
|
||||
operationId: "getReadOnlyID",
|
||||
summary: "returns the read only link of a pad",
|
||||
responseSchema: { readOnlyID: { type: "string" } },
|
||||
},
|
||||
setPublicStatus: {
|
||||
operationId: 'setPublicStatus',
|
||||
summary: 'sets a boolean for the public status of a pad',
|
||||
operationId: "setPublicStatus",
|
||||
summary: "sets a boolean for the public status of a pad",
|
||||
},
|
||||
getPublicStatus: {
|
||||
operationId: 'getPublicStatus',
|
||||
summary: 'return true of false',
|
||||
responseSchema: {publicStatus: {type: 'boolean'}},
|
||||
operationId: "getPublicStatus",
|
||||
summary: "return true of false",
|
||||
responseSchema: { publicStatus: { type: "boolean" } },
|
||||
},
|
||||
authors: {
|
||||
operationId: 'listAuthorsOfPad',
|
||||
summary: 'returns an array of authors who contributed to this pad',
|
||||
responseSchema: {authorIDs: {type: 'array', items: {type: 'string'}}},
|
||||
operationId: "listAuthorsOfPad",
|
||||
summary: "returns an array of authors who contributed to this pad",
|
||||
responseSchema: {
|
||||
authorIDs: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
usersCount: {
|
||||
operationId: 'padUsersCount',
|
||||
summary: 'returns the number of user that are currently editing this pad',
|
||||
responseSchema: {padUsersCount: {type: 'integer'}},
|
||||
operationId: "padUsersCount",
|
||||
summary: "returns the number of user that are currently editing this pad",
|
||||
responseSchema: { padUsersCount: { type: "integer" } },
|
||||
},
|
||||
users: {
|
||||
operationId: 'padUsers',
|
||||
summary: 'returns the list of users that are currently editing this pad',
|
||||
responseSchema: {padUsers: {type: 'array', items: {$ref: '#/components/schemas/UserInfo'}}},
|
||||
operationId: "padUsers",
|
||||
summary: "returns the list of users that are currently editing this pad",
|
||||
responseSchema: {
|
||||
padUsers: {
|
||||
type: "array",
|
||||
items: { $ref: "#/components/schemas/UserInfo" },
|
||||
},
|
||||
},
|
||||
},
|
||||
sendClientsMessage: {
|
||||
operationId: 'sendClientsMessage',
|
||||
summary: 'sends a custom message of type msg to the pad',
|
||||
operationId: "sendClientsMessage",
|
||||
summary: "sends a custom message of type msg to the pad",
|
||||
},
|
||||
checkToken: {
|
||||
operationId: 'checkToken',
|
||||
summary: 'returns ok when the current api token is valid',
|
||||
operationId: "checkToken",
|
||||
summary: "returns ok when the current api token is valid",
|
||||
},
|
||||
getChatHistory: {
|
||||
operationId: 'getChatHistory',
|
||||
summary: 'returns the chat history',
|
||||
responseSchema: {messages: {type: 'array', items: {$ref: '#/components/schemas/Message'}}},
|
||||
operationId: "getChatHistory",
|
||||
summary: "returns the chat history",
|
||||
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 :(
|
||||
getChatHead: {
|
||||
operationId: 'getChatHead',
|
||||
summary: 'returns the chatHead (chat-message) of the pad',
|
||||
responseSchema: {chatHead: {$ref: '#/components/schemas/Message'}},
|
||||
operationId: "getChatHead",
|
||||
summary: "returns the chatHead (chat-message) of the pad",
|
||||
responseSchema: { chatHead: { $ref: "#/components/schemas/Message" } },
|
||||
},
|
||||
appendChatMessage: {
|
||||
operationId: 'appendChatMessage',
|
||||
summary: 'appends a chat message',
|
||||
operationId: "appendChatMessage",
|
||||
summary: "appends a chat message",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultResponses = {
|
||||
Success: {
|
||||
description: 'ok (code 0)',
|
||||
description: "ok (code 0)",
|
||||
content: {
|
||||
'application/json': {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
example: 0,
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'ok',
|
||||
type: "string",
|
||||
example: "ok",
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
example: null,
|
||||
},
|
||||
},
|
||||
|
@ -280,22 +306,22 @@ const defaultResponses = {
|
|||
},
|
||||
},
|
||||
ApiError: {
|
||||
description: 'generic api error (code 1)',
|
||||
description: "generic api error (code 1)",
|
||||
content: {
|
||||
'application/json': {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
example: 1,
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'error message',
|
||||
type: "string",
|
||||
example: "error message",
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
example: null,
|
||||
},
|
||||
},
|
||||
|
@ -304,22 +330,22 @@ const defaultResponses = {
|
|||
},
|
||||
},
|
||||
InternalError: {
|
||||
description: 'internal api error (code 2)',
|
||||
description: "internal api error (code 2)",
|
||||
content: {
|
||||
'application/json': {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
example: 2,
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'internal error',
|
||||
type: "string",
|
||||
example: "internal error",
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
example: null,
|
||||
},
|
||||
},
|
||||
|
@ -328,22 +354,22 @@ const defaultResponses = {
|
|||
},
|
||||
},
|
||||
NotFound: {
|
||||
description: 'no such function (code 4)',
|
||||
description: "no such function (code 4)",
|
||||
content: {
|
||||
'application/json': {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
example: 3,
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'no such function',
|
||||
type: "string",
|
||||
example: "no such function",
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
example: null,
|
||||
},
|
||||
},
|
||||
|
@ -352,22 +378,22 @@ const defaultResponses = {
|
|||
},
|
||||
},
|
||||
Unauthorized: {
|
||||
description: 'no or wrong API key (code 4)',
|
||||
description: "no or wrong API key (code 4)",
|
||||
content: {
|
||||
'application/json': {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
example: 4,
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'no or wrong API key',
|
||||
type: "string",
|
||||
example: "no or wrong API key",
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
example: null,
|
||||
},
|
||||
},
|
||||
|
@ -379,16 +405,16 @@ const defaultResponses = {
|
|||
|
||||
const defaultResponseRefs: OpenAPISuccessResponse = {
|
||||
200: {
|
||||
$ref: '#/components/responses/Success',
|
||||
$ref: "#/components/responses/Success",
|
||||
},
|
||||
400: {
|
||||
$ref: '#/components/responses/ApiError',
|
||||
$ref: "#/components/responses/ApiError",
|
||||
},
|
||||
401: {
|
||||
$ref: '#/components/responses/Unauthorized',
|
||||
$ref: "#/components/responses/Unauthorized",
|
||||
},
|
||||
500: {
|
||||
$ref: '#/components/responses/InternalError',
|
||||
$ref: "#/components/responses/InternalError",
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -402,8 +428,8 @@ for (const [resource, actions] of Object.entries(resources)) {
|
|||
const responses: OpenAPISuccessResponse = { ...defaultResponseRefs };
|
||||
if (responseSchema) {
|
||||
responses[200] = cloneDeep(defaultResponses.Success);
|
||||
responses[200].content!['application/json'].schema.properties.data = {
|
||||
type: 'object',
|
||||
responses[200].content!["application/json"].schema.properties.data = {
|
||||
type: "object",
|
||||
properties: responseSchema,
|
||||
};
|
||||
}
|
||||
|
@ -419,7 +445,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 = {
|
||||
openapi: OPENAPI_VERSION,
|
||||
info,
|
||||
|
@ -428,53 +457,53 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
|
|||
parameters: {},
|
||||
schemas: {
|
||||
SessionInfo: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
authorID: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
groupID: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
validUntil: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
},
|
||||
UserInfo: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
colorId: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
timestamp: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
},
|
||||
Message: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
text: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
userName: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
time: {
|
||||
type: 'integer',
|
||||
type: "integer",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -493,9 +522,9 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
|
|||
openid: "openid",
|
||||
profile: "profile",
|
||||
email: "email",
|
||||
admin: "admin"
|
||||
}
|
||||
}
|
||||
admin: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -519,15 +548,17 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
|
|||
// set parameters
|
||||
operation.parameters = operation.parameters || [];
|
||||
for (const paramName of apiHandler.version[version][funcName]) {
|
||||
operation.parameters.push({$ref: `#/components/parameters/${paramName}`});
|
||||
operation.parameters.push({
|
||||
$ref: `#/components/parameters/${paramName}`,
|
||||
});
|
||||
// @ts-ignore
|
||||
if (!definition.components.parameters[paramName]) {
|
||||
// @ts-ignore
|
||||
definition.components.parameters[paramName] = {
|
||||
name: paramName,
|
||||
in: 'query',
|
||||
in: "query",
|
||||
schema: {
|
||||
type: 'string',
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -572,16 +603,22 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
// serve version specific openapi definition
|
||||
app.get(`${apiRoot}/openapi.json`, (req: any, res: any) => {
|
||||
// For openapi definitions, wide CORS is probably fine
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.json({
|
||||
...definition,
|
||||
servers: [generateServerForApiVersion(apiRoot, req)],
|
||||
});
|
||||
});
|
||||
|
||||
// serve latest openapi definition file under /api/openapi.json
|
||||
const isLatestAPIVersion = version === apiHandler.latestApiVersion;
|
||||
if (isLatestAPIVersion) {
|
||||
app.get(`/${style}/openapi.json`, (req: any, res: any) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.json({
|
||||
...definition,
|
||||
servers: [generateServerForApiVersion(apiRoot, req)],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -597,10 +634,10 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
// register default handlers
|
||||
api.register({
|
||||
notFound: () => {
|
||||
throw new createHTTPError.NotFound('no such function');
|
||||
throw new createHTTPError.NotFound("no such function");
|
||||
},
|
||||
notImplemented: () => {
|
||||
throw new createHTTPError.NotImplemented('function not implemented');
|
||||
throw new createHTTPError.NotImplemented("function not implemented");
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -612,7 +649,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
|
||||
// read form data if method was POST
|
||||
let formData: MapArrayType<any> = {};
|
||||
if (c.request.method === 'post') {
|
||||
if (c.request.method === "post") {
|
||||
const form = new IncomingForm();
|
||||
formData = (await form.parse(req))[0];
|
||||
for (const k of Object.keys(formData)) {
|
||||
|
@ -625,7 +662,9 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
const fields = Object.assign({}, header, params, query, formData);
|
||||
|
||||
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
|
||||
|
@ -633,12 +672,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
try {
|
||||
data = await apiHandler.handle(version, funcName, fields, req, res);
|
||||
} catch (err) {
|
||||
const errCaused = err as ErrorCaused
|
||||
const errCaused = err as ErrorCaused;
|
||||
// convert all errors to http errors
|
||||
if (createHTTPError.isHttpError(err)) {
|
||||
// pass http errors thrown by handler forward
|
||||
throw err;
|
||||
} else if (errCaused.name === 'apierror') {
|
||||
} else if (errCaused.name === "apierror") {
|
||||
// parameters were wrong and the api stopped execution, pass the error
|
||||
// convert to http error
|
||||
throw new createHTTPError.BadRequest(errCaused.message);
|
||||
|
@ -646,12 +685,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
// an unknown error happened
|
||||
// log it and throw internal error
|
||||
logger.error(errCaused.stack || errCaused.toString());
|
||||
throw new createHTTPError.InternalError('internal error');
|
||||
throw new createHTTPError.InternalError("internal error");
|
||||
}
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
logger.debug(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`);
|
||||
|
@ -674,12 +713,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
if (style === APIPathStyle.REST) {
|
||||
// @TODO: Don't allow CORS from everywhere
|
||||
// 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
|
||||
response = await api.handleRequest(req, req, res);
|
||||
} catch (err) {
|
||||
const errCaused = err as ErrorCaused
|
||||
const errCaused = err as ErrorCaused;
|
||||
// handle http errors
|
||||
// @ts-ignore
|
||||
res.statusCode = errCaused.statusCode || 500;
|
||||
|
@ -723,7 +762,10 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
* @param {APIPathStyle} style The style of the API path
|
||||
* @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
|
||||
|
@ -731,8 +773,11 @@ const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): st
|
|||
* @param {Request} req The express request object
|
||||
* @return {url: String} The server object for the OpenAPI definition location
|
||||
*/
|
||||
const generateServerForApiVersion = (apiRoot:string, req:any): {
|
||||
url:string
|
||||
const generateServerForApiVersion = (
|
||||
apiRoot: string,
|
||||
req: any,
|
||||
): {
|
||||
url: string;
|
||||
} => ({
|
||||
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
|
||||
url: `${settings.ssl ? "https" : "http"}://${req.headers.host}${apiRoot}`,
|
||||
});
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||
|
||||
const padManager = require('../../db/PadManager');
|
||||
const padManager = require("../../db/PadManager");
|
||||
|
||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
exports.expressCreateServer = (
|
||||
hookName: string,
|
||||
args: ArgsExpressType,
|
||||
cb: Function,
|
||||
) => {
|
||||
// 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 () => {
|
||||
// ensure the padname is valid and the url doesn't end with a /
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -22,9 +26,14 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
|
|||
} else {
|
||||
// the pad id was sanitized, so we redirect to the sanitized version
|
||||
const realURL =
|
||||
encodeURIComponent(sanitizedPadId) + new URL(req.url, 'http://invalid.invalid').search;
|
||||
res.header('Location', realURL);
|
||||
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
|
||||
encodeURIComponent(sanitizedPadId) +
|
||||
new URL(req.url, "http://invalid.invalid").search;
|
||||
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)));
|
||||
});
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||
|
||||
import events from 'events';
|
||||
const express = require('../express');
|
||||
import log4js from 'log4js';
|
||||
const proxyaddr = require('proxy-addr');
|
||||
const settings = require('../../utils/Settings');
|
||||
import {Server, Socket} from 'socket.io'
|
||||
const socketIORouter = require('../../handler/SocketIORouter');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const padMessageHandler = require('../../handler/PadMessageHandler');
|
||||
import events from "events";
|
||||
const express = require("../express");
|
||||
import log4js from "log4js";
|
||||
const proxyaddr = require("proxy-addr");
|
||||
const settings = require("../../utils/Settings");
|
||||
import { Server, Socket } from "socket.io";
|
||||
const socketIORouter = require("../../handler/SocketIORouter");
|
||||
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||
const padMessageHandler = require("../../handler/PadMessageHandler");
|
||||
|
||||
let io: any;
|
||||
const logger = log4js.getLogger('socket.io');
|
||||
const logger = log4js.getLogger("socket.io");
|
||||
const sockets = new Set();
|
||||
const socketsEvents = new events.EventEmitter();
|
||||
|
||||
export const expressCloseServer = async () => {
|
||||
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
|
||||
// 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
|
||||
|
@ -39,21 +39,25 @@ export const expressCloseServer = async () => {
|
|||
// ourselves, so that is what we do.
|
||||
let lastLogged = 0;
|
||||
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
||||
if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs.
|
||||
logger.info(`Waiting for ${sockets.size} socket.io clients to disconnect...`);
|
||||
if (Date.now() - lastLogged > 1000) {
|
||||
// Rate limit to avoid filling logs.
|
||||
logger.info(
|
||||
`Waiting for ${sockets.size} socket.io clients to disconnect...`,
|
||||
);
|
||||
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;
|
||||
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
|
||||
if (req.ip == null) {
|
||||
if (settings.trustProxy) {
|
||||
req.ip = proxyaddr(req, args.app.get('trust proxy fn'));
|
||||
req.ip = proxyaddr(req, args.app.get("trust proxy fn"));
|
||||
} else {
|
||||
req.ip = socket.handshake.address;
|
||||
}
|
||||
|
@ -65,7 +69,11 @@ const socketSessionMiddleware = (args: any) => (socket: any, next: Function) =>
|
|||
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
|
||||
// there shouldn't be a browser that isn't compatible to all
|
||||
// transports in this list at once
|
||||
|
@ -74,25 +82,24 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
transports: settings.socketTransportProtocols,
|
||||
cookie: false,
|
||||
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
const handleConnection = (socket: Socket) => {
|
||||
sockets.add(socket);
|
||||
socketsEvents.emit('updated');
|
||||
socketsEvents.emit("updated");
|
||||
// https://socket.io/docs/v3/faq/index.html
|
||||
// @ts-ignore
|
||||
const session = socket.request.session;
|
||||
session.connections++;
|
||||
session.save();
|
||||
socket.on('disconnect', () => {
|
||||
socket.on("disconnect", () => {
|
||||
sockets.delete(socket);
|
||||
socketsEvents.emit('updated');
|
||||
socketsEvents.emit("updated");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
// 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
|
||||
|
@ -102,22 +109,21 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
if (socket.request.session != null) socket.request.session.touch();
|
||||
});
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
io.on('connection', handleConnection);
|
||||
io.on("connection", handleConnection);
|
||||
|
||||
io.use(socketSessionMiddleware(args));
|
||||
|
||||
// Temporary workaround so all clients go through middleware and handle connection
|
||||
io.of('/pluginfw/installer')
|
||||
.on('connection',handleConnection)
|
||||
io.of("/pluginfw/installer")
|
||||
.on("connection", handleConnection)
|
||||
.use(socketSessionMiddleware(args))
|
||||
.use(renewSession)
|
||||
io.of('/settings')
|
||||
.on('connection',handleConnection)
|
||||
.use(renewSession);
|
||||
io.of("/settings")
|
||||
.on("connection", handleConnection)
|
||||
.use(socketSessionMiddleware(args))
|
||||
.use(renewSession)
|
||||
.use(renewSession);
|
||||
|
||||
io.use(renewSession);
|
||||
|
||||
|
@ -134,9 +140,9 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
|||
|
||||
// Initialize the Socket.IO Router
|
||||
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();
|
||||
};
|
||||
|
|
|
@ -1,62 +1,78 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const path = require('path');
|
||||
const eejs = require('../../eejs');
|
||||
const fs = require('fs');
|
||||
const path = require("path");
|
||||
const eejs = require("../../eejs");
|
||||
const fs = require("fs");
|
||||
const fsp = fs.promises;
|
||||
const toolbar = require('../../utils/toolbar');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const settings = require('../../utils/Settings');
|
||||
const util = require('util');
|
||||
const webaccess = require('./webaccess');
|
||||
const toolbar = require("../../utils/toolbar");
|
||||
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||
const settings = require("../../utils/Settings");
|
||||
const util = require("util");
|
||||
const webaccess = require("./webaccess");
|
||||
|
||||
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||
// This endpoint is intended to conform to:
|
||||
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
||||
app.get('/health', (req:any, res:any) => {
|
||||
res.set('Content-Type', 'application/health+json');
|
||||
app.get("/health", (req: any, res: any) => {
|
||||
res.set("Content-Type", "application/health+json");
|
||||
res.json({
|
||||
status: 'pass',
|
||||
status: "pass",
|
||||
releaseId: settings.getEpVersion(),
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/stats', (req:any, res:any) => {
|
||||
res.json(require('../../stats').toJSON());
|
||||
app.get("/stats", (req: any, res: any) => {
|
||||
res.json(require("../../stats").toJSON());
|
||||
});
|
||||
|
||||
app.get('/javascript', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
|
||||
app.get("/javascript", (req: any, res: any) => {
|
||||
res.send(
|
||||
eejs.require("ep_etherpad-lite/templates/javascript.html", { req }),
|
||||
);
|
||||
});
|
||||
|
||||
app.get('/robots.txt', (req:any, res:any) => {
|
||||
let filePath =
|
||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
|
||||
app.get("/robots.txt", (req: any, res: any) => {
|
||||
let filePath = path.join(
|
||||
settings.root,
|
||||
"src",
|
||||
"static",
|
||||
"skins",
|
||||
settings.skinName,
|
||||
"robots.txt",
|
||||
);
|
||||
res.sendFile(filePath, (err: any) => {
|
||||
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
||||
if (err) {
|
||||
filePath = path.join(settings.root, 'src', 'static', 'robots.txt');
|
||||
filePath = path.join(settings.root, "src", "static", "robots.txt");
|
||||
res.sendFile(filePath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/favicon.ico', (req:any, res:any, next:Function) => {
|
||||
app.get("/favicon.ico", (req: any, res: any, next: Function) => {
|
||||
(async () => {
|
||||
/*
|
||||
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.send();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const fns = [
|
||||
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
|
||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
|
||||
path.join(settings.root, 'src', 'static', 'favicon.ico'),
|
||||
...(settings.favicon
|
||||
? [path.resolve(settings.root, settings.favicon)]
|
||||
: []),
|
||||
path.join(
|
||||
settings.root,
|
||||
"src",
|
||||
"static",
|
||||
"skins",
|
||||
settings.skinName,
|
||||
"favicon.ico",
|
||||
),
|
||||
path.join(settings.root, "src", "static", "favicon.ico"),
|
||||
];
|
||||
for (const fn of fns) {
|
||||
try {
|
||||
|
@ -64,7 +80,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
} catch (err) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
@ -75,46 +91,50 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
|
||||
exports.expressCreateServer = (hookName: string, args: any, cb: Function) => {
|
||||
// serve index.html under /
|
||||
args.app.get('/', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
|
||||
args.app.get("/", (req: any, res: any) => {
|
||||
res.send(eejs.require("ep_etherpad-lite/templates/index.html", { req }));
|
||||
});
|
||||
|
||||
// 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
|
||||
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
|
||||
|
||||
hooks.callAll('padInitToolbar', {
|
||||
hooks.callAll("padInitToolbar", {
|
||||
toolbar,
|
||||
isReadOnly,
|
||||
});
|
||||
|
||||
// can be removed when require-kernel is dropped
|
||||
res.header('Feature-Policy', 'sync-xhr \'self\'');
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', {
|
||||
res.header("Feature-Policy", "sync-xhr 'self'");
|
||||
res.send(
|
||||
eejs.require("ep_etherpad-lite/templates/pad.html", {
|
||||
req,
|
||||
toolbar,
|
||||
isReadOnly,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// serve timeslider.html under /p/$padname/timeslider
|
||||
args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => {
|
||||
hooks.callAll('padInitToolbar', {
|
||||
args.app.get("/p/:pad/timeslider", (req: any, res: any, next: Function) => {
|
||||
hooks.callAll("padInitToolbar", {
|
||||
toolbar,
|
||||
});
|
||||
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||
res.send(
|
||||
eejs.require("ep_etherpad-lite/templates/timeslider.html", {
|
||||
req,
|
||||
toolbar,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// 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.
|
||||
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.
|
||||
res.json({status: 'ok'});
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
return cb();
|
||||
|
|
|
@ -1,33 +1,39 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { MapArrayType } from "../../types/MapType";
|
||||
import { PartType } from "../../types/PartType";
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const minify = require('../../utils/Minify');
|
||||
const path = require('path');
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const settings = require('../../utils/Settings');
|
||||
import CachingMiddleware from '../../utils/caching_middleware';
|
||||
const Yajsml = require('etherpad-yajsml');
|
||||
const fs = require("fs").promises;
|
||||
const minify = require("../../utils/Minify");
|
||||
const path = require("path");
|
||||
const plugins = require("../../../static/js/pluginfw/plugin_defs");
|
||||
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.
|
||||
const getTar = async () => {
|
||||
const prefixLocalLibraryPath = (path: string) => {
|
||||
if (path.charAt(0) === '$') {
|
||||
if (path.charAt(0) === "$") {
|
||||
return path.slice(1);
|
||||
} else {
|
||||
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[]> = {};
|
||||
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);
|
||||
tar[prefixLocalLibraryPath(key)] = files
|
||||
.concat(files.map((p) => p.replace(/\.js$/, '')))
|
||||
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
|
||||
.concat(files.map((p) => p.replace(/\.js$/, "")))
|
||||
.concat(files.map((p) => `${p.replace(/\.js$/, "")}/index.js`));
|
||||
}
|
||||
return tar;
|
||||
};
|
||||
|
@ -41,21 +47,23 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
|
||||
// Minify will serve static files compressed (minify enabled). It also has
|
||||
// file-specific hacks for ace/require-kernel/etc.
|
||||
app.all('/static/:filename(*)', minify.minify);
|
||||
app.all("/static/:filename(*)", minify.minify);
|
||||
|
||||
// Setup middleware that will package JavaScript files served by minify for
|
||||
// CommonJS loader on the client-side.
|
||||
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
|
||||
const jsServer = new (Yajsml.Server)({
|
||||
rootPath: 'javascripts/src/',
|
||||
rootURI: 'http://invalid.invalid/static/js/',
|
||||
libraryPath: 'javascripts/lib/',
|
||||
libraryURI: 'http://invalid.invalid/static/plugins/',
|
||||
const jsServer = new Yajsml.Server({
|
||||
rootPath: "javascripts/src/",
|
||||
rootURI: "http://invalid.invalid/static/js/",
|
||||
libraryPath: "javascripts/lib/",
|
||||
libraryURI: "http://invalid.invalid/static/plugins/",
|
||||
requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround.
|
||||
});
|
||||
|
||||
const StaticAssociator = Yajsml.associators.StaticAssociator;
|
||||
const associations = Yajsml.associators.associationsForSimpleMapping(await getTar());
|
||||
const associations = Yajsml.associators.associationsForSimpleMapping(
|
||||
await getTar(),
|
||||
);
|
||||
const associator = new StaticAssociator(associations);
|
||||
jsServer.setAssociator(associator);
|
||||
|
||||
|
@ -64,18 +72,25 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
// serve plugin definitions
|
||||
// not very static, but served here so that client can do
|
||||
// require("pluginfw/static/js/plugin-definitions.js");
|
||||
app.get('/pluginfw/plugin-definitions.json', (req: any, res:any, next:Function) => {
|
||||
const clientParts = plugins.parts.filter((part: PartType) => part.client_hooks != null);
|
||||
app.get(
|
||||
"/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> = {};
|
||||
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
|
||||
clientPlugins[name] = { ...plugins.plugins[name] };
|
||||
// @ts-ignore
|
||||
delete clientPlugins[name].package;
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||
res.write(JSON.stringify({ plugins: clientPlugins, parts: clientParts }));
|
||||
res.end();
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { Dirent } from "node:fs";
|
||||
import { PluginDef } from "../../types/PartType";
|
||||
|
||||
const path = require('path');
|
||||
const fsp = require('fs').promises;
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const sanitizePathname = require('../../utils/sanitizePathname');
|
||||
const settings = require('../../utils/Settings');
|
||||
const path = require("path");
|
||||
const fsp = require("fs").promises;
|
||||
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 '/'
|
||||
// instead of path.sep to separate pathname components.
|
||||
|
@ -16,68 +16,88 @@ const findSpecs = async (specDir: string) => {
|
|||
try {
|
||||
dirents = await fsp.readdir(specDir, { withFileTypes: true });
|
||||
} catch (err: any) {
|
||||
if (['ENOENT', 'ENOTDIR'].includes(err.code)) return [];
|
||||
if (["ENOENT", "ENOTDIR"].includes(err.code)) return [];
|
||||
throw err;
|
||||
}
|
||||
const specs: string[] = [];
|
||||
await Promise.all(dirents.map(async (dirent) => {
|
||||
await Promise.all(
|
||||
dirents.map(async (dirent) => {
|
||||
if (dirent.isDirectory()) {
|
||||
const subdirSpecs = await findSpecs(path.join(specDir, dirent.name));
|
||||
specs.push(...subdirSpecs.map((spec) => `${dirent.name}/${spec}`));
|
||||
return;
|
||||
}
|
||||
if (!dirent.name.endsWith('.js')) return;
|
||||
if (!dirent.name.endsWith(".js")) return;
|
||||
specs.push(dirent.name);
|
||||
}));
|
||||
}),
|
||||
);
|
||||
return specs;
|
||||
};
|
||||
|
||||
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 () => {
|
||||
const modules: string[] = [];
|
||||
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
||||
let {package: {path: pluginPath}} = def as PluginDef;
|
||||
await Promise.all(
|
||||
Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
||||
let {
|
||||
package: { path: pluginPath },
|
||||
} = def as PluginDef;
|
||||
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
|
||||
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
|
||||
for (const spec of await findSpecs(path.join(pluginPath, specDir))) {
|
||||
if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests &&
|
||||
spec.startsWith('admin')) continue;
|
||||
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`);
|
||||
const specDir = `${
|
||||
plugin === "ep_etherpad-lite" ? "" : "static/"
|
||||
}tests/frontend/specs`;
|
||||
for (const spec of await findSpecs(
|
||||
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.
|
||||
modules.sort((a, b) => {
|
||||
a = String(a);
|
||||
b = String(b);
|
||||
const aCore = a.startsWith('ep_etherpad-lite/');
|
||||
const bCore = b.startsWith('ep_etherpad-lite/');
|
||||
const aCore = a.startsWith("ep_etherpad-lite/");
|
||||
const bCore = b.startsWith("ep_etherpad-lite/");
|
||||
if (aCore === bCore) return a.localeCompare(b);
|
||||
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);
|
||||
})().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) => {
|
||||
res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));
|
||||
app.get("/tests/frontend/index.html", (req: any, res: any) => {
|
||||
res.redirect(["./", ...req.url.split("?").slice(1)].join("?"));
|
||||
});
|
||||
|
||||
// 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
|
||||
// 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 () => {
|
||||
let file = sanitizePathname(req.params.file);
|
||||
if (['', '.', './'].includes(file)) file = 'index.html';
|
||||
if (["", ".", "./"].includes(file)) file = "index.html";
|
||||
res.sendFile(path.join(rootTestFolder, file));
|
||||
})().catch((err) => next(err || new Error(err)));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.get('/tests/frontend', (req:any, res:any) => {
|
||||
res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?'));
|
||||
app.get("/tests/frontend", (req: any, res: any) => {
|
||||
res.redirect(["./frontend/", ...req.url.split("?").slice(1)].join("?"));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,34 +1,42 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import { strict as assert } from "assert";
|
||||
import log4js from 'log4js';
|
||||
import log4js from "log4js";
|
||||
import { SocketClientRequest } from "../../types/SocketClientRequest";
|
||||
import { WebAccessTypes } from "../../types/WebAccessTypes";
|
||||
import { SettingsUser } from "../../types/SettingsUser";
|
||||
const httpLogger = log4js.getLogger('http');
|
||||
const settings = require('../../utils/Settings');
|
||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
||||
const httpLogger = log4js.getLogger("http");
|
||||
const settings = require("../../utils/Settings");
|
||||
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||
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.
|
||||
const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => {
|
||||
hooks.aCallFirst(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred);
|
||||
const aCallFirst = (hookName: string, context: any, pred = null) =>
|
||||
new Promise((resolve, reject) => {
|
||||
hooks.aCallFirst(
|
||||
hookName,
|
||||
context,
|
||||
(err: any, r: unknown) => (err != null ? reject(err) : resolve(r)),
|
||||
pred,
|
||||
);
|
||||
});
|
||||
|
||||
const aCallFirst0 =
|
||||
// @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) => {
|
||||
if (!level) return false;
|
||||
switch (level) {
|
||||
case true:
|
||||
return 'create';
|
||||
case 'readOnly':
|
||||
case 'modify':
|
||||
case 'create':
|
||||
return "create";
|
||||
case "readOnly":
|
||||
case "modify":
|
||||
case "create":
|
||||
return level;
|
||||
default:
|
||||
httpLogger.warn(`Unknown authorization level '${level}', denying access`);
|
||||
|
@ -39,18 +47,20 @@ exports.normalizeAuthzLevel = (level: string|boolean) => {
|
|||
exports.userCanModify = (padId: string, req: SocketClientRequest) => {
|
||||
if (readOnlyManager.isReadOnlyId(padId)) return false;
|
||||
if (!settings.requireAuthentication) return true;
|
||||
const {session: {user} = {}} = req;
|
||||
const {
|
||||
session: { user } = {},
|
||||
} = req;
|
||||
if (!user || user.readOnly) return false;
|
||||
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
|
||||
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.
|
||||
exports.authnFailureDelayMs = 1000;
|
||||
|
||||
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
|
||||
|
@ -60,19 +70,28 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
|
||||
let results: null | boolean[];
|
||||
let skip = false;
|
||||
const preAuthorizeNext = (...args:any) => { skip = true; next(...args); };
|
||||
const preAuthorizeNext = (...args: any) => {
|
||||
skip = true;
|
||||
next(...args);
|
||||
};
|
||||
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
|
||||
// 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.
|
||||
// This prevents plugin authors from accidentally granting admin privileges to the general
|
||||
// public.
|
||||
// @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) {
|
||||
httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`);
|
||||
if (!skip) res.status(500).send('Internal Server Error');
|
||||
httpLogger.error(
|
||||
`Error in preAuthorize hook: ${err.stack || err.toString()}`,
|
||||
);
|
||||
if (!skip) res.status(500).send("Internal Server Error");
|
||||
return;
|
||||
}
|
||||
if (skip) return;
|
||||
|
@ -84,9 +103,9 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
if (results.length > 0) {
|
||||
// Access was explicitly granted or denied. If any value is false then access is denied.
|
||||
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.
|
||||
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
|
||||
|
@ -112,13 +131,16 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
return true;
|
||||
};
|
||||
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;
|
||||
if (!requireAuthn) return await grant('create');
|
||||
if (!requireAuthn) return await grant("create");
|
||||
if (!isAuthenticated) return await grant(false);
|
||||
if (requireAdmin && !req.session.user.is_admin) return await grant(false);
|
||||
if (!settings.requireAuthorization) return await grant('create');
|
||||
return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path}));
|
||||
if (!settings.requireAuthorization) return await grant("create");
|
||||
return await grant(
|
||||
await aCallFirst0("authorize", { req, res, next, resource: req.path }),
|
||||
);
|
||||
};
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -128,8 +150,8 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
|
||||
if (await authorize()) {
|
||||
if (requireAdmin) {
|
||||
res.status(200).send('Authorized')
|
||||
return
|
||||
res.status(200).send("Authorized");
|
||||
return;
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
@ -146,35 +168,47 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
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
|
||||
// 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) {
|
||||
const userpass =
|
||||
Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':');
|
||||
const userpass = Buffer.from(
|
||||
req.headers.authorization.split(" ")[1],
|
||||
"base64",
|
||||
)
|
||||
.toString()
|
||||
.split(":");
|
||||
ctx.username = userpass.shift();
|
||||
// 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
|
||||
// problem unless the attacker can also set Object.prototype.password.
|
||||
if (ctx.username === '__proto__') ctx.username = null;
|
||||
ctx.password = userpass.join(':');
|
||||
if (ctx.username === "__proto__") ctx.username = null;
|
||||
ctx.password = userpass.join(":");
|
||||
}
|
||||
if (!(await aCallFirst0('authenticate', ctx))) {
|
||||
if (!(await aCallFirst0("authenticate", ctx))) {
|
||||
// Fall back to HTTP basic auth.
|
||||
// @ts-ignore
|
||||
const {[ctx.username]: {password} = {}} = settings.users as SettingsUser;
|
||||
const {
|
||||
[ctx.username]: { password } = {},
|
||||
} = settings.users as SettingsUser;
|
||||
|
||||
if (!httpBasicAuth ||
|
||||
if (
|
||||
!httpBasicAuth ||
|
||||
!ctx.username ||
|
||||
password == null || password.toString() !== ctx.password) {
|
||||
password == null ||
|
||||
password.toString() !== ctx.password
|
||||
) {
|
||||
httpLogger.info(`Failed authentication from IP ${req.ip}`);
|
||||
if (await aCallFirst0('authnFailure', {req, res})) return;
|
||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
||||
if (await aCallFirst0("authnFailure", { req, res })) return;
|
||||
if (await aCallFirst0("authFailure", { req, res, next })) return;
|
||||
// No plugin handled the authentication failure. Fall back to basic authentication.
|
||||
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.
|
||||
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
|
||||
res.status(401).send('Authentication Required');
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, exports.authnFailureDelayMs),
|
||||
);
|
||||
res.status(401).send("Authentication Required");
|
||||
return;
|
||||
}
|
||||
settings.users[ctx.username].username = ctx.username;
|
||||
|
@ -184,11 +218,15 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
delete req.session.user.password;
|
||||
}
|
||||
if (req.session.user == null) {
|
||||
httpLogger.error('authenticate hook failed to add user settings to session');
|
||||
return res.status(500).send('Internal Server Error');
|
||||
httpLogger.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;
|
||||
httpLogger.info(`Successful authentication from IP ${req.ip} for user ${username}`);
|
||||
const { username = "<no username>" } = req.session.user;
|
||||
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
|
||||
|
@ -196,17 +234,17 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
// a login page).
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const auth = await authorize()
|
||||
const auth = await authorize();
|
||||
if (auth && !requireAdmin) return next();
|
||||
if (auth && requireAdmin) {
|
||||
res.status(200).send('Authorized')
|
||||
return
|
||||
res.status(200).send("Authorized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await aCallFirst0('authzFailure', {req, res})) return;
|
||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
||||
if (await aCallFirst0("authzFailure", { req, res })) return;
|
||||
if (await aCallFirst0("authFailure", { req, res, next })) return;
|
||||
// No plugin handled the authorization failure.
|
||||
res.status(403).send('Forbidden');
|
||||
res.status(403).send("Forbidden");
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
import type { MapArrayType } from "../types/MapType";
|
||||
import { I18nPluginDefs } from "../types/I18nPluginDefs";
|
||||
|
||||
const languages = require('languages4translatewiki');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const _ = require('underscore');
|
||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
|
||||
const existsSync = require('../utils/path_exists');
|
||||
const settings = require('../utils/Settings');
|
||||
const languages = require("languages4translatewiki");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const _ = require("underscore");
|
||||
const pluginDefs = require("../../static/js/pluginfw/plugin_defs.js");
|
||||
const existsSync = require("../utils/path_exists");
|
||||
const settings = require("../utils/Settings");
|
||||
|
||||
// returns all existing messages merged together and grouped by langcode
|
||||
// {es: {"foo": "string"}, en:...}
|
||||
|
@ -32,7 +32,7 @@ const getAllLocales = () => {
|
|||
const ext = path.extname(file);
|
||||
const locale = path.basename(file, ext).toLowerCase();
|
||||
|
||||
if ((ext === '.json') && languages.isValid(locale)) {
|
||||
if (ext === ".json" && languages.isValid(locale)) {
|
||||
if (!locales2paths[locale]) locales2paths[locale] = [];
|
||||
locales2paths[locale].push(file);
|
||||
}
|
||||
|
@ -40,13 +40,15 @@ const getAllLocales = () => {
|
|||
};
|
||||
|
||||
// add core supported languages first
|
||||
extractLangs(path.join(settings.root, 'src/locales'));
|
||||
extractLangs(path.join(settings.root, "src/locales"));
|
||||
|
||||
// add plugins languages (if any)
|
||||
for (const {package: {path: pluginPath}} of Object.values<I18nPluginDefs>(pluginDefs.plugins)) {
|
||||
for (const {
|
||||
package: { path: pluginPath },
|
||||
} of Object.values<I18nPluginDefs>(pluginDefs.plugins)) {
|
||||
// plugin locales should overwrite etherpad's core locales
|
||||
if (pluginPath.endsWith('/ep_etherpad-lite')) continue;
|
||||
extractLangs(path.join(pluginPath, 'locales'));
|
||||
if (pluginPath.endsWith("/ep_etherpad-lite")) continue;
|
||||
extractLangs(path.join(pluginPath, "locales"));
|
||||
}
|
||||
|
||||
// Build a locale index (merge all locale data other than user-supplied overrides)
|
||||
|
@ -57,7 +59,7 @@ const getAllLocales = () => {
|
|||
files.forEach((file) => {
|
||||
let fileContents;
|
||||
try {
|
||||
fileContents = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
fileContents = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
} catch (err) {
|
||||
console.error(`failed to read JSON file ${file}: ${err}`);
|
||||
throw err;
|
||||
|
@ -69,35 +71,41 @@ const getAllLocales = () => {
|
|||
// Add custom strings from settings.json
|
||||
// Since this is user-supplied, we'll do some extra sanity checks
|
||||
const wrongFormatErr = Error(
|
||||
'customLocaleStrings in wrong format. See documentation ' +
|
||||
'for Customization for Administrators, under Localization.');
|
||||
"customLocaleStrings in wrong format. See documentation " +
|
||||
"for Customization for Administrators, under Localization.",
|
||||
);
|
||||
if (settings.customLocaleStrings) {
|
||||
if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr;
|
||||
_.each(settings.customLocaleStrings, (overrides:MapArrayType<string> , langcode:string) => {
|
||||
if (typeof overrides !== 'object') throw wrongFormatErr;
|
||||
if (typeof settings.customLocaleStrings !== "object") throw wrongFormatErr;
|
||||
_.each(
|
||||
settings.customLocaleStrings,
|
||||
(overrides: MapArrayType<string>, langcode: string) => {
|
||||
if (typeof overrides !== "object") throw wrongFormatErr;
|
||||
_.each(overrides, (localeString: string | object, key: string) => {
|
||||
if (typeof localeString !== 'string') throw wrongFormatErr;
|
||||
if (typeof localeString !== "string") throw wrongFormatErr;
|
||||
const locale = locales[langcode];
|
||||
|
||||
// Handles the error if an unknown language code is entered
|
||||
if (locale === undefined) {
|
||||
const possibleMatches = [];
|
||||
let strippedLangcode = '';
|
||||
if (langcode.includes('-')) {
|
||||
strippedLangcode = langcode.split('-')[0];
|
||||
let strippedLangcode = "";
|
||||
if (langcode.includes("-")) {
|
||||
strippedLangcode = langcode.split("-")[0];
|
||||
}
|
||||
for (const localeInEtherPad of Object.keys(locales)) {
|
||||
if (localeInEtherPad.includes(strippedLangcode)) {
|
||||
possibleMatches.push(localeInEtherPad);
|
||||
}
|
||||
}
|
||||
throw new Error(`Language code ${langcode} is unknown. ` +
|
||||
`Maybe you meant: ${possibleMatches}`);
|
||||
throw new Error(
|
||||
`Language code ${langcode} is unknown. ` +
|
||||
`Maybe you meant: ${possibleMatches}`,
|
||||
);
|
||||
}
|
||||
|
||||
locales[langcode][key] = localeString;
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return locales;
|
||||
|
@ -117,33 +125,32 @@ const getAvailableLangs = (locales:MapArrayType<any>) => {
|
|||
const generateLocaleIndex = (locales: MapArrayType<string>) => {
|
||||
const result = _.clone(locales); // keep English strings
|
||||
for (const langcode of Object.keys(locales)) {
|
||||
if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`;
|
||||
if (langcode !== "en") result[langcode] = `locales/${langcode}.json`;
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
};
|
||||
|
||||
|
||||
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||
// regenerate locales on server restart
|
||||
const locales = getAllLocales();
|
||||
const localeIndex = generateLocaleIndex(locales);
|
||||
exports.availableLangs = getAvailableLangs(locales);
|
||||
|
||||
app.get('/locales/:locale', (req:any, res:any) => {
|
||||
app.get("/locales/:locale", (req: any, res: any) => {
|
||||
// works with /locale/en and /locale/en.json requests
|
||||
const locale = req.params.locale.split('.')[0];
|
||||
const locale = req.params.locale.split(".")[0];
|
||||
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
|
||||
} else {
|
||||
res.status(404).send('Language not available');
|
||||
res.status(404).send("Language not available");
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/locales.json', (req: any, res:any) => {
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
app.get("/locales.json", (req: any, res: any) => {
|
||||
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.send(localeIndex);
|
||||
});
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue