mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13: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
|
specifier: ^0.9.2
|
||||||
version: 0.9.2
|
version: 0.9.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@biomejs/biome':
|
||||||
|
specifier: 1.7.0
|
||||||
|
version: 1.7.0
|
||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
specifier: ^1.43.1
|
specifier: ^1.43.1
|
||||||
version: 1.43.1
|
version: 1.43.1
|
||||||
|
@ -721,6 +724,94 @@ packages:
|
||||||
to-fast-properties: 2.0.0
|
to-fast-properties: 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@biomejs/biome@1.7.0:
|
||||||
|
resolution: {integrity: sha512-mejiRhnAq6UrXtYvjWJUKdstcT58n0/FfKemFf3d2Ou0HxOdS88HQmWtQ/UgyZvOEPD572YbFTb6IheyROpqkw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
hasBin: true
|
||||||
|
requiresBuild: true
|
||||||
|
optionalDependencies:
|
||||||
|
'@biomejs/cli-darwin-arm64': 1.7.0
|
||||||
|
'@biomejs/cli-darwin-x64': 1.7.0
|
||||||
|
'@biomejs/cli-linux-arm64': 1.7.0
|
||||||
|
'@biomejs/cli-linux-arm64-musl': 1.7.0
|
||||||
|
'@biomejs/cli-linux-x64': 1.7.0
|
||||||
|
'@biomejs/cli-linux-x64-musl': 1.7.0
|
||||||
|
'@biomejs/cli-win32-arm64': 1.7.0
|
||||||
|
'@biomejs/cli-win32-x64': 1.7.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@biomejs/cli-darwin-arm64@1.7.0:
|
||||||
|
resolution: {integrity: sha512-12TaeaKHU4SAZt0fQJ2bYk1jUb4foope7LmgDE5p3c0uMxd3mFkg1k7G721T+K6UHYULcSOQDsNNM8DhYi8Irg==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@biomejs/cli-darwin-x64@1.7.0:
|
||||||
|
resolution: {integrity: sha512-6Qq1BSIB0cpp0cQNqO/+EiUV7FE3jMpF6w7+AgIBXp0oJxUWb2Ff0RDZdO9bfzkimXD58j0vGpNHMGnCcjDV2Q==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@biomejs/cli-linux-arm64-musl@1.7.0:
|
||||||
|
resolution: {integrity: sha512-pwIY80nU7SAxrVVZ6HD9ah1pruwh9ZqlSR0Nvbg4ZJqQa0POhiB+RJx7+/1Ml2mTZdrl8kb/YiwQpD16uwb5wg==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@biomejs/cli-linux-arm64@1.7.0:
|
||||||
|
resolution: {integrity: sha512-GwSci7xBJ2j1CrdDXDUVXnUtrvypEz/xmiYPpFeVdlX5p95eXx+7FekPPbJfhGGw5WKSsKZ+V8AAlbN+kUwJWw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@biomejs/cli-linux-x64-musl@1.7.0:
|
||||||
|
resolution: {integrity: sha512-KzCA0mW4LSbCd7XZWaEJvTOTTBjfJoVEXkfq1fsXxww1HB+ww5PGMbhbIcbYCsj2CTJUifeD5hOkyuBVppU1xQ==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@biomejs/cli-linux-x64@1.7.0:
|
||||||
|
resolution: {integrity: sha512-1y+odKQsyHcw0JCGRuqhbx7Y6jxOVSh4lGIVDdJxW1b55yD22DY1kcMEfhUte6f95OIc2uqfkwtiI6xQAiZJdw==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@biomejs/cli-win32-arm64@1.7.0:
|
||||||
|
resolution: {integrity: sha512-AvLDUYZBpOUFgS/mni4VruIoVV3uSGbKSkZQBPXsHgL0w4KttLll3NBrVanmWxOHsom6C6ocHLyfAY8HUc8TXg==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@biomejs/cli-win32-x64@1.7.0:
|
||||||
|
resolution: {integrity: sha512-Pylm00BAAuLVb40IH9PC17432BTsY8K4pSUvhvgR1eaalnMaD6ug9SYJTTzKDbT6r24MPAGCTiSZERyhGkGzFQ==}
|
||||||
|
engines: {node: '>=14.21.3'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
/@docsearch/css@3.6.0:
|
/@docsearch/css@3.6.0:
|
||||||
resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==}
|
resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -1,138 +1,107 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
// This is a workaround for https://github.com/eslint/eslint/issues/3458
|
// This is a workaround for https://github.com/eslint/eslint/issues/3458
|
||||||
require('eslint-config-etherpad/patch/modern-module-resolution');
|
require("eslint-config-etherpad/patch/modern-module-resolution");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
'/static/js/vendors/browser.js',
|
"/static/js/vendors/browser.js",
|
||||||
'/static/js/vendors/farbtastic.js',
|
"/static/js/vendors/farbtastic.js",
|
||||||
'/static/js/vendors/gritter.js',
|
"/static/js/vendors/gritter.js",
|
||||||
'/static/js/vendors/html10n.js',
|
"/static/js/vendors/html10n.js",
|
||||||
'/static/js/vendors/jquery.js',
|
"/static/js/vendors/jquery.js",
|
||||||
'/static/js/vendors/nice-select.js',
|
"/static/js/vendors/nice-select.js",
|
||||||
'/tests/frontend/lib/',
|
"/tests/frontend/lib/",
|
||||||
],
|
],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: [
|
files: ["**/.eslintrc.*"],
|
||||||
'**/.eslintrc.*',
|
extends: "etherpad/node",
|
||||||
],
|
},
|
||||||
extends: 'etherpad/node',
|
{
|
||||||
|
files: ["**/*"],
|
||||||
|
excludedFiles: ["**/.eslintrc.*", "tests/frontend/**/*"],
|
||||||
|
extends: "etherpad/node",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
'**/*',
|
"static/**/*",
|
||||||
|
"tests/frontend/helper.js",
|
||||||
|
"tests/frontend/helper/**/*",
|
||||||
],
|
],
|
||||||
excludedFiles: [
|
excludedFiles: ["**/.eslintrc.*"],
|
||||||
'**/.eslintrc.*',
|
extends: "etherpad/browser",
|
||||||
'tests/frontend/**/*',
|
|
||||||
],
|
|
||||||
extends: 'etherpad/node',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'static/**/*',
|
|
||||||
'tests/frontend/helper.js',
|
|
||||||
'tests/frontend/helper/**/*',
|
|
||||||
],
|
|
||||||
excludedFiles: [
|
|
||||||
'**/.eslintrc.*',
|
|
||||||
],
|
|
||||||
extends: 'etherpad/browser',
|
|
||||||
env: {
|
env: {
|
||||||
'shared-node-browser': true,
|
"shared-node-browser": true,
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/frontend/helper/**/*"],
|
||||||
'tests/frontend/helper/**/*',
|
|
||||||
],
|
|
||||||
globals: {
|
globals: {
|
||||||
helper: 'readonly',
|
helper: "readonly",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/**/*"],
|
||||||
'tests/**/*',
|
|
||||||
],
|
|
||||||
excludedFiles: [
|
excludedFiles: [
|
||||||
'**/.eslintrc.*',
|
"**/.eslintrc.*",
|
||||||
'tests/frontend/cypress/**/*',
|
"tests/frontend/cypress/**/*",
|
||||||
'tests/frontend/helper.js',
|
"tests/frontend/helper.js",
|
||||||
'tests/frontend/helper/**/*',
|
"tests/frontend/helper/**/*",
|
||||||
'tests/frontend/travis/**/*',
|
"tests/frontend/travis/**/*",
|
||||||
'tests/ratelimit/**/*',
|
"tests/ratelimit/**/*",
|
||||||
],
|
],
|
||||||
extends: 'etherpad/tests',
|
extends: "etherpad/tests",
|
||||||
rules: {
|
rules: {
|
||||||
'mocha/no-exports': 'off',
|
"mocha/no-exports": "off",
|
||||||
'mocha/no-top-level-hooks': 'off',
|
"mocha/no-top-level-hooks": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/backend/**/*"],
|
||||||
'tests/backend/**/*',
|
excludedFiles: ["**/.eslintrc.*"],
|
||||||
],
|
extends: "etherpad/tests/backend",
|
||||||
excludedFiles: [
|
|
||||||
'**/.eslintrc.*',
|
|
||||||
],
|
|
||||||
extends: 'etherpad/tests/backend',
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/backend/**/*"],
|
||||||
'tests/backend/**/*',
|
excludedFiles: ["tests/backend/specs/**/*"],
|
||||||
],
|
|
||||||
excludedFiles: [
|
|
||||||
'tests/backend/specs/**/*',
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
'mocha/no-exports': 'off',
|
"mocha/no-exports": "off",
|
||||||
'mocha/no-top-level-hooks': 'off',
|
"mocha/no-top-level-hooks": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/frontend/**/*"],
|
||||||
'tests/frontend/**/*',
|
|
||||||
],
|
|
||||||
excludedFiles: [
|
excludedFiles: [
|
||||||
'**/.eslintrc.*',
|
"**/.eslintrc.*",
|
||||||
'tests/frontend/cypress/**/*',
|
"tests/frontend/cypress/**/*",
|
||||||
'tests/frontend/helper.js',
|
"tests/frontend/helper.js",
|
||||||
'tests/frontend/helper/**/*',
|
"tests/frontend/helper/**/*",
|
||||||
'tests/frontend/travis/**/*',
|
"tests/frontend/travis/**/*",
|
||||||
],
|
],
|
||||||
extends: 'etherpad/tests/frontend',
|
extends: "etherpad/tests/frontend",
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/frontend/**/*"],
|
||||||
'tests/frontend/**/*',
|
excludedFiles: ["tests/frontend/specs/**/*"],
|
||||||
],
|
|
||||||
excludedFiles: [
|
|
||||||
'tests/frontend/specs/**/*',
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
'mocha/no-exports': 'off',
|
"mocha/no-exports": "off",
|
||||||
'mocha/no-top-level-hooks': 'off',
|
"mocha/no-top-level-hooks": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/frontend/cypress/**/*"],
|
||||||
'tests/frontend/cypress/**/*',
|
extends: "etherpad/tests/cypress",
|
||||||
],
|
|
||||||
extends: 'etherpad/tests/cypress',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/frontend/travis/**/*"],
|
||||||
'tests/frontend/travis/**/*',
|
extends: "etherpad/node",
|
||||||
],
|
|
||||||
extends: 'etherpad/node',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
root: true,
|
root: true,
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Fwolff", "Naudefj"]
|
||||||
"Fwolff",
|
|
||||||
"Naudefj"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Nuwe pad",
|
"index.newPad": "Nuwe pad",
|
||||||
"index.createOpenPad": "of skep/open 'n pad met die naam:",
|
"index.createOpenPad": "of skep/open 'n pad met die naam:",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Xuacu", "YoaR"]
|
||||||
"Xuacu",
|
|
||||||
"YoaR"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Nuevu bloc",
|
"index.newPad": "Nuevu bloc",
|
||||||
"index.createOpenPad": "o crear/abrir un bloc col nome:",
|
"index.createOpenPad": "o crear/abrir un bloc col nome:",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["1AnuraagPandey", "बडा काजी"]
|
||||||
"1AnuraagPandey",
|
|
||||||
"बडा काजी"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "नयाँ प्याड",
|
"index.newPad": "नयाँ प्याड",
|
||||||
"pad.toolbar.bold.title": "मोट (Ctrl-B)",
|
"pad.toolbar.bold.title": "मोट (Ctrl-B)",
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Alp Er Tunqa", "Amir a57", "Ilğım", "Koroğlu", "Mousa"]
|
||||||
"Alp Er Tunqa",
|
|
||||||
"Amir a57",
|
|
||||||
"Ilğım",
|
|
||||||
"Koroğlu",
|
|
||||||
"Mousa"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "یئنی یادداشت دفترچه سی",
|
"index.newPad": "یئنی یادداشت دفترچه سی",
|
||||||
"index.createOpenPad": "یا دا ایجاد /بیر پد آدلا برابر آچماق:",
|
"index.createOpenPad": "یا دا ایجاد /بیر پد آدلا برابر آچماق:",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Baloch Afghanistan", "Moshtank", "Sultanselim baloch"]
|
||||||
"Baloch Afghanistan",
|
|
||||||
"Moshtank",
|
|
||||||
"Sultanselim baloch"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "کارمسترءِ کُرسی - اترپَد",
|
"admin.page-title": "کارمسترءِ کُرسی - اترپَد",
|
||||||
"admin_plugins": "گݔشانکانءِ کار ءُ بار",
|
"admin_plugins": "گݔشانکانءِ کار ءُ بار",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Jim-by", "Red Winged Duck", "Renessaince", "Wizardist"]
|
||||||
"Jim-by",
|
|
||||||
"Red Winged Duck",
|
|
||||||
"Renessaince",
|
|
||||||
"Wizardist"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Адміністрацыйная панэль — Etherpad",
|
"admin.page-title": "Адміністрацыйная панэль — Etherpad",
|
||||||
"admin_plugins": "Кіраўнік плагінаў",
|
"admin_plugins": "Кіраўнік плагінаў",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["StanProg", "Vlad5250", "Vodnokon4e"]
|
||||||
"StanProg",
|
|
||||||
"Vlad5250",
|
|
||||||
"Vodnokon4e"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Нов пад",
|
"index.newPad": "Нов пад",
|
||||||
"index.createOpenPad": "или създаване/отваряне на пад с име:",
|
"index.createOpenPad": "или създаване/отваряне на пад с име:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Baloch Afghanistan"]
|
||||||
"Baloch Afghanistan"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "یاداشتی نوکین کتابچه",
|
"index.newPad": "یاداشتی نوکین کتابچه",
|
||||||
"index.createOpenPad": "یا جوڑ\t کورتین/پاچ کورتین یک کتابچه ئی یاداشتی بی نام:",
|
"index.createOpenPad": "یا جوڑ\t کورتین/پاچ کورتین یک کتابچه ئی یاداشتی بی نام:",
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Fohanno", "Fulup", "Gwenn-Ael", "Huñvreüs", "Y-M D"]
|
||||||
"Fohanno",
|
|
||||||
"Fulup",
|
|
||||||
"Gwenn-Ael",
|
|
||||||
"Huñvreüs",
|
|
||||||
"Y-M D"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Pad nevez",
|
"index.newPad": "Pad nevez",
|
||||||
"index.createOpenPad": "pe krouiñ/digeriñ ur Pad gant an anv :",
|
"index.createOpenPad": "pe krouiñ/digeriñ ur Pad gant an anv :",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Edinwiki", "Semina x", "Srdjan m", "Srđan"]
|
||||||
"Edinwiki",
|
|
||||||
"Semina x",
|
|
||||||
"Srdjan m",
|
|
||||||
"Srđan"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Novi Pad",
|
"index.newPad": "Novi Pad",
|
||||||
"index.createOpenPad": "ili napravite/otvorite Pad sa imenom:",
|
"index.createOpenPad": "ili napravite/otvorite Pad sa imenom:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Michawiki"]
|
||||||
"Michawiki"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Administratorowa delka – Etherpad",
|
"admin.page-title": "Administratorowa delka – Etherpad",
|
||||||
"admin_plugins": "Zastojnik tykacow",
|
"admin_plugins": "Zastojnik tykacow",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Nirajan pant", "बडा काजी", "रमेश सिंह बोहरा", "राम प्रसाद जोशी"]
|
||||||
"Nirajan pant",
|
|
||||||
"बडा काजी",
|
|
||||||
"रमेश सिंह बोहरा",
|
|
||||||
"राम प्रसाद जोशी"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "नौलो प्याड",
|
"index.newPad": "नौलो प्याड",
|
||||||
"index.createOpenPad": "नाउँ सहितको नौलो प्याड सिर्जना गद्य्या / खोल्ल्या :",
|
"index.createOpenPad": "नाउँ सहितको नौलो प्याड सिर्जना गद्य्या / खोल्ल्या :",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Kristian.kankainen", "Tiblu"]
|
||||||
"Kristian.kankainen",
|
|
||||||
"Tiblu"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Uus klade",
|
"index.newPad": "Uus klade",
|
||||||
"index.createOpenPad": "loo või rööptoimeta kladet nimega:",
|
"index.createOpenPad": "loo või rööptoimeta kladet nimega:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Ibrahima Malal Sarr"]
|
||||||
"Ibrahima Malal Sarr"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Tiimtorde Jiiloowo - Etherpad",
|
"admin.page-title": "Tiimtorde Jiiloowo - Etherpad",
|
||||||
"admin_plugins": "Toppitorde Ceŋe",
|
"admin_plugins": "Toppitorde Ceŋe",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["EileenSanda"]
|
||||||
"EileenSanda"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Nýggjur teldil",
|
"index.newPad": "Nýggjur teldil",
|
||||||
"pad.toolbar.bold.title": "Við feitum (Ctrl-B)",
|
"pad.toolbar.bold.title": "Við feitum (Ctrl-B)",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Robin van der Vliet"]
|
||||||
"Robin van der Vliet"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"pad.toolbar.bold.title": "Fet (Ctrl+B)",
|
"pad.toolbar.bold.title": "Fet (Ctrl+B)",
|
||||||
"pad.toolbar.italic.title": "Kursyf (Ctrl+I)",
|
"pad.toolbar.italic.title": "Kursyf (Ctrl+I)",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Elisardojm", "Ghose", "Toliño"]
|
||||||
"Elisardojm",
|
|
||||||
"Ghose",
|
|
||||||
"Toliño"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Panel de administración - Etherpad",
|
"admin.page-title": "Panel de administración - Etherpad",
|
||||||
"admin_plugins": "Xestor de complementos",
|
"admin_plugins": "Xestor de complementos",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Bhatakati aatma", "Dsvyas", "Harsh4101991", "KartikMistry"]
|
||||||
"Bhatakati aatma",
|
|
||||||
"Dsvyas",
|
|
||||||
"Harsh4101991",
|
|
||||||
"KartikMistry"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "નવું પેડ",
|
"index.newPad": "નવું પેડ",
|
||||||
"pad.toolbar.bold.title": "બોલ્ડ",
|
"pad.toolbar.bold.title": "બોલ્ડ",
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Amire80", "Ofrahod", "Steeve815", "YaronSh", "תומר ט"]
|
||||||
"Amire80",
|
|
||||||
"Ofrahod",
|
|
||||||
"Steeve815",
|
|
||||||
"YaronSh",
|
|
||||||
"תומר ט"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "לוח ניהול - Etherpad",
|
"admin.page-title": "לוח ניהול - Etherpad",
|
||||||
"admin_plugins": "מנהל תוספים",
|
"admin_plugins": "מנהל תוספים",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Sfic"]
|
||||||
"Sfic"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"pad.toolbar.bold.title": "गहरा (Ctrl+B)",
|
"pad.toolbar.bold.title": "गहरा (Ctrl+B)",
|
||||||
"pad.toolbar.italic.title": "तिरछा (Ctrl+I)",
|
"pad.toolbar.italic.title": "तिरछा (Ctrl+I)",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Bugoslav", "Hmxhmx", "Ponor"]
|
||||||
"Bugoslav",
|
|
||||||
"Hmxhmx",
|
|
||||||
"Ponor"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Novi blokić",
|
"index.newPad": "Novi blokić",
|
||||||
"index.createOpenPad": "ili stvori/otvori blokić s imenom:",
|
"index.createOpenPad": "ili stvori/otvori blokić s imenom:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Paul Beppler"]
|
||||||
"Paul Beppler"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Neies Pad",
|
"index.newPad": "Neies Pad",
|
||||||
"index.createOpenPad": "Pad mit follichendem Noome uffmache:",
|
"index.createOpenPad": "Pad mit follichendem Noome uffmache:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Michawiki"]
|
||||||
"Michawiki"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Administratorowa deska – Etherpad",
|
"admin.page-title": "Administratorowa deska – Etherpad",
|
||||||
"admin_plugins": "Zrjadowak tykačow",
|
"admin_plugins": "Zrjadowak tykačow",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Armenoid", "Kareyac"]
|
||||||
"Armenoid",
|
|
||||||
"Kareyac"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins.available_install.value": "Տեղադրել",
|
"admin_plugins.available_install.value": "Տեղադրել",
|
||||||
"admin_plugins.description": "Նկարագրություն",
|
"admin_plugins.description": "Նկարագրություն",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["McDutchie"]
|
||||||
"McDutchie"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Pannello administrative – Etherpad",
|
"admin.page-title": "Pannello administrative – Etherpad",
|
||||||
"admin_plugins": "Gestor de plug-ins",
|
"admin_plugins": "Gestor de plug-ins",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Bennylin", "IvanLanin", "Marwan Mohamad", "Veracious"]
|
||||||
"Bennylin",
|
|
||||||
"IvanLanin",
|
|
||||||
"Marwan Mohamad",
|
|
||||||
"Veracious"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Dasbor Pengurus - Etherpad",
|
"admin.page-title": "Dasbor Pengurus - Etherpad",
|
||||||
"admin_plugins": "Manajer plugin",
|
"admin_plugins": "Manajer plugin",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Sveinki", "Sveinn í Felli"]
|
||||||
"Sveinki",
|
|
||||||
"Sveinn í Felli"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Stjórnborð fyrir stjórnendur - Etherpad",
|
"admin.page-title": "Stjórnborð fyrir stjórnendur - Etherpad",
|
||||||
"admin_plugins": "Stýring viðbóta",
|
"admin_plugins": "Stýring viðbóta",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Belkacem77"]
|
||||||
"Belkacem77"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Apad amaynut",
|
"index.newPad": "Apad amaynut",
|
||||||
"index.createOpenPad": "neɣ rnu/ldi apad s yisem:",
|
"index.createOpenPad": "neɣ rnu/ldi apad s yisem:",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Pichnat Thong", "Sovichet", "វ័ណថារិទ្ធ"]
|
||||||
"Pichnat Thong",
|
|
||||||
"Sovichet",
|
|
||||||
"វ័ណថារិទ្ធ"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "ផេតថ្មី",
|
"index.newPad": "ផេតថ្មី",
|
||||||
"index.createOpenPad": "ឬបង្កើត/បើកផេតដែលមានឈ្មោះ៖",
|
"index.createOpenPad": "ឬបង្កើត/បើកផេតដែលមានឈ្មោះ៖",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Nayvik", "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"]
|
||||||
"Nayvik",
|
|
||||||
"ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins.available": "ಲಭ್ಯವಿರುವ ಪ್ಲಗಿನ್ಗಳು",
|
"admin_plugins.available": "ಲಭ್ಯವಿರುವ ಪ್ಲಗಿನ್ಗಳು",
|
||||||
"admin_plugins.available_not-found": "ಯಾವುದೇ ಪ್ಲಗಿನ್ಗಳು ಸಿಗಲಿಲ್ಲ",
|
"admin_plugins.available_not-found": "ಯಾವುದೇ ಪ್ಲಗಿನ್ಗಳು ಸಿಗಲಿಲ್ಲ",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Ernác", "Къарачайлы"]
|
||||||
"Ernác",
|
|
||||||
"Къарачайлы"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Администраторну панели — Etherpad",
|
"admin.page-title": "Администраторну панели — Etherpad",
|
||||||
"admin_plugins": "Плагин менеджер",
|
"admin_plugins": "Плагин менеджер",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Purodha"]
|
||||||
"Purodha"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Neu Pädd",
|
"index.newPad": "Neu Pädd",
|
||||||
"index.createOpenPad": "udder maach e Pädd op med däm Nahme:",
|
"index.createOpenPad": "udder maach e Pädd op med däm Nahme:",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Gromper", "Robby", "Soued031", "Volvox"]
|
||||||
"Gromper",
|
|
||||||
"Robby",
|
|
||||||
"Soued031",
|
|
||||||
"Volvox"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins.available_install.value": "Installéieren",
|
"admin_plugins.available_install.value": "Installéieren",
|
||||||
"admin_plugins.description": "Beschreiwung",
|
"admin_plugins.description": "Beschreiwung",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Arash71", "Hosseinblue", "Lakzon"]
|
||||||
"Arash71",
|
|
||||||
"Hosseinblue",
|
|
||||||
"Lakzon"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "تازۀpad",
|
"index.newPad": "تازۀpad",
|
||||||
"index.createOpenPad": ":وە نۆم Pad یا سازین/واز کردن یإگلە",
|
"index.createOpenPad": ":وە نۆم Pad یا سازین/واز کردن یإگلە",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Lorestani", "Mogoeilor"]
|
||||||
"Lorestani",
|
|
||||||
"Mogoeilor"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "دٱفتٱرچٱ تازٱ",
|
"index.newPad": "دٱفتٱرچٱ تازٱ",
|
||||||
"pad.toolbar.bold.title": "تۊپور",
|
"pad.toolbar.bold.title": "تۊپور",
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Admresdeserv.", "Jmg.cmdi", "Oskars", "Papuass", "Silraks"]
|
||||||
"Admresdeserv.",
|
|
||||||
"Jmg.cmdi",
|
|
||||||
"Oskars",
|
|
||||||
"Papuass",
|
|
||||||
"Silraks"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Tiek izmantots kā pogas teksts. Blociņš, Etherpad kontekstā, ir piezīmju blociņš, uz kura rakstīt.",
|
"index.newPad": "Tiek izmantots kā pogas teksts. Blociņš, Etherpad kontekstā, ir piezīmju blociņš, uz kura rakstīt.",
|
||||||
"index.createOpenPad": "vai izveidojiet/atveriet Blociņu ar nosaukumu:",
|
"index.createOpenPad": "vai izveidojiet/atveriet Blociņu ar nosaukumu:",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Empu", "StefanusRA"]
|
||||||
"Empu",
|
|
||||||
"StefanusRA"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Pad Anyar",
|
"index.newPad": "Pad Anyar",
|
||||||
"index.createOpenPad": "utawa gawe/bukak Pad nganggo jeneng:",
|
"index.createOpenPad": "utawa gawe/bukak Pad nganggo jeneng:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Jagwar"]
|
||||||
"Jagwar"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Pad vaovao",
|
"index.newPad": "Pad vaovao",
|
||||||
"index.createOpenPad": "na hamorona/hanokatra Pad manana anarana:",
|
"index.createOpenPad": "na hamorona/hanokatra Pad manana anarana:",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Bjankuloski06", "Brest", "Vlad5250"]
|
||||||
"Bjankuloski06",
|
|
||||||
"Brest",
|
|
||||||
"Vlad5250"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Администраторска управувачница — Etherpad",
|
"admin.page-title": "Администраторска управувачница — Etherpad",
|
||||||
"admin_plugins": "Раководител со приклучоци",
|
"admin_plugins": "Раководител со приклучоци",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["MongolWiki", "Munkhzaya.E", "Wisdom"]
|
||||||
"MongolWiki",
|
|
||||||
"Munkhzaya.E",
|
|
||||||
"Wisdom"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"pad.toolbar.bold.title": "Болд тескт (Ctrl-B)",
|
"pad.toolbar.bold.title": "Болд тескт (Ctrl-B)",
|
||||||
"pad.toolbar.italic.title": "Налуу тескт (Ctrl-I)",
|
"pad.toolbar.italic.title": "Налуу тескт (Ctrl-I)",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Aue Nai", "咽頭べさ"]
|
||||||
"Aue Nai",
|
|
||||||
"咽頭べさ"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins.description": "တၚ်ထမံက်ထ္ၜး",
|
"admin_plugins.description": "တၚ်ထမံက်ထ္ၜး",
|
||||||
"index.newPad": "တၞးတၟိ",
|
"index.newPad": "တၞးတၟိ",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Ganeshgiram", "V.narsikar", "Ydyashad"]
|
||||||
"Ganeshgiram",
|
|
||||||
"V.narsikar",
|
|
||||||
"Ydyashad"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "नव पान",
|
"index.newPad": "नव पान",
|
||||||
"pad.toolbar.bold.title": "ठळक (Ctrl-B)",
|
"pad.toolbar.bold.title": "ठळक (Ctrl-B)",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Anakmalaysia", "Hakimi97", "Jeluang Terluang"]
|
||||||
"Anakmalaysia",
|
|
||||||
"Hakimi97",
|
|
||||||
"Jeluang Terluang"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Papan muka Penyelia - Etherpad",
|
"admin.page-title": "Papan muka Penyelia - Etherpad",
|
||||||
"index.newPad": "Pad baru",
|
"index.newPad": "Pad baru",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Andibecker", "Dr Lotus Black"]
|
||||||
"Andibecker",
|
|
||||||
"Dr Lotus Black"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad",
|
"admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad",
|
||||||
"admin_plugins": "ပလပ်အင်မန်နေဂျာ",
|
"admin_plugins": "ပလပ်အင်မန်နေဂျာ",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Akapochtli", "Languaeditor", "Taresi"]
|
||||||
"Akapochtli",
|
|
||||||
"Languaeditor",
|
|
||||||
"Taresi"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Yancuic Pad",
|
"index.newPad": "Yancuic Pad",
|
||||||
"index.createOpenPad": "auh xicchīhua/xictlapo cē Pad in ītōcā:",
|
"index.createOpenPad": "auh xicchīhua/xictlapo cē Pad in ītōcā:",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["C.R.", "Chelin", "Finizio", "Ruthven"]
|
||||||
"C.R.",
|
|
||||||
"Chelin",
|
|
||||||
"Finizio",
|
|
||||||
"Ruthven"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins.name": "Nomme",
|
"admin_plugins.name": "Nomme",
|
||||||
"index.newPad": "Nuovo Pad",
|
"index.newPad": "Nuovo Pad",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Gthoele", "Joachim Mos"]
|
||||||
"Gthoele",
|
|
||||||
"Joachim Mos"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Nee'et Pad",
|
"index.newPad": "Nee'et Pad",
|
||||||
"index.createOpenPad": "oder Pad mit düssen Naam apen maken:",
|
"index.createOpenPad": "oder Pad mit düssen Naam apen maken:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Unhammer"]
|
||||||
"Unhammer"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Ny blokk",
|
"index.newPad": "Ny blokk",
|
||||||
"index.createOpenPad": "eller opprett/opna ei blokk med namnet:",
|
"index.createOpenPad": "eller opprett/opna ei blokk med namnet:",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Cedric31", "Quentí"]
|
||||||
"Cedric31",
|
|
||||||
"Quentí"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Panèl d’administracion - Etherpad",
|
"admin.page-title": "Panèl d’administracion - Etherpad",
|
||||||
"admin_plugins": "Gestion de las extensions",
|
"admin_plugins": "Gestion de las extensions",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Denö", "Ilja.mos", "Mashoi7"]
|
||||||
"Denö",
|
|
||||||
"Ilja.mos",
|
|
||||||
"Mashoi7"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"pad.toolbar.underline.title": "Alleviivua (Ctrl+U)",
|
"pad.toolbar.underline.title": "Alleviivua (Ctrl+U)",
|
||||||
"pad.toolbar.settings.title": "Azetukset",
|
"pad.toolbar.settings.title": "Azetukset",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Bouron"]
|
||||||
"Bouron"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Ног",
|
"index.newPad": "Ног",
|
||||||
"index.createOpenPad": "кӕнӕ сараз/бакӕн ног документ ахӕм номимӕ:",
|
"index.createOpenPad": "кӕнӕ сараз/бакӕн ног документ ахӕм номимӕ:",
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Aalam", "Babanwalia", "Tow", "ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ", "ਪ੍ਰਚਾਰਕ"]
|
||||||
"Aalam",
|
|
||||||
"Babanwalia",
|
|
||||||
"Tow",
|
|
||||||
"ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ",
|
|
||||||
"ਪ੍ਰਚਾਰਕ"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "ਨਵਾਂ ਪੈਡ",
|
"index.newPad": "ਨਵਾਂ ਪੈਡ",
|
||||||
"index.createOpenPad": "ਜਾਂ ਨਾਂ ਨਾਲ ਨਵਾਂ ਪੈਡ ਬਣਾਓ/ਖੋਲ੍ਹੋ:",
|
"index.createOpenPad": "ਜਾਂ ਨਾਂ ਨਾਲ ਨਵਾਂ ਪੈਡ ਬਣਾਓ/ਖੋਲ੍ਹੋ:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Borichèt"]
|
||||||
"Borichèt"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Cruscòt d'aministrator - Etherpad",
|
"admin.page-title": "Cruscòt d'aministrator - Etherpad",
|
||||||
"admin_plugins": "Mansé dj'anstalassion",
|
"admin_plugins": "Mansé dj'anstalassion",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Ahmed-Najib-Biabani-Ibrahimkhel"]
|
||||||
"Ahmed-Najib-Biabani-Ibrahimkhel"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "نوې ليکچه",
|
"index.newPad": "نوې ليکچه",
|
||||||
"index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانيستل:",
|
"index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانيستل:",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Adr mm"]
|
||||||
"Adr mm"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Pannellu de amministratzione - Etherpad",
|
"admin.page-title": "Pannellu de amministratzione - Etherpad",
|
||||||
"admin_plugins": "Gestore de connetores",
|
"admin_plugins": "Gestore de connetores",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["BaRaN6161 TURK", "Kaleem Bhatti", "Mehtab ahmed", "Tweety"]
|
||||||
"BaRaN6161 TURK",
|
|
||||||
"Kaleem Bhatti",
|
|
||||||
"Mehtab ahmed",
|
|
||||||
"Tweety"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_settings": "ترتيبون",
|
"admin_settings": "ترتيبون",
|
||||||
"index.newPad": "نئين پٽي",
|
"index.newPad": "نئين پٽي",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Conquistador", "Vlad5250"]
|
||||||
"Conquistador",
|
|
||||||
"Vlad5250"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins.available_not-found": "Nijedan plugin nije pronađen.",
|
"admin_plugins.available_not-found": "Nijedan plugin nije pronađen.",
|
||||||
"admin_plugins.description": "Opis",
|
"admin_plugins.description": "Opis",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Ninjastrikers", "Saimawnkham", "Saosukham"]
|
||||||
"Ninjastrikers",
|
|
||||||
"Saimawnkham",
|
|
||||||
"Saosukham"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "ၽႅတ်ႉမႂ်ႇ",
|
"index.newPad": "ၽႅတ်ႉမႂ်ႇ",
|
||||||
"index.createOpenPad": "ဢမ်ႇၼၼ် ၶူင်မႂ်ႇ/ပိုတ်ႇၽႅတ်ႉၵိုၵ်းၸိုဝ်ႈ",
|
"index.createOpenPad": "ဢမ်ႇၼၼ် ၶူင်မႂ်ႇ/ပိုတ်ႇၽႅတ်ႉၵိုၵ်းၸိုဝ်ႈ",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Saraiki"]
|
||||||
"Saraiki"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins": "پلگ ان منیجر",
|
"admin_plugins": "پلگ ان منیجر",
|
||||||
"admin_plugins.available": "دستیاب پلگ ان",
|
"admin_plugins.available": "دستیاب پلگ ان",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Yupik"]
|
||||||
"Yupik"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin_plugins.description": "Deskriptt",
|
"admin_plugins.description": "Deskriptt",
|
||||||
"admin_plugins.name": "Nõmm",
|
"admin_plugins.name": "Nõmm",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Besnik b", "Eraldkerciku", "Kosovastar", "Liridon"]
|
||||||
"Besnik b",
|
|
||||||
"Eraldkerciku",
|
|
||||||
"Kosovastar",
|
|
||||||
"Liridon"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Pult Përgjegjësi - Etherpad",
|
"admin.page-title": "Pult Përgjegjësi - Etherpad",
|
||||||
"admin_plugins": "Përgjegjës shtojcash",
|
"admin_plugins": "Përgjegjës shtojcash",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Adr mm", "F Samaritani"]
|
||||||
"Adr mm",
|
|
||||||
"F Samaritani"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Pannellu amministrativu - Etherpad",
|
"admin.page-title": "Pannellu amministrativu - Etherpad",
|
||||||
"admin_plugins": "Gestore de connetores",
|
"admin_plugins": "Gestore de connetores",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Andibecker", "Edwingudfriend", "Muddyb"]
|
||||||
"Andibecker",
|
|
||||||
"Edwingudfriend",
|
|
||||||
"Muddyb"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "Dashibodi ya Usimamizi - Etherpad",
|
"admin.page-title": "Dashibodi ya Usimamizi - Etherpad",
|
||||||
"admin_plugins": "Meneja wa programu-jalizi",
|
"admin_plugins": "Meneja wa programu-jalizi",
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Balajijagadesh", "ElangoRamanujam", "Sank"]
|
||||||
"Balajijagadesh",
|
|
||||||
"ElangoRamanujam",
|
|
||||||
"Sank"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "புதிய அட்டை",
|
"index.newPad": "புதிய அட்டை",
|
||||||
"index.createOpenPad": "அல்லது பெயருடன் ஒரு அட்டையை உருவாக்கு/திற",
|
"index.createOpenPad": "அல்லது பெயருடன் ஒரு அட்டையை உருவாக்கு/திற",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["BHARATHESHA ALASANDEMAJALU", "VASANTH S.N."]
|
||||||
"BHARATHESHA ALASANDEMAJALU",
|
|
||||||
"VASANTH S.N."
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "ಪೊಸ ಪ್ಯಾಡ್",
|
"index.newPad": "ಪೊಸ ಪ್ಯಾಡ್",
|
||||||
"index.createOpenPad": "ಅತಂಡ ಈ ಪುದರ್ತ ಪ್ಯಾಡನ್ನು ಉಂಡು ಮನ್ಪು/ತೋಜಾಲ:",
|
"index.createOpenPad": "ಅತಂಡ ಈ ಪುದರ್ತ ಪ್ಯಾಡನ್ನು ಉಂಡು ಮನ್ಪು/ತೋಜಾಲ:",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Aefgh39622", "Andibecker", "Patsagorn Y.", "Trisorn Triboon"]
|
||||||
"Aefgh39622",
|
|
||||||
"Andibecker",
|
|
||||||
"Patsagorn Y.",
|
|
||||||
"Trisorn Triboon"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad",
|
"admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad",
|
||||||
"admin_plugins": "ตัวจัดการปลั๊กอิน",
|
"admin_plugins": "ตัวจัดการปลั๊กอิน",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
{
|
{
|
||||||
"@metadata": {
|
"@metadata": {
|
||||||
"authors": [
|
"authors": ["Fierodelveneto"]
|
||||||
"Fierodelveneto"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"index.newPad": "Novo Pad",
|
"index.newPad": "Novo Pad",
|
||||||
"index.createOpenPad": "O creare o verxare on Pad co'l nome:",
|
"index.createOpenPad": "O creare o verxare on Pad co'l nome:",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* This module provides all API functions
|
* This module provides all API functions
|
||||||
*/
|
*/
|
||||||
|
@ -19,21 +19,21 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Changeset = require('../../static/js/Changeset');
|
const Changeset = require("../../static/js/Changeset");
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
const ChatMessage = require("../../static/js/ChatMessage");
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const padManager = require('./PadManager');
|
const padManager = require("./PadManager");
|
||||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
const padMessageHandler = require("../handler/PadMessageHandler");
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
const readOnlyManager = require("./ReadOnlyManager");
|
||||||
const groupManager = require('./GroupManager');
|
const groupManager = require("./GroupManager");
|
||||||
const authorManager = require('./AuthorManager');
|
const authorManager = require("./AuthorManager");
|
||||||
const sessionManager = require('./SessionManager');
|
const sessionManager = require("./SessionManager");
|
||||||
const exportHtml = require('../utils/ExportHtml');
|
const exportHtml = require("../utils/ExportHtml");
|
||||||
const exportTxt = require('../utils/ExportTxt');
|
const exportTxt = require("../utils/ExportTxt");
|
||||||
const importHtml = require('../utils/ImportHtml');
|
const importHtml = require("../utils/ImportHtml");
|
||||||
const cleanText = require('./Pad').cleanText;
|
const cleanText = require("./Pad").cleanText;
|
||||||
const PadDiff = require('../utils/padDiff');
|
const PadDiff = require("../utils/padDiff");
|
||||||
const {checkValidRev, isInt} = require('../utils/checkValidRev');
|
const { checkValidRev, isInt } = require("../utils/checkValidRev");
|
||||||
|
|
||||||
/* ********************
|
/* ********************
|
||||||
* GROUP FUNCTIONS ****
|
* GROUP FUNCTIONS ****
|
||||||
|
@ -136,7 +136,10 @@ exports.getRevisionChangeset = async (padID: string, rev: string) => {
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
// check if this is a valid revision
|
// check if this is a valid revision
|
||||||
if (rev > head) {
|
if (rev > head) {
|
||||||
throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
|
throw new CustomError(
|
||||||
|
"rev is higher than the head revision of the pad",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the changeset for this revision
|
// get the changeset for this revision
|
||||||
|
@ -169,7 +172,10 @@ exports.getText = async (padID: string, rev: string) => {
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
// check if this is a valid revision
|
// check if this is a valid revision
|
||||||
if (rev > head) {
|
if (rev > head) {
|
||||||
throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
|
throw new CustomError(
|
||||||
|
"rev is higher than the head revision of the pad",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the text of this revision
|
// get the text of this revision
|
||||||
|
@ -200,10 +206,14 @@ Example returns:
|
||||||
* @param {String} authorId the id of the author, defaulting to empty string
|
* @param {String} authorId the id of the author, defaulting to empty string
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise<void> => {
|
exports.setText = async (
|
||||||
|
padID: string,
|
||||||
|
text?: string,
|
||||||
|
authorId: string = "",
|
||||||
|
): Promise<void> => {
|
||||||
// text is required
|
// text is required
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== "string") {
|
||||||
throw new CustomError('text is not a string', 'apierror');
|
throw new CustomError("text is not a string", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the pad
|
// get the pad
|
||||||
|
@ -225,10 +235,14 @@ Example returns:
|
||||||
@param {String} text the text of the pad
|
@param {String} text the text of the pad
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.appendText = async (padID:string, text?: string, authorId:string = '') => {
|
exports.appendText = async (
|
||||||
|
padID: string,
|
||||||
|
text?: string,
|
||||||
|
authorId: string = "",
|
||||||
|
) => {
|
||||||
// text is required
|
// text is required
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== "string") {
|
||||||
throw new CustomError('text is not a string', 'apierror');
|
throw new CustomError("text is not a string", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
|
@ -247,7 +261,10 @@ Example returns:
|
||||||
@param {String} rev the revision number, defaulting to the latest revision
|
@param {String} rev the revision number, defaulting to the latest revision
|
||||||
@return {Promise<{html: string}>} the html of the pad
|
@return {Promise<{html: string}>} the html of the pad
|
||||||
*/
|
*/
|
||||||
exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }> => {
|
exports.getHTML = async (
|
||||||
|
padID: string,
|
||||||
|
rev: string,
|
||||||
|
): Promise<{ html: string }> => {
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
rev = checkValidRev(rev);
|
rev = checkValidRev(rev);
|
||||||
}
|
}
|
||||||
|
@ -259,7 +276,10 @@ exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }>
|
||||||
// check if this is a valid revision
|
// check if this is a valid revision
|
||||||
const head = pad.getHeadRevisionNumber();
|
const head = pad.getHeadRevisionNumber();
|
||||||
if (rev > head) {
|
if (rev > head) {
|
||||||
throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
|
throw new CustomError(
|
||||||
|
"rev is higher than the head revision of the pad",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,10 +303,14 @@ Example returns:
|
||||||
@param {String} html the html of the pad
|
@param {String} html the html of the pad
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.setHTML = async (padID: string, html:string|object, authorId = '') => {
|
exports.setHTML = async (
|
||||||
|
padID: string,
|
||||||
|
html: string | object,
|
||||||
|
authorId = "",
|
||||||
|
) => {
|
||||||
// html string is required
|
// html string is required
|
||||||
if (typeof html !== 'string') {
|
if (typeof html !== "string") {
|
||||||
throw new CustomError('html is not a string', 'apierror');
|
throw new CustomError("html is not a string", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the pad
|
// get the pad
|
||||||
|
@ -296,7 +320,7 @@ exports.setHTML = async (padID: string, html:string|object, authorId = '') => {
|
||||||
try {
|
try {
|
||||||
await importHtml.setPadHTML(pad, cleanText(html), authorId);
|
await importHtml.setPadHTML(pad, cleanText(html), authorId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new CustomError('HTML is malformed', 'apierror');
|
throw new CustomError("HTML is malformed", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the clients on the pad
|
// update the clients on the pad
|
||||||
|
@ -327,13 +351,13 @@ Example returns:
|
||||||
exports.getChatHistory = async (padID: string, start: number, end: number) => {
|
exports.getChatHistory = async (padID: string, start: number, end: number) => {
|
||||||
if (start && end) {
|
if (start && end) {
|
||||||
if (start < 0) {
|
if (start < 0) {
|
||||||
throw new CustomError('start is below zero', 'apierror');
|
throw new CustomError("start is below zero", "apierror");
|
||||||
}
|
}
|
||||||
if (end < 0) {
|
if (end < 0) {
|
||||||
throw new CustomError('end is below zero', 'apierror');
|
throw new CustomError("end is below zero", "apierror");
|
||||||
}
|
}
|
||||||
if (start > end) {
|
if (start > end) {
|
||||||
throw new CustomError('start is higher than end', 'apierror');
|
throw new CustomError("start is higher than end", "apierror");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,10 +373,16 @@ exports.getChatHistory = async (padID: string, start:number, end:number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start > chatHead) {
|
if (start > chatHead) {
|
||||||
throw new CustomError('start is higher than the current chatHead', 'apierror');
|
throw new CustomError(
|
||||||
|
"start is higher than the current chatHead",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (end > chatHead) {
|
if (end > chatHead) {
|
||||||
throw new CustomError('end is higher than the current chatHead', 'apierror');
|
throw new CustomError(
|
||||||
|
"end is higher than the current chatHead",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// the whole message-log and return it to the client
|
// the whole message-log and return it to the client
|
||||||
|
@ -374,10 +404,15 @@ Example returns:
|
||||||
@param {String} authorID the id of the author
|
@param {String} authorID the id of the author
|
||||||
@param {Number} time the timestamp of the chat-message
|
@param {Number} time the timestamp of the chat-message
|
||||||
*/
|
*/
|
||||||
exports.appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => {
|
exports.appendChatMessage = async (
|
||||||
|
padID: string,
|
||||||
|
text: string | object,
|
||||||
|
authorID: string,
|
||||||
|
time: number,
|
||||||
|
) => {
|
||||||
// text is required
|
// text is required
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== "string") {
|
||||||
throw new CustomError('text is not a string', 'apierror');
|
throw new CustomError("text is not a string", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if time is not an integer value set time to current timestamp
|
// if time is not an integer value set time to current timestamp
|
||||||
|
@ -388,7 +423,10 @@ exports.appendChatMessage = async (padID: string, text: string|object, authorID:
|
||||||
// @TODO - missing getPadSafe() call ?
|
// @TODO - missing getPadSafe() call ?
|
||||||
|
|
||||||
// save chat message to database and send message to all connected clients
|
// save chat message to database and send message to all connected clients
|
||||||
await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID);
|
await padMessageHandler.sendChatMessageToPadClients(
|
||||||
|
new ChatMessage(text, authorID, time),
|
||||||
|
padID,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ***************
|
/* ***************
|
||||||
|
@ -463,14 +501,17 @@ exports.saveRevision = async (padID: string, rev: number) => {
|
||||||
// the client asked for a special revision
|
// the client asked for a special revision
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
if (rev > head) {
|
if (rev > head) {
|
||||||
throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
|
throw new CustomError(
|
||||||
|
"rev is higher than the head revision of the pad",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rev = pad.getHeadRevisionNumber();
|
rev = pad.getHeadRevisionNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
const author = await authorManager.createAuthor('API');
|
const author = await authorManager.createAuthor("API");
|
||||||
await pad.addSavedRevision(rev, author.authorID, 'Saved through API call');
|
await pad.addSavedRevision(rev, author.authorID, "Saved through API call");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -483,7 +524,9 @@ Example returns:
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
@return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad
|
@return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad
|
||||||
*/
|
*/
|
||||||
exports.getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => {
|
exports.getLastEdited = async (
|
||||||
|
padID: string,
|
||||||
|
): Promise<{ lastEdited: number }> => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
const lastEdited = await pad.getLastEdit();
|
const lastEdited = await pad.getLastEdit();
|
||||||
|
@ -501,16 +544,19 @@ Example returns:
|
||||||
@param {String} text the initial text of the pad
|
@param {String} text the initial text of the pad
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.createPad = async (padID: string, text: string, authorId = '') => {
|
exports.createPad = async (padID: string, text: string, authorId = "") => {
|
||||||
if (padID) {
|
if (padID) {
|
||||||
// ensure there is no $ in the padID
|
// ensure there is no $ in the padID
|
||||||
if (padID.indexOf('$') !== -1) {
|
if (padID.indexOf("$") !== -1) {
|
||||||
throw new CustomError("createPad can't create group pads", 'apierror');
|
throw new CustomError("createPad can't create group pads", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for url special characters
|
// check for url special characters
|
||||||
if (padID.match(/(\/|\?|&|#)/)) {
|
if (padID.match(/(\/|\?|&|#)/)) {
|
||||||
throw new CustomError('malformed padID: Remove special characters', 'apierror');
|
throw new CustomError(
|
||||||
|
"malformed padID: Remove special characters",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,10 +589,10 @@ exports.deletePad = async (padID: string) => {
|
||||||
@param {Number} rev the revision number, defaulting to the latest revision
|
@param {Number} rev the revision number, defaulting to the latest revision
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
exports.restoreRevision = async (padID: string, rev: number, authorId = "") => {
|
||||||
// check if rev is a number
|
// check if rev is a number
|
||||||
if (rev === undefined) {
|
if (rev === undefined) {
|
||||||
throw new CustomError('rev is not defined', 'apierror');
|
throw new CustomError("rev is not defined", "apierror");
|
||||||
}
|
}
|
||||||
rev = checkValidRev(rev);
|
rev = checkValidRev(rev);
|
||||||
|
|
||||||
|
@ -555,13 +601,16 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
|
|
||||||
// check if this is a valid revision
|
// check if this is a valid revision
|
||||||
if (rev > pad.getHeadRevisionNumber()) {
|
if (rev > pad.getHeadRevisionNumber()) {
|
||||||
throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
|
throw new CustomError(
|
||||||
|
"rev is higher than the head revision of the pad",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const atext = await pad.getInternalRevisionAText(rev);
|
const atext = await pad.getInternalRevisionAText(rev);
|
||||||
|
|
||||||
const oldText = pad.text();
|
const oldText = pad.text();
|
||||||
atext.text += '\n';
|
atext.text += "\n";
|
||||||
|
|
||||||
const eachAttribRun = (attribs: string[], func: Function) => {
|
const eachAttribRun = (attribs: string[], func: Function) => {
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
|
@ -570,7 +619,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
for (const op of Changeset.deserializeOps(attribs)) {
|
for (const op of Changeset.deserializeOps(attribs)) {
|
||||||
const nextIndex = textIndex + op.chars;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
func(
|
||||||
|
Math.max(newTextStart, textIndex),
|
||||||
|
Math.min(newTextEnd, nextIndex),
|
||||||
|
op.attribs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
textIndex = nextIndex;
|
textIndex = nextIndex;
|
||||||
}
|
}
|
||||||
|
@ -580,11 +633,14 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
const builder = Changeset.builder(oldText.length);
|
const builder = Changeset.builder(oldText.length);
|
||||||
|
|
||||||
// assemble each line into the builder
|
// assemble each line into the builder
|
||||||
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
|
eachAttribRun(
|
||||||
|
atext.attribs,
|
||||||
|
(start: number, end: number, attribs: string[]) => {
|
||||||
builder.insert(atext.text.substring(start, end), attribs);
|
builder.insert(atext.text.substring(start, end), attribs);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const lastNewlinePos = oldText.lastIndexOf('\n');
|
const lastNewlinePos = oldText.lastIndexOf("\n");
|
||||||
if (lastNewlinePos < 0) {
|
if (lastNewlinePos < 0) {
|
||||||
builder.remove(oldText.length - 1, 0);
|
builder.remove(oldText.length - 1, 0);
|
||||||
} else {
|
} else {
|
||||||
|
@ -610,7 +666,11 @@ Example returns:
|
||||||
@param {String} destinationID the id of the destination pad
|
@param {String} destinationID the id of the destination pad
|
||||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||||
*/
|
*/
|
||||||
exports.copyPad = async (sourceID: string, destinationID: string, force: boolean) => {
|
exports.copyPad = async (
|
||||||
|
sourceID: string,
|
||||||
|
destinationID: string,
|
||||||
|
force: boolean,
|
||||||
|
) => {
|
||||||
const pad = await getPadSafe(sourceID, true);
|
const pad = await getPadSafe(sourceID, true);
|
||||||
await pad.copy(destinationID, force);
|
await pad.copy(destinationID, force);
|
||||||
};
|
};
|
||||||
|
@ -628,7 +688,12 @@ Example returns:
|
||||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId = '') => {
|
exports.copyPadWithoutHistory = async (
|
||||||
|
sourceID: string,
|
||||||
|
destinationID: string,
|
||||||
|
force: boolean,
|
||||||
|
authorId = "",
|
||||||
|
) => {
|
||||||
const pad = await getPadSafe(sourceID, true);
|
const pad = await getPadSafe(sourceID, true);
|
||||||
await pad.copyPadWithoutHistory(destinationID, force, authorId);
|
await pad.copyPadWithoutHistory(destinationID, force, authorId);
|
||||||
};
|
};
|
||||||
|
@ -645,7 +710,11 @@ Example returns:
|
||||||
@param {String} destinationID the id of the destination pad
|
@param {String} destinationID the id of the destination pad
|
||||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||||
*/
|
*/
|
||||||
exports.movePad = async (sourceID: string, destinationID: string, force:boolean) => {
|
exports.movePad = async (
|
||||||
|
sourceID: string,
|
||||||
|
destinationID: string,
|
||||||
|
force: boolean,
|
||||||
|
) => {
|
||||||
const pad = await getPadSafe(sourceID, true);
|
const pad = await getPadSafe(sourceID, true);
|
||||||
await pad.copy(destinationID, force);
|
await pad.copy(destinationID, force);
|
||||||
await pad.remove();
|
await pad.remove();
|
||||||
|
@ -683,7 +752,7 @@ exports.getPadID = async (roID: string) => {
|
||||||
// get the PadId
|
// get the PadId
|
||||||
const padID = await readOnlyManager.getPadId(roID);
|
const padID = await readOnlyManager.getPadId(roID);
|
||||||
if (padID == null) {
|
if (padID == null) {
|
||||||
throw new CustomError('padID does not exist', 'apierror');
|
throw new CustomError("padID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { padID };
|
return { padID };
|
||||||
|
@ -699,16 +768,19 @@ Example returns:
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
@param {Boolean} publicStatus the public status of the pad
|
@param {Boolean} publicStatus the public status of the pad
|
||||||
*/
|
*/
|
||||||
exports.setPublicStatus = async (padID: string, publicStatus: boolean|string) => {
|
exports.setPublicStatus = async (
|
||||||
|
padID: string,
|
||||||
|
publicStatus: boolean | string,
|
||||||
|
) => {
|
||||||
// ensure this is a group pad
|
// ensure this is a group pad
|
||||||
checkGroupPad(padID, 'publicStatus');
|
checkGroupPad(padID, "publicStatus");
|
||||||
|
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
|
|
||||||
// convert string to boolean
|
// convert string to boolean
|
||||||
if (typeof publicStatus === 'string') {
|
if (typeof publicStatus === "string") {
|
||||||
publicStatus = (publicStatus.toLowerCase() === 'true');
|
publicStatus = publicStatus.toLowerCase() === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
await pad.setPublicStatus(publicStatus);
|
await pad.setPublicStatus(publicStatus);
|
||||||
|
@ -725,7 +797,7 @@ Example returns:
|
||||||
*/
|
*/
|
||||||
exports.getPublicStatus = async (padID: string) => {
|
exports.getPublicStatus = async (padID: string) => {
|
||||||
// ensure this is a group pad
|
// ensure this is a group pad
|
||||||
checkGroupPad(padID, 'publicStatus');
|
checkGroupPad(padID, "publicStatus");
|
||||||
|
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
|
@ -786,8 +858,7 @@ Example returns:
|
||||||
{"code":0,"message":"ok","data":null}
|
{"code":0,"message":"ok","data":null}
|
||||||
{"code":4,"message":"no or wrong API Key","data":null}
|
{"code":4,"message":"no or wrong API Key","data":null}
|
||||||
*/
|
*/
|
||||||
exports.checkToken = async () => {
|
exports.checkToken = async () => {};
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
getChatHead(padID) returns the chatHead (last number of the last chat-message) of the pad
|
getChatHead(padID) returns the chatHead (last number of the last chat-message) of the pad
|
||||||
|
@ -799,7 +870,7 @@ Example returns:
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
@return {Promise<{chatHead: number}>} the chatHead of the pad
|
@return {Promise<{chatHead: number}>} the chatHead of the pad
|
||||||
*/
|
*/
|
||||||
exports.getChatHead = async (padID:string): Promise<{ chatHead: number; }> => {
|
exports.getChatHead = async (padID: string): Promise<{ chatHead: number }> => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
return { chatHead: pad.chatHead };
|
return { chatHead: pad.chatHead };
|
||||||
|
@ -825,7 +896,11 @@ Example returns:
|
||||||
@param {Number} startRev the start revision number
|
@param {Number} startRev the start revision number
|
||||||
@param {Number} endRev the end revision number
|
@param {Number} endRev the end revision number
|
||||||
*/
|
*/
|
||||||
exports.createDiffHTML = async (padID: string, startRev: number, endRev: number) => {
|
exports.createDiffHTML = async (
|
||||||
|
padID: string,
|
||||||
|
startRev: number,
|
||||||
|
endRev: number,
|
||||||
|
) => {
|
||||||
// check if startRev is a number
|
// check if startRev is a number
|
||||||
if (startRev !== undefined) {
|
if (startRev !== undefined) {
|
||||||
startRev = checkValidRev(startRev);
|
startRev = checkValidRev(startRev);
|
||||||
|
@ -873,7 +948,9 @@ exports.getStats = async () => {
|
||||||
|
|
||||||
const sessionKeys = Object.keys(sessionInfos);
|
const sessionKeys = Object.keys(sessionInfos);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId));
|
const activePads = new Set(
|
||||||
|
Object.entries(sessionInfos).map((k) => k[1].padId),
|
||||||
|
);
|
||||||
|
|
||||||
const { padIDs } = await padManager.listAllPads();
|
const { padIDs } = await padManager.listAllPads();
|
||||||
|
|
||||||
|
@ -889,15 +966,20 @@ exports.getStats = async () => {
|
||||||
**************************** */
|
**************************** */
|
||||||
|
|
||||||
// gets a pad safe
|
// gets a pad safe
|
||||||
const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:string, authorId:string = '') => {
|
const getPadSafe = async (
|
||||||
|
padID: string | object,
|
||||||
|
shouldExist: boolean,
|
||||||
|
text?: string,
|
||||||
|
authorId: string = "",
|
||||||
|
) => {
|
||||||
// check if padID is a string
|
// check if padID is a string
|
||||||
if (typeof padID !== 'string') {
|
if (typeof padID !== "string") {
|
||||||
throw new CustomError('padID is not a string', 'apierror');
|
throw new CustomError("padID is not a string", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the padID maches the requirements
|
// check if the padID maches the requirements
|
||||||
if (!padManager.isValidPadId(padID)) {
|
if (!padManager.isValidPadId(padID)) {
|
||||||
throw new CustomError('padID did not match requirements', 'apierror');
|
throw new CustomError("padID did not match requirements", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the pad exists
|
// check if the pad exists
|
||||||
|
@ -905,12 +987,12 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri
|
||||||
|
|
||||||
if (!exists && shouldExist) {
|
if (!exists && shouldExist) {
|
||||||
// does not exist, but should
|
// does not exist, but should
|
||||||
throw new CustomError('padID does not exist', 'apierror');
|
throw new CustomError("padID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exists && !shouldExist) {
|
if (exists && !shouldExist) {
|
||||||
// does exist, but shouldn't
|
// does exist, but shouldn't
|
||||||
throw new CustomError('padID does already exist', 'apierror');
|
throw new CustomError("padID does already exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// pad exists, let's get it
|
// pad exists, let's get it
|
||||||
|
@ -920,8 +1002,10 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri
|
||||||
// checks if a padID is part of a group
|
// checks if a padID is part of a group
|
||||||
const checkGroupPad = (padID: string, field: string) => {
|
const checkGroupPad = (padID: string, field: string) => {
|
||||||
// ensure this is a group pad
|
// ensure this is a group pad
|
||||||
if (padID && padID.indexOf('$') === -1) {
|
if (padID && padID.indexOf("$") === -1) {
|
||||||
throw new CustomError(
|
throw new CustomError(
|
||||||
`You can only get/set the ${field} of pads that belong to a group`, 'apierror');
|
`You can only get/set the ${field} of pads that belong to a group`,
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* The AuthorManager controlls all information about the Pad authors
|
* The AuthorManager controlls all information about the Pad authors
|
||||||
*/
|
*/
|
||||||
|
@ -19,76 +19,79 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
const {
|
||||||
|
randomString,
|
||||||
|
padutils: { warnDeprecated },
|
||||||
|
} = require("../../static/js/pad_utils");
|
||||||
|
|
||||||
exports.getColorPalette = () => [
|
exports.getColorPalette = () => [
|
||||||
'#ffc7c7',
|
"#ffc7c7",
|
||||||
'#fff1c7',
|
"#fff1c7",
|
||||||
'#e3ffc7',
|
"#e3ffc7",
|
||||||
'#c7ffd5',
|
"#c7ffd5",
|
||||||
'#c7ffff',
|
"#c7ffff",
|
||||||
'#c7d5ff',
|
"#c7d5ff",
|
||||||
'#e3c7ff',
|
"#e3c7ff",
|
||||||
'#ffc7f1',
|
"#ffc7f1",
|
||||||
'#ffa8a8',
|
"#ffa8a8",
|
||||||
'#ffe699',
|
"#ffe699",
|
||||||
'#cfff9e',
|
"#cfff9e",
|
||||||
'#99ffb3',
|
"#99ffb3",
|
||||||
'#a3ffff',
|
"#a3ffff",
|
||||||
'#99b3ff',
|
"#99b3ff",
|
||||||
'#cc99ff',
|
"#cc99ff",
|
||||||
'#ff99e5',
|
"#ff99e5",
|
||||||
'#e7b1b1',
|
"#e7b1b1",
|
||||||
'#e9dcAf',
|
"#e9dcAf",
|
||||||
'#cde9af',
|
"#cde9af",
|
||||||
'#bfedcc',
|
"#bfedcc",
|
||||||
'#b1e7e7',
|
"#b1e7e7",
|
||||||
'#c3cdee',
|
"#c3cdee",
|
||||||
'#d2b8ea',
|
"#d2b8ea",
|
||||||
'#eec3e6',
|
"#eec3e6",
|
||||||
'#e9cece',
|
"#e9cece",
|
||||||
'#e7e0ca',
|
"#e7e0ca",
|
||||||
'#d3e5c7',
|
"#d3e5c7",
|
||||||
'#bce1c5',
|
"#bce1c5",
|
||||||
'#c1e2e2',
|
"#c1e2e2",
|
||||||
'#c1c9e2',
|
"#c1c9e2",
|
||||||
'#cfc1e2',
|
"#cfc1e2",
|
||||||
'#e0bdd9',
|
"#e0bdd9",
|
||||||
'#baded3',
|
"#baded3",
|
||||||
'#a0f8eb',
|
"#a0f8eb",
|
||||||
'#b1e7e0',
|
"#b1e7e0",
|
||||||
'#c3c8e4',
|
"#c3c8e4",
|
||||||
'#cec5e2',
|
"#cec5e2",
|
||||||
'#b1d5e7',
|
"#b1d5e7",
|
||||||
'#cda8f0',
|
"#cda8f0",
|
||||||
'#f0f0a8',
|
"#f0f0a8",
|
||||||
'#f2f2a6',
|
"#f2f2a6",
|
||||||
'#f5a8eb',
|
"#f5a8eb",
|
||||||
'#c5f9a9',
|
"#c5f9a9",
|
||||||
'#ececbb',
|
"#ececbb",
|
||||||
'#e7c4bc',
|
"#e7c4bc",
|
||||||
'#daf0b2',
|
"#daf0b2",
|
||||||
'#b0a0fd',
|
"#b0a0fd",
|
||||||
'#bce2e7',
|
"#bce2e7",
|
||||||
'#cce2bb',
|
"#cce2bb",
|
||||||
'#ec9afe',
|
"#ec9afe",
|
||||||
'#edabbd',
|
"#edabbd",
|
||||||
'#aeaeea',
|
"#aeaeea",
|
||||||
'#c4e7b1',
|
"#c4e7b1",
|
||||||
'#d722bb',
|
"#d722bb",
|
||||||
'#f3a5e7',
|
"#f3a5e7",
|
||||||
'#ffa8a8',
|
"#ffa8a8",
|
||||||
'#d8c0c5',
|
"#d8c0c5",
|
||||||
'#eaaedd',
|
"#eaaedd",
|
||||||
'#adc6eb',
|
"#adc6eb",
|
||||||
'#bedad1',
|
"#bedad1",
|
||||||
'#dee9af',
|
"#dee9af",
|
||||||
'#e9afc2',
|
"#e9afc2",
|
||||||
'#f8d2a0',
|
"#f8d2a0",
|
||||||
'#b3b3e6',
|
"#b3b3e6",
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,7 +110,6 @@ exports.doesAuthorExist = async (authorID: string) => {
|
||||||
*/
|
*/
|
||||||
exports.doesAuthorExists = exports.doesAuthorExist;
|
exports.doesAuthorExists = exports.doesAuthorExist;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the AuthorID for a mapper. We can map using a mapperkey,
|
* Returns the AuthorID for a mapper. We can map using a mapperkey,
|
||||||
* so far this is token2author and mapper2author
|
* so far this is token2author and mapper2author
|
||||||
|
@ -131,7 +133,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
|
||||||
|
|
||||||
// there is an author with this mapper
|
// there is an author with this mapper
|
||||||
// update the timestamp of this author
|
// update the timestamp of this author
|
||||||
await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now());
|
await db.setSub(`globalAuthor:${author}`, ["timestamp"], Date.now());
|
||||||
|
|
||||||
// return the author
|
// return the author
|
||||||
return { authorID: author };
|
return { authorID: author };
|
||||||
|
@ -143,7 +145,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
|
||||||
* @return {Promise<string|*|{authorID: string}|{authorID: *}>}
|
* @return {Promise<string|*|{authorID: string}|{authorID: *}>}
|
||||||
*/
|
*/
|
||||||
const getAuthor4Token = async (token: string) => {
|
const getAuthor4Token = async (token: string) => {
|
||||||
const author = await mapAuthorWithDBKey('token2author', token);
|
const author = await mapAuthorWithDBKey("token2author", token);
|
||||||
|
|
||||||
// return only the sub value authorID
|
// return only the sub value authorID
|
||||||
return author ? author.authorID : author;
|
return author ? author.authorID : author;
|
||||||
|
@ -157,7 +159,7 @@ const getAuthor4Token = async (token: string) => {
|
||||||
*/
|
*/
|
||||||
exports.getAuthorId = async (token: string, user: object) => {
|
exports.getAuthorId = async (token: string, user: object) => {
|
||||||
const context = { dbKey: token, token, user };
|
const context = { dbKey: token, token, user };
|
||||||
let [authorId] = await hooks.aCallFirst('getAuthorId', context);
|
let [authorId] = await hooks.aCallFirst("getAuthorId", context);
|
||||||
if (!authorId) authorId = await getAuthor4Token(context.dbKey);
|
if (!authorId) authorId = await getAuthor4Token(context.dbKey);
|
||||||
return authorId;
|
return authorId;
|
||||||
};
|
};
|
||||||
|
@ -170,7 +172,8 @@ exports.getAuthorId = async (token: string, user: object) => {
|
||||||
*/
|
*/
|
||||||
exports.getAuthor4Token = async (token: string) => {
|
exports.getAuthor4Token = async (token: string) => {
|
||||||
warnDeprecated(
|
warnDeprecated(
|
||||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
"AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead",
|
||||||
|
);
|
||||||
return await getAuthor4Token(token);
|
return await getAuthor4Token(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -179,8 +182,11 @@ exports.getAuthor4Token = async (token: string) => {
|
||||||
* @param {String} authorMapper The mapper
|
* @param {String} authorMapper The mapper
|
||||||
* @param {String} name The name of the author (optional)
|
* @param {String} name The name of the author (optional)
|
||||||
*/
|
*/
|
||||||
exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string) => {
|
exports.createAuthorIfNotExistsFor = async (
|
||||||
const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
|
authorMapper: string,
|
||||||
|
name: string,
|
||||||
|
) => {
|
||||||
|
const author = await mapAuthorWithDBKey("mapper2author", authorMapper);
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
// set the name of this author
|
// set the name of this author
|
||||||
|
@ -190,7 +196,6 @@ exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string)
|
||||||
return author;
|
return author;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal function that creates the database entry for an author
|
* Internal function that creates the database entry for an author
|
||||||
* @param {String} name The name of the author
|
* @param {String} name The name of the author
|
||||||
|
@ -201,7 +206,7 @@ exports.createAuthor = async (name: string) => {
|
||||||
|
|
||||||
// create the globalAuthors db entry
|
// create the globalAuthors db entry
|
||||||
const authorObj = {
|
const authorObj = {
|
||||||
colorId: Math.floor(Math.random() * (exports.getColorPalette().length)),
|
colorId: Math.floor(Math.random() * exports.getColorPalette().length),
|
||||||
name,
|
name,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
@ -216,35 +221,38 @@ exports.createAuthor = async (name: string) => {
|
||||||
* Returns the Author Obj of the author
|
* Returns the Author Obj of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
*/
|
*/
|
||||||
exports.getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`);
|
exports.getAuthor = async (author: string) =>
|
||||||
|
await db.get(`globalAuthor:${author}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the color Id of the author
|
* Returns the color Id of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
*/
|
*/
|
||||||
exports.getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']);
|
exports.getAuthorColorId = async (author: string) =>
|
||||||
|
await db.getSub(`globalAuthor:${author}`, ["colorId"]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the color Id of the author
|
* Sets the color Id of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
* @param {String} colorId The color id of the author
|
* @param {String} colorId The color id of the author
|
||||||
*/
|
*/
|
||||||
exports.setAuthorColorId = async (author: string, colorId: string) => await db.setSub(
|
exports.setAuthorColorId = async (author: string, colorId: string) =>
|
||||||
`globalAuthor:${author}`, ['colorId'], colorId);
|
await db.setSub(`globalAuthor:${author}`, ["colorId"], colorId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the author
|
* Returns the name of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
*/
|
*/
|
||||||
exports.getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']);
|
exports.getAuthorName = async (author: string) =>
|
||||||
|
await db.getSub(`globalAuthor:${author}`, ["name"]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the name of the author
|
* Sets the name of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
* @param {String} name The name of the author
|
* @param {String} name The name of the author
|
||||||
*/
|
*/
|
||||||
exports.setAuthorName = async (author: string, name: string) => await db.setSub(
|
exports.setAuthorName = async (author: string, name: string) =>
|
||||||
`globalAuthor:${author}`, ['name'], name);
|
await db.setSub(`globalAuthor:${author}`, ["name"], name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of all pads this author contributed to
|
* Returns an array of all pads this author contributed to
|
||||||
|
@ -261,7 +269,7 @@ exports.listPadsOfAuthor = async (authorID: string) => {
|
||||||
|
|
||||||
if (author == null) {
|
if (author == null) {
|
||||||
// author does not exist
|
// author does not exist
|
||||||
throw new CustomError('authorID does not exist', 'apierror');
|
throw new CustomError("authorID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// everything is fine, return the pad IDs
|
// everything is fine, return the pad IDs
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The DB Module provides a database initialized with the settings
|
* The DB Module provides a database initialized with the settings
|
||||||
|
@ -21,12 +21,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ueberDB from 'ueberdb2';
|
import ueberDB from "ueberdb2";
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
import log4js from 'log4js';
|
import log4js from "log4js";
|
||||||
const stats = require('../stats')
|
const stats = require("../stats");
|
||||||
|
|
||||||
const logger = log4js.getLogger('ueberDB');
|
const logger = log4js.getLogger("ueberDB");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UeberDB Object that provides the database functions
|
* The UeberDB Object that provides the database functions
|
||||||
|
@ -37,17 +37,23 @@ exports.db = null;
|
||||||
* Initializes the database with the settings provided by the settings module
|
* Initializes the database with the settings provided by the settings module
|
||||||
*/
|
*/
|
||||||
exports.init = async () => {
|
exports.init = async () => {
|
||||||
exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger);
|
exports.db = new ueberDB.Database(
|
||||||
|
settings.dbType,
|
||||||
|
settings.dbSettings,
|
||||||
|
null,
|
||||||
|
logger,
|
||||||
|
);
|
||||||
await exports.db.init();
|
await exports.db.init();
|
||||||
if (exports.db.metrics != null) {
|
if (exports.db.metrics != null) {
|
||||||
for (const [metric, value] of Object.entries(exports.db.metrics)) {
|
for (const [metric, value] of Object.entries(exports.db.metrics)) {
|
||||||
if (typeof value !== 'number') continue;
|
if (typeof value !== "number") continue;
|
||||||
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
|
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
|
for (const fn of ["get", "set", "findKeys", "getSub", "setSub", "remove"]) {
|
||||||
const f = exports.db[fn];
|
const f = exports.db[fn];
|
||||||
exports[fn] = async (...args:string[]) => await f.call(exports.db, ...args);
|
exports[fn] = async (...args: string[]) =>
|
||||||
|
await f.call(exports.db, ...args);
|
||||||
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
|
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
|
||||||
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
|
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
|
||||||
}
|
}
|
||||||
|
@ -56,5 +62,5 @@ exports.init = async () => {
|
||||||
exports.shutdown = async (hookName: string, context: any) => {
|
exports.shutdown = async (hookName: string, context: any) => {
|
||||||
if (exports.db != null) await exports.db.close();
|
if (exports.db != null) await exports.db.close();
|
||||||
exports.db = null;
|
exports.db = null;
|
||||||
logger.log('Database closed');
|
logger.log("Database closed");
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* The Group Manager provides functions to manage groups in the database
|
* The Group Manager provides functions to manage groups in the database
|
||||||
*/
|
*/
|
||||||
|
@ -19,18 +19,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const randomString = require('../../static/js/pad_utils').randomString;
|
const randomString = require("../../static/js/pad_utils").randomString;
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const padManager = require('./PadManager');
|
const padManager = require("./PadManager");
|
||||||
const sessionManager = require('./SessionManager');
|
const sessionManager = require("./SessionManager");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all groups
|
* Lists all groups
|
||||||
* @return {Promise<{groupIDs: string[]}>} The ids of all groups
|
* @return {Promise<{groupIDs: string[]}>} The ids of all groups
|
||||||
*/
|
*/
|
||||||
exports.listAllGroups = async () => {
|
exports.listAllGroups = async () => {
|
||||||
let groups = await db.get('groups');
|
let groups = await db.get("groups");
|
||||||
groups = groups || {};
|
groups = groups || {};
|
||||||
|
|
||||||
const groupIDs = Object.keys(groups);
|
const groupIDs = Object.keys(groups);
|
||||||
|
@ -48,29 +48,35 @@ exports.deleteGroup = async (groupID: string): Promise<void> => {
|
||||||
// ensure group exists
|
// ensure group exists
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
// group does not exist
|
// group does not exist
|
||||||
throw new CustomError('groupID does not exist', 'apierror');
|
throw new CustomError("groupID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterate through all pads of this group and delete them (in parallel)
|
// iterate through all pads of this group and delete them (in parallel)
|
||||||
await Promise.all(Object.keys(group.pads).map(async (padId) => {
|
await Promise.all(
|
||||||
|
Object.keys(group.pads).map(async (padId) => {
|
||||||
const pad = await padManager.getPad(padId);
|
const pad = await padManager.getPad(padId);
|
||||||
await pad.remove();
|
await pad.remove();
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Delete associated sessions in parallel. This should be done before deleting the group2sessions
|
// Delete associated sessions in parallel. This should be done before deleting the group2sessions
|
||||||
// record because deleting a session updates the group2sessions record.
|
// record because deleting a session updates the group2sessions record.
|
||||||
const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {};
|
const { sessionIDs = {} } = (await db.get(`group2sessions:${groupID}`)) || {};
|
||||||
await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => {
|
await Promise.all(
|
||||||
|
Object.keys(sessionIDs).map(async (sessionId) => {
|
||||||
await sessionManager.deleteSession(sessionId);
|
await sessionManager.deleteSession(sessionId);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.remove(`group2sessions:${groupID}`),
|
db.remove(`group2sessions:${groupID}`),
|
||||||
// UeberDB's setSub() method atomically reads the record, updates the appropriate property, and
|
// UeberDB's setSub() method atomically reads the record, updates the appropriate property, and
|
||||||
// writes the result. Setting a property to `undefined` deletes that property (JSON.stringify()
|
// writes the result. Setting a property to `undefined` deletes that property (JSON.stringify()
|
||||||
// ignores such properties).
|
// ignores such properties).
|
||||||
db.setSub('groups', [groupID], undefined),
|
db.setSub("groups", [groupID], undefined),
|
||||||
...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)),
|
...Object.keys(group.mappings || {}).map(
|
||||||
|
async (m) => await db.remove(`mapper2group:${m}`),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Remove the group record after updating the `groups` record so that the state is consistent.
|
// Remove the group record after updating the `groups` record so that the state is consistent.
|
||||||
|
@ -86,7 +92,7 @@ exports.doesGroupExist = async (groupID: string) => {
|
||||||
// try to get the group entry
|
// try to get the group entry
|
||||||
const group = await db.get(`group:${groupID}`);
|
const group = await db.get(`group:${groupID}`);
|
||||||
|
|
||||||
return (group != null);
|
return group != null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,7 +105,7 @@ exports.createGroup = async () => {
|
||||||
// Add the group to the `groups` record after the group's individual record is created so that
|
// Add the group to the `groups` record after the group's individual record is created so that
|
||||||
// the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates
|
// the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates
|
||||||
// the appropriate property, and writes the result.
|
// the appropriate property, and writes the result.
|
||||||
await db.setSub('groups', [groupID], 1);
|
await db.setSub("groups", [groupID], 1);
|
||||||
return { groupID };
|
return { groupID };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,11 +115,11 @@ exports.createGroup = async () => {
|
||||||
* @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID
|
* @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID
|
||||||
*/
|
*/
|
||||||
exports.createGroupIfNotExistsFor = async (groupMapper: string | object) => {
|
exports.createGroupIfNotExistsFor = async (groupMapper: string | object) => {
|
||||||
if (typeof groupMapper !== 'string') {
|
if (typeof groupMapper !== "string") {
|
||||||
throw new CustomError('groupMapper is not a string', 'apierror');
|
throw new CustomError("groupMapper is not a string", "apierror");
|
||||||
}
|
}
|
||||||
const groupID = await db.get(`mapper2group:${groupMapper}`);
|
const groupID = await db.get(`mapper2group:${groupMapper}`);
|
||||||
if (groupID && await exports.doesGroupExist(groupID)) return {groupID};
|
if (groupID && (await exports.doesGroupExist(groupID))) return { groupID };
|
||||||
const result = await exports.createGroup();
|
const result = await exports.createGroup();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.set(`mapper2group:${groupMapper}`, result.groupID),
|
db.set(`mapper2group:${groupMapper}`, result.groupID),
|
||||||
|
@ -121,7 +127,7 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
|
||||||
// deleted. Although the core Etherpad API does not support multiple mappings for the same
|
// deleted. Although the core Etherpad API does not support multiple mappings for the same
|
||||||
// group, the database record does support multiple mappings in case a plugin decides to extend
|
// group, the database record does support multiple mappings in case a plugin decides to extend
|
||||||
// the core Etherpad functionality. (It's also easy to implement it this way.)
|
// the core Etherpad functionality. (It's also easy to implement it this way.)
|
||||||
db.setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1),
|
db.setSub(`group:${result.groupID}`, ["mappings", groupMapper], 1),
|
||||||
]);
|
]);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@ -134,7 +140,12 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
|
||||||
* @param {String} authorId The id of the author
|
* @param {String} authorId The id of the author
|
||||||
* @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad
|
* @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad
|
||||||
*/
|
*/
|
||||||
exports.createGroupPad = async (groupID: string, padName: string, text: string, authorId: string = ''): Promise<{ padID: string; }> => {
|
exports.createGroupPad = async (
|
||||||
|
groupID: string,
|
||||||
|
padName: string,
|
||||||
|
text: string,
|
||||||
|
authorId: string = "",
|
||||||
|
): Promise<{ padID: string }> => {
|
||||||
// create the padID
|
// create the padID
|
||||||
const padID = `${groupID}$${padName}`;
|
const padID = `${groupID}$${padName}`;
|
||||||
|
|
||||||
|
@ -142,7 +153,7 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
|
||||||
const groupExists = await exports.doesGroupExist(groupID);
|
const groupExists = await exports.doesGroupExist(groupID);
|
||||||
|
|
||||||
if (!groupExists) {
|
if (!groupExists) {
|
||||||
throw new CustomError('groupID does not exist', 'apierror');
|
throw new CustomError("groupID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure pad doesn't exist already
|
// ensure pad doesn't exist already
|
||||||
|
@ -150,14 +161,14 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
|
||||||
|
|
||||||
if (padExists) {
|
if (padExists) {
|
||||||
// pad exists already
|
// pad exists already
|
||||||
throw new CustomError('padName does already exist', 'apierror');
|
throw new CustomError("padName does already exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the pad
|
// create the pad
|
||||||
await padManager.getPad(padID, text, authorId);
|
await padManager.getPad(padID, text, authorId);
|
||||||
|
|
||||||
// create an entry in the group for this pad
|
// create an entry in the group for this pad
|
||||||
await db.setSub(`group:${groupID}`, ['pads', padID], 1);
|
await db.setSub(`group:${groupID}`, ["pads", padID], 1);
|
||||||
|
|
||||||
return { padID };
|
return { padID };
|
||||||
};
|
};
|
||||||
|
@ -167,16 +178,16 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
|
||||||
* @param {String} groupID The id of the group
|
* @param {String} groupID The id of the group
|
||||||
* @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group
|
* @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group
|
||||||
*/
|
*/
|
||||||
exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => {
|
exports.listPads = async (groupID: string): Promise<{ padIDs: string[] }> => {
|
||||||
const exists = await exports.doesGroupExist(groupID);
|
const exists = await exports.doesGroupExist(groupID);
|
||||||
|
|
||||||
// ensure the group exists
|
// ensure the group exists
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new CustomError('groupID does not exist', 'apierror');
|
throw new CustomError("groupID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// group exists, let's get the pads
|
// group exists, let's get the pads
|
||||||
const result = await db.getSub(`group:${groupID}`, ['pads']);
|
const result = await db.getSub(`group:${groupID}`, ["pads"]);
|
||||||
const padIDs = Object.keys(result);
|
const padIDs = Object.keys(result);
|
||||||
|
|
||||||
return { padIDs };
|
return { padIDs };
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
import { Database } from "ueberdb2";
|
import { Database } from "ueberdb2";
|
||||||
import { AChangeSet, APool, AText } from "../types/PadType";
|
import { AChangeSet, APool, AText } from "../types/PadType";
|
||||||
import { MapArrayType } from "../types/MapType";
|
import { MapArrayType } from "../types/MapType";
|
||||||
|
@ -7,24 +7,26 @@ import {MapArrayType} from "../types/MapType";
|
||||||
* The pad object, defined with joose
|
* The pad object, defined with joose
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const AttributeMap = require('../../static/js/AttributeMap');
|
const AttributeMap = require("../../static/js/AttributeMap");
|
||||||
const Changeset = require('../../static/js/Changeset');
|
const Changeset = require("../../static/js/Changeset");
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
const ChatMessage = require("../../static/js/ChatMessage");
|
||||||
const AttributePool = require('../../static/js/AttributePool');
|
const AttributePool = require("../../static/js/AttributePool");
|
||||||
const Stream = require('../utils/Stream');
|
const Stream = require("../utils/Stream");
|
||||||
const assert = require('assert').strict;
|
const assert = require("assert").strict;
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const authorManager = require('./AuthorManager');
|
const authorManager = require("./AuthorManager");
|
||||||
const padManager = require('./PadManager');
|
const padManager = require("./PadManager");
|
||||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
const padMessageHandler = require("../handler/PadMessageHandler");
|
||||||
const groupManager = require('./GroupManager');
|
const groupManager = require("./GroupManager");
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
const readOnlyManager = require("./ReadOnlyManager");
|
||||||
const randomString = require('../utils/randomstring');
|
const randomString = require("../utils/randomstring");
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require("../../static/js/pluginfw/hooks");
|
||||||
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
const {
|
||||||
const promises = require('../utils/promises');
|
padutils: { warnDeprecated },
|
||||||
|
} = require("../../static/js/pad_utils");
|
||||||
|
const promises = require("../utils/promises");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
||||||
|
@ -32,10 +34,12 @@ const promises = require('../utils/promises');
|
||||||
* @param {String} txt The text to clean
|
* @param {String} txt The text to clean
|
||||||
* @returns {String} The cleaned text
|
* @returns {String} The cleaned text
|
||||||
*/
|
*/
|
||||||
exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
|
exports.cleanText = (txt: string): string =>
|
||||||
.replace(/\r/g, '\n')
|
txt
|
||||||
.replace(/\t/g, ' ')
|
.replace(/\r\n/g, "\n")
|
||||||
.replace(/\xa0/g, ' ');
|
.replace(/\r/g, "\n")
|
||||||
|
.replace(/\t/g, " ")
|
||||||
|
.replace(/\xa0/g, " ");
|
||||||
|
|
||||||
class Pad {
|
class Pad {
|
||||||
private db: Database;
|
private db: Database;
|
||||||
|
@ -56,7 +60,7 @@ class Pad {
|
||||||
*/
|
*/
|
||||||
constructor(id: string, database = db) {
|
constructor(id: string, database = db) {
|
||||||
this.db = database;
|
this.db = database;
|
||||||
this.atext = Changeset.makeAText('\n');
|
this.atext = Changeset.makeAText("\n");
|
||||||
this.pool = new AttributePool();
|
this.pool = new AttributePool();
|
||||||
this.head = -1;
|
this.head = -1;
|
||||||
this.chatHead = -1;
|
this.chatHead = -1;
|
||||||
|
@ -93,10 +97,13 @@ class Pad {
|
||||||
* @param {String} authorId The id of the author
|
* @param {String} authorId The id of the author
|
||||||
* @return {Promise<number|string>}
|
* @return {Promise<number|string>}
|
||||||
*/
|
*/
|
||||||
async appendRevision(aChangeset:AChangeSet, authorId = '') {
|
async appendRevision(aChangeset: AChangeSet, authorId = "") {
|
||||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
||||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
if (
|
||||||
this.head !== -1) {
|
newAText.text === this.atext.text &&
|
||||||
|
newAText.attribs === this.atext.attribs &&
|
||||||
|
this.head !== -1
|
||||||
|
) {
|
||||||
return this.head;
|
return this.head;
|
||||||
}
|
}
|
||||||
Changeset.copyAText(newAText, this.atext);
|
Changeset.copyAText(newAText, this.atext);
|
||||||
|
@ -104,9 +111,9 @@ class Pad {
|
||||||
const newRev = ++this.head;
|
const newRev = ++this.head;
|
||||||
|
|
||||||
// ex. getNumForAuthor
|
// ex. getNumForAuthor
|
||||||
if (authorId !== '') this.pool.putAttrib(['author', authorId]);
|
if (authorId !== "") this.pool.putAttrib(["author", authorId]);
|
||||||
|
|
||||||
const hook = this.head === 0 ? 'padCreate' : 'padUpdate';
|
const hook = this.head === 0 ? "padCreate" : "padUpdate";
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.db.set(`pad:${this.id}:revs:${newRev}`, {
|
this.db.set(`pad:${this.id}:revs:${newRev}`, {
|
||||||
|
@ -114,10 +121,12 @@ class Pad {
|
||||||
meta: {
|
meta: {
|
||||||
author: authorId,
|
author: authorId,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
...newRev === this.getKeyRevisionNumber(newRev) ? {
|
...(newRev === this.getKeyRevisionNumber(newRev)
|
||||||
|
? {
|
||||||
pool: this.pool,
|
pool: this.pool,
|
||||||
atext: this.atext,
|
atext: this.atext,
|
||||||
} : {},
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.saveToDatabase(),
|
this.saveToDatabase(),
|
||||||
|
@ -126,17 +135,23 @@ class Pad {
|
||||||
pad: this,
|
pad: this,
|
||||||
authorId,
|
authorId,
|
||||||
get author() {
|
get author() {
|
||||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
warnDeprecated(
|
||||||
|
`${hook} hook author context is deprecated; use authorId instead`,
|
||||||
|
);
|
||||||
return this.authorId;
|
return this.authorId;
|
||||||
},
|
},
|
||||||
set author(authorId) {
|
set author(authorId) {
|
||||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
warnDeprecated(
|
||||||
|
`${hook} hook author context is deprecated; use authorId instead`,
|
||||||
|
);
|
||||||
this.authorId = authorId;
|
this.authorId = authorId;
|
||||||
},
|
},
|
||||||
...this.head === 0 ? {} : {
|
...(this.head === 0
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
revs: newRev,
|
revs: newRev,
|
||||||
changeset: aChangeset,
|
changeset: aChangeset,
|
||||||
},
|
}),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
return newRev;
|
return newRev;
|
||||||
|
@ -161,22 +176,31 @@ class Pad {
|
||||||
async getLastEdit() {
|
async getLastEdit() {
|
||||||
const revNum = this.getHeadRevisionNumber();
|
const revNum = this.getHeadRevisionNumber();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']);
|
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
|
||||||
|
"meta",
|
||||||
|
"timestamp",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRevisionChangeset(revNum: number) {
|
async getRevisionChangeset(revNum: number) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']);
|
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ["changeset"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRevisionAuthor(revNum: number) {
|
async getRevisionAuthor(revNum: number) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']);
|
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
|
||||||
|
"meta",
|
||||||
|
"author",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRevisionDate(revNum: number) {
|
async getRevisionDate(revNum: number) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']);
|
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
|
||||||
|
"meta",
|
||||||
|
"timestamp",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -185,7 +209,10 @@ class Pad {
|
||||||
*/
|
*/
|
||||||
async _getKeyRevisionAText(revNum: number) {
|
async _getKeyRevisionAText(revNum: number) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'atext']);
|
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
|
||||||
|
"meta",
|
||||||
|
"atext",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -196,7 +223,10 @@ class Pad {
|
||||||
const authorIds = [];
|
const authorIds = [];
|
||||||
|
|
||||||
for (const key in this.pool.numToAttrib) {
|
for (const key in this.pool.numToAttrib) {
|
||||||
if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') {
|
if (
|
||||||
|
this.pool.numToAttrib[key][0] === "author" &&
|
||||||
|
this.pool.numToAttrib[key][1] !== ""
|
||||||
|
) {
|
||||||
authorIds.push(this.pool.numToAttrib[key][1]);
|
authorIds.push(this.pool.numToAttrib[key][1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,11 +241,15 @@ class Pad {
|
||||||
const [keyAText, changesets] = await Promise.all([
|
const [keyAText, changesets] = await Promise.all([
|
||||||
this._getKeyRevisionAText(keyRev),
|
this._getKeyRevisionAText(keyRev),
|
||||||
Promise.all(
|
Promise.all(
|
||||||
Stream.range(keyRev + 1, targetRev + 1).map(this.getRevisionChangeset.bind(this))),
|
Stream.range(keyRev + 1, targetRev + 1).map(
|
||||||
|
this.getRevisionChangeset.bind(this),
|
||||||
|
),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
const apool = this.apool();
|
const apool = this.apool();
|
||||||
let atext = keyAText;
|
let atext = keyAText;
|
||||||
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
|
for (const cs of changesets)
|
||||||
|
atext = Changeset.applyToAText(cs, atext, apool);
|
||||||
return atext;
|
return atext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,10 +263,13 @@ class Pad {
|
||||||
const colorPalette = authorManager.getColorPalette();
|
const colorPalette = authorManager.getColorPalette();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId:string) => {
|
authorIds.map((authorId) =>
|
||||||
|
authorManager.getAuthorColorId(authorId).then((colorId: string) => {
|
||||||
// colorId might be a hex color or an number out of the palette
|
// colorId might be a hex color or an number out of the palette
|
||||||
returnTable[authorId] = colorPalette[colorId] || colorId;
|
returnTable[authorId] = colorPalette[colorId] || colorId;
|
||||||
})));
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return returnTable;
|
return returnTable;
|
||||||
}
|
}
|
||||||
|
@ -280,18 +317,28 @@ class Pad {
|
||||||
* @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted).
|
* @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted).
|
||||||
* @param {string} [authorId] - Author ID of the user making the change (if applicable).
|
* @param {string} [authorId] - Author ID of the user making the change (if applicable).
|
||||||
*/
|
*/
|
||||||
async spliceText(start:number, ndel:number, ins: string, authorId: string = '') {
|
async spliceText(
|
||||||
if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`);
|
start: number,
|
||||||
if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`);
|
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();
|
const orig = this.text();
|
||||||
assert(orig.endsWith('\n'));
|
assert(orig.endsWith("\n"));
|
||||||
if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text');
|
if (start + ndel > orig.length)
|
||||||
|
throw new RangeError("start/delete past the end of the text");
|
||||||
ins = exports.cleanText(ins);
|
ins = exports.cleanText(ins);
|
||||||
const willEndWithNewline =
|
const willEndWithNewline =
|
||||||
start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
|
start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
|
||||||
ins.endsWith('\n') ||
|
ins.endsWith("\n") ||
|
||||||
(!ins && start > 0 && orig[start - 1] === '\n');
|
(!ins && start > 0 && orig[start - 1] === "\n");
|
||||||
if (!willEndWithNewline) ins += '\n';
|
if (!willEndWithNewline) ins += "\n";
|
||||||
if (ndel === 0 && ins.length === 0) return;
|
if (ndel === 0 && ins.length === 0) return;
|
||||||
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
|
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
|
||||||
await this.appendRevision(changeset, authorId);
|
await this.appendRevision(changeset, authorId);
|
||||||
|
@ -305,7 +352,7 @@ class Pad {
|
||||||
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
||||||
* applicable.
|
* applicable.
|
||||||
*/
|
*/
|
||||||
async setText(newText: string, authorId = '') {
|
async setText(newText: string, authorId = "") {
|
||||||
await this.spliceText(0, this.text().length, newText, authorId);
|
await this.spliceText(0, this.text().length, newText, authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,7 +363,7 @@ class Pad {
|
||||||
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
||||||
* applicable.
|
* applicable.
|
||||||
*/
|
*/
|
||||||
async appendText(newText:string, authorId = '') {
|
async appendText(newText: string, authorId = "") {
|
||||||
await this.spliceText(this.text().length - 1, 0, newText, authorId);
|
await this.spliceText(this.text().length - 1, 0, newText, authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,15 +377,24 @@ class Pad {
|
||||||
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
||||||
* `msgOrText.time` instead.
|
* `msgOrText.time` instead.
|
||||||
*/
|
*/
|
||||||
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
|
async appendChatMessage(
|
||||||
|
msgOrText: string | typeof ChatMessage,
|
||||||
|
authorId = null,
|
||||||
|
time = null,
|
||||||
|
) {
|
||||||
const msg =
|
const msg =
|
||||||
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
msgOrText instanceof ChatMessage
|
||||||
|
? msgOrText
|
||||||
|
: new ChatMessage(msgOrText, authorId, time);
|
||||||
this.chatHead++;
|
this.chatHead++;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// Don't save the display name in the database because the user can change it at any time. The
|
// Don't save the display name in the database because the user can change it at any time. The
|
||||||
// `displayName` property will be populated with the current value when the message is read
|
// `displayName` property will be populated with the current value when the message is read
|
||||||
// from the database.
|
// from the database.
|
||||||
this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}),
|
this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {
|
||||||
|
...msg,
|
||||||
|
displayName: undefined,
|
||||||
|
}),
|
||||||
this.saveToDatabase(),
|
this.saveToDatabase(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -363,14 +419,15 @@ class Pad {
|
||||||
* interval as is typical in code.
|
* interval as is typical in code.
|
||||||
*/
|
*/
|
||||||
async getChatMessages(start: string, end: number) {
|
async getChatMessages(start: string, end: number) {
|
||||||
const entries =
|
const entries = await Promise.all(
|
||||||
await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this)));
|
Stream.range(start, end + 1).map(this.getChatMessage.bind(this)),
|
||||||
|
);
|
||||||
|
|
||||||
// sort out broken chat entries
|
// sort out broken chat entries
|
||||||
// it looks like in happened in the past that the chat head was
|
// it looks like in happened in the past that the chat head was
|
||||||
// incremented, but the chat message wasn't added
|
// incremented, but the chat message wasn't added
|
||||||
return entries.filter((entry) => {
|
return entries.filter((entry) => {
|
||||||
const pass = (entry != null);
|
const pass = entry != null;
|
||||||
if (!pass) {
|
if (!pass) {
|
||||||
console.warn(`WARNING: Found broken chat entry in pad ${this.id}`);
|
console.warn(`WARNING: Found broken chat entry in pad ${this.id}`);
|
||||||
}
|
}
|
||||||
|
@ -378,25 +435,32 @@ class Pad {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(text:string, authorId = '') {
|
async init(text: string, authorId = "") {
|
||||||
// try to load the pad
|
// try to load the pad
|
||||||
const value = await this.db.get(`pad:${this.id}`);
|
const value = await this.db.get(`pad:${this.id}`);
|
||||||
|
|
||||||
// if this pad exists, load it
|
// if this pad exists, load it
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
Object.assign(this, value);
|
Object.assign(this, value);
|
||||||
if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool);
|
if ("pool" in value)
|
||||||
|
this.pool = new AttributePool().fromJsonable(value.pool);
|
||||||
} else {
|
} else {
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
|
const context = {
|
||||||
await hooks.aCallAll('padDefaultContent', context);
|
pad: this,
|
||||||
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
authorId,
|
||||||
|
type: "text",
|
||||||
|
content: settings.defaultPadText,
|
||||||
|
};
|
||||||
|
await hooks.aCallAll("padDefaultContent", context);
|
||||||
|
if (context.type !== "text")
|
||||||
|
throw new Error(`unsupported content type: ${context.type}`);
|
||||||
text = exports.cleanText(context.content);
|
text = exports.cleanText(context.content);
|
||||||
}
|
}
|
||||||
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
|
const firstChangeset = Changeset.makeSplice("\n", 0, 0, text);
|
||||||
await this.appendRevision(firstChangeset, authorId);
|
await this.appendRevision(firstChangeset, authorId);
|
||||||
}
|
}
|
||||||
await hooks.aCallAll('padLoad', {pad: this});
|
await hooks.aCallAll("padLoad", { pad: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
async copy(destinationID: string, force: boolean) {
|
async copy(destinationID: string, force: boolean) {
|
||||||
|
@ -419,30 +483,38 @@ class Pad {
|
||||||
await db.set(`pad:${destinationID}${keySuffix}`, val);
|
await db.set(`pad:${destinationID}${keySuffix}`, val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises = (function* () {
|
const promises = function* () {
|
||||||
yield copyRecord('');
|
yield copyRecord("");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
yield* Stream.range(0, this.head + 1).map((i) => copyRecord(`:revs:${i}`));
|
yield* Stream.range(0, this.head + 1).map((i) =>
|
||||||
|
copyRecord(`:revs:${i}`),
|
||||||
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`));
|
yield* Stream.range(0, this.chatHead + 1).map((i) =>
|
||||||
|
copyRecord(`:chat:${i}`),
|
||||||
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
yield this.copyAuthorInfoToDestinationPad(destinationID);
|
yield this.copyAuthorInfoToDestinationPad(destinationID);
|
||||||
if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
|
if (destGroupID)
|
||||||
}).call(this);
|
yield db.setSub(`group:${destGroupID}`, ["pads", destinationID], 1);
|
||||||
|
}.call(this);
|
||||||
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
|
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
|
||||||
|
|
||||||
// Initialize the new pad (will update the listAllPads cache)
|
// Initialize the new pad (will update the listAllPads cache)
|
||||||
const dstPad = await padManager.getPad(destinationID, null);
|
const dstPad = await padManager.getPad(destinationID, null);
|
||||||
|
|
||||||
// let the plugins know the pad was copied
|
// let the plugins know the pad was copied
|
||||||
await hooks.aCallAll('padCopy', {
|
await hooks.aCallAll("padCopy", {
|
||||||
get originalPad() {
|
get originalPad() {
|
||||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
warnDeprecated(
|
||||||
|
"padCopy originalPad context property is deprecated; use srcPad instead",
|
||||||
|
);
|
||||||
return this.srcPad;
|
return this.srcPad;
|
||||||
},
|
},
|
||||||
get destinationID() {
|
get destinationID() {
|
||||||
warnDeprecated(
|
warnDeprecated(
|
||||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
"padCopy destinationID context property is deprecated; use dstPad.id instead",
|
||||||
|
);
|
||||||
return this.dstPad.id;
|
return this.dstPad.id;
|
||||||
},
|
},
|
||||||
srcPad: this,
|
srcPad: this,
|
||||||
|
@ -455,33 +527,39 @@ class Pad {
|
||||||
async checkIfGroupExistAndReturnIt(destinationID: string) {
|
async checkIfGroupExistAndReturnIt(destinationID: string) {
|
||||||
let destGroupID: false | string = false;
|
let destGroupID: false | string = false;
|
||||||
|
|
||||||
if (destinationID.indexOf('$') >= 0) {
|
if (destinationID.indexOf("$") >= 0) {
|
||||||
destGroupID = destinationID.split('$')[0];
|
destGroupID = destinationID.split("$")[0];
|
||||||
const groupExists = await groupManager.doesGroupExist(destGroupID);
|
const groupExists = await groupManager.doesGroupExist(destGroupID);
|
||||||
|
|
||||||
// group does not exist
|
// group does not exist
|
||||||
if (!groupExists) {
|
if (!groupExists) {
|
||||||
throw new CustomError('groupID does not exist for destinationID', 'apierror');
|
throw new CustomError(
|
||||||
|
"groupID does not exist for destinationID",
|
||||||
|
"apierror",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return destGroupID;
|
return destGroupID;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) {
|
async removePadIfForceIsTrueAndAlreadyExist(
|
||||||
|
destinationID: string,
|
||||||
|
force: boolean | string,
|
||||||
|
) {
|
||||||
// if the pad exists, we should abort, unless forced.
|
// if the pad exists, we should abort, unless forced.
|
||||||
const exists = await padManager.doesPadExist(destinationID);
|
const exists = await padManager.doesPadExist(destinationID);
|
||||||
|
|
||||||
// allow force to be a string
|
// allow force to be a string
|
||||||
if (typeof force === 'string') {
|
if (typeof force === "string") {
|
||||||
force = (force.toLowerCase() === 'true');
|
force = force.toLowerCase() === "true";
|
||||||
} else {
|
} else {
|
||||||
force = !!force;
|
force = !!force;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
console.error('erroring out without force');
|
console.error("erroring out without force");
|
||||||
throw new CustomError('destinationID already exists', 'apierror');
|
throw new CustomError("destinationID already exists", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// exists and forcing
|
// exists and forcing
|
||||||
|
@ -492,11 +570,18 @@ class Pad {
|
||||||
|
|
||||||
async copyAuthorInfoToDestinationPad(destinationID: string) {
|
async copyAuthorInfoToDestinationPad(destinationID: string) {
|
||||||
// add the new sourcePad to all authors who contributed to the old one
|
// add the new sourcePad to all authors who contributed to the old one
|
||||||
await Promise.all(this.getAllAuthors().map(
|
await Promise.all(
|
||||||
(authorID) => authorManager.addPad(authorID, destinationID)));
|
this.getAllAuthors().map((authorID) =>
|
||||||
|
authorManager.addPad(authorID, destinationID),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') {
|
async copyPadWithoutHistory(
|
||||||
|
destinationID: string,
|
||||||
|
force: string | boolean,
|
||||||
|
authorId = "",
|
||||||
|
) {
|
||||||
// flush the source pad
|
// flush the source pad
|
||||||
this.saveToDatabase();
|
this.saveToDatabase();
|
||||||
|
|
||||||
|
@ -510,11 +595,11 @@ class Pad {
|
||||||
|
|
||||||
// Group pad? Add it to the group's list
|
// Group pad? Add it to the group's list
|
||||||
if (destGroupID) {
|
if (destGroupID) {
|
||||||
await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
|
await db.setSub(`group:${destGroupID}`, ["pads", destinationID], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize the pad with a new line to avoid getting the defaultText
|
// initialize the pad with a new line to avoid getting the defaultText
|
||||||
const dstPad = await padManager.getPad(destinationID, '\n', authorId);
|
const dstPad = await padManager.getPad(destinationID, "\n", authorId);
|
||||||
dstPad.pool = this.pool.clone();
|
dstPad.pool = this.pool.clone();
|
||||||
|
|
||||||
const oldAText = this.atext;
|
const oldAText = this.atext;
|
||||||
|
@ -533,17 +618,25 @@ class Pad {
|
||||||
|
|
||||||
// create a changeset that removes the previous text and add the newText with
|
// create a changeset that removes the previous text and add the newText with
|
||||||
// all atributes present on the source pad
|
// all atributes present on the source pad
|
||||||
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
|
const changeset = Changeset.pack(
|
||||||
|
oldLength,
|
||||||
|
newLength,
|
||||||
|
assem.toString(),
|
||||||
|
newText,
|
||||||
|
);
|
||||||
dstPad.appendRevision(changeset, authorId);
|
dstPad.appendRevision(changeset, authorId);
|
||||||
|
|
||||||
await hooks.aCallAll('padCopy', {
|
await hooks.aCallAll("padCopy", {
|
||||||
get originalPad() {
|
get originalPad() {
|
||||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
warnDeprecated(
|
||||||
|
"padCopy originalPad context property is deprecated; use srcPad instead",
|
||||||
|
);
|
||||||
return this.srcPad;
|
return this.srcPad;
|
||||||
},
|
},
|
||||||
get destinationID() {
|
get destinationID() {
|
||||||
warnDeprecated(
|
warnDeprecated(
|
||||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
"padCopy destinationID context property is deprecated; use dstPad.id instead",
|
||||||
|
);
|
||||||
return this.dstPad.id;
|
return this.dstPad.id;
|
||||||
},
|
},
|
||||||
srcPad: this,
|
srcPad: this,
|
||||||
|
@ -566,9 +659,9 @@ class Pad {
|
||||||
// run to completion
|
// run to completion
|
||||||
|
|
||||||
// is it a group pad? -> delete the entry of this pad in the group
|
// is it a group pad? -> delete the entry of this pad in the group
|
||||||
if (padID.indexOf('$') >= 0) {
|
if (padID.indexOf("$") >= 0) {
|
||||||
// it is a group pad
|
// it is a group pad
|
||||||
const groupID = padID.substring(0, padID.indexOf('$'));
|
const groupID = padID.substring(0, padID.indexOf("$"));
|
||||||
const group = await db.get(`group:${groupID}`);
|
const group = await db.get(`group:${groupID}`);
|
||||||
|
|
||||||
// remove the pad entry
|
// remove the pad entry
|
||||||
|
@ -579,20 +672,26 @@ class Pad {
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the readonly entries
|
// remove the readonly entries
|
||||||
p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => {
|
p.push(
|
||||||
|
readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => {
|
||||||
await db.remove(`readonly2pad:${readonlyID}`);
|
await db.remove(`readonly2pad:${readonlyID}`);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
p.push(db.remove(`pad2readonly:${padID}`));
|
p.push(db.remove(`pad2readonly:${padID}`));
|
||||||
|
|
||||||
// delete all chat messages
|
// delete all chat messages
|
||||||
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
p.push(
|
||||||
|
promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
||||||
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
|
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// delete all revisions
|
// delete all revisions
|
||||||
p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => {
|
p.push(
|
||||||
|
promises.timesLimit(this.head + 1, 500, async (i: string) => {
|
||||||
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
|
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// remove pad from all authors who contributed
|
// remove pad from all authors who contributed
|
||||||
this.getAllAuthors().forEach((authorId) => {
|
this.getAllAuthors().forEach((authorId) => {
|
||||||
|
@ -601,13 +700,17 @@ class Pad {
|
||||||
|
|
||||||
// delete the pad entry and delete pad from padManager
|
// delete the pad entry and delete pad from padManager
|
||||||
p.push(padManager.removePad(padID));
|
p.push(padManager.removePad(padID));
|
||||||
p.push(hooks.aCallAll('padRemove', {
|
p.push(
|
||||||
|
hooks.aCallAll("padRemove", {
|
||||||
get padID() {
|
get padID() {
|
||||||
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
warnDeprecated(
|
||||||
|
"padRemove padID context property is deprecated; use pad.id instead",
|
||||||
|
);
|
||||||
return this.pad.id;
|
return this.pad.id;
|
||||||
},
|
},
|
||||||
pad: this,
|
pad: this,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
await Promise.all(p);
|
await Promise.all(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -647,7 +750,7 @@ class Pad {
|
||||||
*/
|
*/
|
||||||
async check() {
|
async check() {
|
||||||
assert(this.id != null);
|
assert(this.id != null);
|
||||||
assert.equal(typeof this.id, 'string');
|
assert.equal(typeof this.id, "string");
|
||||||
|
|
||||||
const head = this.getHeadRevisionNumber();
|
const head = this.getHeadRevisionNumber();
|
||||||
assert(head != null);
|
assert(head != null);
|
||||||
|
@ -672,10 +775,10 @@ class Pad {
|
||||||
const savedRevisionsIds = new Set();
|
const savedRevisionsIds = new Set();
|
||||||
for (const savedRev of savedRevisions) {
|
for (const savedRev of savedRevisions) {
|
||||||
assert(savedRev != null);
|
assert(savedRev != null);
|
||||||
assert.equal(typeof savedRev, 'object');
|
assert.equal(typeof savedRev, "object");
|
||||||
assert(savedRevisionsList.includes(savedRev.revNum));
|
assert(savedRevisionsList.includes(savedRev.revNum));
|
||||||
assert(savedRev.id != null);
|
assert(savedRev.id != null);
|
||||||
assert.equal(typeof savedRev.id, 'string');
|
assert.equal(typeof savedRev.id, "string");
|
||||||
assert(!savedRevisionsIds.has(savedRev.id));
|
assert(!savedRevisionsIds.has(savedRev.id));
|
||||||
savedRevisionsIds.add(savedRev.id);
|
savedRevisionsIds.add(savedRev.id);
|
||||||
}
|
}
|
||||||
|
@ -686,7 +789,7 @@ class Pad {
|
||||||
|
|
||||||
const authorIds = new Set();
|
const authorIds = new Set();
|
||||||
pool.eachAttrib((k, v) => {
|
pool.eachAttrib((k, v) => {
|
||||||
if (k === 'author' && v) authorIds.add(v);
|
if (k === "author" && v) authorIds.add(v);
|
||||||
});
|
});
|
||||||
const revs = Stream.range(0, head + 1)
|
const revs = Stream.range(0, head + 1)
|
||||||
.map(async (r: number) => {
|
.map(async (r: number) => {
|
||||||
|
@ -705,31 +808,42 @@ class Pad {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.batch(100).buffer(99);
|
.batch(100)
|
||||||
let atext = Changeset.makeAText('\n');
|
.buffer(99);
|
||||||
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
let atext = Changeset.makeAText("\n");
|
||||||
|
for await (const [
|
||||||
|
r,
|
||||||
|
changeset,
|
||||||
|
authorId,
|
||||||
|
timestamp,
|
||||||
|
isKeyRev,
|
||||||
|
keyAText,
|
||||||
|
] of revs) {
|
||||||
try {
|
try {
|
||||||
assert(authorId != null);
|
assert(authorId != null);
|
||||||
assert.equal(typeof authorId, 'string');
|
assert.equal(typeof authorId, "string");
|
||||||
if (authorId) authorIds.add(authorId);
|
if (authorId) authorIds.add(authorId);
|
||||||
assert(timestamp != null);
|
assert(timestamp != null);
|
||||||
assert.equal(typeof timestamp, 'number');
|
assert.equal(typeof timestamp, "number");
|
||||||
assert(timestamp > 0);
|
assert(timestamp > 0);
|
||||||
assert(changeset != null);
|
assert(changeset != null);
|
||||||
assert.equal(typeof changeset, 'string');
|
assert.equal(typeof changeset, "string");
|
||||||
Changeset.checkRep(changeset);
|
Changeset.checkRep(changeset);
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = Changeset.unpack(changeset);
|
||||||
let text = atext.text;
|
let text = atext.text;
|
||||||
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
||||||
if (['=', '-'].includes(op.opcode)) {
|
if (["=", "-"].includes(op.opcode)) {
|
||||||
assert(text.length >= op.chars);
|
assert(text.length >= op.chars);
|
||||||
const consumed = text.slice(0, op.chars);
|
const consumed = text.slice(0, op.chars);
|
||||||
const nlines = (consumed.match(/\n/g) || []).length;
|
const nlines = (consumed.match(/\n/g) || []).length;
|
||||||
assert.equal(op.lines, nlines);
|
assert.equal(op.lines, nlines);
|
||||||
if (op.lines > 0) assert(consumed.endsWith('\n'));
|
if (op.lines > 0) assert(consumed.endsWith("\n"));
|
||||||
text = text.slice(op.chars);
|
text = text.slice(op.chars);
|
||||||
}
|
}
|
||||||
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
|
assert.equal(
|
||||||
|
op.attribs,
|
||||||
|
AttributeMap.fromString(op.attribs, pool).toString(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
atext = Changeset.applyToAText(changeset, atext, pool);
|
atext = Changeset.applyToAText(changeset, atext, pool);
|
||||||
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
||||||
|
@ -756,10 +870,11 @@ class Pad {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.batch(100).buffer(99);
|
.batch(100)
|
||||||
|
.buffer(99);
|
||||||
for (const p of chats) await p;
|
for (const p of chats) await p;
|
||||||
|
|
||||||
await hooks.aCallAll('padCheck', {pad: this});
|
await hooks.aCallAll("padCheck", { pad: this });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.Pad = Pad;
|
exports.Pad = Pad;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* The Pad Manager is a Factory for pad Objects
|
* The Pad Manager is a Factory for pad Objects
|
||||||
*/
|
*/
|
||||||
|
@ -22,10 +22,10 @@
|
||||||
import { MapArrayType } from "../types/MapType";
|
import { MapArrayType } from "../types/MapType";
|
||||||
import { PadType } from "../types/PadType";
|
import { PadType } from "../types/PadType";
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const Pad = require('../db/Pad');
|
const Pad = require("../db/Pad");
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache of all loaded Pads.
|
* A cache of all loaded Pads.
|
||||||
|
@ -39,12 +39,10 @@ const settings = require('../utils/Settings');
|
||||||
* that's defined somewhere more sensible.
|
* that's defined somewhere more sensible.
|
||||||
*/
|
*/
|
||||||
const globalPads: MapArrayType<any> = {
|
const globalPads: MapArrayType<any> = {
|
||||||
get(name: string)
|
get(name: string) {
|
||||||
{
|
|
||||||
return this[`:${name}`];
|
return this[`:${name}`];
|
||||||
},
|
},
|
||||||
set(name: string, value: any)
|
set(name: string, value: any) {
|
||||||
{
|
|
||||||
this[`:${name}`] = value;
|
this[`:${name}`] = value;
|
||||||
},
|
},
|
||||||
remove(name: string) {
|
remove(name: string) {
|
||||||
|
@ -57,7 +55,7 @@ const globalPads:MapArrayType<any> = {
|
||||||
*
|
*
|
||||||
* Updated without db access as new pads are created/old ones removed.
|
* Updated without db access as new pads are created/old ones removed.
|
||||||
*/
|
*/
|
||||||
const padList = new class {
|
const padList = new (class {
|
||||||
private _cachedList: string[] | null;
|
private _cachedList: string[] | null;
|
||||||
private _list: Set<string>;
|
private _list: Set<string>;
|
||||||
private _loaded: Promise<void> | null;
|
private _loaded: Promise<void> | null;
|
||||||
|
@ -74,9 +72,9 @@ const padList = new class {
|
||||||
async getPads() {
|
async getPads() {
|
||||||
if (!this._loaded) {
|
if (!this._loaded) {
|
||||||
this._loaded = (async () => {
|
this._loaded = (async () => {
|
||||||
const dbData = await db.findKeys('pad:*', '*:*:*');
|
const dbData = await db.findKeys("pad:*", "*:*:*");
|
||||||
if (dbData == null) return;
|
if (dbData == null) return;
|
||||||
for (const val of dbData) this.addPad(val.replace(/^pad:/, ''));
|
for (const val of dbData) this.addPad(val.replace(/^pad:/, ""));
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
await this._loaded;
|
await this._loaded;
|
||||||
|
@ -95,7 +93,7 @@ const padList = new class {
|
||||||
this._list.delete(name);
|
this._list.delete(name);
|
||||||
this._cachedList = null;
|
this._cachedList = null;
|
||||||
}
|
}
|
||||||
}();
|
})();
|
||||||
|
|
||||||
// initialises the all-knowing data structure
|
// initialises the all-knowing data structure
|
||||||
|
|
||||||
|
@ -106,22 +104,26 @@ const padList = new class {
|
||||||
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
|
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
|
||||||
* applicable).
|
* applicable).
|
||||||
*/
|
*/
|
||||||
exports.getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise<PadType> => {
|
exports.getPad = async (
|
||||||
|
id: string,
|
||||||
|
text?: string | null,
|
||||||
|
authorId: string | null = "",
|
||||||
|
): Promise<PadType> => {
|
||||||
// check if this is a valid padId
|
// check if this is a valid padId
|
||||||
if (!exports.isValidPadId(id)) {
|
if (!exports.isValidPadId(id)) {
|
||||||
throw new CustomError(`${id} is not a valid padId`, 'apierror');
|
throw new CustomError(`${id} is not a valid padId`, "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if this is a valid text
|
// check if this is a valid text
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
// check if text is a string
|
// check if text is a string
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== "string") {
|
||||||
throw new CustomError('text is not a string', 'apierror');
|
throw new CustomError("text is not a string", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if text is less than 100k chars
|
// check if text is less than 100k chars
|
||||||
if (text.length > 100000) {
|
if (text.length > 100000) {
|
||||||
throw new CustomError('text must be less than 100k chars', 'apierror');
|
throw new CustomError("text must be less than 100k chars", "apierror");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,14 +151,11 @@ exports.listAllPads = async () => {
|
||||||
return { padIDs };
|
return { padIDs };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// checks if a pad exists
|
// checks if a pad exists
|
||||||
exports.doesPadExist = async (padId: string) => {
|
exports.doesPadExist = async (padId: string) => {
|
||||||
const value = await db.get(`pad:${padId}`);
|
const value = await db.get(`pad:${padId}`);
|
||||||
|
|
||||||
return (value != null && value.atext);
|
return value != null && value.atext;
|
||||||
};
|
};
|
||||||
|
|
||||||
// alias for backwards compatibility
|
// alias for backwards compatibility
|
||||||
|
@ -167,8 +166,8 @@ exports.doesPadExists = exports.doesPadExist;
|
||||||
* time, and allow us to "play back" these changes so legacy padIds can be found.
|
* time, and allow us to "play back" these changes so legacy padIds can be found.
|
||||||
*/
|
*/
|
||||||
const padIdTransforms = [
|
const padIdTransforms = [
|
||||||
[/\s+/g, '_'],
|
[/\s+/g, "_"],
|
||||||
[/:+/g, '_'],
|
[/:+/g, "_"],
|
||||||
];
|
];
|
||||||
|
|
||||||
// returns a sanitized padId, respecting legacy pad id formats
|
// returns a sanitized padId, respecting legacy pad id formats
|
||||||
|
@ -192,7 +191,8 @@ exports.sanitizePadId = async (padId: string) => {
|
||||||
return padId;
|
return padId;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.isValidPadId = (padId: string) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
|
exports.isValidPadId = (padId: string) =>
|
||||||
|
/^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the pad from database and unloads it.
|
* Removes the pad from database and unloads it.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* The ReadOnlyManager manages the database and rendering releated to read only pads
|
* The ReadOnlyManager manages the database and rendering releated to read only pads
|
||||||
*/
|
*/
|
||||||
|
@ -19,17 +19,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const db = require("./DB");
|
||||||
const db = require('./DB');
|
const randomString = require("../utils/randomstring");
|
||||||
const randomString = require('../utils/randomstring');
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if the id pattern matches a read-only pad id
|
* checks if the id pattern matches a read-only pad id
|
||||||
* @param {String} id the pad's id
|
* @param {String} id the pad's id
|
||||||
* @return {Boolean} true if the id is readonly
|
* @return {Boolean} true if the id is readonly
|
||||||
*/
|
*/
|
||||||
exports.isReadOnlyId = (id:string) => id.startsWith('r.');
|
exports.isReadOnlyId = (id: string) => id.startsWith("r.");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a read only id for a pad
|
* returns a read only id for a pad
|
||||||
|
@ -57,7 +55,8 @@ exports.getReadOnlyId = async (padId:string) => {
|
||||||
* @param {String} readOnlyId read only id
|
* @param {String} readOnlyId read only id
|
||||||
* @return {String} the padId
|
* @return {String} the padId
|
||||||
*/
|
*/
|
||||||
exports.getPadId = async (readOnlyId:string) => await db.get(`readonly2pad:${readOnlyId}`);
|
exports.getPadId = async (readOnlyId: string) =>
|
||||||
|
await db.get(`readonly2pad:${readOnlyId}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns the padId and readonlyPadId in an object for any id
|
* returns the padId and readonlyPadId in an object for any id
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* Controls the security of pad access
|
* Controls the security of pad access
|
||||||
*/
|
*/
|
||||||
|
@ -21,18 +21,18 @@
|
||||||
|
|
||||||
import { UserSettingsObject } from "../types/UserSettingsObject";
|
import { UserSettingsObject } from "../types/UserSettingsObject";
|
||||||
|
|
||||||
const authorManager = require('./AuthorManager');
|
const authorManager = require("./AuthorManager");
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||||
const padManager = require('./PadManager');
|
const padManager = require("./PadManager");
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
const readOnlyManager = require("./ReadOnlyManager");
|
||||||
const sessionManager = require('./SessionManager');
|
const sessionManager = require("./SessionManager");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const webaccess = require('../hooks/express/webaccess');
|
const webaccess = require("../hooks/express/webaccess");
|
||||||
const log4js = require('log4js');
|
const log4js = require("log4js");
|
||||||
const authLogger = log4js.getLogger('auth');
|
const authLogger = log4js.getLogger("auth");
|
||||||
const {padutils} = require('../../static/js/pad_utils');
|
const { padutils } = require("../../static/js/pad_utils");
|
||||||
|
|
||||||
const DENY = Object.freeze({accessStatus: 'deny'});
|
const DENY = Object.freeze({ accessStatus: "deny" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the user can access a pad.
|
* Determines whether the user can access a pad.
|
||||||
|
@ -57,9 +57,14 @@ const DENY = Object.freeze({accessStatus: 'deny'});
|
||||||
* @param {Object} userSettings
|
* @param {Object} userSettings
|
||||||
* @return {DENY|{accessStatus: String, authorID: String}}
|
* @return {DENY|{accessStatus: String, authorID: String}}
|
||||||
*/
|
*/
|
||||||
exports.checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject) => {
|
exports.checkAccess = async (
|
||||||
|
padID: string,
|
||||||
|
sessionCookie: string,
|
||||||
|
token: string,
|
||||||
|
userSettings: UserSettingsObject,
|
||||||
|
) => {
|
||||||
if (!padID) {
|
if (!padID) {
|
||||||
authLogger.debug('access denied: missing padID');
|
authLogger.debug("access denied: missing padID");
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +74,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
canCreate = false;
|
canCreate = false;
|
||||||
padID = await readOnlyManager.getPadId(padID);
|
padID = await readOnlyManager.getPadId(padID);
|
||||||
if (padID == null) {
|
if (padID == null) {
|
||||||
authLogger.debug('access denied: read-only pad ID for a pad that does not exist');
|
authLogger.debug(
|
||||||
|
"access denied: read-only pad ID for a pad that does not exist",
|
||||||
|
);
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,62 +84,82 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
// Authentication and authorization checks.
|
// Authentication and authorization checks.
|
||||||
if (settings.loadTest) {
|
if (settings.loadTest) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'bypassing socket.io authentication and authorization checks due to settings.loadTest');
|
"bypassing socket.io authentication and authorization checks due to settings.loadTest",
|
||||||
|
);
|
||||||
} else if (settings.requireAuthentication) {
|
} else if (settings.requireAuthentication) {
|
||||||
if (userSettings == null) {
|
if (userSettings == null) {
|
||||||
authLogger.debug('access denied: authentication is required');
|
authLogger.debug("access denied: authentication is required");
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false;
|
if (userSettings.canCreate != null && !userSettings.canCreate)
|
||||||
|
canCreate = false;
|
||||||
if (userSettings.readOnly) canCreate = false;
|
if (userSettings.readOnly) canCreate = false;
|
||||||
// Note: userSettings.padAuthorizations should still be populated even if
|
// Note: userSettings.padAuthorizations should still be populated even if
|
||||||
// settings.requireAuthorization is false.
|
// settings.requireAuthorization is false.
|
||||||
const padAuthzs = userSettings.padAuthorizations || {};
|
const padAuthzs = userSettings.padAuthorizations || {};
|
||||||
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
|
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
|
||||||
if (!level) {
|
if (!level) {
|
||||||
authLogger.debug('access denied: unauthorized');
|
authLogger.debug("access denied: unauthorized");
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
if (level !== 'create') canCreate = false;
|
if (level !== "create") canCreate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow plugins to deny access
|
// allow plugins to deny access
|
||||||
const isFalse = (x: boolean) => x === false;
|
const isFalse = (x: boolean) => x === false;
|
||||||
if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
|
if (
|
||||||
authLogger.debug('access denied: an onAccessCheck hook function returned false');
|
hooks
|
||||||
|
.callAll("onAccessCheck", { padID, token, sessionCookie })
|
||||||
|
.some(isFalse)
|
||||||
|
) {
|
||||||
|
authLogger.debug(
|
||||||
|
"access denied: an onAccessCheck hook function returned false",
|
||||||
|
);
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const padExists = await padManager.doesPadExist(padID);
|
const padExists = await padManager.doesPadExist(padID);
|
||||||
if (!padExists && !canCreate) {
|
if (!padExists && !canCreate) {
|
||||||
authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
|
authLogger.debug(
|
||||||
|
"access denied: user attempted to create a pad, which is prohibited",
|
||||||
|
);
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie);
|
const sessionAuthorID = await sessionManager.findAuthorID(
|
||||||
|
padID.split("$")[0],
|
||||||
|
sessionCookie,
|
||||||
|
);
|
||||||
if (settings.requireSession && !sessionAuthorID) {
|
if (settings.requireSession && !sessionAuthorID) {
|
||||||
authLogger.debug('access denied: HTTP API session is required');
|
authLogger.debug("access denied: HTTP API session is required");
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
if (!sessionAuthorID && token != null && !padutils.isValidAuthorToken(token)) {
|
if (
|
||||||
|
!sessionAuthorID &&
|
||||||
|
token != null &&
|
||||||
|
!padutils.isValidAuthorToken(token)
|
||||||
|
) {
|
||||||
// The author token should be kept secret, so do not log it.
|
// The author token should be kept secret, so do not log it.
|
||||||
authLogger.debug('access denied: invalid author token');
|
authLogger.debug("access denied: invalid author token");
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const grant = {
|
const grant = {
|
||||||
accessStatus: 'grant',
|
accessStatus: "grant",
|
||||||
authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings),
|
authorID:
|
||||||
|
sessionAuthorID || (await authorManager.getAuthorId(token, userSettings)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!padID.includes('$')) {
|
if (!padID.includes("$")) {
|
||||||
// Only group pads can be private, so there is nothing more to check for this non-group pad.
|
// Only group pads can be private, so there is nothing more to check for this non-group pad.
|
||||||
return grant;
|
return grant;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!padExists) {
|
if (!padExists) {
|
||||||
if (sessionAuthorID == null) {
|
if (sessionAuthorID == null) {
|
||||||
authLogger.debug('access denied: must have an HTTP API session to create a group pad');
|
authLogger.debug(
|
||||||
|
"access denied: must have an HTTP API session to create a group pad",
|
||||||
|
);
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
// Creating a group pad, so there is no public status to check.
|
// Creating a group pad, so there is no public status to check.
|
||||||
|
@ -142,7 +169,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
const pad = await padManager.getPad(padID);
|
const pad = await padManager.getPad(padID);
|
||||||
|
|
||||||
if (!pad.getPublicStatus() && sessionAuthorID == null) {
|
if (!pad.getPublicStatus() && sessionAuthorID == null) {
|
||||||
authLogger.debug('access denied: must have an HTTP API session to access private group pads');
|
authLogger.debug(
|
||||||
|
"access denied: must have an HTTP API session to access private group pads",
|
||||||
|
);
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* The Session Manager provides functions to manage session in the database,
|
* The Session Manager provides functions to manage session in the database,
|
||||||
* it only provides session management for sessions created by the API
|
* it only provides session management for sessions created by the API
|
||||||
|
@ -20,12 +20,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const promises = require('../utils/promises');
|
const promises = require("../utils/promises");
|
||||||
const randomString = require('../utils/randomstring');
|
const randomString = require("../utils/randomstring");
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const groupManager = require('./GroupManager');
|
const groupManager = require("./GroupManager");
|
||||||
const authorManager = require('./AuthorManager');
|
const authorManager = require("./AuthorManager");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the author ID for a session with matching ID and group.
|
* Finds the author ID for a session with matching ID and group.
|
||||||
|
@ -61,13 +61,15 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
* Fixes #3819.
|
* Fixes #3819.
|
||||||
* Also, see #3820.
|
* Also, see #3820.
|
||||||
*/
|
*/
|
||||||
const sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(',');
|
const sessionIDs = sessionCookie.replace(/^"|"$/g, "").split(",");
|
||||||
const sessionInfoPromises = sessionIDs.map(async (id) => {
|
const sessionInfoPromises = sessionIDs.map(async (id) => {
|
||||||
try {
|
try {
|
||||||
return await exports.getSessionInfo(id);
|
return await exports.getSessionInfo(id);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.message === 'sessionID does not exist') {
|
if (err.message === "sessionID does not exist") {
|
||||||
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
|
console.debug(
|
||||||
|
`SessionManager getAuthorID: no session exists with ID ${id}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -75,11 +77,16 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const isMatch = (si: {
|
const isMatch = (
|
||||||
|
si: {
|
||||||
groupID: string;
|
groupID: string;
|
||||||
validUntil: number;
|
validUntil: number;
|
||||||
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
} | null,
|
||||||
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
|
) => si != null && si.groupID === groupID && now < si.validUntil;
|
||||||
|
const sessionInfo = await promises.firstSatisfies(
|
||||||
|
sessionInfoPromises,
|
||||||
|
isMatch,
|
||||||
|
);
|
||||||
if (sessionInfo == null) return undefined;
|
if (sessionInfo == null) return undefined;
|
||||||
return sessionInfo.authorID;
|
return sessionInfo.authorID;
|
||||||
};
|
};
|
||||||
|
@ -92,7 +99,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
exports.doesSessionExist = async (sessionID: string) => {
|
exports.doesSessionExist = async (sessionID: string) => {
|
||||||
// check if the database entry of this session exists
|
// check if the database entry of this session exists
|
||||||
const session = await db.get(`session:${sessionID}`);
|
const session = await db.get(`session:${sessionID}`);
|
||||||
return (session != null);
|
return session != null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,42 +109,46 @@ exports.doesSessionExist = async (sessionID: string) => {
|
||||||
* @param {Number} validUntil The unix timestamp when the session should expire
|
* @param {Number} validUntil The unix timestamp when the session should expire
|
||||||
* @return {Promise<{sessionID: string}>} the id of the new session
|
* @return {Promise<{sessionID: string}>} the id of the new session
|
||||||
*/
|
*/
|
||||||
exports.createSession = async (groupID: string, authorID: string, validUntil: number) => {
|
exports.createSession = async (
|
||||||
|
groupID: string,
|
||||||
|
authorID: string,
|
||||||
|
validUntil: number,
|
||||||
|
) => {
|
||||||
// check if the group exists
|
// check if the group exists
|
||||||
const groupExists = await groupManager.doesGroupExist(groupID);
|
const groupExists = await groupManager.doesGroupExist(groupID);
|
||||||
if (!groupExists) {
|
if (!groupExists) {
|
||||||
throw new CustomError('groupID does not exist', 'apierror');
|
throw new CustomError("groupID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the author exists
|
// check if the author exists
|
||||||
const authorExists = await authorManager.doesAuthorExist(authorID);
|
const authorExists = await authorManager.doesAuthorExist(authorID);
|
||||||
if (!authorExists) {
|
if (!authorExists) {
|
||||||
throw new CustomError('authorID does not exist', 'apierror');
|
throw new CustomError("authorID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to parse validUntil if it's not a number
|
// try to parse validUntil if it's not a number
|
||||||
if (typeof validUntil !== 'number') {
|
if (typeof validUntil !== "number") {
|
||||||
validUntil = parseInt(validUntil);
|
validUntil = parseInt(validUntil);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check it's a valid number
|
// check it's a valid number
|
||||||
if (isNaN(validUntil)) {
|
if (isNaN(validUntil)) {
|
||||||
throw new CustomError('validUntil is not a number', 'apierror');
|
throw new CustomError("validUntil is not a number", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure this is not a negative number
|
// ensure this is not a negative number
|
||||||
if (validUntil < 0) {
|
if (validUntil < 0) {
|
||||||
throw new CustomError('validUntil is a negative number', 'apierror');
|
throw new CustomError("validUntil is a negative number", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure this is not a float value
|
// ensure this is not a float value
|
||||||
if (!isInt(validUntil)) {
|
if (!isInt(validUntil)) {
|
||||||
throw new CustomError('validUntil is a float value', 'apierror');
|
throw new CustomError("validUntil is a float value", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if validUntil is in the future
|
// check if validUntil is in the future
|
||||||
if (validUntil < Math.floor(Date.now() / 1000)) {
|
if (validUntil < Math.floor(Date.now() / 1000)) {
|
||||||
throw new CustomError('validUntil is in the past', 'apierror');
|
throw new CustomError("validUntil is in the past", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate sessionID
|
// generate sessionID
|
||||||
|
@ -151,8 +162,8 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
||||||
// property, and writes the result.
|
// property, and writes the result.
|
||||||
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1),
|
db.setSub(`group2sessions:${groupID}`, ["sessionIDs", sessionID], 1),
|
||||||
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1),
|
db.setSub(`author2sessions:${authorID}`, ["sessionIDs", sessionID], 1),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { sessionID };
|
return { sessionID };
|
||||||
|
@ -169,7 +180,7 @@ exports.getSessionInfo = async (sessionID:string) => {
|
||||||
|
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
// session does not exist
|
// session does not exist
|
||||||
throw new CustomError('sessionID does not exist', 'apierror');
|
throw new CustomError("sessionID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// everything is fine, return the sessioninfos
|
// everything is fine, return the sessioninfos
|
||||||
|
@ -185,7 +196,7 @@ exports.deleteSession = async (sessionID:string) => {
|
||||||
// ensure that the session exists
|
// ensure that the session exists
|
||||||
const session = await db.get(`session:${sessionID}`);
|
const session = await db.get(`session:${sessionID}`);
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
throw new CustomError('sessionID does not exist', 'apierror');
|
throw new CustomError("sessionID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// everything is fine, use the sessioninfos
|
// everything is fine, use the sessioninfos
|
||||||
|
@ -196,8 +207,16 @@ exports.deleteSession = async (sessionID:string) => {
|
||||||
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
||||||
// property, and writes the result. Setting a property to `undefined` deletes that property
|
// property, and writes the result. Setting a property to `undefined` deletes that property
|
||||||
// (JSON.stringify() ignores such properties).
|
// (JSON.stringify() ignores such properties).
|
||||||
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined),
|
db.setSub(
|
||||||
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined),
|
`group2sessions:${groupID}`,
|
||||||
|
["sessionIDs", sessionID],
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
db.setSub(
|
||||||
|
`author2sessions:${authorID}`,
|
||||||
|
["sessionIDs", sessionID],
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Delete the session record after updating group2sessions and author2sessions so that the state
|
// Delete the session record after updating group2sessions and author2sessions so that the state
|
||||||
|
@ -214,7 +233,7 @@ exports.listSessionsOfGroup = async (groupID: string) => {
|
||||||
// check that the group exists
|
// check that the group exists
|
||||||
const exists = await groupManager.doesGroupExist(groupID);
|
const exists = await groupManager.doesGroupExist(groupID);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new CustomError('groupID does not exist', 'apierror');
|
throw new CustomError("groupID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`);
|
const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`);
|
||||||
|
@ -230,7 +249,7 @@ exports.listSessionsOfAuthor = async (authorID: string) => {
|
||||||
// check that the author exists
|
// check that the author exists
|
||||||
const exists = await authorManager.doesAuthorExist(authorID);
|
const exists = await authorManager.doesAuthorExist(authorID);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new CustomError('authorID does not exist', 'apierror');
|
throw new CustomError("authorID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await listSessionsWithDBKey(`author2sessions:${authorID}`);
|
return await listSessionsWithDBKey(`author2sessions:${authorID}`);
|
||||||
|
@ -253,7 +272,7 @@ const listSessionsWithDBKey = async (dbkey: string) => {
|
||||||
try {
|
try {
|
||||||
sessions[sessionID] = await exports.getSessionInfo(sessionID);
|
sessions[sessionID] = await exports.getSessionInfo(sessionID);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name === 'apierror') {
|
if (err.name === "apierror") {
|
||||||
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
|
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
|
||||||
sessions[sessionID] = null;
|
sessions[sessionID] = null;
|
||||||
} else {
|
} else {
|
||||||
|
@ -265,11 +284,11 @@ const listSessionsWithDBKey = async (dbkey: string) => {
|
||||||
return sessions;
|
return sessions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if a number is an int
|
* checks if a number is an int
|
||||||
* @param {number|string} value
|
* @param {number|string} value
|
||||||
* @return {boolean} If the value is an integer
|
* @return {boolean} If the value is an integer
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const isInt = (value:number|string): boolean => (parseFloat(value) === parseInt(value)) && !isNaN(value);
|
const isInt = (value: number | string): boolean =>
|
||||||
|
parseFloat(value) === parseInt(value) && !isNaN(value);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
const DB = require('./DB');
|
const DB = require("./DB");
|
||||||
const Store = require('express-session').Store;
|
const Store = require("express-session").Store;
|
||||||
const log4js = require('log4js');
|
const log4js = require("log4js");
|
||||||
const util = require('util');
|
const util = require("util");
|
||||||
|
|
||||||
const logger = log4js.getLogger('SessionStore');
|
const logger = log4js.getLogger("SessionStore");
|
||||||
|
|
||||||
class SessionStore extends Store {
|
class SessionStore extends Store {
|
||||||
/**
|
/**
|
||||||
|
@ -38,7 +38,9 @@ class SessionStore extends Store {
|
||||||
const exp = this._expirations.get(sid) || {};
|
const exp = this._expirations.get(sid) || {};
|
||||||
clearTimeout(exp.timeout);
|
clearTimeout(exp.timeout);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const {cookie: {expires} = {}} = sess || {};
|
const {
|
||||||
|
cookie: { expires } = {},
|
||||||
|
} = sess || {};
|
||||||
if (expires) {
|
if (expires) {
|
||||||
const sessExp = new Date(expires).getTime();
|
const sessExp = new Date(expires).getTime();
|
||||||
if (updateDbExp) exp.db = sessExp;
|
if (updateDbExp) exp.db = sessExp;
|
||||||
|
@ -47,7 +49,8 @@ class SessionStore extends Store {
|
||||||
if (exp.real <= now) return await this._destroy(sid);
|
if (exp.real <= now) return await this._destroy(sid);
|
||||||
// If reading from the database, update the expiration with the latest value from touch() so
|
// If reading from the database, update the expiration with the latest value from touch() so
|
||||||
// that touch() appears to write to the database every time even though it doesn't.
|
// that touch() appears to write to the database every time even though it doesn't.
|
||||||
if (typeof expires === 'string') sess.cookie.expires = new Date(exp.real).toJSON();
|
if (typeof expires === "string")
|
||||||
|
sess.cookie.expires = new Date(exp.real).toJSON();
|
||||||
// Use this._get(), not this._destroy(), to destroy the DB record for the expired session.
|
// Use this._get(), not this._destroy(), to destroy the DB record for the expired session.
|
||||||
// This is done in case multiple Etherpad instances are sharing the same database and users
|
// This is done in case multiple Etherpad instances are sharing the same database and users
|
||||||
// are bouncing between the instances. By using this._get(), this instance will query the DB
|
// are bouncing between the instances. By using this._get(), this instance will query the DB
|
||||||
|
@ -99,7 +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 if it didn't already exist, but we have no way of knowing that without querying the
|
||||||
// database. The query overhead is not worth it because set() should be called soon anyway.
|
// database. The query overhead is not worth it because set() should be called soon anyway.
|
||||||
if (exp == null) return;
|
if (exp == null) return;
|
||||||
if (exp.db != null && (this._refresh == null || exp.real < exp.db + this._refresh)) return;
|
if (
|
||||||
|
exp.db != null &&
|
||||||
|
(this._refresh == null || exp.real < exp.db + this._refresh)
|
||||||
|
)
|
||||||
|
return;
|
||||||
await this._write(sid, sess);
|
await this._write(sid, sess);
|
||||||
exp.db = new Date(sess.cookie.expires).getTime();
|
exp.db = new Date(sess.cookie.expires).getTime();
|
||||||
}
|
}
|
||||||
|
@ -107,7 +114,7 @@ class SessionStore extends Store {
|
||||||
|
|
||||||
// express-session doesn't support Promise-based methods. This is where the callbackified versions
|
// express-session doesn't support Promise-based methods. This is where the callbackified versions
|
||||||
// used by express-session are defined.
|
// used by express-session are defined.
|
||||||
for (const m of ['get', 'set', 'destroy', 'touch']) {
|
for (const m of ["get", "set", "destroy", "touch"]) {
|
||||||
SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]);
|
SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
|
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
|
||||||
*
|
*
|
||||||
|
@ -20,13 +20,13 @@
|
||||||
* require("./index").require("./path/to/template.ejs")
|
* require("./index").require("./path/to/template.ejs")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ejs = require('ejs');
|
const ejs = require("ejs");
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const resolve = require('resolve');
|
const resolve = require("resolve");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
import {pluginInstallPath} from '../../static/js/pluginfw/installer'
|
import { pluginInstallPath } from "../../static/js/pluginfw/installer";
|
||||||
|
|
||||||
const templateCache = new Map();
|
const templateCache = new Map();
|
||||||
|
|
||||||
|
@ -37,7 +37,8 @@ exports.info = {
|
||||||
args: [],
|
args: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1];
|
const getCurrentFile = () =>
|
||||||
|
exports.info.file_stack[exports.info.file_stack.length - 1];
|
||||||
|
|
||||||
exports._init = (b: any, recursive: boolean) => {
|
exports._init = (b: any, recursive: boolean) => {
|
||||||
exports.info.__output_stack.push(exports.info.__output);
|
exports.info.__output_stack.push(exports.info.__output);
|
||||||
|
@ -51,7 +52,7 @@ exports._exit = (b:any, recursive:boolean) => {
|
||||||
exports.begin_block = (name: string) => {
|
exports.begin_block = (name: string) => {
|
||||||
exports.info.block_stack.push(name);
|
exports.info.block_stack.push(name);
|
||||||
exports.info.__output_stack.push(exports.info.__output.get());
|
exports.info.__output_stack.push(exports.info.__output.get());
|
||||||
exports.info.__output.set('');
|
exports.info.__output.set("");
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.end_block = () => {
|
exports.end_block = () => {
|
||||||
|
@ -64,13 +65,17 @@ exports.end_block = () => {
|
||||||
exports.info.__output.set(exports.info.__output.get().concat(args.content));
|
exports.info.__output.set(exports.info.__output.get().concat(args.content));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.require = (name:string, args:{
|
exports.require = (
|
||||||
e?: Function,
|
name: string,
|
||||||
require?: Function,
|
args: {
|
||||||
}, mod:{
|
e?: Function;
|
||||||
filename:string,
|
require?: Function;
|
||||||
paths:string[],
|
},
|
||||||
}) => {
|
mod: {
|
||||||
|
filename: string;
|
||||||
|
paths: string[];
|
||||||
|
},
|
||||||
|
) => {
|
||||||
if (args == null) args = {};
|
if (args == null) args = {};
|
||||||
|
|
||||||
let basedir = __dirname;
|
let basedir = __dirname;
|
||||||
|
@ -88,19 +93,26 @@ exports.require = (name:string, args:{
|
||||||
* Add the plugin install path to the paths array
|
* Add the plugin install path to the paths array
|
||||||
*/
|
*/
|
||||||
if (!paths.includes(pluginInstallPath)) {
|
if (!paths.includes(pluginInstallPath)) {
|
||||||
paths.push(pluginInstallPath)
|
paths.push(pluginInstallPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ejspath = resolve.sync(name, {paths, basedir, extensions: ['.html', '.ejs']});
|
const ejspath = resolve.sync(name, {
|
||||||
|
paths,
|
||||||
|
basedir,
|
||||||
|
extensions: [".html", ".ejs"],
|
||||||
|
});
|
||||||
|
|
||||||
args.e = exports;
|
args.e = exports;
|
||||||
args.require = require;
|
args.require = require;
|
||||||
|
|
||||||
const cache = settings.maxAge !== 0;
|
const cache = settings.maxAge !== 0;
|
||||||
const template = cache && templateCache.get(ejspath) || ejs.compile(
|
const template =
|
||||||
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
|
(cache && templateCache.get(ejspath)) ||
|
||||||
|
ejs.compile(
|
||||||
|
"<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>" +
|
||||||
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
|
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
|
||||||
{filename: ejspath});
|
{ filename: ejspath },
|
||||||
|
);
|
||||||
if (cache) templateCache.set(ejspath, template);
|
if (cache) templateCache.set(ejspath, template);
|
||||||
|
|
||||||
exports.info.args.push(args);
|
exports.info.args.push(args);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* The API Handler handles all API http requests
|
* The API Handler handles all API http requests
|
||||||
*/
|
*/
|
||||||
|
@ -21,9 +21,9 @@
|
||||||
|
|
||||||
import { MapArrayType } from "../types/MapType";
|
import { MapArrayType } from "../types/MapType";
|
||||||
|
|
||||||
const api = require('../db/API');
|
const api = require("../db/API");
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require("../db/PadManager");
|
||||||
import createHTTPError from 'http-errors';
|
import createHTTPError from "http-errors";
|
||||||
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
|
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
|
||||||
import { publicKeyExported } from "../security/OAuth2Provider";
|
import { publicKeyExported } from "../security/OAuth2Provider";
|
||||||
import { jwtVerify } from "jose";
|
import { jwtVerify } from "jose";
|
||||||
|
@ -31,128 +31,127 @@ import {jwtVerify} from "jose";
|
||||||
// a list of all functions
|
// a list of all functions
|
||||||
const version: MapArrayType<any> = {};
|
const version: MapArrayType<any> = {};
|
||||||
|
|
||||||
version['1'] = {
|
version["1"] = {
|
||||||
createGroup: [],
|
createGroup: [],
|
||||||
createGroupIfNotExistsFor: ['groupMapper'],
|
createGroupIfNotExistsFor: ["groupMapper"],
|
||||||
deleteGroup: ['groupID'],
|
deleteGroup: ["groupID"],
|
||||||
listPads: ['groupID'],
|
listPads: ["groupID"],
|
||||||
createPad: ['padID', 'text'],
|
createPad: ["padID", "text"],
|
||||||
createGroupPad: ['groupID', 'padName', 'text'],
|
createGroupPad: ["groupID", "padName", "text"],
|
||||||
createAuthor: ['name'],
|
createAuthor: ["name"],
|
||||||
createAuthorIfNotExistsFor: ['authorMapper', 'name'],
|
createAuthorIfNotExistsFor: ["authorMapper", "name"],
|
||||||
listPadsOfAuthor: ['authorID'],
|
listPadsOfAuthor: ["authorID"],
|
||||||
createSession: ['groupID', 'authorID', 'validUntil'],
|
createSession: ["groupID", "authorID", "validUntil"],
|
||||||
deleteSession: ['sessionID'],
|
deleteSession: ["sessionID"],
|
||||||
getSessionInfo: ['sessionID'],
|
getSessionInfo: ["sessionID"],
|
||||||
listSessionsOfGroup: ['groupID'],
|
listSessionsOfGroup: ["groupID"],
|
||||||
listSessionsOfAuthor: ['authorID'],
|
listSessionsOfAuthor: ["authorID"],
|
||||||
getText: ['padID', 'rev'],
|
getText: ["padID", "rev"],
|
||||||
setText: ['padID', 'text'],
|
setText: ["padID", "text"],
|
||||||
getHTML: ['padID', 'rev'],
|
getHTML: ["padID", "rev"],
|
||||||
setHTML: ['padID', 'html'],
|
setHTML: ["padID", "html"],
|
||||||
getRevisionsCount: ['padID'],
|
getRevisionsCount: ["padID"],
|
||||||
getLastEdited: ['padID'],
|
getLastEdited: ["padID"],
|
||||||
deletePad: ['padID'],
|
deletePad: ["padID"],
|
||||||
getReadOnlyID: ['padID'],
|
getReadOnlyID: ["padID"],
|
||||||
setPublicStatus: ['padID', 'publicStatus'],
|
setPublicStatus: ["padID", "publicStatus"],
|
||||||
getPublicStatus: ['padID'],
|
getPublicStatus: ["padID"],
|
||||||
listAuthorsOfPad: ['padID'],
|
listAuthorsOfPad: ["padID"],
|
||||||
padUsersCount: ['padID'],
|
padUsersCount: ["padID"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.1'] = {
|
version["1.1"] = {
|
||||||
...version['1'],
|
...version["1"],
|
||||||
getAuthorName: ['authorID'],
|
getAuthorName: ["authorID"],
|
||||||
padUsers: ['padID'],
|
padUsers: ["padID"],
|
||||||
sendClientsMessage: ['padID', 'msg'],
|
sendClientsMessage: ["padID", "msg"],
|
||||||
listAllGroups: [],
|
listAllGroups: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2'] = {
|
version["1.2"] = {
|
||||||
...version['1.1'],
|
...version["1.1"],
|
||||||
checkToken: [],
|
checkToken: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.1'] = {
|
version["1.2.1"] = {
|
||||||
...version['1.2'],
|
...version["1.2"],
|
||||||
listAllPads: [],
|
listAllPads: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.7'] = {
|
version["1.2.7"] = {
|
||||||
...version['1.2.1'],
|
...version["1.2.1"],
|
||||||
createDiffHTML: ['padID', 'startRev', 'endRev'],
|
createDiffHTML: ["padID", "startRev", "endRev"],
|
||||||
getChatHistory: ['padID', 'start', 'end'],
|
getChatHistory: ["padID", "start", "end"],
|
||||||
getChatHead: ['padID'],
|
getChatHead: ["padID"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.8'] = {
|
version["1.2.8"] = {
|
||||||
...version['1.2.7'],
|
...version["1.2.7"],
|
||||||
getAttributePool: ['padID'],
|
getAttributePool: ["padID"],
|
||||||
getRevisionChangeset: ['padID', 'rev'],
|
getRevisionChangeset: ["padID", "rev"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.9'] = {
|
version["1.2.9"] = {
|
||||||
...version['1.2.8'],
|
...version["1.2.8"],
|
||||||
copyPad: ['sourceID', 'destinationID', 'force'],
|
copyPad: ["sourceID", "destinationID", "force"],
|
||||||
movePad: ['sourceID', 'destinationID', 'force'],
|
movePad: ["sourceID", "destinationID", "force"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.10'] = {
|
version["1.2.10"] = {
|
||||||
...version['1.2.9'],
|
...version["1.2.9"],
|
||||||
getPadID: ['roID'],
|
getPadID: ["roID"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.11'] = {
|
version["1.2.11"] = {
|
||||||
...version['1.2.10'],
|
...version["1.2.10"],
|
||||||
getSavedRevisionsCount: ['padID'],
|
getSavedRevisionsCount: ["padID"],
|
||||||
listSavedRevisions: ['padID'],
|
listSavedRevisions: ["padID"],
|
||||||
saveRevision: ['padID', 'rev'],
|
saveRevision: ["padID", "rev"],
|
||||||
restoreRevision: ['padID', 'rev'],
|
restoreRevision: ["padID", "rev"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.12'] = {
|
version["1.2.12"] = {
|
||||||
...version['1.2.11'],
|
...version["1.2.11"],
|
||||||
appendChatMessage: ['padID', 'text', 'authorID', 'time'],
|
appendChatMessage: ["padID", "text", "authorID", "time"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.13'] = {
|
version["1.2.13"] = {
|
||||||
...version['1.2.12'],
|
...version["1.2.12"],
|
||||||
appendText: ['padID', 'text'],
|
appendText: ["padID", "text"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.14'] = {
|
version["1.2.14"] = {
|
||||||
...version['1.2.13'],
|
...version["1.2.13"],
|
||||||
getStats: [],
|
getStats: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.15'] = {
|
version["1.2.15"] = {
|
||||||
...version['1.2.14'],
|
...version["1.2.14"],
|
||||||
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force'],
|
copyPadWithoutHistory: ["sourceID", "destinationID", "force"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.3.0'] = {
|
version["1.3.0"] = {
|
||||||
...version['1.2.15'],
|
...version["1.2.15"],
|
||||||
appendText: ['padID', 'text', 'authorId'],
|
appendText: ["padID", "text", "authorId"],
|
||||||
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId'],
|
copyPadWithoutHistory: ["sourceID", "destinationID", "force", "authorId"],
|
||||||
createGroupPad: ['groupID', 'padName', 'text', 'authorId'],
|
createGroupPad: ["groupID", "padName", "text", "authorId"],
|
||||||
createPad: ['padID', 'text', 'authorId'],
|
createPad: ["padID", "text", "authorId"],
|
||||||
restoreRevision: ['padID', 'rev', 'authorId'],
|
restoreRevision: ["padID", "rev", "authorId"],
|
||||||
setHTML: ['padID', 'html', 'authorId'],
|
setHTML: ["padID", "html", "authorId"],
|
||||||
setText: ['padID', 'text', 'authorId'],
|
setText: ["padID", "text", "authorId"],
|
||||||
};
|
};
|
||||||
|
|
||||||
// set the latest available API version here
|
// set the latest available API version here
|
||||||
exports.latestApiVersion = '1.3.0';
|
exports.latestApiVersion = "1.3.0";
|
||||||
|
|
||||||
// exports the versions so it can be used by the new Swagger endpoint
|
// exports the versions so it can be used by the new Swagger endpoint
|
||||||
exports.version = version;
|
exports.version = version;
|
||||||
|
|
||||||
|
|
||||||
type APIFields = {
|
type APIFields = {
|
||||||
api_key: string;
|
api_key: string;
|
||||||
padID: string;
|
padID: string;
|
||||||
padName: string;
|
padName: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles an HTTP API call
|
* Handles an HTTP API call
|
||||||
|
@ -162,31 +161,37 @@ type APIFields = {
|
||||||
* @param req express request object
|
* @param req express request object
|
||||||
* @param res express response object
|
* @param res express response object
|
||||||
*/
|
*/
|
||||||
exports.handle = async function (apiVersion: string, functionName: string, fields: APIFields, req: Http2ServerRequest, res: Http2ServerResponse) {
|
exports.handle = async function (
|
||||||
|
apiVersion: string,
|
||||||
|
functionName: string,
|
||||||
|
fields: APIFields,
|
||||||
|
req: Http2ServerRequest,
|
||||||
|
res: Http2ServerResponse,
|
||||||
|
) {
|
||||||
// say goodbye if this is an unknown API version
|
// say goodbye if this is an unknown API version
|
||||||
if (!(apiVersion in version)) {
|
if (!(apiVersion in version)) {
|
||||||
throw new createHTTPError.NotFound('no such api version');
|
throw new createHTTPError.NotFound("no such api version");
|
||||||
}
|
}
|
||||||
|
|
||||||
// say goodbye if this is an unknown function
|
// say goodbye if this is an unknown function
|
||||||
if (!(functionName in version[apiVersion])) {
|
if (!(functionName in version[apiVersion])) {
|
||||||
throw new createHTTPError.NotFound('no such function');
|
throw new createHTTPError.NotFound("no such function");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.headers.authorization) {
|
if (!req.headers.authorization) {
|
||||||
throw new createHTTPError.Unauthorized('no or wrong API Key');
|
throw new createHTTPError.Unauthorized("no or wrong API Key");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await jwtVerify(req.headers.authorization!.replace("Bearer ", ""), publicKeyExported!, {algorithms: ['RS256'],
|
await jwtVerify(
|
||||||
requiredClaims: ["admin"]})
|
req.headers.authorization!.replace("Bearer ", ""),
|
||||||
|
publicKeyExported!,
|
||||||
|
{ algorithms: ["RS256"], requiredClaims: ["admin"] },
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new createHTTPError.Unauthorized('no or wrong API Key');
|
throw new createHTTPError.Unauthorized("no or wrong API Key");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// sanitize any padIDs before continuing
|
// sanitize any padIDs before continuing
|
||||||
if (fields.padID) {
|
if (fields.padID) {
|
||||||
fields.padID = await padManager.sanitizePadId(fields.padID);
|
fields.padID = await padManager.sanitizePadId(fields.padID);
|
||||||
|
@ -200,7 +205,9 @@ exports.handle = async function (apiVersion: string, functionName: string, field
|
||||||
|
|
||||||
// put the function parameters in an array
|
// put the function parameters in an array
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const functionParams = version[apiVersion][functionName].map((field) => fields[field]);
|
const functionParams = version[apiVersion][functionName].map(
|
||||||
|
(field) => fields[field],
|
||||||
|
);
|
||||||
|
|
||||||
// call the api function
|
// call the api function
|
||||||
return api[functionName].apply(this, functionParams);
|
return api[functionName].apply(this, functionParams);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* Handles the export requests
|
* Handles the export requests
|
||||||
*/
|
*/
|
||||||
|
@ -20,15 +20,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const exporthtml = require('../utils/ExportHtml');
|
const exporthtml = require("../utils/ExportHtml");
|
||||||
const exporttxt = require('../utils/ExportTxt');
|
const exporttxt = require("../utils/ExportTxt");
|
||||||
const exportEtherpad = require('../utils/ExportEtherpad');
|
const exportEtherpad = require("../utils/ExportEtherpad");
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
import os from 'os';
|
import os from "os";
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require("../../static/js/pluginfw/hooks");
|
||||||
import util from 'util';
|
import util from "util";
|
||||||
const { checkValidRev } = require('../utils/checkValidRev');
|
const { checkValidRev } = require("../utils/checkValidRev");
|
||||||
|
|
||||||
const fsp_writeFile = util.promisify(fs.writeFile);
|
const fsp_writeFile = util.promisify(fs.writeFile);
|
||||||
const fsp_unlink = util.promisify(fs.unlink);
|
const fsp_unlink = util.promisify(fs.unlink);
|
||||||
|
@ -43,12 +43,18 @@ const tempDirectory = os.tmpdir();
|
||||||
* @param {String} readOnlyId the read only id of the pad to export
|
* @param {String} readOnlyId the read only id of the pad to export
|
||||||
* @param {String} type the type to export
|
* @param {String} type the type to export
|
||||||
*/
|
*/
|
||||||
exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string, type:string) => {
|
exports.doExport = async (
|
||||||
|
req: any,
|
||||||
|
res: any,
|
||||||
|
padId: string,
|
||||||
|
readOnlyId: string,
|
||||||
|
type: string,
|
||||||
|
) => {
|
||||||
// avoid naming the read-only file as the original pad's id
|
// avoid naming the read-only file as the original pad's id
|
||||||
let fileName = readOnlyId ? readOnlyId : padId;
|
let fileName = readOnlyId ? readOnlyId : padId;
|
||||||
|
|
||||||
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
|
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
|
||||||
const hookFileName = await hooks.aCallFirst('exportFileName', padId);
|
const hookFileName = await hooks.aCallFirst("exportFileName", padId);
|
||||||
|
|
||||||
// if fileName is set then set it to the padId, note that fileName is returned as an array.
|
// if fileName is set then set it to the padId, note that fileName is returned as an array.
|
||||||
if (hookFileName.length) {
|
if (hookFileName.length) {
|
||||||
|
@ -66,29 +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
|
// if this is a plain text export, we can do this directly
|
||||||
// We have to over engineer this because tabs are stored as attributes and not plain text
|
// We have to over engineer this because tabs are stored as attributes and not plain text
|
||||||
if (type === 'etherpad') {
|
if (type === "etherpad") {
|
||||||
const pad = await exportEtherpad.getPadRaw(padId, readOnlyId);
|
const pad = await exportEtherpad.getPadRaw(padId, readOnlyId);
|
||||||
res.send(pad);
|
res.send(pad);
|
||||||
} else if (type === 'txt') {
|
} else if (type === "txt") {
|
||||||
const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
|
const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev);
|
||||||
res.send(txt);
|
res.send(txt);
|
||||||
} else {
|
} else {
|
||||||
// render the html document
|
// render the html document
|
||||||
let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev, readOnlyId);
|
let html = await exporthtml.getPadHTMLDocument(
|
||||||
|
padId,
|
||||||
|
req.params.rev,
|
||||||
|
readOnlyId,
|
||||||
|
);
|
||||||
|
|
||||||
// decide what to do with the html export
|
// decide what to do with the html export
|
||||||
|
|
||||||
// if this is a html export, we can send this from here directly
|
// if this is a html export, we can send this from here directly
|
||||||
if (type === 'html') {
|
if (type === "html") {
|
||||||
// do any final changes the plugin might want to make
|
// do any final changes the plugin might want to make
|
||||||
const newHTML = await hooks.aCallFirst('exportHTMLSend', html);
|
const newHTML = await hooks.aCallFirst("exportHTMLSend", html);
|
||||||
if (newHTML.length) html = newHTML;
|
if (newHTML.length) html = newHTML;
|
||||||
res.send(html);
|
res.send(html);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// else write the html export to a file
|
// else write the html export to a file
|
||||||
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
|
const randNum = Math.floor(Math.random() * 0xffffffff);
|
||||||
const srcFile = `${tempDirectory}/etherpad_export_${randNum}.html`;
|
const srcFile = `${tempDirectory}/etherpad_export_${randNum}.html`;
|
||||||
await fsp_writeFile(srcFile, html);
|
await fsp_writeFile(srcFile, html);
|
||||||
|
|
||||||
|
@ -99,13 +109,20 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
|
||||||
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
|
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
|
||||||
|
|
||||||
// Allow plugins to overwrite the convert in export process
|
// Allow plugins to overwrite the convert in export process
|
||||||
const result = await hooks.aCallAll('exportConvert', {srcFile, destFile, req, res});
|
const result = await hooks.aCallAll("exportConvert", {
|
||||||
|
srcFile,
|
||||||
|
destFile,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
});
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
// console.log("export handled by plugin", destFile);
|
// console.log("export handled by plugin", destFile);
|
||||||
} else {
|
} else {
|
||||||
const converter =
|
const converter =
|
||||||
settings.soffice != null ? require('../utils/LibreOffice')
|
settings.soffice != null
|
||||||
: settings.abiword != null ? require('../utils/Abiword')
|
? require("../utils/LibreOffice")
|
||||||
|
: settings.abiword != null
|
||||||
|
? require("../utils/Abiword")
|
||||||
: null;
|
: null;
|
||||||
await converter.convertFile(srcFile, destFile, type);
|
await converter.convertFile(srcFile, destFile, type);
|
||||||
}
|
}
|
||||||
|
@ -117,7 +134,7 @@ exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string,
|
||||||
await fsp_unlink(srcFile);
|
await fsp_unlink(srcFile);
|
||||||
|
|
||||||
// 100ms delay to accommodate for slow windows fs
|
// 100ms delay to accommodate for slow windows fs
|
||||||
if (os.type().indexOf('Windows') > -1) {
|
if (os.type().indexOf("Windows") > -1) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* Handles the import requests
|
* Handles the import requests
|
||||||
*/
|
*/
|
||||||
|
@ -21,19 +21,19 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require("../db/PadManager");
|
||||||
const padMessageHandler = require('./PadMessageHandler');
|
const padMessageHandler = require("./PadMessageHandler");
|
||||||
import {promises as fs} from 'fs';
|
import { promises as fs } from "fs";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const {Formidable} = require('formidable');
|
const { Formidable } = require("formidable");
|
||||||
import os from 'os';
|
import os from "os";
|
||||||
const importHtml = require('../utils/ImportHtml');
|
const importHtml = require("../utils/ImportHtml");
|
||||||
const importEtherpad = require('../utils/ImportEtherpad');
|
const importEtherpad = require("../utils/ImportEtherpad");
|
||||||
import log4js from 'log4js';
|
import log4js from "log4js";
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||||
|
|
||||||
const logger = log4js.getLogger('ImportHandler');
|
const logger = log4js.getLogger("ImportHandler");
|
||||||
|
|
||||||
// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`.
|
// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`.
|
||||||
class ImportError extends Error {
|
class ImportError extends Error {
|
||||||
|
@ -41,10 +41,10 @@ class ImportError extends Error {
|
||||||
constructor(status: string, ...args: any) {
|
constructor(status: string, ...args: any) {
|
||||||
super(...args);
|
super(...args);
|
||||||
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
|
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
|
||||||
this.name = 'ImportError';
|
this.name = "ImportError";
|
||||||
this.status = status;
|
this.status = status;
|
||||||
const msg = this.message == null ? '' : String(this.message);
|
const msg = this.message == null ? "" : String(this.message);
|
||||||
if (status !== '') this.message = msg === '' ? status : `${status}: ${msg}`;
|
if (status !== "") this.message = msg === "" ? status : `${status}: ${msg}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,22 +52,22 @@ const rm = async (path: string) => {
|
||||||
try {
|
try {
|
||||||
await fs.unlink(path);
|
await fs.unlink(path);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.code !== 'ENOENT') throw err;
|
if (err.code !== "ENOENT") throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let converter: any = null;
|
let converter: any = null;
|
||||||
let exportExtension = 'htm';
|
let exportExtension = "htm";
|
||||||
|
|
||||||
// load abiword only if it is enabled and if soffice is disabled
|
// load abiword only if it is enabled and if soffice is disabled
|
||||||
if (settings.abiword != null && settings.soffice == null) {
|
if (settings.abiword != null && settings.soffice == null) {
|
||||||
converter = require('../utils/Abiword');
|
converter = require("../utils/Abiword");
|
||||||
}
|
}
|
||||||
|
|
||||||
// load soffice only if it is enabled
|
// load soffice only if it is enabled
|
||||||
if (settings.soffice != null) {
|
if (settings.soffice != null) {
|
||||||
converter = require('../utils/LibreOffice');
|
converter = require("../utils/LibreOffice");
|
||||||
exportExtension = 'html';
|
exportExtension = "html";
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmpDirectory = os.tmpdir();
|
const tmpDirectory = os.tmpdir();
|
||||||
|
@ -79,14 +79,19 @@ const tmpDirectory = os.tmpdir();
|
||||||
* @param {String} padId the pad id to export
|
* @param {String} padId the pad id to export
|
||||||
* @param {String} authorId the author id to use for the import
|
* @param {String} authorId the author id to use for the import
|
||||||
*/
|
*/
|
||||||
const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
const doImport = async (
|
||||||
|
req: any,
|
||||||
|
res: any,
|
||||||
|
padId: string,
|
||||||
|
authorId: string,
|
||||||
|
) => {
|
||||||
// pipe to a file
|
// pipe to a file
|
||||||
// convert file to html via abiword or soffice
|
// convert file to html via abiword or soffice
|
||||||
// set html in the pad
|
// set html in the pad
|
||||||
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
|
const randNum = Math.floor(Math.random() * 0xffffffff);
|
||||||
|
|
||||||
// setting flag for whether to use converter or not
|
// setting flag for whether to use converter or not
|
||||||
let useConverter = (converter != null);
|
let useConverter = converter != null;
|
||||||
|
|
||||||
const form = new Formidable({
|
const form = new Formidable({
|
||||||
keepExtensions: true,
|
keepExtensions: true,
|
||||||
|
@ -102,13 +107,13 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.warn(`Import failed due to form error: ${err.stack || err}`);
|
logger.warn(`Import failed due to form error: ${err.stack || err}`);
|
||||||
if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) {
|
if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) {
|
||||||
throw new ImportError('maxFileSize');
|
throw new ImportError("maxFileSize");
|
||||||
}
|
}
|
||||||
throw new ImportError('uploadFailed');
|
throw new ImportError("uploadFailed");
|
||||||
}
|
}
|
||||||
if (!files.file) {
|
if (!files.file) {
|
||||||
logger.warn('Import failed because form had no file');
|
logger.warn("Import failed because form had no file");
|
||||||
throw new ImportError('uploadFailed');
|
throw new ImportError("uploadFailed");
|
||||||
} else {
|
} else {
|
||||||
srcFile = files.file[0].filepath;
|
srcFile = files.file[0].filepath;
|
||||||
}
|
}
|
||||||
|
@ -116,9 +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
|
// ensure this is a file ending we know, else we change the file ending to .txt
|
||||||
// this allows us to accept source code files like .c or .java
|
// this allows us to accept source code files like .c or .java
|
||||||
const fileEnding = path.extname(files.file[0].originalFilename).toLowerCase();
|
const fileEnding = path.extname(files.file[0].originalFilename).toLowerCase();
|
||||||
const knownFileEndings =
|
const knownFileEndings = [
|
||||||
['.txt', '.doc', '.docx', '.pdf', '.odt', '.html', '.htm', '.etherpad', '.rtf'];
|
".txt",
|
||||||
const fileEndingUnknown = (knownFileEndings.indexOf(fileEnding) < 0);
|
".doc",
|
||||||
|
".docx",
|
||||||
|
".pdf",
|
||||||
|
".odt",
|
||||||
|
".html",
|
||||||
|
".htm",
|
||||||
|
".etherpad",
|
||||||
|
".rtf",
|
||||||
|
];
|
||||||
|
const fileEndingUnknown = knownFileEndings.indexOf(fileEnding) < 0;
|
||||||
|
|
||||||
if (fileEndingUnknown) {
|
if (fileEndingUnknown) {
|
||||||
// the file ending is not known
|
// the file ending is not known
|
||||||
|
@ -127,31 +141,43 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
// we need to rename this file with a .txt ending
|
// we need to rename this file with a .txt ending
|
||||||
const oldSrcFile = srcFile;
|
const oldSrcFile = srcFile;
|
||||||
|
|
||||||
srcFile = path.join(path.dirname(srcFile), `${path.basename(srcFile, fileEnding)}.txt`);
|
srcFile = path.join(
|
||||||
|
path.dirname(srcFile),
|
||||||
|
`${path.basename(srcFile, fileEnding)}.txt`,
|
||||||
|
);
|
||||||
await fs.rename(oldSrcFile, srcFile);
|
await fs.rename(oldSrcFile, srcFile);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Not allowing unknown file type to be imported: ${fileEnding}`);
|
logger.warn(
|
||||||
throw new ImportError('uploadFailed');
|
`Not allowing unknown file type to be imported: ${fileEnding}`,
|
||||||
|
);
|
||||||
|
throw new ImportError("uploadFailed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`);
|
const destFile = path.join(
|
||||||
|
tmpDirectory,
|
||||||
|
`etherpad_import_${randNum}.${exportExtension}`,
|
||||||
|
);
|
||||||
const context = { srcFile, destFile, fileEnding, padId, ImportError };
|
const context = { srcFile, destFile, fileEnding, padId, ImportError };
|
||||||
const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x:string) => x);
|
const importHandledByPlugin = (await hooks.aCallAll("import", context)).some(
|
||||||
const fileIsEtherpad = (fileEnding === '.etherpad');
|
(x: string) => x,
|
||||||
const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm');
|
);
|
||||||
const fileIsTXT = (fileEnding === '.txt');
|
const fileIsEtherpad = fileEnding === ".etherpad";
|
||||||
|
const fileIsHTML = fileEnding === ".html" || fileEnding === ".htm";
|
||||||
|
const fileIsTXT = fileEnding === ".txt";
|
||||||
|
|
||||||
let directDatabaseAccess = false;
|
let directDatabaseAccess = false;
|
||||||
if (fileIsEtherpad) {
|
if (fileIsEtherpad) {
|
||||||
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
|
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
|
||||||
const pad = await padManager.getPad(padId, '\n', authorId);
|
const pad = await padManager.getPad(padId, "\n", authorId);
|
||||||
const headCount = pad.head;
|
const headCount = pad.head;
|
||||||
if (headCount >= 10) {
|
if (headCount >= 10) {
|
||||||
logger.warn('Aborting direct database import attempt of a pad that already has content');
|
logger.warn(
|
||||||
throw new ImportError('padHasData');
|
"Aborting direct database import attempt of a pad that already has content",
|
||||||
|
);
|
||||||
|
throw new ImportError("padHasData");
|
||||||
}
|
}
|
||||||
const text = await fs.readFile(srcFile, 'utf8');
|
const text = await fs.readFile(srcFile, "utf8");
|
||||||
directDatabaseAccess = true;
|
directDatabaseAccess = true;
|
||||||
await importEtherpad.setPadRaw(padId, text, authorId);
|
await importEtherpad.setPadRaw(padId, text, authorId);
|
||||||
}
|
}
|
||||||
|
@ -172,7 +198,7 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
await converter.convertFile(srcFile, destFile, exportExtension);
|
await converter.convertFile(srcFile, destFile, exportExtension);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.warn(`Converting Error: ${err.stack || err}`);
|
logger.warn(`Converting Error: ${err.stack || err}`);
|
||||||
throw new ImportError('convertFailed');
|
throw new ImportError("convertFailed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,26 +208,26 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
const buf = await fs.readFile(destFile);
|
const buf = await fs.readFile(destFile);
|
||||||
|
|
||||||
// Check if there are only ascii chars in the uploaded file
|
// Check if there are only ascii chars in the uploaded file
|
||||||
const isAscii = !Array.prototype.some.call(buf, (c) => (c > 240));
|
const isAscii = !Array.prototype.some.call(buf, (c) => c > 240);
|
||||||
|
|
||||||
if (!isAscii) {
|
if (!isAscii) {
|
||||||
logger.warn('Attempt to import non-ASCII file');
|
logger.warn("Attempt to import non-ASCII file");
|
||||||
throw new ImportError('uploadFailed');
|
throw new ImportError("uploadFailed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
|
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
|
||||||
let pad = await padManager.getPad(padId, '\n', authorId);
|
let pad = await padManager.getPad(padId, "\n", authorId);
|
||||||
|
|
||||||
// read the text
|
// read the text
|
||||||
let text;
|
let text;
|
||||||
|
|
||||||
if (!directDatabaseAccess) {
|
if (!directDatabaseAccess) {
|
||||||
text = await fs.readFile(destFile, 'utf8');
|
text = await fs.readFile(destFile, "utf8");
|
||||||
|
|
||||||
// node on windows has a delay on releasing of the file lock.
|
// node on windows has a delay on releasing of the file lock.
|
||||||
// We add a 100ms delay to work around this
|
// We add a 100ms delay to work around this
|
||||||
if (os.type().indexOf('Windows') > -1) {
|
if (os.type().indexOf("Windows") > -1) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,7 +238,11 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
try {
|
try {
|
||||||
await importHtml.setPadHTML(pad, text, authorId);
|
await importHtml.setPadHTML(pad, text, authorId);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`);
|
logger.warn(
|
||||||
|
`Error importing, possibly caused by malformed HTML: ${
|
||||||
|
err.stack || err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await pad.setText(text, authorId);
|
await pad.setText(text, authorId);
|
||||||
|
@ -221,7 +251,7 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
|
|
||||||
// Load the Pad into memory then broadcast updates to all clients
|
// Load the Pad into memory then broadcast updates to all clients
|
||||||
padManager.unloadPad(padId);
|
padManager.unloadPad(padId);
|
||||||
pad = await padManager.getPad(padId, '\n', authorId);
|
pad = await padManager.getPad(padId, "\n", authorId);
|
||||||
padManager.unloadPad(padId);
|
padManager.unloadPad(padId);
|
||||||
|
|
||||||
// Direct database access means a pad user should reload the pad and not attempt to receive
|
// Direct database access means a pad user should reload the pad and not attempt to receive
|
||||||
|
@ -246,19 +276,27 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
* @param {String} authorId the author id to use for the import
|
* @param {String} authorId the author id to use for the import
|
||||||
* @return {Promise<void>} a promise
|
* @return {Promise<void>} a promise
|
||||||
*/
|
*/
|
||||||
exports.doImport = async (req:any, res:any, padId:string, authorId:string = '') => {
|
exports.doImport = async (
|
||||||
|
req: any,
|
||||||
|
res: any,
|
||||||
|
padId: string,
|
||||||
|
authorId: string = "",
|
||||||
|
) => {
|
||||||
let httpStatus = 200;
|
let httpStatus = 200;
|
||||||
let code = 0;
|
let code = 0;
|
||||||
let message = 'ok';
|
let message = "ok";
|
||||||
let directDatabaseAccess;
|
let directDatabaseAccess;
|
||||||
try {
|
try {
|
||||||
directDatabaseAccess = await doImport(req, res, padId, authorId);
|
directDatabaseAccess = await doImport(req, res, padId, authorId);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const known = err instanceof ImportError && err.status;
|
const known = err instanceof ImportError && err.status;
|
||||||
if (!known) logger.error(`Internal error during import: ${err.stack || err}`);
|
if (!known)
|
||||||
|
logger.error(`Internal error during import: ${err.stack || err}`);
|
||||||
httpStatus = known ? 400 : 500;
|
httpStatus = known ? 400 : 500;
|
||||||
code = known ? 1 : 2;
|
code = known ? 1 : 2;
|
||||||
message = known ? err.status : 'internalError';
|
message = known ? err.status : "internalError";
|
||||||
}
|
}
|
||||||
res.status(httpStatus).json({code, message, data: {directDatabaseAccess}});
|
res
|
||||||
|
.status(httpStatus)
|
||||||
|
.json({ code, message, data: { directDatabaseAccess } });
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* This is the Socket.IO Router. It routes the Messages between the
|
* This is the Socket.IO Router. It routes the Messages between the
|
||||||
* components of the Server. The components are at the moment: pad and timeslider
|
* components of the Server. The components are at the moment: pad and timeslider
|
||||||
|
@ -22,11 +22,11 @@
|
||||||
|
|
||||||
import { MapArrayType } from "../types/MapType";
|
import { MapArrayType } from "../types/MapType";
|
||||||
import { SocketModule } from "../types/SocketModule";
|
import { SocketModule } from "../types/SocketModule";
|
||||||
const log4js = require('log4js');
|
const log4js = require("log4js");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const stats = require('../../node/stats')
|
const stats = require("../../node/stats");
|
||||||
|
|
||||||
const logger = log4js.getLogger('socket.io');
|
const logger = log4js.getLogger("socket.io");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves all components
|
* Saves all components
|
||||||
|
@ -51,7 +51,9 @@ exports.addComponent = (moduleName: string, module: SocketModule) => {
|
||||||
* removes a component
|
* removes a component
|
||||||
* @param {Module} moduleName
|
* @param {Module} moduleName
|
||||||
*/
|
*/
|
||||||
exports.deleteComponent = (moduleName: string) => { delete components[moduleName]; };
|
exports.deleteComponent = (moduleName: string) => {
|
||||||
|
delete components[moduleName];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sets the socket.io and adds event functions for routing
|
* sets the socket.io and adds event functions for routing
|
||||||
|
@ -60,8 +62,8 @@ exports.deleteComponent = (moduleName: string) => { delete components[moduleName
|
||||||
exports.setSocketIO = (_io: any) => {
|
exports.setSocketIO = (_io: any) => {
|
||||||
io = _io;
|
io = _io;
|
||||||
|
|
||||||
io.sockets.on('connection', (socket:any) => {
|
io.sockets.on("connection", (socket: any) => {
|
||||||
const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
|
const ip = settings.disableIPlogging ? "ANONYMOUS" : socket.request.ip;
|
||||||
logger.debug(`${socket.id} connected from IP ${ip}`);
|
logger.debug(`${socket.id} connected from IP ${ip}`);
|
||||||
|
|
||||||
// wrap the original send function to log the messages
|
// wrap the original send function to log the messages
|
||||||
|
@ -76,27 +78,36 @@ exports.setSocketIO = (_io:any) => {
|
||||||
components[i].handleConnect(socket);
|
components[i].handleConnect(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('message', (message: any, ack: any = () => {}) => (async () => {
|
socket.on("message", (message: any, ack: any = () => {}) =>
|
||||||
|
(async () => {
|
||||||
if (!message.component || !components[message.component]) {
|
if (!message.component || !components[message.component]) {
|
||||||
throw new Error(`unknown message component: ${message.component}`);
|
throw new Error(`unknown message component: ${message.component}`);
|
||||||
}
|
}
|
||||||
logger.debug(`from ${socket.id}:`, message);
|
logger.debug(`from ${socket.id}:`, message);
|
||||||
return await components[message.component].handleMessage(socket, message);
|
return await components[message.component].handleMessage(
|
||||||
|
socket,
|
||||||
|
message,
|
||||||
|
);
|
||||||
})().then(
|
})().then(
|
||||||
(val) => ack(null, val),
|
(val) => ack(null, val),
|
||||||
(err) => {
|
(err) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`);
|
`Error handling ${message.component} message from ${socket.id}: ${
|
||||||
|
err.stack || err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
ack({ name: err.name, message: err.message }); // socket.io can't handle Error objects.
|
ack({ name: err.name, message: err.message }); // socket.io can't handle Error objects.
|
||||||
}));
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
socket.on('disconnect', (reason: string) => {
|
socket.on("disconnect", (reason: string) => {
|
||||||
logger.debug(`${socket.id} disconnected: ${reason}`);
|
logger.debug(`${socket.id} disconnected: ${reason}`);
|
||||||
// store the lastDisconnect as a timestamp, this is useful if you want to know
|
// store the lastDisconnect as a timestamp, this is useful if you want to know
|
||||||
// when the last user disconnected. If your activePads is 0 and totalUsers is 0
|
// when the last user disconnected. If your activePads is 0 and totalUsers is 0
|
||||||
// you can say, if there has been no active pads or active users for 10 minutes
|
// you can say, if there has been no active pads or active users for 10 minutes
|
||||||
// this instance can be brought out of a scaling cluster.
|
// this instance can be brought out of a scaling cluster.
|
||||||
stats.gauge('lastDisconnect', () => Date.now());
|
stats.gauge("lastDisconnect", () => Date.now());
|
||||||
// tell all components about this disconnect
|
// tell all components about this disconnect
|
||||||
for (const i of Object.keys(components)) {
|
for (const i of Object.keys(components)) {
|
||||||
components[i].handleDisconnect(socket);
|
components[i].handleDisconnect(socket);
|
||||||
|
|
|
@ -1,63 +1,69 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { Socket } from "node:net";
|
import { Socket } from "node:net";
|
||||||
import type { MapArrayType } from "../types/MapType";
|
import type { MapArrayType } from "../types/MapType";
|
||||||
|
|
||||||
import _ from 'underscore';
|
import _ from "underscore";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from "cookie-parser";
|
||||||
import events from 'events';
|
import events from "events";
|
||||||
import express from 'express';
|
import express from "express";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import expressSession from 'express-session';
|
import expressSession from "express-session";
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require("../../static/js/pluginfw/hooks");
|
||||||
import log4js from 'log4js';
|
import log4js from "log4js";
|
||||||
const SessionStore = require('../db/SessionStore');
|
const SessionStore = require("../db/SessionStore");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const stats = require('../stats')
|
const stats = require("../stats");
|
||||||
import util from 'util';
|
import util from "util";
|
||||||
const webaccess = require('./express/webaccess');
|
const webaccess = require("./express/webaccess");
|
||||||
|
|
||||||
import SecretRotator from '../security/SecretRotator';
|
import SecretRotator from "../security/SecretRotator";
|
||||||
|
|
||||||
let secretRotator: SecretRotator | null = null;
|
let secretRotator: SecretRotator | null = null;
|
||||||
const logger = log4js.getLogger('http');
|
const logger = log4js.getLogger("http");
|
||||||
let serverName: string;
|
let serverName: string;
|
||||||
let sessionStore: { shutdown: () => void; } | null;
|
let sessionStore: { shutdown: () => void } | null;
|
||||||
const sockets: Set<Socket> = new Set();
|
const sockets: Set<Socket> = new Set();
|
||||||
const socketsEvents = new events.EventEmitter();
|
const socketsEvents = new events.EventEmitter();
|
||||||
const startTime = stats.settableGauge('httpStartTime');
|
const startTime = stats.settableGauge("httpStartTime");
|
||||||
|
|
||||||
exports.server = null;
|
exports.server = null;
|
||||||
|
|
||||||
const closeServer = async () => {
|
const closeServer = async () => {
|
||||||
if (exports.server != null) {
|
if (exports.server != null) {
|
||||||
logger.info('Closing HTTP server...');
|
logger.info("Closing HTTP server...");
|
||||||
// Call exports.server.close() to reject new connections but don't await just yet because the
|
// Call exports.server.close() to reject new connections but don't await just yet because the
|
||||||
// Promise won't resolve until all preexisting connections are closed.
|
// Promise won't resolve until all preexisting connections are closed.
|
||||||
const p = util.promisify(exports.server.close.bind(exports.server))();
|
const p = util.promisify(exports.server.close.bind(exports.server))();
|
||||||
await hooks.aCallAll('expressCloseServer');
|
await hooks.aCallAll("expressCloseServer");
|
||||||
// Give existing connections some time to close on their own before forcibly terminating. The
|
// Give existing connections some time to close on their own before forcibly terminating. The
|
||||||
// time should be long enough to avoid interrupting most preexisting transmissions but short
|
// time should be long enough to avoid interrupting most preexisting transmissions but short
|
||||||
// enough to avoid a noticeable outage.
|
// enough to avoid a noticeable outage.
|
||||||
const timeout = setTimeout(async () => {
|
const timeout = setTimeout(async () => {
|
||||||
logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`);
|
logger.info(
|
||||||
for (const socket of sockets) socket.destroy(new Error('HTTP server is closing'));
|
`Forcibly terminating remaining ${sockets.size} HTTP connections...`,
|
||||||
|
);
|
||||||
|
for (const socket of sockets)
|
||||||
|
socket.destroy(new Error("HTTP server is closing"));
|
||||||
}, 5000);
|
}, 5000);
|
||||||
let lastLogged = 0;
|
let lastLogged = 0;
|
||||||
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
||||||
if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs.
|
if (Date.now() - lastLogged > 1000) {
|
||||||
logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`);
|
// Rate limit to avoid filling logs.
|
||||||
|
logger.info(
|
||||||
|
`Waiting for ${sockets.size} HTTP clients to disconnect...`,
|
||||||
|
);
|
||||||
lastLogged = Date.now();
|
lastLogged = Date.now();
|
||||||
}
|
}
|
||||||
await events.once(socketsEvents, 'updated');
|
await events.once(socketsEvents, "updated");
|
||||||
}
|
}
|
||||||
await p;
|
await p;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
exports.server = null;
|
exports.server = null;
|
||||||
startTime.setValue(0);
|
startTime.setValue(0);
|
||||||
logger.info('HTTP server closed');
|
logger.info("HTTP server closed");
|
||||||
}
|
}
|
||||||
if (sessionStore) sessionStore.shutdown();
|
if (sessionStore) sessionStore.shutdown();
|
||||||
sessionStore = null;
|
sessionStore = null;
|
||||||
|
@ -66,34 +72,46 @@ const closeServer = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.createServer = async () => {
|
exports.createServer = async () => {
|
||||||
console.log('Report bugs at https://github.com/ether/etherpad-lite/issues');
|
console.log("Report bugs at https://github.com/ether/etherpad-lite/issues");
|
||||||
|
|
||||||
serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`;
|
serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`;
|
||||||
|
|
||||||
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`);
|
console.log(
|
||||||
|
`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`,
|
||||||
|
);
|
||||||
|
|
||||||
await exports.restartServer();
|
await exports.restartServer();
|
||||||
|
|
||||||
if (settings.ip === '') {
|
if (settings.ip === "") {
|
||||||
// using Unix socket for connectivity
|
// using Unix socket for connectivity
|
||||||
console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`);
|
console.log(
|
||||||
|
`You can access your Etherpad instance using the Unix socket at ${settings.port}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`);
|
console.log(
|
||||||
|
`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(settings.users)) {
|
if (!_.isEmpty(settings.users)) {
|
||||||
console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`);
|
console.log(
|
||||||
|
`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Admin username and password not set in settings.json. ' +
|
console.warn(
|
||||||
'To access admin please uncomment and edit "users" in settings.json');
|
"Admin username and password not set in settings.json. " +
|
||||||
|
'To access admin please uncomment and edit "users" in settings.json',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = process.env.NODE_ENV || 'development';
|
const env = process.env.NODE_ENV || "development";
|
||||||
|
|
||||||
if (env !== 'production') {
|
if (env !== "production") {
|
||||||
console.warn('Etherpad is running in Development mode. This mode is slower for users and ' +
|
console.warn(
|
||||||
'less secure than production mode. You should set the NODE_ENV environment ' +
|
"Etherpad is running in Development mode. This mode is slower for users and " +
|
||||||
'variable to production by using: export NODE_ENV=production');
|
"less secure than production mode. You should set the NODE_ENV environment " +
|
||||||
|
"variable to production by using: export NODE_ENV=production",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -103,9 +121,11 @@ exports.restartServer = async () => {
|
||||||
const app = express(); // New syntax for express v3
|
const app = express(); // New syntax for express v3
|
||||||
|
|
||||||
if (settings.ssl) {
|
if (settings.ssl) {
|
||||||
console.log('SSL -- enabled');
|
console.log("SSL -- enabled");
|
||||||
console.log(`SSL -- server key file: ${settings.ssl.key}`);
|
console.log(`SSL -- server key file: ${settings.ssl.key}`);
|
||||||
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
|
console.log(
|
||||||
|
`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`,
|
||||||
|
);
|
||||||
|
|
||||||
const options: MapArrayType<any> = {
|
const options: MapArrayType<any> = {
|
||||||
key: fs.readFileSync(settings.ssl.key),
|
key: fs.readFileSync(settings.ssl.key),
|
||||||
|
@ -120,10 +140,10 @@ exports.restartServer = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const https = require('https');
|
const https = require("https");
|
||||||
exports.server = https.createServer(options, app);
|
exports.server = https.createServer(options, app);
|
||||||
} else {
|
} else {
|
||||||
const http = require('http');
|
const http = require("http");
|
||||||
exports.server = http.createServer(app);
|
exports.server = http.createServer(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,12 +151,15 @@ exports.restartServer = async () => {
|
||||||
// res.header("X-Frame-Options", "deny"); // breaks embedded pads
|
// res.header("X-Frame-Options", "deny"); // breaks embedded pads
|
||||||
if (settings.ssl) {
|
if (settings.ssl) {
|
||||||
// we use SSL
|
// we use SSL
|
||||||
res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
res.header(
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=31536000; includeSubDomains",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop IE going into compatability mode
|
// Stop IE going into compatability mode
|
||||||
// https://github.com/ether/etherpad-lite/issues/2547
|
// https://github.com/ether/etherpad-lite/issues/2547
|
||||||
res.header('X-UA-Compatible', 'IE=Edge,chrome=1');
|
res.header("X-UA-Compatible", "IE=Edge,chrome=1");
|
||||||
|
|
||||||
// Enable a strong referrer policy. Same-origin won't drop Referers when
|
// Enable a strong referrer policy. Same-origin won't drop Referers when
|
||||||
// loading local resources, but it will drop them when loading foreign resources.
|
// loading local resources, but it will drop them when loading foreign resources.
|
||||||
|
@ -145,11 +168,11 @@ exports.restartServer = async () => {
|
||||||
// marked with <meta name="referrer" content="no-referrer">
|
// marked with <meta name="referrer" content="no-referrer">
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
|
||||||
// https://github.com/ether/etherpad-lite/pull/3636
|
// https://github.com/ether/etherpad-lite/pull/3636
|
||||||
res.header('Referrer-Policy', 'same-origin');
|
res.header("Referrer-Policy", "same-origin");
|
||||||
|
|
||||||
// send git version in the Server response header if exposeVersion is true.
|
// send git version in the Server response header if exposeVersion is true.
|
||||||
if (settings.exposeVersion) {
|
if (settings.exposeVersion) {
|
||||||
res.header('Server', serverName);
|
res.header("Server", serverName);
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
@ -162,14 +185,17 @@ exports.restartServer = async () => {
|
||||||
*
|
*
|
||||||
* Source: https://expressjs.com/en/guide/behind-proxies.html
|
* Source: https://expressjs.com/en/guide/behind-proxies.html
|
||||||
*/
|
*/
|
||||||
app.enable('trust proxy');
|
app.enable("trust proxy");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure response time
|
// Measure response time
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const stopWatch = stats.timer('httpRequests').start();
|
const stopWatch = stats.timer("httpRequests").start();
|
||||||
const sendFn = res.send.bind(res);
|
const sendFn = res.send.bind(res);
|
||||||
res.send = (...args) => { stopWatch.end(); return sendFn(...args); };
|
res.send = (...args) => {
|
||||||
|
stopWatch.end();
|
||||||
|
return sendFn(...args);
|
||||||
|
};
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -177,22 +203,28 @@ exports.restartServer = async () => {
|
||||||
// starts listening to requests as reported in issue #158. Not installing the log4js connect
|
// starts listening to requests as reported in issue #158. Not installing the log4js connect
|
||||||
// logger when the log level has a higher severity than INFO since it would not log at that level
|
// logger when the log level has a higher severity than INFO since it would not log at that level
|
||||||
// anyway.
|
// anyway.
|
||||||
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
|
if (!(settings.loglevel === "WARN" && settings.loglevel === "ERROR")) {
|
||||||
app.use(log4js.connectLogger(logger, {
|
app.use(
|
||||||
|
log4js.connectLogger(logger, {
|
||||||
level: log4js.levels.DEBUG.levelStr,
|
level: log4js.levels.DEBUG.levelStr,
|
||||||
format: ':status, :method :url',
|
format: ":status, :method :url",
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { keyRotationInterval, sessionLifetime } = settings.cookie;
|
const { keyRotationInterval, sessionLifetime } = settings.cookie;
|
||||||
let secret = settings.sessionKey;
|
let secret = settings.sessionKey;
|
||||||
if (keyRotationInterval && sessionLifetime) {
|
if (keyRotationInterval && sessionLifetime) {
|
||||||
secretRotator = new SecretRotator(
|
secretRotator = new SecretRotator(
|
||||||
'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey);
|
"expressSessionSecrets",
|
||||||
|
keyRotationInterval,
|
||||||
|
sessionLifetime,
|
||||||
|
settings.sessionKey,
|
||||||
|
);
|
||||||
await secretRotator.start();
|
await secretRotator.start();
|
||||||
secret = secretRotator.secrets;
|
secret = secretRotator.secrets;
|
||||||
}
|
}
|
||||||
if (!secret) throw new Error('missing cookie signing secret');
|
if (!secret) throw new Error("missing cookie signing secret");
|
||||||
|
|
||||||
app.use(cookieParser(secret, {}));
|
app.use(cookieParser(secret, {}));
|
||||||
|
|
||||||
|
@ -206,7 +238,7 @@ exports.restartServer = async () => {
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
// Set the cookie name to a javascript identifier compatible string. Makes code handling it
|
// Set the cookie name to a javascript identifier compatible string. Makes code handling it
|
||||||
// cleaner :)
|
// cleaner :)
|
||||||
name: 'express_sid',
|
name: "express_sid",
|
||||||
cookie: {
|
cookie: {
|
||||||
maxAge: sessionLifetime || null, // Convert 0 to null.
|
maxAge: sessionLifetime || null, // Convert 0 to null.
|
||||||
sameSite: settings.cookie.sameSite,
|
sameSite: settings.cookie.sameSite,
|
||||||
|
@ -227,33 +259,36 @@ exports.restartServer = async () => {
|
||||||
// https at the same time.
|
// https at the same time.
|
||||||
//
|
//
|
||||||
// reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure
|
// reference: https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecure
|
||||||
secure: 'auto',
|
secure: "auto",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Give plugins an opportunity to install handlers/middleware before the express-session
|
// Give plugins an opportunity to install handlers/middleware before the express-session
|
||||||
// middleware. This allows plugins to avoid creating an express-session record in the database
|
// middleware. This allows plugins to avoid creating an express-session record in the database
|
||||||
// when it is not needed (e.g., public static content).
|
// when it is not needed (e.g., public static content).
|
||||||
await hooks.aCallAll('expressPreSession', {app});
|
await hooks.aCallAll("expressPreSession", { app });
|
||||||
app.use(exports.sessionMiddleware);
|
app.use(exports.sessionMiddleware);
|
||||||
|
|
||||||
app.use(webaccess.checkAccess);
|
app.use(webaccess.checkAccess);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
hooks.aCallAll('expressConfigure', {app}),
|
hooks.aCallAll("expressConfigure", { app }),
|
||||||
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
|
hooks.aCallAll("expressCreateServer", { app, server: exports.server }),
|
||||||
]);
|
]);
|
||||||
exports.server.on('connection', (socket:Socket) => {
|
exports.server.on("connection", (socket: Socket) => {
|
||||||
sockets.add(socket);
|
sockets.add(socket);
|
||||||
socketsEvents.emit('updated');
|
socketsEvents.emit("updated");
|
||||||
socket.on('close', () => {
|
socket.on("close", () => {
|
||||||
sockets.delete(socket);
|
sockets.delete(socket);
|
||||||
socketsEvents.emit('updated');
|
socketsEvents.emit("updated");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip);
|
await util.promisify(exports.server.listen).bind(exports.server)(
|
||||||
|
settings.port,
|
||||||
|
settings.ip,
|
||||||
|
);
|
||||||
startTime.setValue(Date.now());
|
startTime.setValue(Date.now());
|
||||||
logger.info('HTTP server listening for connections');
|
logger.info("HTTP server listening for connections");
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.shutdown = async (hookName: string, context: any) => {
|
exports.shutdown = async (hookName: string, context: any) => {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import express from "express";
|
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
|
* 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
|
* @param {Function} cb the callback function
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
|
exports.expressCreateServer = (
|
||||||
args.app.use('/admin/', express.static(path.join(__dirname, '../../../templates/admin'), {maxAge: 1000 * 60 * 60 * 24}));
|
hookName: string,
|
||||||
args.app.get('/admin/*', (_request:any, response:any)=>{
|
args: ArgsExpressType,
|
||||||
response.sendFile(path.resolve(__dirname,'../../../templates/admin', 'index.html'));
|
cb: Function,
|
||||||
} )
|
): any => {
|
||||||
args.app.get('/admin', (req:any, res:any, next:Function) => {
|
args.app.use(
|
||||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
"/admin/",
|
||||||
})
|
express.static(path.join(__dirname, "../../../templates/admin"), {
|
||||||
|
maxAge: 1000 * 60 * 60 * 24,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
args.app.get("/admin/*", (_request: any, response: any) => {
|
||||||
|
response.sendFile(
|
||||||
|
path.resolve(__dirname, "../../../templates/admin", "index.html"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
args.app.get("/admin", (req: any, res: any, next: Function) => {
|
||||||
|
if ("/" !== req.path[req.path.length - 1]) return res.redirect("./admin/");
|
||||||
|
});
|
||||||
return cb();
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,32 +1,41 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
import { ErrorCaused } from "../../types/ErrorCaused";
|
import { ErrorCaused } from "../../types/ErrorCaused";
|
||||||
import { QueryType } from "../../types/QueryType";
|
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";
|
import { PackageData } from "../../types/PackageInfo";
|
||||||
|
|
||||||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
const pluginDefs = require("../../../static/js/pluginfw/plugin_defs");
|
||||||
import semver from 'semver';
|
import semver from "semver";
|
||||||
|
|
||||||
|
|
||||||
exports.socketio = (hookName: string, args: ArgsExpressType, cb: Function) => {
|
exports.socketio = (hookName: string, args: ArgsExpressType, cb: Function) => {
|
||||||
const io = args.io.of('/pluginfw/installer');
|
const io = args.io.of("/pluginfw/installer");
|
||||||
io.on('connection', (socket:any) => {
|
io.on("connection", (socket: any) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
const {
|
||||||
|
session: {
|
||||||
|
user: { is_admin: isAdmin } = {},
|
||||||
|
} = {},
|
||||||
|
} = socket.conn.request;
|
||||||
if (!isAdmin) return;
|
if (!isAdmin) return;
|
||||||
|
|
||||||
socket.on('getInstalled', (query:string) => {
|
socket.on("getInstalled", (query: string) => {
|
||||||
// send currently installed plugins
|
// send currently installed plugins
|
||||||
const installed =
|
const installed = Object.keys(pluginDefs.plugins).map(
|
||||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
(plugin) => pluginDefs.plugins[plugin].package,
|
||||||
|
);
|
||||||
|
|
||||||
socket.emit('results:installed', {installed});
|
socket.emit("results:installed", { installed });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('checkUpdates', async () => {
|
socket.on("checkUpdates", async () => {
|
||||||
// Check plugins for updates
|
// Check plugins for updates
|
||||||
try {
|
try {
|
||||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
|
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
|
||||||
|
@ -40,46 +49,51 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||||
return semver.gt(latestVersion, currentVersion);
|
return semver.gt(latestVersion, currentVersion);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.emit('results:updatable', {updatable});
|
socket.emit("results:updatable", { updatable });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errc = err as ErrorCaused
|
const errc = err as ErrorCaused;
|
||||||
console.warn(errc.stack || errc.toString());
|
console.warn(errc.stack || errc.toString());
|
||||||
|
|
||||||
socket.emit('results:updatable', {updatable: {}});
|
socket.emit("results:updatable", { updatable: {} });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('getAvailable', async (query:string) => {
|
socket.on("getAvailable", async (query: string) => {
|
||||||
try {
|
try {
|
||||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
||||||
socket.emit('results:available', results);
|
socket.emit("results:available", results);
|
||||||
} catch (er) {
|
} catch (er) {
|
||||||
console.error(er);
|
console.error(er);
|
||||||
socket.emit('results:available', {});
|
socket.emit("results:available", {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('search', async (query: QueryType) => {
|
socket.on("search", async (query: QueryType) => {
|
||||||
try {
|
try {
|
||||||
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
|
const results = await search(
|
||||||
|
query.searchTerm,
|
||||||
|
/* maxCacheAge:*/ 60 * 10,
|
||||||
|
);
|
||||||
let res = Object.keys(results)
|
let res = Object.keys(results)
|
||||||
.map((pluginName) => results[pluginName])
|
.map((pluginName) => results[pluginName])
|
||||||
.filter((plugin) => !pluginDefs.plugins[plugin.name]);
|
.filter((plugin) => !pluginDefs.plugins[plugin.name]);
|
||||||
res = sortPluginList(res, query.sortBy, query.sortDir)
|
res = sortPluginList(res, query.sortBy, query.sortDir).slice(
|
||||||
.slice(query.offset, query.offset + query.limit);
|
query.offset,
|
||||||
socket.emit('results:search', {results: res, query});
|
query.offset + query.limit,
|
||||||
|
);
|
||||||
|
socket.emit("results:search", { results: res, query });
|
||||||
} catch (er) {
|
} catch (er) {
|
||||||
console.error(er);
|
console.error(er);
|
||||||
|
|
||||||
socket.emit('results:search', {results: {}, query});
|
socket.emit("results:search", { results: {}, query });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('install', (pluginName: string) => {
|
socket.on("install", (pluginName: string) => {
|
||||||
install(pluginName, (err: ErrorCaused) => {
|
install(pluginName, (err: ErrorCaused) => {
|
||||||
if (err) console.warn(err.stack || err.toString());
|
if (err) console.warn(err.stack || err.toString());
|
||||||
|
|
||||||
socket.emit('finished:install', {
|
socket.emit("finished:install", {
|
||||||
plugin: pluginName,
|
plugin: pluginName,
|
||||||
code: err ? err.code : null,
|
code: err ? err.code : null,
|
||||||
error: err ? err.message : null,
|
error: err ? err.message : null,
|
||||||
|
@ -87,11 +101,14 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('uninstall', (pluginName:string) => {
|
socket.on("uninstall", (pluginName: string) => {
|
||||||
uninstall(pluginName, (err: ErrorCaused) => {
|
uninstall(pluginName, (err: ErrorCaused) => {
|
||||||
if (err) console.warn(err.stack || err.toString());
|
if (err) console.warn(err.stack || err.toString());
|
||||||
|
|
||||||
socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null});
|
socket.emit("finished:uninstall", {
|
||||||
|
plugin: pluginName,
|
||||||
|
error: err ? err.message : null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -105,7 +122,12 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||||
* @param {String} dir The directory of the plugin
|
* @param {String} dir The directory of the plugin
|
||||||
* @return {Object[]}
|
* @return {Object[]}
|
||||||
*/
|
*/
|
||||||
const sortPluginList = (plugins:PackageData[], property:string, /* ASC?*/dir:string): PackageData[] => plugins.sort((a, b) => {
|
const sortPluginList = (
|
||||||
|
plugins: PackageData[],
|
||||||
|
property: string,
|
||||||
|
/* ASC?*/ dir: string,
|
||||||
|
): PackageData[] =>
|
||||||
|
plugins.sort((a, b) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (a[property] < b[property]) {
|
if (a[property] < b[property]) {
|
||||||
return dir ? -1 : 1;
|
return dir ? -1 : 1;
|
||||||
|
|
|
@ -1,56 +1,64 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
import { PadQueryResult, PadSearchQuery } from "../../types/PadSearchQuery";
|
import { PadQueryResult, PadSearchQuery } from "../../types/PadSearchQuery";
|
||||||
import { PadType } from "../../types/PadType";
|
import { PadType } from "../../types/PadType";
|
||||||
|
|
||||||
const eejs = require('../../eejs');
|
const eejs = require("../../eejs");
|
||||||
const fsp = require('fs').promises;
|
const fsp = require("fs").promises;
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||||
const plugins = require('../../../static/js/pluginfw/plugins');
|
const plugins = require("../../../static/js/pluginfw/plugins");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
const UpdateCheck = require("../../utils/UpdateCheck");
|
||||||
const padManager = require('../../db/PadManager');
|
const padManager = require("../../db/PadManager");
|
||||||
const api = require('../../db/API');
|
const api = require("../../db/API");
|
||||||
|
|
||||||
|
|
||||||
const queryPadLimit = 12;
|
const queryPadLimit = 12;
|
||||||
|
|
||||||
|
|
||||||
exports.socketio = (hookName: string, { io }: any) => {
|
exports.socketio = (hookName: string, { io }: any) => {
|
||||||
io.of('/settings').on('connection', (socket: any ) => {
|
io.of("/settings").on("connection", (socket: any) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
const {
|
||||||
|
session: {
|
||||||
|
user: { is_admin: isAdmin } = {},
|
||||||
|
} = {},
|
||||||
|
} = socket.conn.request;
|
||||||
if (!isAdmin) return;
|
if (!isAdmin) return;
|
||||||
|
|
||||||
socket.on('load', async (query:string):Promise<any> => {
|
socket.on("load", async (query: string): Promise<any> => {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await fsp.readFile(settings.settingsFilename, 'utf8');
|
data = await fsp.readFile(settings.settingsFilename, "utf8");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return console.log(err);
|
return console.log(err);
|
||||||
}
|
}
|
||||||
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
|
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
|
||||||
if (settings.showSettingsInAdminPage === false) {
|
if (settings.showSettingsInAdminPage === false) {
|
||||||
socket.emit('settings', {results: 'NOT_ALLOWED'});
|
socket.emit("settings", { results: "NOT_ALLOWED" });
|
||||||
} else {
|
} else {
|
||||||
socket.emit('settings', {results: data});
|
socket.emit("settings", { results: data });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('saveSettings', async (newSettings:string) => {
|
socket.on("saveSettings", async (newSettings: string) => {
|
||||||
console.log('Admin request to save settings through a socket on /admin/settings');
|
console.log(
|
||||||
|
"Admin request to save settings through a socket on /admin/settings",
|
||||||
|
);
|
||||||
await fsp.writeFile(settings.settingsFilename, newSettings);
|
await fsp.writeFile(settings.settingsFilename, newSettings);
|
||||||
socket.emit('saveprogress', 'saved');
|
socket.emit("saveprogress", "saved");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("help", () => {
|
||||||
socket.on('help', ()=> {
|
|
||||||
const gitCommit = settings.getGitCommit();
|
const gitCommit = settings.getGitCommit();
|
||||||
const epVersion = settings.getEpVersion();
|
const epVersion = settings.getEpVersion();
|
||||||
|
|
||||||
const hooks:Map<string, Map<string,string>> = plugins.getHooks('hooks', false);
|
const hooks: Map<string, Map<string, string>> = plugins.getHooks(
|
||||||
const clientHooks:Map<string, Map<string,string>> = plugins.getHooks('client_hooks', false);
|
"hooks",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const clientHooks: Map<string, Map<string, string>> = plugins.getHooks(
|
||||||
|
"client_hooks",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
function mapToObject(map: Map<string, any>) {
|
function mapToObject(map: Map<string, any>) {
|
||||||
let obj = Object.create(null);
|
let obj = Object.create(null);
|
||||||
|
@ -64,7 +72,7 @@ exports.socketio = (hookName:string, {io}:any) => {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('reply:help', {
|
socket.emit("reply:help", {
|
||||||
gitCommit,
|
gitCommit,
|
||||||
epVersion,
|
epVersion,
|
||||||
installedPlugins: plugins.getPlugins(),
|
installedPlugins: plugins.getPlugins(),
|
||||||
|
@ -72,16 +80,15 @@ exports.socketio = (hookName:string, {io}:any) => {
|
||||||
installedServerHooks: mapToObject(hooks),
|
installedServerHooks: mapToObject(hooks),
|
||||||
installedClientHooks: mapToObject(clientHooks),
|
installedClientHooks: mapToObject(clientHooks),
|
||||||
latestVersion: UpdateCheck.getLatestVersion(),
|
latestVersion: UpdateCheck.getLatestVersion(),
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("padLoad", async (query: PadSearchQuery) => {
|
||||||
socket.on('padLoad', async (query: PadSearchQuery) => {
|
|
||||||
const { padIDs } = await padManager.listAllPads();
|
const { padIDs } = await padManager.listAllPads();
|
||||||
|
|
||||||
const data: {
|
const data: {
|
||||||
total: number,
|
total: number;
|
||||||
results?: PadQueryResult[]
|
results?: PadQueryResult[];
|
||||||
} = {
|
} = {
|
||||||
total: padIDs.length,
|
total: padIDs.length,
|
||||||
};
|
};
|
||||||
|
@ -90,7 +97,9 @@ exports.socketio = (hookName:string, {io}:any) => {
|
||||||
|
|
||||||
// Filter out matches
|
// Filter out matches
|
||||||
if (query.pattern) {
|
if (query.pattern) {
|
||||||
result = result.filter((padName: string) => padName.includes(query.pattern));
|
result = result.filter((padName: string) =>
|
||||||
|
padName.includes(query.pattern),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
data.total = result.length;
|
data.total = result.length;
|
||||||
|
@ -112,16 +121,19 @@ exports.socketio = (hookName:string, {io}:any) => {
|
||||||
query.limit = queryPadLimit;
|
query.limit = queryPadLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.sortBy === 'padName') {
|
if (query.sortBy === "padName") {
|
||||||
result = result.sort((a,b)=>{
|
result = result
|
||||||
|
.sort((a, b) => {
|
||||||
if (a < b) return query.ascending ? -1 : 1;
|
if (a < b) return query.ascending ? -1 : 1;
|
||||||
if (a > b) return query.ascending ? 1 : -1;
|
if (a > b) return query.ascending ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
}).slice(query.offset, query.offset + query.limit);
|
})
|
||||||
|
.slice(query.offset, query.offset + query.limit);
|
||||||
|
|
||||||
data.results = await Promise.all(result.map(async (padName: string) => {
|
data.results = await Promise.all(
|
||||||
|
result.map(async (padName: string) => {
|
||||||
const pad = await padManager.getPad(padName);
|
const pad = await padManager.getPad(padName);
|
||||||
const revisionNumber = pad.getHeadRevisionNumber()
|
const revisionNumber = pad.getHeadRevisionNumber();
|
||||||
const userCount = api.padUsersCount(padName).padUsersCount;
|
const userCount = api.padUsersCount(padName).padUsersCount;
|
||||||
const lastEdited = await pad.getLastEdit();
|
const lastEdited = await pad.getLastEdit();
|
||||||
|
|
||||||
|
@ -129,83 +141,85 @@ exports.socketio = (hookName:string, {io}:any) => {
|
||||||
padName,
|
padName,
|
||||||
lastEdited,
|
lastEdited,
|
||||||
userCount,
|
userCount,
|
||||||
revisionNumber
|
revisionNumber,
|
||||||
}}));
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const currentWinners: PadQueryResult[] = []
|
const currentWinners: PadQueryResult[] = [];
|
||||||
let queryOffsetCounter = 0
|
let queryOffsetCounter = 0;
|
||||||
for (let res of result) {
|
for (let res of result) {
|
||||||
|
|
||||||
const pad = await padManager.getPad(res);
|
const pad = await padManager.getPad(res);
|
||||||
const padType = {
|
const padType = {
|
||||||
padName: res,
|
padName: res,
|
||||||
lastEdited: await pad.getLastEdit(),
|
lastEdited: await pad.getLastEdit(),
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
userCount: api.padUsersCount(res).padUsersCount,
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
revisionNumber: pad.getHeadRevisionNumber(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentWinners.length < query.limit) {
|
if (currentWinners.length < query.limit) {
|
||||||
if (queryOffsetCounter < query.offset) {
|
if (queryOffsetCounter < query.offset) {
|
||||||
queryOffsetCounter++
|
queryOffsetCounter++;
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
currentWinners.push({
|
currentWinners.push({
|
||||||
padName: res,
|
padName: res,
|
||||||
lastEdited: await pad.getLastEdit(),
|
lastEdited: await pad.getLastEdit(),
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
userCount: api.padUsersCount(res).padUsersCount,
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
revisionNumber: pad.getHeadRevisionNumber(),
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
// Kick out worst pad and replace by current pad
|
// Kick out worst pad and replace by current pad
|
||||||
let worstPad = currentWinners.sort((a, b) => {
|
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])
|
||||||
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
|
return query.ascending ? -1 : 1;
|
||||||
|
if (a[query.sortBy] > b[query.sortBy])
|
||||||
|
return query.ascending ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
})
|
});
|
||||||
if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){
|
if (
|
||||||
|
worstPad[0] &&
|
||||||
|
worstPad[0][query.sortBy] < padType[query.sortBy]
|
||||||
|
) {
|
||||||
if (queryOffsetCounter < query.offset) {
|
if (queryOffsetCounter < query.offset) {
|
||||||
queryOffsetCounter++
|
queryOffsetCounter++;
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
|
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1);
|
||||||
currentWinners.push({
|
currentWinners.push({
|
||||||
padName: res,
|
padName: res,
|
||||||
lastEdited: await pad.getLastEdit(),
|
lastEdited: await pad.getLastEdit(),
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
userCount: api.padUsersCount(res).padUsersCount,
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
revisionNumber: pad.getHeadRevisionNumber(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.results = currentWinners;
|
data.results = currentWinners;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('results:padLoad', data);
|
socket.emit("results:padLoad", data);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
socket.on("deletePad", async (padId: string) => {
|
||||||
socket.on('deletePad', async (padId: string) => {
|
|
||||||
const padExists = await padManager.doesPadExists(padId);
|
const padExists = await padManager.doesPadExists(padId);
|
||||||
if (padExists) {
|
if (padExists) {
|
||||||
const pad = await padManager.getPad(padId);
|
const pad = await padManager.getPad(padId);
|
||||||
await pad.remove();
|
await pad.remove();
|
||||||
socket.emit('results:deletePad', padId);
|
socket.emit("results:deletePad", padId);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.on('restartServer', async () => {
|
socket.on("restartServer", async () => {
|
||||||
console.log('Admin request to restart server through a socket on /admin/settings');
|
console.log(
|
||||||
|
"Admin request to restart server through a socket on /admin/settings",
|
||||||
|
);
|
||||||
settings.reloadSettings();
|
settings.reloadSettings();
|
||||||
await plugins.update();
|
await plugins.update();
|
||||||
await hooks.aCallAll('loadSettings', {settings});
|
await hooks.aCallAll("loadSettings", { settings });
|
||||||
await hooks.aCallAll('restartServer');
|
await hooks.aCallAll("restartServer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchPad = async (query: PadSearchQuery) => {};
|
||||||
|
|
||||||
const searchPad = async (query:PadSearchQuery) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
const log4js = require('log4js');
|
const log4js = require("log4js");
|
||||||
const clientLogger = log4js.getLogger('client');
|
const clientLogger = log4js.getLogger("client");
|
||||||
const {Formidable} = require('formidable');
|
const { Formidable } = require("formidable");
|
||||||
const apiHandler = require('../../handler/APIHandler');
|
const apiHandler = require("../../handler/APIHandler");
|
||||||
const util = require('util');
|
const util = require("util");
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||||
// The Etherpad client side sends information about how a disconnect happened
|
// The Etherpad client side sends information about how a disconnect happened
|
||||||
app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => {
|
app.post("/ep/pad/connection-diagnostic-info", async (req: any, res: any) => {
|
||||||
const [fields, files] = await (new Formidable({})).parse(req);
|
const [fields, files] = await new Formidable({}).parse(req);
|
||||||
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
||||||
res.end('OK');
|
res.end("OK");
|
||||||
});
|
});
|
||||||
|
|
||||||
const parseJserrorForm = async (req: any) => {
|
const parseJserrorForm = async (req: any) => {
|
||||||
|
@ -23,22 +23,25 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// The Etherpad client side sends information about client side javscript errors
|
// The Etherpad client side sends information about client side javscript errors
|
||||||
app.post('/jserror', (req:any, res:any, next:Function) => {
|
app.post("/jserror", (req: any, res: any, next: Function) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const data = JSON.parse(await parseJserrorForm(req));
|
const data = JSON.parse(await parseJserrorForm(req));
|
||||||
clientLogger.warn(`${data.msg} --`, {
|
clientLogger.warn(`${data.msg} --`, {
|
||||||
[util.inspect.custom]: (depth: number, options: any) => {
|
[util.inspect.custom]: (depth: number, options: any) => {
|
||||||
// Depth is forced to infinity to ensure that all of the provided data is logged.
|
// Depth is forced to infinity to ensure that all of the provided data is logged.
|
||||||
options = Object.assign({}, options, {depth: Infinity, colors: true});
|
options = Object.assign({}, options, {
|
||||||
|
depth: Infinity,
|
||||||
|
colors: true,
|
||||||
|
});
|
||||||
return util.inspect(data, options);
|
return util.inspect(data, options);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
res.end('OK');
|
res.end("OK");
|
||||||
})().catch((err) => next(err || new Error(err)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provide a possibility to query the latest available API version
|
// Provide a possibility to query the latest available API version
|
||||||
app.get('/api', (req:any, res:any) => {
|
app.get("/api", (req: any, res: any) => {
|
||||||
res.json({ currentVersion: apiHandler.latestApiVersion });
|
res.json({ currentVersion: apiHandler.latestApiVersion });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
import { ErrorCaused } from "../../types/ErrorCaused";
|
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;
|
exports.app = args.app;
|
||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
|
@ -13,9 +17,9 @@ exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Funct
|
||||||
// if an error occurs Connect will pass it down
|
// if an error occurs Connect will pass it down
|
||||||
// through these "error-handling" middleware
|
// through these "error-handling" middleware
|
||||||
// allowing you to respond however you like
|
// allowing you to respond however you like
|
||||||
res.status(500).send({error: 'Sorry, something bad happened!'});
|
res.status(500).send({ error: "Sorry, something bad happened!" });
|
||||||
console.error(err.stack ? err.stack : err.toString());
|
console.error(err.stack ? err.stack : err.toString());
|
||||||
stats.meter('http500').mark();
|
stats.meter("http500").mark();
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb();
|
return cb();
|
||||||
|
|
|
@ -1,53 +1,67 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
|
||||||
const hasPadAccess = require('../../padaccess');
|
const hasPadAccess = require("../../padaccess");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
const exportHandler = require('../../handler/ExportHandler');
|
const exportHandler = require("../../handler/ExportHandler");
|
||||||
const importHandler = require('../../handler/ImportHandler');
|
const importHandler = require("../../handler/ImportHandler");
|
||||||
const padManager = require('../../db/PadManager');
|
const padManager = require("../../db/PadManager");
|
||||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
const readOnlyManager = require("../../db/ReadOnlyManager");
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require("express-rate-limit");
|
||||||
const securityManager = require('../../db/SecurityManager');
|
const securityManager = require("../../db/SecurityManager");
|
||||||
const webaccess = require('./webaccess');
|
const webaccess = require("./webaccess");
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
exports.expressCreateServer = (
|
||||||
|
hookName: string,
|
||||||
|
args: ArgsExpressType,
|
||||||
|
cb: Function,
|
||||||
|
) => {
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
...settings.importExportRateLimiting,
|
...settings.importExportRateLimiting,
|
||||||
handler: (request: any) => {
|
handler: (request: any) => {
|
||||||
if (request.rateLimit.current === request.rateLimit.limit + 1) {
|
if (request.rateLimit.current === request.rateLimit.limit + 1) {
|
||||||
// when the rate limiter triggers, write a warning in the logs
|
// when the rate limiter triggers, write a warning in the logs
|
||||||
console.warn('Import/Export rate limiter triggered on ' +
|
console.warn(
|
||||||
`"${request.originalUrl}" for IP address ${request.ip}`);
|
"Import/Export rate limiter triggered on " +
|
||||||
|
`"${request.originalUrl}" for IP address ${request.ip}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle export requests
|
// handle export requests
|
||||||
args.app.use('/p/:pad/:rev?/export/:type', limiter);
|
args.app.use("/p/:pad/:rev?/export/:type", limiter);
|
||||||
args.app.get('/p/:pad/:rev?/export/:type', (req:any, res:any, next:Function) => {
|
args.app.get(
|
||||||
|
"/p/:pad/:rev?/export/:type",
|
||||||
|
(req: any, res: any, next: Function) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
|
const types = ["pdf", "doc", "txt", "html", "odt", "etherpad"];
|
||||||
// send a 404 if we don't support this filetype
|
// send a 404 if we don't support this filetype
|
||||||
if (types.indexOf(req.params.type) === -1) {
|
if (types.indexOf(req.params.type) === -1) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// if abiword is disabled, and this is a format we only support with abiword, output a message
|
// if abiword is disabled, and this is a format we only support with abiword, output a message
|
||||||
if (settings.exportAvailable() === 'no' &&
|
if (
|
||||||
['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) {
|
settings.exportAvailable() === "no" &&
|
||||||
console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
|
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1
|
||||||
' There is no converter configured');
|
) {
|
||||||
|
console.error(
|
||||||
|
`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
|
||||||
|
" There is no converter configured",
|
||||||
|
);
|
||||||
|
|
||||||
// ACHTUNG: do not include req.params.type in res.send() because there is
|
// ACHTUNG: do not include req.params.type in res.send() because there is
|
||||||
// no HTML escaping and it would lead to an XSS
|
// no HTML escaping and it would lead to an XSS
|
||||||
res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' +
|
res.send(
|
||||||
' or soffice (LibreOffice) in settings.json to enable this feature');
|
"This export is not enabled at this Etherpad instance. Set the path to Abiword" +
|
||||||
|
" or soffice (LibreOffice) in settings.json to enable this feature",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
if (await hasPadAccess(req, res)) {
|
if (await hasPadAccess(req, res)) {
|
||||||
let padId = req.params.pad;
|
let padId = req.params.pad;
|
||||||
|
@ -60,26 +74,47 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
|
||||||
|
|
||||||
const exists = await padManager.doesPadExists(padId);
|
const exists = await padManager.doesPadExists(padId);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
console.warn(`Someone tried to export a pad that doesn't exist (${padId})`);
|
console.warn(
|
||||||
|
`Someone tried to export a pad that doesn't exist (${padId})`,
|
||||||
|
);
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
|
console.log(
|
||||||
await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
|
`Exporting pad "${req.params.pad}" in ${req.params.type} format`,
|
||||||
|
);
|
||||||
|
await exportHandler.doExport(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
padId,
|
||||||
|
readOnlyId,
|
||||||
|
req.params.type,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})().catch((err) => next(err || new Error(err)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// handle import requests
|
// handle import requests
|
||||||
args.app.use('/p/:pad/import', limiter);
|
args.app.use("/p/:pad/import", limiter);
|
||||||
args.app.post('/p/:pad/import', (req:any, res:any, next:Function) => {
|
args.app.post("/p/:pad/import", (req: any, res: any, next: Function) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const {session: {user} = {}} = req;
|
const {
|
||||||
const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
|
session: { user } = {},
|
||||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
} = req;
|
||||||
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
|
const { accessStatus, authorID: authorId } =
|
||||||
return res.status(403).send('Forbidden');
|
await securityManager.checkAccess(
|
||||||
|
req.params.pad,
|
||||||
|
req.cookies.sessionID,
|
||||||
|
req.cookies.token,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
accessStatus !== "grant" ||
|
||||||
|
!webaccess.userCanModify(req.params.pad, req)
|
||||||
|
) {
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
}
|
}
|
||||||
await importHandler.doImport(req, res, req.params.pad, authorId);
|
await importHandler.doImport(req, res, req.params.pad, authorId);
|
||||||
})().catch((err) => next(err || new Error(err)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
|
|
|
@ -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 { MapArrayType } from "../../types/MapType";
|
||||||
import { ErrorCaused } from "../../types/ErrorCaused";
|
import { ErrorCaused } from "../../types/ErrorCaused";
|
||||||
|
|
||||||
|
@ -18,260 +22,282 @@ import {ErrorCaused} from "../../types/ErrorCaused";
|
||||||
* - /rest/{version}/openapi.json
|
* - /rest/{version}/openapi.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const OpenAPIBackend = require('openapi-backend').default;
|
const OpenAPIBackend = require("openapi-backend").default;
|
||||||
const IncomingForm = require('formidable').IncomingForm;
|
const IncomingForm = require("formidable").IncomingForm;
|
||||||
const cloneDeep = require('lodash.clonedeep');
|
const cloneDeep = require("lodash.clonedeep");
|
||||||
const createHTTPError = require('http-errors');
|
const createHTTPError = require("http-errors");
|
||||||
|
|
||||||
const apiHandler = require('../../handler/APIHandler');
|
const apiHandler = require("../../handler/APIHandler");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
|
|
||||||
const log4js = require('log4js');
|
const log4js = require("log4js");
|
||||||
const logger = log4js.getLogger('API');
|
const logger = log4js.getLogger("API");
|
||||||
|
|
||||||
// https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0
|
// https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0
|
||||||
const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version
|
const OPENAPI_VERSION = "3.0.2"; // Swagger/OAS version
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
title: 'Etherpad API',
|
title: "Etherpad API",
|
||||||
description:
|
description:
|
||||||
'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous ' +
|
"Etherpad is a real-time collaborative editor scalable to thousands of simultaneous " +
|
||||||
'real time users. It provides full data export capabilities, and runs on your server, ' +
|
"real time users. It provides full data export capabilities, and runs on your server, " +
|
||||||
'under your control.',
|
"under your control.",
|
||||||
termsOfService: 'https://etherpad.org/',
|
termsOfService: "https://etherpad.org/",
|
||||||
contact: {
|
contact: {
|
||||||
name: 'The Etherpad Foundation',
|
name: "The Etherpad Foundation",
|
||||||
url: 'https://etherpad.org/',
|
url: "https://etherpad.org/",
|
||||||
email: 'support@example.com',
|
email: "support@example.com",
|
||||||
},
|
},
|
||||||
license: {
|
license: {
|
||||||
name: 'Apache 2.0',
|
name: "Apache 2.0",
|
||||||
url: 'https://www.apache.org/licenses/LICENSE-2.0.html',
|
url: "https://www.apache.org/licenses/LICENSE-2.0.html",
|
||||||
},
|
},
|
||||||
version: apiHandler.latestApiVersion,
|
version: apiHandler.latestApiVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
const APIPathStyle = {
|
const APIPathStyle = {
|
||||||
FLAT: 'api', // flat paths e.g. /api/createGroup
|
FLAT: "api", // flat paths e.g. /api/createGroup
|
||||||
REST: 'rest', // restful paths e.g. /rest/group/create
|
REST: "rest", // restful paths e.g. /rest/group/create
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// API resources - describe your API endpoints here
|
// API resources - describe your API endpoints here
|
||||||
const resources: SwaggerUIResource = {
|
const resources: SwaggerUIResource = {
|
||||||
// Group
|
// Group
|
||||||
group: {
|
group: {
|
||||||
create: {
|
create: {
|
||||||
operationId: 'createGroup',
|
operationId: "createGroup",
|
||||||
summary: 'creates a new group',
|
summary: "creates a new group",
|
||||||
responseSchema: {groupID: {type: 'string'}},
|
responseSchema: { groupID: { type: "string" } },
|
||||||
},
|
},
|
||||||
createIfNotExistsFor: {
|
createIfNotExistsFor: {
|
||||||
operationId: 'createGroupIfNotExistsFor',
|
operationId: "createGroupIfNotExistsFor",
|
||||||
summary: 'this functions helps you to map your application group ids to Etherpad group ids',
|
summary:
|
||||||
responseSchema: {groupID: {type: 'string'}},
|
"this functions helps you to map your application group ids to Etherpad group ids",
|
||||||
|
responseSchema: { groupID: { type: "string" } },
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
operationId: 'deleteGroup',
|
operationId: "deleteGroup",
|
||||||
summary: 'deletes a group',
|
summary: "deletes a group",
|
||||||
},
|
},
|
||||||
listPads: {
|
listPads: {
|
||||||
operationId: 'listPads',
|
operationId: "listPads",
|
||||||
summary: 'returns all pads of this group',
|
summary: "returns all pads of this group",
|
||||||
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}},
|
responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
|
||||||
},
|
},
|
||||||
createPad: {
|
createPad: {
|
||||||
operationId: 'createGroupPad',
|
operationId: "createGroupPad",
|
||||||
summary: 'creates a new pad in this group',
|
summary: "creates a new pad in this group",
|
||||||
},
|
},
|
||||||
listSessions: {
|
listSessions: {
|
||||||
operationId: 'listSessionsOfGroup',
|
operationId: "listSessionsOfGroup",
|
||||||
summary: '',
|
summary: "",
|
||||||
responseSchema: {
|
responseSchema: {
|
||||||
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}},
|
sessions: {
|
||||||
|
type: "array",
|
||||||
|
items: { $ref: "#/components/schemas/SessionInfo" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
operationId: 'listAllGroups',
|
operationId: "listAllGroups",
|
||||||
summary: '',
|
summary: "",
|
||||||
responseSchema: {groupIDs: {type: 'array', items: {type: 'string'}}},
|
responseSchema: {
|
||||||
|
groupIDs: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Author
|
// Author
|
||||||
author: {
|
author: {
|
||||||
create: {
|
create: {
|
||||||
operationId: 'createAuthor',
|
operationId: "createAuthor",
|
||||||
summary: 'creates a new author',
|
summary: "creates a new author",
|
||||||
responseSchema: {authorID: {type: 'string'}},
|
responseSchema: { authorID: { type: "string" } },
|
||||||
},
|
},
|
||||||
createIfNotExistsFor: {
|
createIfNotExistsFor: {
|
||||||
operationId: 'createAuthorIfNotExistsFor',
|
operationId: "createAuthorIfNotExistsFor",
|
||||||
summary: 'this functions helps you to map your application author ids to Etherpad author ids',
|
summary:
|
||||||
responseSchema: {authorID: {type: 'string'}},
|
"this functions helps you to map your application author ids to Etherpad author ids",
|
||||||
|
responseSchema: { authorID: { type: "string" } },
|
||||||
},
|
},
|
||||||
listPads: {
|
listPads: {
|
||||||
operationId: 'listPadsOfAuthor',
|
operationId: "listPadsOfAuthor",
|
||||||
summary: 'returns an array of all pads this author contributed to',
|
summary: "returns an array of all pads this author contributed to",
|
||||||
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}},
|
responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
|
||||||
},
|
},
|
||||||
listSessions: {
|
listSessions: {
|
||||||
operationId: 'listSessionsOfAuthor',
|
operationId: "listSessionsOfAuthor",
|
||||||
summary: 'returns all sessions of an author',
|
summary: "returns all sessions of an author",
|
||||||
responseSchema: {
|
responseSchema: {
|
||||||
sessions: {type: 'array', items: {$ref: '#/components/schemas/SessionInfo'}},
|
sessions: {
|
||||||
|
type: "array",
|
||||||
|
items: { $ref: "#/components/schemas/SessionInfo" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// We need an operation that return a UserInfo so it can be picked up by the codegen :(
|
// We need an operation that return a UserInfo so it can be picked up by the codegen :(
|
||||||
getName: {
|
getName: {
|
||||||
operationId: 'getAuthorName',
|
operationId: "getAuthorName",
|
||||||
summary: 'Returns the Author Name of the author',
|
summary: "Returns the Author Name of the author",
|
||||||
responseSchema: {info: {$ref: '#/components/schemas/UserInfo'}},
|
responseSchema: { info: { $ref: "#/components/schemas/UserInfo" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
session: {
|
session: {
|
||||||
create: {
|
create: {
|
||||||
operationId: 'createSession',
|
operationId: "createSession",
|
||||||
summary: 'creates a new session. validUntil is an unix timestamp in seconds',
|
summary:
|
||||||
responseSchema: {sessionID: {type: 'string'}},
|
"creates a new session. validUntil is an unix timestamp in seconds",
|
||||||
|
responseSchema: { sessionID: { type: "string" } },
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
operationId: 'deleteSession',
|
operationId: "deleteSession",
|
||||||
summary: 'deletes a session',
|
summary: "deletes a session",
|
||||||
},
|
},
|
||||||
// We need an operation that returns a SessionInfo so it can be picked up by the codegen :(
|
// We need an operation that returns a SessionInfo so it can be picked up by the codegen :(
|
||||||
info: {
|
info: {
|
||||||
operationId: 'getSessionInfo',
|
operationId: "getSessionInfo",
|
||||||
summary: 'returns information about a session',
|
summary: "returns information about a session",
|
||||||
responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}},
|
responseSchema: { info: { $ref: "#/components/schemas/SessionInfo" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Pad
|
// Pad
|
||||||
pad: {
|
pad: {
|
||||||
listAll: {
|
listAll: {
|
||||||
operationId: 'listAllPads',
|
operationId: "listAllPads",
|
||||||
summary: 'list all the pads',
|
summary: "list all the pads",
|
||||||
responseSchema: {padIDs: {type: 'array', items: {type: 'string'}}},
|
responseSchema: { padIDs: { type: "array", items: { type: "string" } } },
|
||||||
},
|
},
|
||||||
createDiffHTML: {
|
createDiffHTML: {
|
||||||
operationId: 'createDiffHTML',
|
operationId: "createDiffHTML",
|
||||||
summary: '',
|
summary: "",
|
||||||
responseSchema: {},
|
responseSchema: {},
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
operationId: 'createPad',
|
operationId: "createPad",
|
||||||
description:
|
description:
|
||||||
'creates a new (non-group) pad. Note that if you need to create a group Pad, ' +
|
"creates a new (non-group) pad. Note that if you need to create a group Pad, " +
|
||||||
'you should call createGroupPad',
|
"you should call createGroupPad",
|
||||||
},
|
},
|
||||||
getText: {
|
getText: {
|
||||||
operationId: 'getText',
|
operationId: "getText",
|
||||||
summary: 'returns the text of a pad',
|
summary: "returns the text of a pad",
|
||||||
responseSchema: {text: {type: 'string'}},
|
responseSchema: { text: { type: "string" } },
|
||||||
},
|
},
|
||||||
setText: {
|
setText: {
|
||||||
operationId: 'setText',
|
operationId: "setText",
|
||||||
summary: 'sets the text of a pad',
|
summary: "sets the text of a pad",
|
||||||
},
|
},
|
||||||
getHTML: {
|
getHTML: {
|
||||||
operationId: 'getHTML',
|
operationId: "getHTML",
|
||||||
summary: 'returns the text of a pad formatted as HTML',
|
summary: "returns the text of a pad formatted as HTML",
|
||||||
responseSchema: {html: {type: 'string'}},
|
responseSchema: { html: { type: "string" } },
|
||||||
},
|
},
|
||||||
setHTML: {
|
setHTML: {
|
||||||
operationId: 'setHTML',
|
operationId: "setHTML",
|
||||||
summary: 'sets the text of a pad with HTML',
|
summary: "sets the text of a pad with HTML",
|
||||||
},
|
},
|
||||||
getRevisionsCount: {
|
getRevisionsCount: {
|
||||||
operationId: 'getRevisionsCount',
|
operationId: "getRevisionsCount",
|
||||||
summary: 'returns the number of revisions of this pad',
|
summary: "returns the number of revisions of this pad",
|
||||||
responseSchema: {revisions: {type: 'integer'}},
|
responseSchema: { revisions: { type: "integer" } },
|
||||||
},
|
},
|
||||||
getLastEdited: {
|
getLastEdited: {
|
||||||
operationId: 'getLastEdited',
|
operationId: "getLastEdited",
|
||||||
summary: 'returns the timestamp of the last revision of the pad',
|
summary: "returns the timestamp of the last revision of the pad",
|
||||||
responseSchema: {lastEdited: {type: 'integer'}},
|
responseSchema: { lastEdited: { type: "integer" } },
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
operationId: 'deletePad',
|
operationId: "deletePad",
|
||||||
summary: 'deletes a pad',
|
summary: "deletes a pad",
|
||||||
},
|
},
|
||||||
getReadOnlyID: {
|
getReadOnlyID: {
|
||||||
operationId: 'getReadOnlyID',
|
operationId: "getReadOnlyID",
|
||||||
summary: 'returns the read only link of a pad',
|
summary: "returns the read only link of a pad",
|
||||||
responseSchema: {readOnlyID: {type: 'string'}},
|
responseSchema: { readOnlyID: { type: "string" } },
|
||||||
},
|
},
|
||||||
setPublicStatus: {
|
setPublicStatus: {
|
||||||
operationId: 'setPublicStatus',
|
operationId: "setPublicStatus",
|
||||||
summary: 'sets a boolean for the public status of a pad',
|
summary: "sets a boolean for the public status of a pad",
|
||||||
},
|
},
|
||||||
getPublicStatus: {
|
getPublicStatus: {
|
||||||
operationId: 'getPublicStatus',
|
operationId: "getPublicStatus",
|
||||||
summary: 'return true of false',
|
summary: "return true of false",
|
||||||
responseSchema: {publicStatus: {type: 'boolean'}},
|
responseSchema: { publicStatus: { type: "boolean" } },
|
||||||
},
|
},
|
||||||
authors: {
|
authors: {
|
||||||
operationId: 'listAuthorsOfPad',
|
operationId: "listAuthorsOfPad",
|
||||||
summary: 'returns an array of authors who contributed to this pad',
|
summary: "returns an array of authors who contributed to this pad",
|
||||||
responseSchema: {authorIDs: {type: 'array', items: {type: 'string'}}},
|
responseSchema: {
|
||||||
|
authorIDs: { type: "array", items: { type: "string" } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
usersCount: {
|
usersCount: {
|
||||||
operationId: 'padUsersCount',
|
operationId: "padUsersCount",
|
||||||
summary: 'returns the number of user that are currently editing this pad',
|
summary: "returns the number of user that are currently editing this pad",
|
||||||
responseSchema: {padUsersCount: {type: 'integer'}},
|
responseSchema: { padUsersCount: { type: "integer" } },
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
operationId: 'padUsers',
|
operationId: "padUsers",
|
||||||
summary: 'returns the list of users that are currently editing this pad',
|
summary: "returns the list of users that are currently editing this pad",
|
||||||
responseSchema: {padUsers: {type: 'array', items: {$ref: '#/components/schemas/UserInfo'}}},
|
responseSchema: {
|
||||||
|
padUsers: {
|
||||||
|
type: "array",
|
||||||
|
items: { $ref: "#/components/schemas/UserInfo" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
sendClientsMessage: {
|
sendClientsMessage: {
|
||||||
operationId: 'sendClientsMessage',
|
operationId: "sendClientsMessage",
|
||||||
summary: 'sends a custom message of type msg to the pad',
|
summary: "sends a custom message of type msg to the pad",
|
||||||
},
|
},
|
||||||
checkToken: {
|
checkToken: {
|
||||||
operationId: 'checkToken',
|
operationId: "checkToken",
|
||||||
summary: 'returns ok when the current api token is valid',
|
summary: "returns ok when the current api token is valid",
|
||||||
},
|
},
|
||||||
getChatHistory: {
|
getChatHistory: {
|
||||||
operationId: 'getChatHistory',
|
operationId: "getChatHistory",
|
||||||
summary: 'returns the chat history',
|
summary: "returns the chat history",
|
||||||
responseSchema: {messages: {type: 'array', items: {$ref: '#/components/schemas/Message'}}},
|
responseSchema: {
|
||||||
|
messages: {
|
||||||
|
type: "array",
|
||||||
|
items: { $ref: "#/components/schemas/Message" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// We need an operation that returns a Message so it can be picked up by the codegen :(
|
// We need an operation that returns a Message so it can be picked up by the codegen :(
|
||||||
getChatHead: {
|
getChatHead: {
|
||||||
operationId: 'getChatHead',
|
operationId: "getChatHead",
|
||||||
summary: 'returns the chatHead (chat-message) of the pad',
|
summary: "returns the chatHead (chat-message) of the pad",
|
||||||
responseSchema: {chatHead: {$ref: '#/components/schemas/Message'}},
|
responseSchema: { chatHead: { $ref: "#/components/schemas/Message" } },
|
||||||
},
|
},
|
||||||
appendChatMessage: {
|
appendChatMessage: {
|
||||||
operationId: 'appendChatMessage',
|
operationId: "appendChatMessage",
|
||||||
summary: 'appends a chat message',
|
summary: "appends a chat message",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultResponses = {
|
const defaultResponses = {
|
||||||
Success: {
|
Success: {
|
||||||
description: 'ok (code 0)',
|
description: "ok (code 0)",
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
"application/json": {
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
code: {
|
code: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
example: 0,
|
example: 0,
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
example: 'ok',
|
example: "ok",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
example: null,
|
example: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -280,22 +306,22 @@ const defaultResponses = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ApiError: {
|
ApiError: {
|
||||||
description: 'generic api error (code 1)',
|
description: "generic api error (code 1)",
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
"application/json": {
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
code: {
|
code: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
example: 1,
|
example: 1,
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
example: 'error message',
|
example: "error message",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
example: null,
|
example: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -304,22 +330,22 @@ const defaultResponses = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
InternalError: {
|
InternalError: {
|
||||||
description: 'internal api error (code 2)',
|
description: "internal api error (code 2)",
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
"application/json": {
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
code: {
|
code: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
example: 2,
|
example: 2,
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
example: 'internal error',
|
example: "internal error",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
example: null,
|
example: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -328,22 +354,22 @@ const defaultResponses = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NotFound: {
|
NotFound: {
|
||||||
description: 'no such function (code 4)',
|
description: "no such function (code 4)",
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
"application/json": {
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
code: {
|
code: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
example: 3,
|
example: 3,
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
example: 'no such function',
|
example: "no such function",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
example: null,
|
example: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -352,22 +378,22 @@ const defaultResponses = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Unauthorized: {
|
Unauthorized: {
|
||||||
description: 'no or wrong API key (code 4)',
|
description: "no or wrong API key (code 4)",
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
"application/json": {
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
code: {
|
code: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
example: 4,
|
example: 4,
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
example: 'no or wrong API key',
|
example: "no or wrong API key",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
example: null,
|
example: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -379,16 +405,16 @@ const defaultResponses = {
|
||||||
|
|
||||||
const defaultResponseRefs: OpenAPISuccessResponse = {
|
const defaultResponseRefs: OpenAPISuccessResponse = {
|
||||||
200: {
|
200: {
|
||||||
$ref: '#/components/responses/Success',
|
$ref: "#/components/responses/Success",
|
||||||
},
|
},
|
||||||
400: {
|
400: {
|
||||||
$ref: '#/components/responses/ApiError',
|
$ref: "#/components/responses/ApiError",
|
||||||
},
|
},
|
||||||
401: {
|
401: {
|
||||||
$ref: '#/components/responses/Unauthorized',
|
$ref: "#/components/responses/Unauthorized",
|
||||||
},
|
},
|
||||||
500: {
|
500: {
|
||||||
$ref: '#/components/responses/InternalError',
|
$ref: "#/components/responses/InternalError",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -402,8 +428,8 @@ for (const [resource, actions] of Object.entries(resources)) {
|
||||||
const responses: OpenAPISuccessResponse = { ...defaultResponseRefs };
|
const responses: OpenAPISuccessResponse = { ...defaultResponseRefs };
|
||||||
if (responseSchema) {
|
if (responseSchema) {
|
||||||
responses[200] = cloneDeep(defaultResponses.Success);
|
responses[200] = cloneDeep(defaultResponses.Success);
|
||||||
responses[200].content!['application/json'].schema.properties.data = {
|
responses[200].content!["application/json"].schema.properties.data = {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: responseSchema,
|
properties: responseSchema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -419,7 +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 = {
|
const definition = {
|
||||||
openapi: OPENAPI_VERSION,
|
openapi: OPENAPI_VERSION,
|
||||||
info,
|
info,
|
||||||
|
@ -428,53 +457,53 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
|
||||||
parameters: {},
|
parameters: {},
|
||||||
schemas: {
|
schemas: {
|
||||||
SessionInfo: {
|
SessionInfo: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
authorID: {
|
authorID: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
groupID: {
|
groupID: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
validUntil: {
|
validUntil: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UserInfo: {
|
UserInfo: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
colorId: {
|
colorId: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
timestamp: {
|
timestamp: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Message: {
|
Message: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
text: {
|
text: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
userName: {
|
userName: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -493,9 +522,9 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
|
||||||
openid: "openid",
|
openid: "openid",
|
||||||
profile: "profile",
|
profile: "profile",
|
||||||
email: "email",
|
email: "email",
|
||||||
admin: "admin"
|
admin: "admin",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -519,15 +548,17 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT)
|
||||||
// set parameters
|
// set parameters
|
||||||
operation.parameters = operation.parameters || [];
|
operation.parameters = operation.parameters || [];
|
||||||
for (const paramName of apiHandler.version[version][funcName]) {
|
for (const paramName of apiHandler.version[version][funcName]) {
|
||||||
operation.parameters.push({$ref: `#/components/parameters/${paramName}`});
|
operation.parameters.push({
|
||||||
|
$ref: `#/components/parameters/${paramName}`,
|
||||||
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!definition.components.parameters[paramName]) {
|
if (!definition.components.parameters[paramName]) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
definition.components.parameters[paramName] = {
|
definition.components.parameters[paramName] = {
|
||||||
name: paramName,
|
name: paramName,
|
||||||
in: 'query',
|
in: "query",
|
||||||
schema: {
|
schema: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -572,16 +603,22 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
// serve version specific openapi definition
|
// serve version specific openapi definition
|
||||||
app.get(`${apiRoot}/openapi.json`, (req: any, res: any) => {
|
app.get(`${apiRoot}/openapi.json`, (req: any, res: any) => {
|
||||||
// For openapi definitions, wide CORS is probably fine
|
// For openapi definitions, wide CORS is probably fine
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
|
res.json({
|
||||||
|
...definition,
|
||||||
|
servers: [generateServerForApiVersion(apiRoot, req)],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// serve latest openapi definition file under /api/openapi.json
|
// serve latest openapi definition file under /api/openapi.json
|
||||||
const isLatestAPIVersion = version === apiHandler.latestApiVersion;
|
const isLatestAPIVersion = version === apiHandler.latestApiVersion;
|
||||||
if (isLatestAPIVersion) {
|
if (isLatestAPIVersion) {
|
||||||
app.get(`/${style}/openapi.json`, (req: any, res: any) => {
|
app.get(`/${style}/openapi.json`, (req: any, res: any) => {
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
|
res.json({
|
||||||
|
...definition,
|
||||||
|
servers: [generateServerForApiVersion(apiRoot, req)],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -597,10 +634,10 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
// register default handlers
|
// register default handlers
|
||||||
api.register({
|
api.register({
|
||||||
notFound: () => {
|
notFound: () => {
|
||||||
throw new createHTTPError.NotFound('no such function');
|
throw new createHTTPError.NotFound("no such function");
|
||||||
},
|
},
|
||||||
notImplemented: () => {
|
notImplemented: () => {
|
||||||
throw new createHTTPError.NotImplemented('function not implemented');
|
throw new createHTTPError.NotImplemented("function not implemented");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -612,7 +649,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
|
|
||||||
// read form data if method was POST
|
// read form data if method was POST
|
||||||
let formData: MapArrayType<any> = {};
|
let formData: MapArrayType<any> = {};
|
||||||
if (c.request.method === 'post') {
|
if (c.request.method === "post") {
|
||||||
const form = new IncomingForm();
|
const form = new IncomingForm();
|
||||||
formData = (await form.parse(req))[0];
|
formData = (await form.parse(req))[0];
|
||||||
for (const k of Object.keys(formData)) {
|
for (const k of Object.keys(formData)) {
|
||||||
|
@ -625,7 +662,9 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
const fields = Object.assign({}, header, params, query, formData);
|
const fields = Object.assign({}, header, params, query, formData);
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`);
|
logger.debug(
|
||||||
|
`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass to api handler
|
// pass to api handler
|
||||||
|
@ -633,12 +672,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
try {
|
try {
|
||||||
data = await apiHandler.handle(version, funcName, fields, req, res);
|
data = await apiHandler.handle(version, funcName, fields, req, res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errCaused = err as ErrorCaused
|
const errCaused = err as ErrorCaused;
|
||||||
// convert all errors to http errors
|
// convert all errors to http errors
|
||||||
if (createHTTPError.isHttpError(err)) {
|
if (createHTTPError.isHttpError(err)) {
|
||||||
// pass http errors thrown by handler forward
|
// pass http errors thrown by handler forward
|
||||||
throw err;
|
throw err;
|
||||||
} else if (errCaused.name === 'apierror') {
|
} else if (errCaused.name === "apierror") {
|
||||||
// parameters were wrong and the api stopped execution, pass the error
|
// parameters were wrong and the api stopped execution, pass the error
|
||||||
// convert to http error
|
// convert to http error
|
||||||
throw new createHTTPError.BadRequest(errCaused.message);
|
throw new createHTTPError.BadRequest(errCaused.message);
|
||||||
|
@ -646,12 +685,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
// an unknown error happened
|
// an unknown error happened
|
||||||
// log it and throw internal error
|
// log it and throw internal error
|
||||||
logger.error(errCaused.stack || errCaused.toString());
|
logger.error(errCaused.stack || errCaused.toString());
|
||||||
throw new createHTTPError.InternalError('internal error');
|
throw new createHTTPError.InternalError("internal error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return in common format
|
// return in common format
|
||||||
const response = {code: 0, message: 'ok', data: data || null};
|
const response = { code: 0, message: "ok", data: data || null };
|
||||||
|
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`);
|
logger.debug(`RESPONSE, ${funcName}, ${JSON.stringify(response)}`);
|
||||||
|
@ -674,12 +713,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
if (style === APIPathStyle.REST) {
|
if (style === APIPathStyle.REST) {
|
||||||
// @TODO: Don't allow CORS from everywhere
|
// @TODO: Don't allow CORS from everywhere
|
||||||
// This is purely to maintain compatibility with old swagger-node-express
|
// This is purely to maintain compatibility with old swagger-node-express
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
}
|
}
|
||||||
// pass to openapi-backend handler
|
// pass to openapi-backend handler
|
||||||
response = await api.handleRequest(req, req, res);
|
response = await api.handleRequest(req, req, res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errCaused = err as ErrorCaused
|
const errCaused = err as ErrorCaused;
|
||||||
// handle http errors
|
// handle http errors
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
res.statusCode = errCaused.statusCode || 500;
|
res.statusCode = errCaused.statusCode || 500;
|
||||||
|
@ -723,7 +762,10 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
* @param {APIPathStyle} style The style of the API path
|
* @param {APIPathStyle} style The style of the API path
|
||||||
* @return {String} The root path for the API version
|
* @return {String} The root path for the API version
|
||||||
*/
|
*/
|
||||||
const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): string => `/${style}/${version}`;
|
const getApiRootForVersion = (
|
||||||
|
version: string,
|
||||||
|
style: any = APIPathStyle.FLAT,
|
||||||
|
): string => `/${style}/${version}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to generate an OpenAPI server object when serving definitions
|
* Helper to generate an OpenAPI server object when serving definitions
|
||||||
|
@ -731,8 +773,11 @@ const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): st
|
||||||
* @param {Request} req The express request object
|
* @param {Request} req The express request object
|
||||||
* @return {url: String} The server object for the OpenAPI definition location
|
* @return {url: String} The server object for the OpenAPI definition location
|
||||||
*/
|
*/
|
||||||
const generateServerForApiVersion = (apiRoot:string, req:any): {
|
const generateServerForApiVersion = (
|
||||||
url:string
|
apiRoot: string,
|
||||||
|
req: any,
|
||||||
|
): {
|
||||||
|
url: string;
|
||||||
} => ({
|
} => ({
|
||||||
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
|
url: `${settings.ssl ? "https" : "http"}://${req.headers.host}${apiRoot}`,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
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
|
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
|
||||||
args.app.param('pad', (req:any, res:any, next:Function, padId:string) => {
|
args.app.param("pad", (req: any, res: any, next: Function, padId: string) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
// ensure the padname is valid and the url doesn't end with a /
|
// ensure the padname is valid and the url doesn't end with a /
|
||||||
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
|
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
|
||||||
res.status(404).send('Such a padname is forbidden');
|
res.status(404).send("Such a padname is forbidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,9 +26,14 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio
|
||||||
} else {
|
} else {
|
||||||
// the pad id was sanitized, so we redirect to the sanitized version
|
// the pad id was sanitized, so we redirect to the sanitized version
|
||||||
const realURL =
|
const realURL =
|
||||||
encodeURIComponent(sanitizedPadId) + new URL(req.url, 'http://invalid.invalid').search;
|
encodeURIComponent(sanitizedPadId) +
|
||||||
res.header('Location', realURL);
|
new URL(req.url, "http://invalid.invalid").search;
|
||||||
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
|
res.header("Location", realURL);
|
||||||
|
res
|
||||||
|
.status(302)
|
||||||
|
.send(
|
||||||
|
`You should be redirected to <a href="${realURL}">${realURL}</a>`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})().catch((err) => next(err || new Error(err)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
import { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
|
||||||
import events from 'events';
|
import events from "events";
|
||||||
const express = require('../express');
|
const express = require("../express");
|
||||||
import log4js from 'log4js';
|
import log4js from "log4js";
|
||||||
const proxyaddr = require('proxy-addr');
|
const proxyaddr = require("proxy-addr");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
import {Server, Socket} from 'socket.io'
|
import { Server, Socket } from "socket.io";
|
||||||
const socketIORouter = require('../../handler/SocketIORouter');
|
const socketIORouter = require("../../handler/SocketIORouter");
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||||
const padMessageHandler = require('../../handler/PadMessageHandler');
|
const padMessageHandler = require("../../handler/PadMessageHandler");
|
||||||
|
|
||||||
let io: any;
|
let io: any;
|
||||||
const logger = log4js.getLogger('socket.io');
|
const logger = log4js.getLogger("socket.io");
|
||||||
const sockets = new Set();
|
const sockets = new Set();
|
||||||
const socketsEvents = new events.EventEmitter();
|
const socketsEvents = new events.EventEmitter();
|
||||||
|
|
||||||
export const expressCloseServer = async () => {
|
export const expressCloseServer = async () => {
|
||||||
if (io == null) return;
|
if (io == null) return;
|
||||||
logger.info('Closing socket.io engine...');
|
logger.info("Closing socket.io engine...");
|
||||||
// Close the socket.io engine to disconnect existing clients and reject new clients. Don't call
|
// Close the socket.io engine to disconnect existing clients and reject new clients. Don't call
|
||||||
// io.close() because that closes the underlying HTTP server, which is already done elsewhere.
|
// io.close() because that closes the underlying HTTP server, which is already done elsewhere.
|
||||||
// (Closing an HTTP server twice throws an exception.) The `engine` property of socket.io Server
|
// (Closing an HTTP server twice throws an exception.) The `engine` property of socket.io Server
|
||||||
|
@ -39,21 +39,25 @@ export const expressCloseServer = async () => {
|
||||||
// ourselves, so that is what we do.
|
// ourselves, so that is what we do.
|
||||||
let lastLogged = 0;
|
let lastLogged = 0;
|
||||||
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
||||||
if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs.
|
if (Date.now() - lastLogged > 1000) {
|
||||||
logger.info(`Waiting for ${sockets.size} socket.io clients to disconnect...`);
|
// Rate limit to avoid filling logs.
|
||||||
|
logger.info(
|
||||||
|
`Waiting for ${sockets.size} socket.io clients to disconnect...`,
|
||||||
|
);
|
||||||
lastLogged = Date.now();
|
lastLogged = Date.now();
|
||||||
}
|
}
|
||||||
await events.once(socketsEvents, 'updated');
|
await events.once(socketsEvents, "updated");
|
||||||
}
|
}
|
||||||
logger.info('All socket.io clients have disconnected');
|
logger.info("All socket.io clients have disconnected");
|
||||||
};
|
};
|
||||||
|
|
||||||
const socketSessionMiddleware = (args: any) => (socket: any, next: Function) => {
|
const socketSessionMiddleware =
|
||||||
|
(args: any) => (socket: any, next: Function) => {
|
||||||
const req = socket.request;
|
const req = socket.request;
|
||||||
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
|
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
|
||||||
if (req.ip == null) {
|
if (req.ip == null) {
|
||||||
if (settings.trustProxy) {
|
if (settings.trustProxy) {
|
||||||
req.ip = proxyaddr(req, args.app.get('trust proxy fn'));
|
req.ip = proxyaddr(req, args.app.get("trust proxy fn"));
|
||||||
} else {
|
} else {
|
||||||
req.ip = socket.handshake.address;
|
req.ip = socket.handshake.address;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +69,11 @@ const socketSessionMiddleware = (args: any) => (socket: any, next: Function) =>
|
||||||
express.sessionMiddleware(req, {}, next);
|
express.sessionMiddleware(req, {}, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
export const expressCreateServer = (
|
||||||
|
hookName: string,
|
||||||
|
args: ArgsExpressType,
|
||||||
|
cb: Function,
|
||||||
|
) => {
|
||||||
// init socket.io and redirect all requests to the MessageHandler
|
// init socket.io and redirect all requests to the MessageHandler
|
||||||
// there shouldn't be a browser that isn't compatible to all
|
// there shouldn't be a browser that isn't compatible to all
|
||||||
// transports in this list at once
|
// transports in this list at once
|
||||||
|
@ -74,25 +82,24 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
||||||
transports: settings.socketTransportProtocols,
|
transports: settings.socketTransportProtocols,
|
||||||
cookie: false,
|
cookie: false,
|
||||||
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleConnection = (socket: Socket) => {
|
const handleConnection = (socket: Socket) => {
|
||||||
sockets.add(socket);
|
sockets.add(socket);
|
||||||
socketsEvents.emit('updated');
|
socketsEvents.emit("updated");
|
||||||
// https://socket.io/docs/v3/faq/index.html
|
// https://socket.io/docs/v3/faq/index.html
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const session = socket.request.session;
|
const session = socket.request.session;
|
||||||
session.connections++;
|
session.connections++;
|
||||||
session.save();
|
session.save();
|
||||||
socket.on('disconnect', () => {
|
socket.on("disconnect", () => {
|
||||||
sockets.delete(socket);
|
sockets.delete(socket);
|
||||||
socketsEvents.emit('updated');
|
socketsEvents.emit("updated");
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const renewSession = (socket: any, next: Function) => {
|
const renewSession = (socket: any, next: Function) => {
|
||||||
socket.conn.on('packet', (packet:string) => {
|
socket.conn.on("packet", (packet: string) => {
|
||||||
// Tell express-session that the session is still active. The session store can use these
|
// Tell express-session that the session is still active. The session store can use these
|
||||||
// touch events to defer automatic session cleanup, and if express-session is configured with
|
// touch events to defer automatic session cleanup, and if express-session is configured with
|
||||||
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not
|
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not
|
||||||
|
@ -102,22 +109,21 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
||||||
if (socket.request.session != null) socket.request.session.touch();
|
if (socket.request.session != null) socket.request.session.touch();
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
}
|
};
|
||||||
|
|
||||||
|
io.on("connection", handleConnection);
|
||||||
io.on('connection', handleConnection);
|
|
||||||
|
|
||||||
io.use(socketSessionMiddleware(args));
|
io.use(socketSessionMiddleware(args));
|
||||||
|
|
||||||
// Temporary workaround so all clients go through middleware and handle connection
|
// Temporary workaround so all clients go through middleware and handle connection
|
||||||
io.of('/pluginfw/installer')
|
io.of("/pluginfw/installer")
|
||||||
.on('connection',handleConnection)
|
.on("connection", handleConnection)
|
||||||
.use(socketSessionMiddleware(args))
|
.use(socketSessionMiddleware(args))
|
||||||
.use(renewSession)
|
.use(renewSession);
|
||||||
io.of('/settings')
|
io.of("/settings")
|
||||||
.on('connection',handleConnection)
|
.on("connection", handleConnection)
|
||||||
.use(socketSessionMiddleware(args))
|
.use(socketSessionMiddleware(args))
|
||||||
.use(renewSession)
|
.use(renewSession);
|
||||||
|
|
||||||
io.use(renewSession);
|
io.use(renewSession);
|
||||||
|
|
||||||
|
@ -134,9 +140,9 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu
|
||||||
|
|
||||||
// Initialize the Socket.IO Router
|
// Initialize the Socket.IO Router
|
||||||
socketIORouter.setSocketIO(io);
|
socketIORouter.setSocketIO(io);
|
||||||
socketIORouter.addComponent('pad', padMessageHandler);
|
socketIORouter.addComponent("pad", padMessageHandler);
|
||||||
|
|
||||||
hooks.callAll('socketio', {app: args.app, io, server: args.server});
|
hooks.callAll("socketio", { app: args.app, io, server: args.server });
|
||||||
|
|
||||||
return cb();
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,62 +1,78 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const eejs = require('../../eejs');
|
const eejs = require("../../eejs");
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const fsp = fs.promises;
|
const fsp = fs.promises;
|
||||||
const toolbar = require('../../utils/toolbar');
|
const toolbar = require("../../utils/toolbar");
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
const util = require('util');
|
const util = require("util");
|
||||||
const webaccess = require('./webaccess');
|
const webaccess = require("./webaccess");
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||||
// This endpoint is intended to conform to:
|
// This endpoint is intended to conform to:
|
||||||
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
||||||
app.get('/health', (req:any, res:any) => {
|
app.get("/health", (req: any, res: any) => {
|
||||||
res.set('Content-Type', 'application/health+json');
|
res.set("Content-Type", "application/health+json");
|
||||||
res.json({
|
res.json({
|
||||||
status: 'pass',
|
status: "pass",
|
||||||
releaseId: settings.getEpVersion(),
|
releaseId: settings.getEpVersion(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/stats', (req:any, res:any) => {
|
app.get("/stats", (req: any, res: any) => {
|
||||||
res.json(require('../../stats').toJSON());
|
res.json(require("../../stats").toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/javascript', (req:any, res:any) => {
|
app.get("/javascript", (req: any, res: any) => {
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
|
res.send(
|
||||||
|
eejs.require("ep_etherpad-lite/templates/javascript.html", { req }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/robots.txt', (req:any, res:any) => {
|
app.get("/robots.txt", (req: any, res: any) => {
|
||||||
let filePath =
|
let filePath = path.join(
|
||||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
|
settings.root,
|
||||||
|
"src",
|
||||||
|
"static",
|
||||||
|
"skins",
|
||||||
|
settings.skinName,
|
||||||
|
"robots.txt",
|
||||||
|
);
|
||||||
res.sendFile(filePath, (err: any) => {
|
res.sendFile(filePath, (err: any) => {
|
||||||
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
||||||
if (err) {
|
if (err) {
|
||||||
filePath = path.join(settings.root, 'src', 'static', 'robots.txt');
|
filePath = path.join(settings.root, "src", "static", "robots.txt");
|
||||||
res.sendFile(filePath);
|
res.sendFile(filePath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/favicon.ico', (req:any, res:any, next:Function) => {
|
app.get("/favicon.ico", (req: any, res: any, next: Function) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
/*
|
/*
|
||||||
If this is a url we simply redirect to that one.
|
If this is a url we simply redirect to that one.
|
||||||
*/
|
*/
|
||||||
if (settings.favicon && settings.favicon.startsWith('http')) {
|
if (settings.favicon && settings.favicon.startsWith("http")) {
|
||||||
res.redirect(settings.favicon);
|
res.redirect(settings.favicon);
|
||||||
res.send();
|
res.send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const fns = [
|
const fns = [
|
||||||
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
|
...(settings.favicon
|
||||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
|
? [path.resolve(settings.root, settings.favicon)]
|
||||||
path.join(settings.root, 'src', 'static', 'favicon.ico'),
|
: []),
|
||||||
|
path.join(
|
||||||
|
settings.root,
|
||||||
|
"src",
|
||||||
|
"static",
|
||||||
|
"skins",
|
||||||
|
settings.skinName,
|
||||||
|
"favicon.ico",
|
||||||
|
),
|
||||||
|
path.join(settings.root, "src", "static", "favicon.ico"),
|
||||||
];
|
];
|
||||||
for (const fn of fns) {
|
for (const fn of fns) {
|
||||||
try {
|
try {
|
||||||
|
@ -64,7 +80,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||||
await util.promisify(res.sendFile.bind(res))(fn);
|
await util.promisify(res.sendFile.bind(res))(fn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -75,46 +91,50 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName: string, args: any, cb: Function) => {
|
exports.expressCreateServer = (hookName: string, args: any, cb: Function) => {
|
||||||
// serve index.html under /
|
// serve index.html under /
|
||||||
args.app.get('/', (req:any, res:any) => {
|
args.app.get("/", (req: any, res: any) => {
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
|
res.send(eejs.require("ep_etherpad-lite/templates/index.html", { req }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// serve pad.html under /p
|
// serve pad.html under /p
|
||||||
args.app.get('/p/:pad', (req:any, res:any, next:Function) => {
|
args.app.get("/p/:pad", (req: any, res: any, next: Function) => {
|
||||||
// The below might break for pads being rewritten
|
// The below might break for pads being rewritten
|
||||||
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
|
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
|
||||||
|
|
||||||
hooks.callAll('padInitToolbar', {
|
hooks.callAll("padInitToolbar", {
|
||||||
toolbar,
|
toolbar,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
// can be removed when require-kernel is dropped
|
// can be removed when require-kernel is dropped
|
||||||
res.header('Feature-Policy', 'sync-xhr \'self\'');
|
res.header("Feature-Policy", "sync-xhr 'self'");
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', {
|
res.send(
|
||||||
|
eejs.require("ep_etherpad-lite/templates/pad.html", {
|
||||||
req,
|
req,
|
||||||
toolbar,
|
toolbar,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// serve timeslider.html under /p/$padname/timeslider
|
// serve timeslider.html under /p/$padname/timeslider
|
||||||
args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => {
|
args.app.get("/p/:pad/timeslider", (req: any, res: any, next: Function) => {
|
||||||
hooks.callAll('padInitToolbar', {
|
hooks.callAll("padInitToolbar", {
|
||||||
toolbar,
|
toolbar,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
res.send(
|
||||||
|
eejs.require("ep_etherpad-lite/templates/timeslider.html", {
|
||||||
req,
|
req,
|
||||||
toolbar,
|
toolbar,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The client occasionally polls this endpoint to get an updated expiration for the express_sid
|
// The client occasionally polls this endpoint to get an updated expiration for the express_sid
|
||||||
// cookie. This handler must be installed after the express-session middleware.
|
// cookie. This handler must be installed after the express-session middleware.
|
||||||
args.app.put('/_extendExpressSessionLifetime', (req:any, res:any) => {
|
args.app.put("/_extendExpressSessionLifetime", (req: any, res: any) => {
|
||||||
// express-session automatically calls req.session.touch() so we don't need to do it here.
|
// express-session automatically calls req.session.touch() so we don't need to do it here.
|
||||||
res.json({status: 'ok'});
|
res.json({ status: "ok" });
|
||||||
});
|
});
|
||||||
|
|
||||||
return cb();
|
return cb();
|
||||||
|
|
|
@ -1,33 +1,39 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { MapArrayType } from "../../types/MapType";
|
import { MapArrayType } from "../../types/MapType";
|
||||||
import { PartType } from "../../types/PartType";
|
import { PartType } from "../../types/PartType";
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
const fs = require("fs").promises;
|
||||||
const minify = require('../../utils/Minify');
|
const minify = require("../../utils/Minify");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
const plugins = require("../../../static/js/pluginfw/plugin_defs");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
import CachingMiddleware from '../../utils/caching_middleware';
|
import CachingMiddleware from "../../utils/caching_middleware";
|
||||||
const Yajsml = require('etherpad-yajsml');
|
const Yajsml = require("etherpad-yajsml");
|
||||||
|
|
||||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
||||||
const getTar = async () => {
|
const getTar = async () => {
|
||||||
const prefixLocalLibraryPath = (path: string) => {
|
const prefixLocalLibraryPath = (path: string) => {
|
||||||
if (path.charAt(0) === '$') {
|
if (path.charAt(0) === "$") {
|
||||||
return path.slice(1);
|
return path.slice(1);
|
||||||
} else {
|
} else {
|
||||||
return `ep_etherpad-lite/static/js/${path}`;
|
return `ep_etherpad-lite/static/js/${path}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
|
const tarJson = await fs.readFile(
|
||||||
|
path.join(settings.root, "src/node/utils/tar.json"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
const tar: MapArrayType<string[]> = {};
|
const tar: MapArrayType<string[]> = {};
|
||||||
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [string, string[]][]) {
|
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [
|
||||||
|
string,
|
||||||
|
string[],
|
||||||
|
][]) {
|
||||||
const files = relativeFiles.map(prefixLocalLibraryPath);
|
const files = relativeFiles.map(prefixLocalLibraryPath);
|
||||||
tar[prefixLocalLibraryPath(key)] = files
|
tar[prefixLocalLibraryPath(key)] = files
|
||||||
.concat(files.map((p) => p.replace(/\.js$/, '')))
|
.concat(files.map((p) => p.replace(/\.js$/, "")))
|
||||||
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
|
.concat(files.map((p) => `${p.replace(/\.js$/, "")}/index.js`));
|
||||||
}
|
}
|
||||||
return tar;
|
return tar;
|
||||||
};
|
};
|
||||||
|
@ -41,21 +47,23 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
|
|
||||||
// Minify will serve static files compressed (minify enabled). It also has
|
// Minify will serve static files compressed (minify enabled). It also has
|
||||||
// file-specific hacks for ace/require-kernel/etc.
|
// file-specific hacks for ace/require-kernel/etc.
|
||||||
app.all('/static/:filename(*)', minify.minify);
|
app.all("/static/:filename(*)", minify.minify);
|
||||||
|
|
||||||
// Setup middleware that will package JavaScript files served by minify for
|
// Setup middleware that will package JavaScript files served by minify for
|
||||||
// CommonJS loader on the client-side.
|
// CommonJS loader on the client-side.
|
||||||
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
|
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
|
||||||
const jsServer = new (Yajsml.Server)({
|
const jsServer = new Yajsml.Server({
|
||||||
rootPath: 'javascripts/src/',
|
rootPath: "javascripts/src/",
|
||||||
rootURI: 'http://invalid.invalid/static/js/',
|
rootURI: "http://invalid.invalid/static/js/",
|
||||||
libraryPath: 'javascripts/lib/',
|
libraryPath: "javascripts/lib/",
|
||||||
libraryURI: 'http://invalid.invalid/static/plugins/',
|
libraryURI: "http://invalid.invalid/static/plugins/",
|
||||||
requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround.
|
requestURIs: minify.requestURIs, // Loop-back is causing problems, this is a workaround.
|
||||||
});
|
});
|
||||||
|
|
||||||
const StaticAssociator = Yajsml.associators.StaticAssociator;
|
const StaticAssociator = Yajsml.associators.StaticAssociator;
|
||||||
const associations = Yajsml.associators.associationsForSimpleMapping(await getTar());
|
const associations = Yajsml.associators.associationsForSimpleMapping(
|
||||||
|
await getTar(),
|
||||||
|
);
|
||||||
const associator = new StaticAssociator(associations);
|
const associator = new StaticAssociator(associations);
|
||||||
jsServer.setAssociator(associator);
|
jsServer.setAssociator(associator);
|
||||||
|
|
||||||
|
@ -64,18 +72,25 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
// serve plugin definitions
|
// serve plugin definitions
|
||||||
// not very static, but served here so that client can do
|
// not very static, but served here so that client can do
|
||||||
// require("pluginfw/static/js/plugin-definitions.js");
|
// require("pluginfw/static/js/plugin-definitions.js");
|
||||||
app.get('/pluginfw/plugin-definitions.json', (req: any, res:any, next:Function) => {
|
app.get(
|
||||||
const clientParts = plugins.parts.filter((part: PartType) => part.client_hooks != null);
|
"/pluginfw/plugin-definitions.json",
|
||||||
|
(req: any, res: any, next: Function) => {
|
||||||
|
const clientParts = plugins.parts.filter(
|
||||||
|
(part: PartType) => part.client_hooks != null,
|
||||||
|
);
|
||||||
const clientPlugins: MapArrayType<string> = {};
|
const clientPlugins: MapArrayType<string> = {};
|
||||||
for (const name of new Set(clientParts.map((part: PartType) => part.plugin))) {
|
for (const name of new Set(
|
||||||
|
clientParts.map((part: PartType) => part.plugin),
|
||||||
|
)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
clientPlugins[name] = { ...plugins.plugins[name] };
|
clientPlugins[name] = { ...plugins.plugins[name] };
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
delete clientPlugins[name].package;
|
delete clientPlugins[name].package;
|
||||||
}
|
}
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||||
res.write(JSON.stringify({ plugins: clientPlugins, parts: clientParts }));
|
res.write(JSON.stringify({ plugins: clientPlugins, parts: clientParts }));
|
||||||
res.end();
|
res.end();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { Dirent } from "node:fs";
|
import { Dirent } from "node:fs";
|
||||||
import { PluginDef } from "../../types/PartType";
|
import { PluginDef } from "../../types/PartType";
|
||||||
|
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const fsp = require('fs').promises;
|
const fsp = require("fs").promises;
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
const plugins = require("../../../static/js/pluginfw/plugin_defs");
|
||||||
const sanitizePathname = require('../../utils/sanitizePathname');
|
const sanitizePathname = require("../../utils/sanitizePathname");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
|
|
||||||
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
|
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
|
||||||
// instead of path.sep to separate pathname components.
|
// instead of path.sep to separate pathname components.
|
||||||
|
@ -16,68 +16,88 @@ const findSpecs = async (specDir: string) => {
|
||||||
try {
|
try {
|
||||||
dirents = await fsp.readdir(specDir, { withFileTypes: true });
|
dirents = await fsp.readdir(specDir, { withFileTypes: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (['ENOENT', 'ENOTDIR'].includes(err.code)) return [];
|
if (["ENOENT", "ENOTDIR"].includes(err.code)) return [];
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const specs: string[] = [];
|
const specs: string[] = [];
|
||||||
await Promise.all(dirents.map(async (dirent) => {
|
await Promise.all(
|
||||||
|
dirents.map(async (dirent) => {
|
||||||
if (dirent.isDirectory()) {
|
if (dirent.isDirectory()) {
|
||||||
const subdirSpecs = await findSpecs(path.join(specDir, dirent.name));
|
const subdirSpecs = await findSpecs(path.join(specDir, dirent.name));
|
||||||
specs.push(...subdirSpecs.map((spec) => `${dirent.name}/${spec}`));
|
specs.push(...subdirSpecs.map((spec) => `${dirent.name}/${spec}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!dirent.name.endsWith('.js')) return;
|
if (!dirent.name.endsWith(".js")) return;
|
||||||
specs.push(dirent.name);
|
specs.push(dirent.name);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
return specs;
|
return specs;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||||
app.get('/tests/frontend/frontendTestSpecs.json', (req:any, res:any, next:Function) => {
|
app.get(
|
||||||
|
"/tests/frontend/frontendTestSpecs.json",
|
||||||
|
(req: any, res: any, next: Function) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const modules: string[] = [];
|
const modules: string[] = [];
|
||||||
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
await Promise.all(
|
||||||
let {package: {path: pluginPath}} = def as PluginDef;
|
Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
||||||
|
let {
|
||||||
|
package: { path: pluginPath },
|
||||||
|
} = def as PluginDef;
|
||||||
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
|
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
|
||||||
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
|
const specDir = `${
|
||||||
for (const spec of await findSpecs(path.join(pluginPath, specDir))) {
|
plugin === "ep_etherpad-lite" ? "" : "static/"
|
||||||
if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests &&
|
}tests/frontend/specs`;
|
||||||
spec.startsWith('admin')) continue;
|
for (const spec of await findSpecs(
|
||||||
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`);
|
path.join(pluginPath, specDir),
|
||||||
|
)) {
|
||||||
|
if (
|
||||||
|
plugin === "ep_etherpad-lite" &&
|
||||||
|
!settings.enableAdminUITests &&
|
||||||
|
spec.startsWith("admin")
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, "")}`);
|
||||||
}
|
}
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
// Sort plugin tests before core tests.
|
// Sort plugin tests before core tests.
|
||||||
modules.sort((a, b) => {
|
modules.sort((a, b) => {
|
||||||
a = String(a);
|
a = String(a);
|
||||||
b = String(b);
|
b = String(b);
|
||||||
const aCore = a.startsWith('ep_etherpad-lite/');
|
const aCore = a.startsWith("ep_etherpad-lite/");
|
||||||
const bCore = b.startsWith('ep_etherpad-lite/');
|
const bCore = b.startsWith("ep_etherpad-lite/");
|
||||||
if (aCore === bCore) return a.localeCompare(b);
|
if (aCore === bCore) return a.localeCompare(b);
|
||||||
return aCore ? 1 : -1;
|
return aCore ? 1 : -1;
|
||||||
});
|
});
|
||||||
console.debug('Sent browser the following test spec modules:', modules);
|
console.debug("Sent browser the following test spec modules:", modules);
|
||||||
res.json(modules);
|
res.json(modules);
|
||||||
})().catch((err) => next(err || new Error(err)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const rootTestFolder = path.join(settings.root, 'src/tests/frontend/');
|
const rootTestFolder = path.join(settings.root, "src/tests/frontend/");
|
||||||
|
|
||||||
app.get('/tests/frontend/index.html', (req:any, res:any) => {
|
app.get("/tests/frontend/index.html", (req: any, res: any) => {
|
||||||
res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));
|
res.redirect(["./", ...req.url.split("?").slice(1)].join("?"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here
|
// The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here
|
||||||
// uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the
|
// uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the
|
||||||
// version used with Express v4.x) interprets '.' and '*' differently than regexp.
|
// version used with Express v4.x) interprets '.' and '*' differently than regexp.
|
||||||
app.get('/tests/frontend/:file([\\d\\D]{0,})', (req:any, res:any, next:Function) => {
|
app.get(
|
||||||
|
"/tests/frontend/:file([\\d\\D]{0,})",
|
||||||
|
(req: any, res: any, next: Function) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
let file = sanitizePathname(req.params.file);
|
let file = sanitizePathname(req.params.file);
|
||||||
if (['', '.', './'].includes(file)) file = 'index.html';
|
if (["", ".", "./"].includes(file)) file = "index.html";
|
||||||
res.sendFile(path.join(rootTestFolder, file));
|
res.sendFile(path.join(rootTestFolder, file));
|
||||||
})().catch((err) => next(err || new Error(err)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
app.get('/tests/frontend', (req:any, res:any) => {
|
app.get("/tests/frontend", (req: any, res: any) => {
|
||||||
res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?'));
|
res.redirect(["./frontend/", ...req.url.split("?").slice(1)].join("?"));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,34 +1,42 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
import log4js from 'log4js';
|
import log4js from "log4js";
|
||||||
import { SocketClientRequest } from "../../types/SocketClientRequest";
|
import { SocketClientRequest } from "../../types/SocketClientRequest";
|
||||||
import { WebAccessTypes } from "../../types/WebAccessTypes";
|
import { WebAccessTypes } from "../../types/WebAccessTypes";
|
||||||
import { SettingsUser } from "../../types/SettingsUser";
|
import { SettingsUser } from "../../types/SettingsUser";
|
||||||
const httpLogger = log4js.getLogger('http');
|
const httpLogger = log4js.getLogger("http");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
const readOnlyManager = require("../../db/ReadOnlyManager");
|
||||||
|
|
||||||
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
hooks.deprecationNotices.authFailure =
|
||||||
|
"use the authnFailure and authzFailure hooks instead";
|
||||||
|
|
||||||
// Promisified wrapper around hooks.aCallFirst.
|
// Promisified wrapper around hooks.aCallFirst.
|
||||||
const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => {
|
const aCallFirst = (hookName: string, context: any, pred = null) =>
|
||||||
hooks.aCallFirst(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred);
|
new Promise((resolve, reject) => {
|
||||||
|
hooks.aCallFirst(
|
||||||
|
hookName,
|
||||||
|
context,
|
||||||
|
(err: any, r: unknown) => (err != null ? reject(err) : resolve(r)),
|
||||||
|
pred,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const aCallFirst0 =
|
const aCallFirst0 =
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
async (hookName: string, context:any, pred = null) => (await aCallFirst(hookName, context, pred))[0];
|
async (hookName: string, context: any, pred = null) =>
|
||||||
|
(await aCallFirst(hookName, context, pred))[0];
|
||||||
|
|
||||||
exports.normalizeAuthzLevel = (level: string | boolean) => {
|
exports.normalizeAuthzLevel = (level: string | boolean) => {
|
||||||
if (!level) return false;
|
if (!level) return false;
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case true:
|
case true:
|
||||||
return 'create';
|
return "create";
|
||||||
case 'readOnly':
|
case "readOnly":
|
||||||
case 'modify':
|
case "modify":
|
||||||
case 'create':
|
case "create":
|
||||||
return level;
|
return level;
|
||||||
default:
|
default:
|
||||||
httpLogger.warn(`Unknown authorization level '${level}', denying access`);
|
httpLogger.warn(`Unknown authorization level '${level}', denying access`);
|
||||||
|
@ -39,18 +47,20 @@ exports.normalizeAuthzLevel = (level: string|boolean) => {
|
||||||
exports.userCanModify = (padId: string, req: SocketClientRequest) => {
|
exports.userCanModify = (padId: string, req: SocketClientRequest) => {
|
||||||
if (readOnlyManager.isReadOnlyId(padId)) return false;
|
if (readOnlyManager.isReadOnlyId(padId)) return false;
|
||||||
if (!settings.requireAuthentication) return true;
|
if (!settings.requireAuthentication) return true;
|
||||||
const {session: {user} = {}} = req;
|
const {
|
||||||
|
session: { user } = {},
|
||||||
|
} = req;
|
||||||
if (!user || user.readOnly) return false;
|
if (!user || user.readOnly) return false;
|
||||||
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
|
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
|
||||||
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
|
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
|
||||||
return level && level !== 'readOnly';
|
return level && level !== "readOnly";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
|
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
|
||||||
exports.authnFailureDelayMs = 1000;
|
exports.authnFailureDelayMs = 1000;
|
||||||
|
|
||||||
const checkAccess = async (req: any, res: any, next: Function) => {
|
const checkAccess = async (req: any, res: any, next: Function) => {
|
||||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth');
|
const requireAdmin = req.path.toLowerCase().startsWith("/admin-auth");
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin
|
// Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin
|
||||||
|
@ -60,19 +70,28 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
|
|
||||||
let results: null | boolean[];
|
let results: null | boolean[];
|
||||||
let skip = false;
|
let skip = false;
|
||||||
const preAuthorizeNext = (...args:any) => { skip = true; next(...args); };
|
const preAuthorizeNext = (...args: any) => {
|
||||||
|
skip = true;
|
||||||
|
next(...args);
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
results = await aCallFirst('preAuthorize', {req, res, next: preAuthorizeNext},
|
results = (await aCallFirst(
|
||||||
|
"preAuthorize",
|
||||||
|
{ req, res, next: preAuthorizeNext },
|
||||||
// This predicate will cause aCallFirst to call the hook functions one at a time until one
|
// This predicate will cause aCallFirst to call the hook functions one at a time until one
|
||||||
// of them returns a non-empty list, with an exception: If the request is for an /admin
|
// of them returns a non-empty list, with an exception: If the request is for an /admin
|
||||||
// page, truthy entries are filtered out before checking to see whether the list is empty.
|
// page, truthy entries are filtered out before checking to see whether the list is empty.
|
||||||
// This prevents plugin authors from accidentally granting admin privileges to the general
|
// This prevents plugin authors from accidentally granting admin privileges to the general
|
||||||
// public.
|
// public.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(r) => (skip || (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0))) as boolean[];
|
(r) =>
|
||||||
|
skip || (r != null && r.filter((x) => !requireAdmin || !x).length > 0),
|
||||||
|
)) as boolean[];
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`);
|
httpLogger.error(
|
||||||
if (!skip) res.status(500).send('Internal Server Error');
|
`Error in preAuthorize hook: ${err.stack || err.toString()}`,
|
||||||
|
);
|
||||||
|
if (!skip) res.status(500).send("Internal Server Error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (skip) return;
|
if (skip) return;
|
||||||
|
@ -84,9 +103,9 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
// Access was explicitly granted or denied. If any value is false then access is denied.
|
// Access was explicitly granted or denied. If any value is false then access is denied.
|
||||||
if (results.every((x) => x)) return next();
|
if (results.every((x) => x)) return next();
|
||||||
if (await aCallFirst0('preAuthzFailure', {req, res})) return;
|
if (await aCallFirst0("preAuthzFailure", { req, res })) return;
|
||||||
// No plugin handled the pre-authentication authorization failure.
|
// No plugin handled the pre-authentication authorization failure.
|
||||||
return res.status(403).send('Forbidden');
|
return res.status(403).send("Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
// This helper is used in steps 2 and 4 below, so it may be called twice per access: once before
|
// This helper is used in steps 2 and 4 below, so it may be called twice per access: once before
|
||||||
|
@ -112,13 +131,16 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const isAuthenticated = req.session && req.session.user;
|
const isAuthenticated = req.session && req.session.user;
|
||||||
if (isAuthenticated && req.session.user.is_admin) return await grant('create');
|
if (isAuthenticated && req.session.user.is_admin)
|
||||||
|
return await grant("create");
|
||||||
const requireAuthn = requireAdmin || settings.requireAuthentication;
|
const requireAuthn = requireAdmin || settings.requireAuthentication;
|
||||||
if (!requireAuthn) return await grant('create');
|
if (!requireAuthn) return await grant("create");
|
||||||
if (!isAuthenticated) return await grant(false);
|
if (!isAuthenticated) return await grant(false);
|
||||||
if (requireAdmin && !req.session.user.is_admin) return await grant(false);
|
if (requireAdmin && !req.session.user.is_admin) return await grant(false);
|
||||||
if (!settings.requireAuthorization) return await grant('create');
|
if (!settings.requireAuthorization) return await grant("create");
|
||||||
return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path}));
|
return await grant(
|
||||||
|
await aCallFirst0("authorize", { req, res, next, resource: req.path }),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -128,8 +150,8 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
|
|
||||||
if (await authorize()) {
|
if (await authorize()) {
|
||||||
if (requireAdmin) {
|
if (requireAdmin) {
|
||||||
res.status(200).send('Authorized')
|
res.status(200).send("Authorized");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -146,35 +168,47 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
const ctx: WebAccessTypes = { req, res, users: settings.users, next };
|
const ctx: WebAccessTypes = { req, res, users: settings.users, next };
|
||||||
// If the HTTP basic auth header is present, extract the username and password so it can be given
|
// If the HTTP basic auth header is present, extract the username and password so it can be given
|
||||||
// to authn plugins.
|
// to authn plugins.
|
||||||
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic ');
|
const httpBasicAuth =
|
||||||
|
req.headers.authorization && req.headers.authorization.startsWith("Basic ");
|
||||||
if (httpBasicAuth) {
|
if (httpBasicAuth) {
|
||||||
const userpass =
|
const userpass = Buffer.from(
|
||||||
Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':');
|
req.headers.authorization.split(" ")[1],
|
||||||
|
"base64",
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.split(":");
|
||||||
ctx.username = userpass.shift();
|
ctx.username = userpass.shift();
|
||||||
// Prevent prototype pollution vulnerabilities in plugins. This also silences a prototype
|
// Prevent prototype pollution vulnerabilities in plugins. This also silences a prototype
|
||||||
// pollution warning below (when setting settings.users[ctx.username]) that isn't actually a
|
// pollution warning below (when setting settings.users[ctx.username]) that isn't actually a
|
||||||
// problem unless the attacker can also set Object.prototype.password.
|
// problem unless the attacker can also set Object.prototype.password.
|
||||||
if (ctx.username === '__proto__') ctx.username = null;
|
if (ctx.username === "__proto__") ctx.username = null;
|
||||||
ctx.password = userpass.join(':');
|
ctx.password = userpass.join(":");
|
||||||
}
|
}
|
||||||
if (!(await aCallFirst0('authenticate', ctx))) {
|
if (!(await aCallFirst0("authenticate", ctx))) {
|
||||||
// Fall back to HTTP basic auth.
|
// Fall back to HTTP basic auth.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const {[ctx.username]: {password} = {}} = settings.users as SettingsUser;
|
const {
|
||||||
|
[ctx.username]: { password } = {},
|
||||||
|
} = settings.users as SettingsUser;
|
||||||
|
|
||||||
if (!httpBasicAuth ||
|
if (
|
||||||
|
!httpBasicAuth ||
|
||||||
!ctx.username ||
|
!ctx.username ||
|
||||||
password == null || password.toString() !== ctx.password) {
|
password == null ||
|
||||||
|
password.toString() !== ctx.password
|
||||||
|
) {
|
||||||
httpLogger.info(`Failed authentication from IP ${req.ip}`);
|
httpLogger.info(`Failed authentication from IP ${req.ip}`);
|
||||||
if (await aCallFirst0('authnFailure', {req, res})) return;
|
if (await aCallFirst0("authnFailure", { req, res })) return;
|
||||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
if (await aCallFirst0("authFailure", { req, res, next })) return;
|
||||||
// No plugin handled the authentication failure. Fall back to basic authentication.
|
// No plugin handled the authentication failure. Fall back to basic authentication.
|
||||||
if (!requireAdmin) {
|
if (!requireAdmin) {
|
||||||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
res.header("WWW-Authenticate", 'Basic realm="Protected Area"');
|
||||||
}
|
}
|
||||||
// Delay the error response for 1s to slow down brute force attacks.
|
// Delay the error response for 1s to slow down brute force attacks.
|
||||||
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
|
await new Promise((resolve) =>
|
||||||
res.status(401).send('Authentication Required');
|
setTimeout(resolve, exports.authnFailureDelayMs),
|
||||||
|
);
|
||||||
|
res.status(401).send("Authentication Required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.users[ctx.username].username = ctx.username;
|
settings.users[ctx.username].username = ctx.username;
|
||||||
|
@ -184,11 +218,15 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
delete req.session.user.password;
|
delete req.session.user.password;
|
||||||
}
|
}
|
||||||
if (req.session.user == null) {
|
if (req.session.user == null) {
|
||||||
httpLogger.error('authenticate hook failed to add user settings to session');
|
httpLogger.error(
|
||||||
return res.status(500).send('Internal Server Error');
|
"authenticate hook failed to add user settings to session",
|
||||||
|
);
|
||||||
|
return res.status(500).send("Internal Server Error");
|
||||||
}
|
}
|
||||||
const {username = '<no username>'} = req.session.user;
|
const { username = "<no username>" } = req.session.user;
|
||||||
httpLogger.info(`Successful authentication from IP ${req.ip} for user ${username}`);
|
httpLogger.info(
|
||||||
|
`Successful authentication from IP ${req.ip} for user ${username}`,
|
||||||
|
);
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Step 4: Try to access the thing again. If this fails, give the user a 403 error. Plugins can
|
// Step 4: Try to access the thing again. If this fails, give the user a 403 error. Plugins can
|
||||||
|
@ -196,17 +234,17 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
// a login page).
|
// a login page).
|
||||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
const auth = await authorize()
|
const auth = await authorize();
|
||||||
if (auth && !requireAdmin) return next();
|
if (auth && !requireAdmin) return next();
|
||||||
if (auth && requireAdmin) {
|
if (auth && requireAdmin) {
|
||||||
res.status(200).send('Authorized')
|
res.status(200).send("Authorized");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await aCallFirst0('authzFailure', {req, res})) return;
|
if (await aCallFirst0("authzFailure", { req, res })) return;
|
||||||
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
if (await aCallFirst0("authFailure", { req, res, next })) return;
|
||||||
// No plugin handled the authorization failure.
|
// No plugin handled the authorization failure.
|
||||||
res.status(403).send('Forbidden');
|
res.status(403).send("Forbidden");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
import type { MapArrayType } from "../types/MapType";
|
import type { MapArrayType } from "../types/MapType";
|
||||||
import { I18nPluginDefs } from "../types/I18nPluginDefs";
|
import { I18nPluginDefs } from "../types/I18nPluginDefs";
|
||||||
|
|
||||||
const languages = require('languages4translatewiki');
|
const languages = require("languages4translatewiki");
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const _ = require('underscore');
|
const _ = require("underscore");
|
||||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
|
const pluginDefs = require("../../static/js/pluginfw/plugin_defs.js");
|
||||||
const existsSync = require('../utils/path_exists');
|
const existsSync = require("../utils/path_exists");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
|
|
||||||
// returns all existing messages merged together and grouped by langcode
|
// returns all existing messages merged together and grouped by langcode
|
||||||
// {es: {"foo": "string"}, en:...}
|
// {es: {"foo": "string"}, en:...}
|
||||||
|
@ -32,7 +32,7 @@ const getAllLocales = () => {
|
||||||
const ext = path.extname(file);
|
const ext = path.extname(file);
|
||||||
const locale = path.basename(file, ext).toLowerCase();
|
const locale = path.basename(file, ext).toLowerCase();
|
||||||
|
|
||||||
if ((ext === '.json') && languages.isValid(locale)) {
|
if (ext === ".json" && languages.isValid(locale)) {
|
||||||
if (!locales2paths[locale]) locales2paths[locale] = [];
|
if (!locales2paths[locale]) locales2paths[locale] = [];
|
||||||
locales2paths[locale].push(file);
|
locales2paths[locale].push(file);
|
||||||
}
|
}
|
||||||
|
@ -40,13 +40,15 @@ const getAllLocales = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// add core supported languages first
|
// add core supported languages first
|
||||||
extractLangs(path.join(settings.root, 'src/locales'));
|
extractLangs(path.join(settings.root, "src/locales"));
|
||||||
|
|
||||||
// add plugins languages (if any)
|
// 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
|
// plugin locales should overwrite etherpad's core locales
|
||||||
if (pluginPath.endsWith('/ep_etherpad-lite')) continue;
|
if (pluginPath.endsWith("/ep_etherpad-lite")) continue;
|
||||||
extractLangs(path.join(pluginPath, 'locales'));
|
extractLangs(path.join(pluginPath, "locales"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a locale index (merge all locale data other than user-supplied overrides)
|
// Build a locale index (merge all locale data other than user-supplied overrides)
|
||||||
|
@ -57,7 +59,7 @@ const getAllLocales = () => {
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
let fileContents;
|
let fileContents;
|
||||||
try {
|
try {
|
||||||
fileContents = JSON.parse(fs.readFileSync(file, 'utf8'));
|
fileContents = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`failed to read JSON file ${file}: ${err}`);
|
console.error(`failed to read JSON file ${file}: ${err}`);
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -69,35 +71,41 @@ const getAllLocales = () => {
|
||||||
// Add custom strings from settings.json
|
// Add custom strings from settings.json
|
||||||
// Since this is user-supplied, we'll do some extra sanity checks
|
// Since this is user-supplied, we'll do some extra sanity checks
|
||||||
const wrongFormatErr = Error(
|
const wrongFormatErr = Error(
|
||||||
'customLocaleStrings in wrong format. See documentation ' +
|
"customLocaleStrings in wrong format. See documentation " +
|
||||||
'for Customization for Administrators, under Localization.');
|
"for Customization for Administrators, under Localization.",
|
||||||
|
);
|
||||||
if (settings.customLocaleStrings) {
|
if (settings.customLocaleStrings) {
|
||||||
if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr;
|
if (typeof settings.customLocaleStrings !== "object") throw wrongFormatErr;
|
||||||
_.each(settings.customLocaleStrings, (overrides:MapArrayType<string> , langcode:string) => {
|
_.each(
|
||||||
if (typeof overrides !== 'object') throw wrongFormatErr;
|
settings.customLocaleStrings,
|
||||||
|
(overrides: MapArrayType<string>, langcode: string) => {
|
||||||
|
if (typeof overrides !== "object") throw wrongFormatErr;
|
||||||
_.each(overrides, (localeString: string | object, key: string) => {
|
_.each(overrides, (localeString: string | object, key: string) => {
|
||||||
if (typeof localeString !== 'string') throw wrongFormatErr;
|
if (typeof localeString !== "string") throw wrongFormatErr;
|
||||||
const locale = locales[langcode];
|
const locale = locales[langcode];
|
||||||
|
|
||||||
// Handles the error if an unknown language code is entered
|
// Handles the error if an unknown language code is entered
|
||||||
if (locale === undefined) {
|
if (locale === undefined) {
|
||||||
const possibleMatches = [];
|
const possibleMatches = [];
|
||||||
let strippedLangcode = '';
|
let strippedLangcode = "";
|
||||||
if (langcode.includes('-')) {
|
if (langcode.includes("-")) {
|
||||||
strippedLangcode = langcode.split('-')[0];
|
strippedLangcode = langcode.split("-")[0];
|
||||||
}
|
}
|
||||||
for (const localeInEtherPad of Object.keys(locales)) {
|
for (const localeInEtherPad of Object.keys(locales)) {
|
||||||
if (localeInEtherPad.includes(strippedLangcode)) {
|
if (localeInEtherPad.includes(strippedLangcode)) {
|
||||||
possibleMatches.push(localeInEtherPad);
|
possibleMatches.push(localeInEtherPad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Language code ${langcode} is unknown. ` +
|
throw new Error(
|
||||||
`Maybe you meant: ${possibleMatches}`);
|
`Language code ${langcode} is unknown. ` +
|
||||||
|
`Maybe you meant: ${possibleMatches}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
locales[langcode][key] = localeString;
|
locales[langcode][key] = localeString;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return locales;
|
return locales;
|
||||||
|
@ -117,33 +125,32 @@ const getAvailableLangs = (locales:MapArrayType<any>) => {
|
||||||
const generateLocaleIndex = (locales: MapArrayType<string>) => {
|
const generateLocaleIndex = (locales: MapArrayType<string>) => {
|
||||||
const result = _.clone(locales); // keep English strings
|
const result = _.clone(locales); // keep English strings
|
||||||
for (const langcode of Object.keys(locales)) {
|
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);
|
return JSON.stringify(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||||
// regenerate locales on server restart
|
// regenerate locales on server restart
|
||||||
const locales = getAllLocales();
|
const locales = getAllLocales();
|
||||||
const localeIndex = generateLocaleIndex(locales);
|
const localeIndex = generateLocaleIndex(locales);
|
||||||
exports.availableLangs = getAvailableLangs(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
|
// 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)) {
|
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
|
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send('Language not available');
|
res.status(404).send("Language not available");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/locales.json', (req: any, res:any) => {
|
app.get("/locales.json", (req: any, res: any) => {
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
res.send(localeIndex);
|
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