mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Fixed formatting.
This commit is contained in:
parent
9f5ff6171a
commit
f115ea9241
340 changed files with 77690 additions and 66928 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,139 +1,108 @@
|
||||||
'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/**/*"],
|
||||||
files: [
|
extends: "etherpad/node",
|
||||||
'**/*',
|
},
|
||||||
],
|
{
|
||||||
excludedFiles: [
|
files: [
|
||||||
'**/.eslintrc.*',
|
"static/**/*",
|
||||||
'tests/frontend/**/*',
|
"tests/frontend/helper.js",
|
||||||
],
|
"tests/frontend/helper/**/*",
|
||||||
extends: 'etherpad/node',
|
],
|
||||||
},
|
excludedFiles: ["**/.eslintrc.*"],
|
||||||
{
|
extends: "etherpad/browser",
|
||||||
files: [
|
env: {
|
||||||
'static/**/*',
|
"shared-node-browser": true,
|
||||||
'tests/frontend/helper.js',
|
},
|
||||||
'tests/frontend/helper/**/*',
|
overrides: [
|
||||||
],
|
{
|
||||||
excludedFiles: [
|
files: ["tests/frontend/helper/**/*"],
|
||||||
'**/.eslintrc.*',
|
globals: {
|
||||||
],
|
helper: "readonly",
|
||||||
extends: 'etherpad/browser',
|
},
|
||||||
env: {
|
},
|
||||||
'shared-node-browser': true,
|
],
|
||||||
},
|
},
|
||||||
overrides: [
|
{
|
||||||
{
|
files: ["tests/**/*"],
|
||||||
files: [
|
excludedFiles: [
|
||||||
'tests/frontend/helper/**/*',
|
"**/.eslintrc.*",
|
||||||
],
|
"tests/frontend/cypress/**/*",
|
||||||
globals: {
|
"tests/frontend/helper.js",
|
||||||
helper: 'readonly',
|
"tests/frontend/helper/**/*",
|
||||||
},
|
"tests/frontend/travis/**/*",
|
||||||
},
|
"tests/ratelimit/**/*",
|
||||||
],
|
],
|
||||||
},
|
extends: "etherpad/tests",
|
||||||
{
|
rules: {
|
||||||
files: [
|
"mocha/no-exports": "off",
|
||||||
'tests/**/*',
|
"mocha/no-top-level-hooks": "off",
|
||||||
],
|
},
|
||||||
excludedFiles: [
|
},
|
||||||
'**/.eslintrc.*',
|
{
|
||||||
'tests/frontend/cypress/**/*',
|
files: ["tests/backend/**/*"],
|
||||||
'tests/frontend/helper.js',
|
excludedFiles: ["**/.eslintrc.*"],
|
||||||
'tests/frontend/helper/**/*',
|
extends: "etherpad/tests/backend",
|
||||||
'tests/frontend/travis/**/*',
|
overrides: [
|
||||||
'tests/ratelimit/**/*',
|
{
|
||||||
],
|
files: ["tests/backend/**/*"],
|
||||||
extends: 'etherpad/tests',
|
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: [
|
},
|
||||||
'tests/backend/**/*',
|
{
|
||||||
],
|
files: ["tests/frontend/**/*"],
|
||||||
excludedFiles: [
|
excludedFiles: [
|
||||||
'**/.eslintrc.*',
|
"**/.eslintrc.*",
|
||||||
],
|
"tests/frontend/cypress/**/*",
|
||||||
extends: 'etherpad/tests/backend',
|
"tests/frontend/helper.js",
|
||||||
overrides: [
|
"tests/frontend/helper/**/*",
|
||||||
{
|
"tests/frontend/travis/**/*",
|
||||||
files: [
|
],
|
||||||
'tests/backend/**/*',
|
extends: "etherpad/tests/frontend",
|
||||||
],
|
overrides: [
|
||||||
excludedFiles: [
|
{
|
||||||
'tests/backend/specs/**/*',
|
files: ["tests/frontend/**/*"],
|
||||||
],
|
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/**/*',
|
extends: "etherpad/tests/cypress",
|
||||||
],
|
},
|
||||||
excludedFiles: [
|
{
|
||||||
'**/.eslintrc.*',
|
files: ["tests/frontend/travis/**/*"],
|
||||||
'tests/frontend/cypress/**/*',
|
extends: "etherpad/node",
|
||||||
'tests/frontend/helper.js',
|
},
|
||||||
'tests/frontend/helper/**/*',
|
],
|
||||||
'tests/frontend/travis/**/*',
|
root: true,
|
||||||
],
|
|
||||||
extends: 'etherpad/tests/frontend',
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'tests/frontend/**/*',
|
|
||||||
],
|
|
||||||
excludedFiles: [
|
|
||||||
'tests/frontend/specs/**/*',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'mocha/no-exports': 'off',
|
|
||||||
'mocha/no-top-level-hooks': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'tests/frontend/cypress/**/*',
|
|
||||||
],
|
|
||||||
extends: 'etherpad/tests/cypress',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'tests/frontend/travis/**/*',
|
|
||||||
],
|
|
||||||
extends: 'etherpad/node',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
root: true,
|
|
||||||
};
|
};
|
||||||
|
|
12
src/biome.json
Normal file
12
src/biome.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
230
src/ep.json
230
src/ep.json
|
@ -1,117 +1,117 @@
|
||||||
{
|
{
|
||||||
"parts": [
|
"parts": [
|
||||||
{
|
{
|
||||||
"name": "DB",
|
"name": "DB",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"shutdown": "ep_etherpad-lite/node/db/DB"
|
"shutdown": "ep_etherpad-lite/node/db/DB"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Minify",
|
"name": "Minify",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"shutdown": "ep_etherpad-lite/node/utils/Minify"
|
"shutdown": "ep_etherpad-lite/node/utils/Minify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "express",
|
"name": "express",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"createServer": "ep_etherpad-lite/node/hooks/express",
|
"createServer": "ep_etherpad-lite/node/hooks/express",
|
||||||
"restartServer": "ep_etherpad-lite/node/hooks/express",
|
"restartServer": "ep_etherpad-lite/node/hooks/express",
|
||||||
"shutdown": "ep_etherpad-lite/node/hooks/express"
|
"shutdown": "ep_etherpad-lite/node/hooks/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "static",
|
"name": "static",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressPreSession": "ep_etherpad-lite/node/hooks/express/static"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/static"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "stats",
|
"name": "stats",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"shutdown": "ep_etherpad-lite/node/stats"
|
"shutdown": "ep_etherpad-lite/node/stats"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "i18n",
|
"name": "i18n",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressPreSession": "ep_etherpad-lite/node/hooks/i18n"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/i18n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "specialpages",
|
"name": "specialpages",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
|
||||||
"expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "oauth2",
|
"name": "oauth2",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/security/OAuth2Provider"
|
"expressCreateServer": "ep_etherpad-lite/node/security/OAuth2Provider"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padurlsanitize",
|
"name": "padurlsanitize",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize"
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "apicalls",
|
"name": "apicalls",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressPreSession": "ep_etherpad-lite/node/hooks/express/apicalls"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/apicalls"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "importexport",
|
"name": "importexport",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport"
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "errorhandling",
|
"name": "errorhandling",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "socketio",
|
"name": "socketio",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCloseServer": "ep_etherpad-lite/node/hooks/express/socketio",
|
"expressCloseServer": "ep_etherpad-lite/node/hooks/express/socketio",
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio",
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio",
|
||||||
"socketio": "ep_etherpad-lite/node/handler/PadMessageHandler"
|
"socketio": "ep_etherpad-lite/node/handler/PadMessageHandler"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "tests",
|
"name": "tests",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressPreSession": "ep_etherpad-lite/node/hooks/express/tests"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/tests"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin"
|
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "adminplugins",
|
"name": "adminplugins",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins"
|
"socketio": "ep_etherpad-lite/node/hooks/express/adminplugins"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "adminsettings",
|
"name": "adminsettings",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings"
|
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openapi",
|
"name": "openapi",
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
|
"expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,187 +1,187 @@
|
||||||
{
|
{
|
||||||
"admin.page-title": "Admin Dashboard - Etherpad",
|
"admin.page-title": "Admin Dashboard - Etherpad",
|
||||||
"admin_plugins": "Plugin manager",
|
"admin_plugins": "Plugin manager",
|
||||||
"admin_plugins.available": "Available plugins",
|
"admin_plugins.available": "Available plugins",
|
||||||
"admin_plugins.available_not-found": "No plugins found.",
|
"admin_plugins.available_not-found": "No plugins found.",
|
||||||
"admin_plugins.available_fetching": "Fetching…",
|
"admin_plugins.available_fetching": "Fetching…",
|
||||||
"admin_plugins.available_install.value": "Install",
|
"admin_plugins.available_install.value": "Install",
|
||||||
"admin_plugins.available_search.placeholder": "Search for plugins to install",
|
"admin_plugins.available_search.placeholder": "Search for plugins to install",
|
||||||
"admin_plugins.description": "Description",
|
"admin_plugins.description": "Description",
|
||||||
"admin_plugins.installed": "Installed plugins",
|
"admin_plugins.installed": "Installed plugins",
|
||||||
"admin_plugins.installed_fetching": "Fetching installed plugins…",
|
"admin_plugins.installed_fetching": "Fetching installed plugins…",
|
||||||
"admin_plugins.installed_nothing": "You haven't installed any plugins yet.",
|
"admin_plugins.installed_nothing": "You haven't installed any plugins yet.",
|
||||||
"admin_plugins.installed_uninstall.value": "Uninstall",
|
"admin_plugins.installed_uninstall.value": "Uninstall",
|
||||||
"admin_plugins.last-update": "Last update",
|
"admin_plugins.last-update": "Last update",
|
||||||
"admin_plugins.name": "Name",
|
"admin_plugins.name": "Name",
|
||||||
"admin_plugins.page-title": "Plugin manager - Etherpad",
|
"admin_plugins.page-title": "Plugin manager - Etherpad",
|
||||||
"admin_plugins.version": "Version",
|
"admin_plugins.version": "Version",
|
||||||
"admin_plugins_info": "Troubleshooting information",
|
"admin_plugins_info": "Troubleshooting information",
|
||||||
"admin_plugins_info.hooks": "Installed hooks",
|
"admin_plugins_info.hooks": "Installed hooks",
|
||||||
"admin_plugins_info.hooks_client": "Client-side hooks",
|
"admin_plugins_info.hooks_client": "Client-side hooks",
|
||||||
"admin_plugins_info.hooks_server": "Server-side hooks",
|
"admin_plugins_info.hooks_server": "Server-side hooks",
|
||||||
"admin_plugins_info.parts": "Installed parts",
|
"admin_plugins_info.parts": "Installed parts",
|
||||||
"admin_plugins_info.plugins": "Installed plugins",
|
"admin_plugins_info.plugins": "Installed plugins",
|
||||||
"admin_plugins_info.page-title": "Plugin information - Etherpad",
|
"admin_plugins_info.page-title": "Plugin information - Etherpad",
|
||||||
"admin_plugins_info.version": "Etherpad version",
|
"admin_plugins_info.version": "Etherpad version",
|
||||||
"admin_plugins_info.version_latest": "Latest available version",
|
"admin_plugins_info.version_latest": "Latest available version",
|
||||||
"admin_plugins_info.version_number": "Version number",
|
"admin_plugins_info.version_number": "Version number",
|
||||||
"admin_settings": "Settings",
|
"admin_settings": "Settings",
|
||||||
"admin_settings.current": "Current configuration",
|
"admin_settings.current": "Current configuration",
|
||||||
"admin_settings.current_example-devel": "Example development settings template",
|
"admin_settings.current_example-devel": "Example development settings template",
|
||||||
"admin_settings.current_example-prod": "Example production settings template",
|
"admin_settings.current_example-prod": "Example production settings template",
|
||||||
"admin_settings.current_restart.value": "Restart Etherpad",
|
"admin_settings.current_restart.value": "Restart Etherpad",
|
||||||
"admin_settings.current_save.value": "Save Settings",
|
"admin_settings.current_save.value": "Save Settings",
|
||||||
"admin_settings.page-title": "Settings - Etherpad",
|
"admin_settings.page-title": "Settings - Etherpad",
|
||||||
|
|
||||||
"index.newPad": "New Pad",
|
"index.newPad": "New Pad",
|
||||||
"index.createOpenPad": "or create/open a Pad with the name:",
|
"index.createOpenPad": "or create/open a Pad with the name:",
|
||||||
"index.openPad": "open an existing Pad with the name:",
|
"index.openPad": "open an existing Pad with the name:",
|
||||||
|
|
||||||
"pad.toolbar.bold.title": "Bold (Ctrl+B)",
|
"pad.toolbar.bold.title": "Bold (Ctrl+B)",
|
||||||
"pad.toolbar.italic.title": "Italic (Ctrl+I)",
|
"pad.toolbar.italic.title": "Italic (Ctrl+I)",
|
||||||
"pad.toolbar.underline.title": "Underline (Ctrl+U)",
|
"pad.toolbar.underline.title": "Underline (Ctrl+U)",
|
||||||
"pad.toolbar.strikethrough.title": "Strikethrough (Ctrl+5)",
|
"pad.toolbar.strikethrough.title": "Strikethrough (Ctrl+5)",
|
||||||
"pad.toolbar.ol.title": "Ordered list (Ctrl+Shift+N)",
|
"pad.toolbar.ol.title": "Ordered list (Ctrl+Shift+N)",
|
||||||
"pad.toolbar.ul.title": "Unordered List (Ctrl+Shift+L)",
|
"pad.toolbar.ul.title": "Unordered List (Ctrl+Shift+L)",
|
||||||
"pad.toolbar.indent.title": "Indent (TAB)",
|
"pad.toolbar.indent.title": "Indent (TAB)",
|
||||||
"pad.toolbar.unindent.title": "Outdent (Shift+TAB)",
|
"pad.toolbar.unindent.title": "Outdent (Shift+TAB)",
|
||||||
"pad.toolbar.undo.title": "Undo (Ctrl+Z)",
|
"pad.toolbar.undo.title": "Undo (Ctrl+Z)",
|
||||||
"pad.toolbar.redo.title": "Redo (Ctrl+Y)",
|
"pad.toolbar.redo.title": "Redo (Ctrl+Y)",
|
||||||
"pad.toolbar.clearAuthorship.title": "Clear Authorship Colors (Ctrl+Shift+C)",
|
"pad.toolbar.clearAuthorship.title": "Clear Authorship Colors (Ctrl+Shift+C)",
|
||||||
"pad.toolbar.import_export.title": "Import/Export from/to different file formats",
|
"pad.toolbar.import_export.title": "Import/Export from/to different file formats",
|
||||||
"pad.toolbar.timeslider.title": "Timeslider",
|
"pad.toolbar.timeslider.title": "Timeslider",
|
||||||
"pad.toolbar.savedRevision.title": "Save Revision",
|
"pad.toolbar.savedRevision.title": "Save Revision",
|
||||||
"pad.toolbar.settings.title": "Settings",
|
"pad.toolbar.settings.title": "Settings",
|
||||||
"pad.toolbar.embed.title": "Share and Embed this pad",
|
"pad.toolbar.embed.title": "Share and Embed this pad",
|
||||||
"pad.toolbar.showusers.title": "Show the users on this pad",
|
"pad.toolbar.showusers.title": "Show the users on this pad",
|
||||||
|
|
||||||
"pad.colorpicker.save": "Save",
|
"pad.colorpicker.save": "Save",
|
||||||
"pad.colorpicker.cancel": "Cancel",
|
"pad.colorpicker.cancel": "Cancel",
|
||||||
|
|
||||||
"pad.loading": "Loading...",
|
"pad.loading": "Loading...",
|
||||||
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
|
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
|
||||||
"pad.permissionDenied": "You do not have permission to access this pad",
|
"pad.permissionDenied": "You do not have permission to access this pad",
|
||||||
|
|
||||||
"pad.settings.padSettings": "Pad Settings",
|
"pad.settings.padSettings": "Pad Settings",
|
||||||
"pad.settings.myView": "My View",
|
"pad.settings.myView": "My View",
|
||||||
"pad.settings.stickychat": "Chat always on screen",
|
"pad.settings.stickychat": "Chat always on screen",
|
||||||
"pad.settings.chatandusers": "Show Chat and Users",
|
"pad.settings.chatandusers": "Show Chat and Users",
|
||||||
"pad.settings.colorcheck": "Authorship colors",
|
"pad.settings.colorcheck": "Authorship colors",
|
||||||
"pad.settings.linenocheck": "Line numbers",
|
"pad.settings.linenocheck": "Line numbers",
|
||||||
"pad.settings.rtlcheck": "Read content from right to left?",
|
"pad.settings.rtlcheck": "Read content from right to left?",
|
||||||
"pad.settings.fontType": "Font type:",
|
"pad.settings.fontType": "Font type:",
|
||||||
"pad.settings.fontType.normal": "Normal",
|
"pad.settings.fontType.normal": "Normal",
|
||||||
"pad.settings.language": "Language:",
|
"pad.settings.language": "Language:",
|
||||||
"pad.settings.about": "About",
|
"pad.settings.about": "About",
|
||||||
"pad.settings.poweredBy": "Powered by",
|
"pad.settings.poweredBy": "Powered by",
|
||||||
|
|
||||||
"pad.importExport.import_export": "Import/Export",
|
"pad.importExport.import_export": "Import/Export",
|
||||||
"pad.importExport.import": "Upload any text file or document",
|
"pad.importExport.import": "Upload any text file or document",
|
||||||
"pad.importExport.importSuccessful": "Successful!",
|
"pad.importExport.importSuccessful": "Successful!",
|
||||||
"pad.importExport.export": "Export current pad as:",
|
"pad.importExport.export": "Export current pad as:",
|
||||||
"pad.importExport.exportetherpad": "Etherpad",
|
"pad.importExport.exportetherpad": "Etherpad",
|
||||||
"pad.importExport.exporthtml": "HTML",
|
"pad.importExport.exporthtml": "HTML",
|
||||||
"pad.importExport.exportplain": "Plain text",
|
"pad.importExport.exportplain": "Plain text",
|
||||||
"pad.importExport.exportword": "Microsoft Word",
|
"pad.importExport.exportword": "Microsoft Word",
|
||||||
"pad.importExport.exportpdf": "PDF",
|
"pad.importExport.exportpdf": "PDF",
|
||||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||||
"pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install AbiWord or LibreOffice</a>.",
|
"pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install AbiWord or LibreOffice</a>.",
|
||||||
|
|
||||||
"pad.modals.connected": "Connected.",
|
"pad.modals.connected": "Connected.",
|
||||||
"pad.modals.reconnecting": "Reconnecting to your pad…",
|
"pad.modals.reconnecting": "Reconnecting to your pad…",
|
||||||
"pad.modals.forcereconnect": "Force reconnect",
|
"pad.modals.forcereconnect": "Force reconnect",
|
||||||
"pad.modals.reconnecttimer": "Trying to reconnect in",
|
"pad.modals.reconnecttimer": "Trying to reconnect in",
|
||||||
"pad.modals.cancel": "Cancel",
|
"pad.modals.cancel": "Cancel",
|
||||||
|
|
||||||
"pad.modals.userdup": "Opened in another window",
|
"pad.modals.userdup": "Opened in another window",
|
||||||
"pad.modals.userdup.explanation": "This pad seems to be opened in more than one browser window on this computer.",
|
"pad.modals.userdup.explanation": "This pad seems to be opened in more than one browser window on this computer.",
|
||||||
"pad.modals.userdup.advice": "Reconnect to use this window instead.",
|
"pad.modals.userdup.advice": "Reconnect to use this window instead.",
|
||||||
|
|
||||||
"pad.modals.unauth": "Not authorized",
|
"pad.modals.unauth": "Not authorized",
|
||||||
"pad.modals.unauth.explanation": "Your permissions have changed while viewing this page. Try to reconnect.",
|
"pad.modals.unauth.explanation": "Your permissions have changed while viewing this page. Try to reconnect.",
|
||||||
|
|
||||||
"pad.modals.looping.explanation": "There are communication problems with the synchronization server.",
|
"pad.modals.looping.explanation": "There are communication problems with the synchronization server.",
|
||||||
"pad.modals.looping.cause": "Perhaps you connected through an incompatible firewall or proxy.",
|
"pad.modals.looping.cause": "Perhaps you connected through an incompatible firewall or proxy.",
|
||||||
|
|
||||||
"pad.modals.initsocketfail": "Server is unreachable.",
|
"pad.modals.initsocketfail": "Server is unreachable.",
|
||||||
"pad.modals.initsocketfail.explanation": "Couldn't connect to the synchronization server.",
|
"pad.modals.initsocketfail.explanation": "Couldn't connect to the synchronization server.",
|
||||||
"pad.modals.initsocketfail.cause": "This is probably due to a problem with your browser or your internet connection.",
|
"pad.modals.initsocketfail.cause": "This is probably due to a problem with your browser or your internet connection.",
|
||||||
|
|
||||||
"pad.modals.slowcommit.explanation": "The server is not responding.",
|
"pad.modals.slowcommit.explanation": "The server is not responding.",
|
||||||
"pad.modals.slowcommit.cause": "This could be due to problems with network connectivity.",
|
"pad.modals.slowcommit.cause": "This could be due to problems with network connectivity.",
|
||||||
|
|
||||||
"pad.modals.badChangeset.explanation": "An edit you have made was classified illegal by the synchronization server.",
|
"pad.modals.badChangeset.explanation": "An edit you have made was classified illegal by the synchronization server.",
|
||||||
"pad.modals.badChangeset.cause": "This could be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator, if you feel this is an error. Try to reconnect in order to continue editing.",
|
"pad.modals.badChangeset.cause": "This could be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator, if you feel this is an error. Try to reconnect in order to continue editing.",
|
||||||
|
|
||||||
"pad.modals.corruptPad.explanation": "The pad you are trying to access is corrupt.",
|
"pad.modals.corruptPad.explanation": "The pad you are trying to access is corrupt.",
|
||||||
"pad.modals.corruptPad.cause": "This may be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator.",
|
"pad.modals.corruptPad.cause": "This may be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator.",
|
||||||
|
|
||||||
"pad.modals.deleted": "Deleted.",
|
"pad.modals.deleted": "Deleted.",
|
||||||
"pad.modals.deleted.explanation": "This pad has been removed.",
|
"pad.modals.deleted.explanation": "This pad has been removed.",
|
||||||
|
|
||||||
"pad.modals.rateLimited": "Rate Limited.",
|
"pad.modals.rateLimited": "Rate Limited.",
|
||||||
"pad.modals.rateLimited.explanation": "You sent too many messages to this pad so it disconnected you.",
|
"pad.modals.rateLimited.explanation": "You sent too many messages to this pad so it disconnected you.",
|
||||||
|
|
||||||
"pad.modals.rejected.explanation": "The server rejected a message that was sent by your browser.",
|
"pad.modals.rejected.explanation": "The server rejected a message that was sent by your browser.",
|
||||||
"pad.modals.rejected.cause": "The server may have been updated while you were viewing the pad, or maybe there is a bug in Etherpad. Try reloading the page.",
|
"pad.modals.rejected.cause": "The server may have been updated while you were viewing the pad, or maybe there is a bug in Etherpad. Try reloading the page.",
|
||||||
|
|
||||||
"pad.modals.disconnected": "You have been disconnected.",
|
"pad.modals.disconnected": "You have been disconnected.",
|
||||||
"pad.modals.disconnected.explanation": "The connection to the server was lost",
|
"pad.modals.disconnected.explanation": "The connection to the server was lost",
|
||||||
"pad.modals.disconnected.cause": "The server may be unavailable. Please notify the service administrator if this continues to happen.",
|
"pad.modals.disconnected.cause": "The server may be unavailable. Please notify the service administrator if this continues to happen.",
|
||||||
|
|
||||||
"pad.share": "Share this pad",
|
"pad.share": "Share this pad",
|
||||||
"pad.share.readonly": "Read only",
|
"pad.share.readonly": "Read only",
|
||||||
"pad.share.link": "Link",
|
"pad.share.link": "Link",
|
||||||
"pad.share.emebdcode": "Embed URL",
|
"pad.share.emebdcode": "Embed URL",
|
||||||
"pad.chat": "Chat",
|
"pad.chat": "Chat",
|
||||||
"pad.chat.title": "Open the chat for this pad.",
|
"pad.chat.title": "Open the chat for this pad.",
|
||||||
"pad.chat.loadmessages": "Load more messages",
|
"pad.chat.loadmessages": "Load more messages",
|
||||||
"pad.chat.stick.title": "Stick chat to screen",
|
"pad.chat.stick.title": "Stick chat to screen",
|
||||||
"pad.chat.writeMessage.placeholder": "Write your message here",
|
"pad.chat.writeMessage.placeholder": "Write your message here",
|
||||||
|
|
||||||
"timeslider.followContents": "Follow pad content updates",
|
"timeslider.followContents": "Follow pad content updates",
|
||||||
"timeslider.pageTitle": "{{appTitle}} Timeslider",
|
"timeslider.pageTitle": "{{appTitle}} Timeslider",
|
||||||
"timeslider.toolbar.returnbutton": "Return to pad",
|
"timeslider.toolbar.returnbutton": "Return to pad",
|
||||||
"timeslider.toolbar.authors": "Authors:",
|
"timeslider.toolbar.authors": "Authors:",
|
||||||
"timeslider.toolbar.authorsList": "No Authors",
|
"timeslider.toolbar.authorsList": "No Authors",
|
||||||
"timeslider.toolbar.exportlink.title": "Export",
|
"timeslider.toolbar.exportlink.title": "Export",
|
||||||
"timeslider.exportCurrent": "Export current version as:",
|
"timeslider.exportCurrent": "Export current version as:",
|
||||||
"timeslider.version": "Version {{version}}",
|
"timeslider.version": "Version {{version}}",
|
||||||
"timeslider.saved": "Saved {{month}} {{day}}, {{year}}",
|
"timeslider.saved": "Saved {{month}} {{day}}, {{year}}",
|
||||||
|
|
||||||
"timeslider.playPause": "Playback / Pause Pad Contents",
|
"timeslider.playPause": "Playback / Pause Pad Contents",
|
||||||
"timeslider.backRevision":"Go back a revision in this Pad",
|
"timeslider.backRevision": "Go back a revision in this Pad",
|
||||||
"timeslider.forwardRevision":"Go forward a revision in this Pad",
|
"timeslider.forwardRevision": "Go forward a revision in this Pad",
|
||||||
|
|
||||||
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||||
"timeslider.month.january": "January",
|
"timeslider.month.january": "January",
|
||||||
"timeslider.month.february": "February",
|
"timeslider.month.february": "February",
|
||||||
"timeslider.month.march": "March",
|
"timeslider.month.march": "March",
|
||||||
"timeslider.month.april": "April",
|
"timeslider.month.april": "April",
|
||||||
"timeslider.month.may": "May",
|
"timeslider.month.may": "May",
|
||||||
"timeslider.month.june": "June",
|
"timeslider.month.june": "June",
|
||||||
"timeslider.month.july": "July",
|
"timeslider.month.july": "July",
|
||||||
"timeslider.month.august": "August",
|
"timeslider.month.august": "August",
|
||||||
"timeslider.month.september": "September",
|
"timeslider.month.september": "September",
|
||||||
"timeslider.month.october": "October",
|
"timeslider.month.october": "October",
|
||||||
"timeslider.month.november": "November",
|
"timeslider.month.november": "November",
|
||||||
"timeslider.month.december": "December",
|
"timeslider.month.december": "December",
|
||||||
|
|
||||||
"timeslider.unnamedauthors": "{{num}} unnamed {[plural(num) one: author, other: authors ]}",
|
"timeslider.unnamedauthors": "{{num}} unnamed {[plural(num) one: author, other: authors ]}",
|
||||||
"pad.savedrevs.marked": "This revision is now marked as a saved revision",
|
"pad.savedrevs.marked": "This revision is now marked as a saved revision",
|
||||||
"pad.savedrevs.timeslider": "You can see saved revisions by visiting the timeslider",
|
"pad.savedrevs.timeslider": "You can see saved revisions by visiting the timeslider",
|
||||||
"pad.userlist.entername": "Enter your name",
|
"pad.userlist.entername": "Enter your name",
|
||||||
"pad.userlist.unnamed": "unnamed",
|
"pad.userlist.unnamed": "unnamed",
|
||||||
"pad.editbar.clearcolors": "Clear authorship colors on entire document? This cannot be undone",
|
"pad.editbar.clearcolors": "Clear authorship colors on entire document? This cannot be undone",
|
||||||
|
|
||||||
"pad.impexp.importbutton": "Import Now",
|
"pad.impexp.importbutton": "Import Now",
|
||||||
"pad.impexp.importing": "Importing...",
|
"pad.impexp.importing": "Importing...",
|
||||||
"pad.impexp.confirmimport": "Importing a file will overwrite the current text of the pad. Are you sure you want to proceed?",
|
"pad.impexp.confirmimport": "Importing a file will overwrite the current text of the pad. Are you sure you want to proceed?",
|
||||||
"pad.impexp.convertFailed": "We were not able to import this file. Please use a different document format or copy paste manually",
|
"pad.impexp.convertFailed": "We were not able to import this file. Please use a different document format or copy paste manually",
|
||||||
"pad.impexp.padHasData": "We were not able to import this file because this Pad has already had changes, please import to a new pad",
|
"pad.impexp.padHasData": "We were not able to import this file because this Pad has already had changes, please import to a new pad",
|
||||||
"pad.impexp.uploadFailed": "The upload failed, please try again",
|
"pad.impexp.uploadFailed": "The upload failed, please try again",
|
||||||
"pad.impexp.importfailed": "Import failed",
|
"pad.impexp.importfailed": "Import failed",
|
||||||
"pad.impexp.copypaste": "Please copy paste",
|
"pad.impexp.copypaste": "Please copy paste",
|
||||||
"pad.impexp.exportdisabled": "Exporting as {{type}} format is disabled. Please contact your system administrator for details.",
|
"pad.impexp.exportdisabled": "Exporting as {{type}} format is disabled. Please contact your system administrator for details.",
|
||||||
"pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import"
|
"pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:",
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* The AuthorManager controlls all information about the Pad authors
|
* The AuthorManager controlls all information about the Pad authors
|
||||||
*/
|
*/
|
||||||
|
@ -19,76 +18,79 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
const {
|
||||||
|
randomString,
|
||||||
|
padutils: { warnDeprecated },
|
||||||
|
} = require("../../static/js/pad_utils");
|
||||||
|
|
||||||
exports.getColorPalette = () => [
|
exports.getColorPalette = () => [
|
||||||
'#ffc7c7',
|
"#ffc7c7",
|
||||||
'#fff1c7',
|
"#fff1c7",
|
||||||
'#e3ffc7',
|
"#e3ffc7",
|
||||||
'#c7ffd5',
|
"#c7ffd5",
|
||||||
'#c7ffff',
|
"#c7ffff",
|
||||||
'#c7d5ff',
|
"#c7d5ff",
|
||||||
'#e3c7ff',
|
"#e3c7ff",
|
||||||
'#ffc7f1',
|
"#ffc7f1",
|
||||||
'#ffa8a8',
|
"#ffa8a8",
|
||||||
'#ffe699',
|
"#ffe699",
|
||||||
'#cfff9e',
|
"#cfff9e",
|
||||||
'#99ffb3',
|
"#99ffb3",
|
||||||
'#a3ffff',
|
"#a3ffff",
|
||||||
'#99b3ff',
|
"#99b3ff",
|
||||||
'#cc99ff',
|
"#cc99ff",
|
||||||
'#ff99e5',
|
"#ff99e5",
|
||||||
'#e7b1b1',
|
"#e7b1b1",
|
||||||
'#e9dcAf',
|
"#e9dcAf",
|
||||||
'#cde9af',
|
"#cde9af",
|
||||||
'#bfedcc',
|
"#bfedcc",
|
||||||
'#b1e7e7',
|
"#b1e7e7",
|
||||||
'#c3cdee',
|
"#c3cdee",
|
||||||
'#d2b8ea',
|
"#d2b8ea",
|
||||||
'#eec3e6',
|
"#eec3e6",
|
||||||
'#e9cece',
|
"#e9cece",
|
||||||
'#e7e0ca',
|
"#e7e0ca",
|
||||||
'#d3e5c7',
|
"#d3e5c7",
|
||||||
'#bce1c5',
|
"#bce1c5",
|
||||||
'#c1e2e2',
|
"#c1e2e2",
|
||||||
'#c1c9e2',
|
"#c1c9e2",
|
||||||
'#cfc1e2',
|
"#cfc1e2",
|
||||||
'#e0bdd9',
|
"#e0bdd9",
|
||||||
'#baded3',
|
"#baded3",
|
||||||
'#a0f8eb',
|
"#a0f8eb",
|
||||||
'#b1e7e0',
|
"#b1e7e0",
|
||||||
'#c3c8e4',
|
"#c3c8e4",
|
||||||
'#cec5e2',
|
"#cec5e2",
|
||||||
'#b1d5e7',
|
"#b1d5e7",
|
||||||
'#cda8f0',
|
"#cda8f0",
|
||||||
'#f0f0a8',
|
"#f0f0a8",
|
||||||
'#f2f2a6',
|
"#f2f2a6",
|
||||||
'#f5a8eb',
|
"#f5a8eb",
|
||||||
'#c5f9a9',
|
"#c5f9a9",
|
||||||
'#ececbb',
|
"#ececbb",
|
||||||
'#e7c4bc',
|
"#e7c4bc",
|
||||||
'#daf0b2',
|
"#daf0b2",
|
||||||
'#b0a0fd',
|
"#b0a0fd",
|
||||||
'#bce2e7',
|
"#bce2e7",
|
||||||
'#cce2bb',
|
"#cce2bb",
|
||||||
'#ec9afe',
|
"#ec9afe",
|
||||||
'#edabbd',
|
"#edabbd",
|
||||||
'#aeaeea',
|
"#aeaeea",
|
||||||
'#c4e7b1',
|
"#c4e7b1",
|
||||||
'#d722bb',
|
"#d722bb",
|
||||||
'#f3a5e7',
|
"#f3a5e7",
|
||||||
'#ffa8a8',
|
"#ffa8a8",
|
||||||
'#d8c0c5',
|
"#d8c0c5",
|
||||||
'#eaaedd',
|
"#eaaedd",
|
||||||
'#adc6eb',
|
"#adc6eb",
|
||||||
'#bedad1',
|
"#bedad1",
|
||||||
'#dee9af',
|
"#dee9af",
|
||||||
'#e9afc2',
|
"#e9afc2",
|
||||||
'#f8d2a0',
|
"#f8d2a0",
|
||||||
'#b3b3e6',
|
"#b3b3e6",
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,9 +98,9 @@ exports.getColorPalette = () => [
|
||||||
* @param {String} authorID The id of the author
|
* @param {String} authorID The id of the author
|
||||||
*/
|
*/
|
||||||
exports.doesAuthorExist = async (authorID: string) => {
|
exports.doesAuthorExist = async (authorID: string) => {
|
||||||
const author = await db.get(`globalAuthor:${authorID}`);
|
const author = await db.get(`globalAuthor:${authorID}`);
|
||||||
|
|
||||||
return author != null;
|
return author != null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,34 +109,33 @@ 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
|
||||||
* @param {String} mapperkey The database key name for this mapper
|
* @param {String} mapperkey The database key name for this mapper
|
||||||
* @param {String} mapper The mapper
|
* @param {String} mapper The mapper
|
||||||
*/
|
*/
|
||||||
const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
|
const mapAuthorWithDBKey = async (mapperkey: string, mapper: string) => {
|
||||||
// try to map to an author
|
// try to map to an author
|
||||||
const author = await db.get(`${mapperkey}:${mapper}`);
|
const author = await db.get(`${mapperkey}:${mapper}`);
|
||||||
|
|
||||||
if (author == null) {
|
if (author == null) {
|
||||||
// there is no author with this mapper, so create one
|
// there is no author with this mapper, so create one
|
||||||
const author = await exports.createAuthor(null);
|
const author = await exports.createAuthor(null);
|
||||||
|
|
||||||
// create the token2author relation
|
// create the token2author relation
|
||||||
await db.set(`${mapperkey}:${mapper}`, author.authorID);
|
await db.set(`${mapperkey}:${mapper}`, author.authorID);
|
||||||
|
|
||||||
// return the author
|
// return the author
|
||||||
return author;
|
return author;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,10 +144,10 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -156,10 +157,10 @@ const getAuthor4Token = async (token: string) => {
|
||||||
* @return {Promise<*>}
|
* @return {Promise<*>}
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,9 +170,10 @@ exports.getAuthorId = async (token: string, user: object) => {
|
||||||
* @param {String} token The token
|
* @param {String} token The token
|
||||||
*/
|
*/
|
||||||
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,95 +181,100 @@ 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
|
||||||
await exports.setAuthorName(author.authorID, name);
|
await exports.setAuthorName(author.authorID, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
*/
|
*/
|
||||||
exports.createAuthor = async (name: string) => {
|
exports.createAuthor = async (name: string) => {
|
||||||
// create the new author name
|
// create the new author name
|
||||||
const author = `a.${randomString(16)}`;
|
const author = `a.${randomString(16)}`;
|
||||||
|
|
||||||
// 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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// set the global author db entry
|
// set the global author db entry
|
||||||
await db.set(`globalAuthor:${author}`, authorObj);
|
await db.set(`globalAuthor:${author}`, authorObj);
|
||||||
|
|
||||||
return {authorID: author};
|
return { authorID: author };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* @param {String} authorID The id of the author
|
* @param {String} authorID The id of the author
|
||||||
*/
|
*/
|
||||||
exports.listPadsOfAuthor = async (authorID: string) => {
|
exports.listPadsOfAuthor = async (authorID: string) => {
|
||||||
/* There are two other places where this array is manipulated:
|
/* There are two other places where this array is manipulated:
|
||||||
* (1) When the author is added to a pad, the author object is also updated
|
* (1) When the author is added to a pad, the author object is also updated
|
||||||
* (2) When a pad is deleted, each author of that pad is also updated
|
* (2) When a pad is deleted, each author of that pad is also updated
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// get the globalAuthor
|
// get the globalAuthor
|
||||||
const author = await db.get(`globalAuthor:${authorID}`);
|
const author = await db.get(`globalAuthor:${authorID}`);
|
||||||
|
|
||||||
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
|
||||||
const padIDs = Object.keys(author.padIDs || {});
|
const padIDs = Object.keys(author.padIDs || {});
|
||||||
|
|
||||||
return {padIDs};
|
return { padIDs };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -276,25 +283,25 @@ exports.listPadsOfAuthor = async (authorID: string) => {
|
||||||
* @param {String} padID The id of the pad the author contributes to
|
* @param {String} padID The id of the pad the author contributes to
|
||||||
*/
|
*/
|
||||||
exports.addPad = async (authorID: string, padID: string) => {
|
exports.addPad = async (authorID: string, padID: string) => {
|
||||||
// get the entry
|
// get the entry
|
||||||
const author = await db.get(`globalAuthor:${authorID}`);
|
const author = await db.get(`globalAuthor:${authorID}`);
|
||||||
|
|
||||||
if (author == null) return;
|
if (author == null) return;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
|
* ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
|
||||||
* to perform a strict check here
|
* to perform a strict check here
|
||||||
*/
|
*/
|
||||||
if (!author.padIDs) {
|
if (!author.padIDs) {
|
||||||
// the entry doesn't exist so far, let's create it
|
// the entry doesn't exist so far, let's create it
|
||||||
author.padIDs = {};
|
author.padIDs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the entry for this pad
|
// add the entry for this pad
|
||||||
author.padIDs[padID] = 1; // anything, because value is not used
|
author.padIDs[padID] = 1; // anything, because value is not used
|
||||||
|
|
||||||
// save the new element back
|
// save the new element back
|
||||||
await db.set(`globalAuthor:${authorID}`, author);
|
await db.set(`globalAuthor:${authorID}`, author);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -303,13 +310,13 @@ exports.addPad = async (authorID: string, padID: string) => {
|
||||||
* @param {String} padID The id of the pad the author contributes to
|
* @param {String} padID The id of the pad the author contributes to
|
||||||
*/
|
*/
|
||||||
exports.removePad = async (authorID: string, padID: string) => {
|
exports.removePad = async (authorID: string, padID: string) => {
|
||||||
const author = await db.get(`globalAuthor:${authorID}`);
|
const author = await db.get(`globalAuthor:${authorID}`);
|
||||||
|
|
||||||
if (author == null) return;
|
if (author == null) return;
|
||||||
|
|
||||||
if (author.padIDs != null) {
|
if (author.padIDs != null) {
|
||||||
// remove pad from author
|
// remove pad from author
|
||||||
delete author.padIDs[padID];
|
delete author.padIDs[padID];
|
||||||
await db.set(`globalAuthor:${authorID}`, author);
|
await db.set(`globalAuthor:${authorID}`, author);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The DB Module provides a database initialized with the settings
|
* The DB Module provides a database initialized with the settings
|
||||||
* provided by the settings module
|
* provided by the settings module
|
||||||
|
@ -21,12 +19,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ueberDB from 'ueberdb2';
|
import ueberDB from "ueberdb2";
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
import log4js from 'log4js';
|
import log4js from "log4js";
|
||||||
const stats = require('../stats')
|
const stats = require("../stats");
|
||||||
|
|
||||||
const logger = log4js.getLogger('ueberDB');
|
const logger = log4js.getLogger("ueberDB");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UeberDB Object that provides the database functions
|
* The UeberDB Object that provides the database functions
|
||||||
|
@ -37,24 +35,30 @@ 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(
|
||||||
await exports.db.init();
|
settings.dbType,
|
||||||
if (exports.db.metrics != null) {
|
settings.dbSettings,
|
||||||
for (const [metric, value] of Object.entries(exports.db.metrics)) {
|
null,
|
||||||
if (typeof value !== 'number') continue;
|
logger,
|
||||||
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
|
);
|
||||||
}
|
await exports.db.init();
|
||||||
}
|
if (exports.db.metrics != null) {
|
||||||
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
|
for (const [metric, value] of Object.entries(exports.db.metrics)) {
|
||||||
const f = exports.db[fn];
|
if (typeof value !== "number") continue;
|
||||||
exports[fn] = async (...args:string[]) => await f.call(exports.db, ...args);
|
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
|
||||||
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
|
}
|
||||||
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
|
}
|
||||||
}
|
for (const fn of ["get", "set", "findKeys", "getSub", "setSub", "remove"]) {
|
||||||
|
const f = exports.db[fn];
|
||||||
|
exports[fn] = async (...args: string[]) =>
|
||||||
|
await f.call(exports.db, ...args);
|
||||||
|
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
|
||||||
|
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* The Group Manager provides functions to manage groups in the database
|
* The Group Manager provides functions to manage groups in the database
|
||||||
*/
|
*/
|
||||||
|
@ -19,22 +18,22 @@
|
||||||
* 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);
|
||||||
return {groupIDs};
|
return { groupIDs };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,38 +42,44 @@ exports.listAllGroups = async () => {
|
||||||
* @return {Promise<void>} Resolves when the group is deleted
|
* @return {Promise<void>} Resolves when the group is deleted
|
||||||
*/
|
*/
|
||||||
exports.deleteGroup = async (groupID: string): Promise<void> => {
|
exports.deleteGroup = async (groupID: string): Promise<void> => {
|
||||||
const group = await db.get(`group:${groupID}`);
|
const group = await db.get(`group:${groupID}`);
|
||||||
|
|
||||||
// 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(
|
||||||
const pad = await padManager.getPad(padId);
|
Object.keys(group.pads).map(async (padId) => {
|
||||||
await pad.remove();
|
const pad = await padManager.getPad(padId);
|
||||||
}));
|
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(
|
||||||
await sessionManager.deleteSession(sessionId);
|
Object.keys(sessionIDs).map(async (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.
|
||||||
await db.remove(`group:${groupID}`);
|
await db.remove(`group:${groupID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -83,10 +88,10 @@ exports.deleteGroup = async (groupID: string): Promise<void> => {
|
||||||
* @return {Promise<boolean>} Resolves to true if the group exists
|
* @return {Promise<boolean>} Resolves to true if the group exists
|
||||||
*/
|
*/
|
||||||
exports.doesGroupExist = async (groupID: string) => {
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,13 +99,13 @@ exports.doesGroupExist = async (groupID: string) => {
|
||||||
* @return {Promise<{groupID: string}>} the id of the new group
|
* @return {Promise<{groupID: string}>} the id of the new group
|
||||||
*/
|
*/
|
||||||
exports.createGroup = async () => {
|
exports.createGroup = async () => {
|
||||||
const groupID = `g.${randomString(16)}`;
|
const groupID = `g.${randomString(16)}`;
|
||||||
await db.set(`group:${groupID}`, {pads: {}, mappings: {}});
|
await db.set(`group:${groupID}`, { pads: {}, mappings: {} });
|
||||||
// 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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -108,22 +113,22 @@ exports.createGroup = async () => {
|
||||||
* @param groupMapper the mapper of the group
|
* @param groupMapper the mapper of the group
|
||||||
* @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),
|
||||||
// Remember the mapping in the group record so that it can be cleaned up when the group is
|
// Remember the mapping in the group record so that it can be cleaned up when the group is
|
||||||
// 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,32 +139,37 @@ 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 (
|
||||||
// create the padID
|
groupID: string,
|
||||||
const padID = `${groupID}$${padName}`;
|
padName: string,
|
||||||
|
text: string,
|
||||||
|
authorId = "",
|
||||||
|
): Promise<{ padID: string }> => {
|
||||||
|
// create the padID
|
||||||
|
const padID = `${groupID}$${padName}`;
|
||||||
|
|
||||||
// ensure group exists
|
// ensure group exists
|
||||||
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
|
||||||
const padExists = await padManager.doesPadExists(padID);
|
const padExists = await padManager.doesPadExists(padID);
|
||||||
|
|
||||||
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,17 +177,17 @@ 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 };
|
||||||
};
|
};
|
||||||
|
|
1607
src/node/db/Pad.ts
1607
src/node/db/Pad.ts
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* The Pad Manager is a Factory for pad Objects
|
* The Pad Manager is a Factory for pad Objects
|
||||||
*/
|
*/
|
||||||
|
@ -19,13 +18,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MapArrayType} from "../types/MapType";
|
import type { MapArrayType } from "../types/MapType";
|
||||||
import {PadType} from "../types/PadType";
|
import type { PadType } from "../types/PadType";
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const Pad = require('../db/Pad');
|
const Pad = require("../db/Pad");
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache of all loaded Pads.
|
* A cache of all loaded Pads.
|
||||||
|
@ -38,18 +37,16 @@ const settings = require('../utils/Settings');
|
||||||
* If this is needed in other places, it would be wise to make this a prototype
|
* If this is needed in other places, it would be wise to make this a prototype
|
||||||
* 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) {
|
||||||
},
|
delete this[`:${name}`];
|
||||||
remove(name: string) {
|
},
|
||||||
delete this[`:${name}`];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,45 +54,45 @@ 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;
|
||||||
constructor() {
|
constructor() {
|
||||||
this._cachedList = null;
|
this._cachedList = null;
|
||||||
this._list = new Set();
|
this._list = new Set();
|
||||||
this._loaded = null;
|
this._loaded = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all pads in alphabetical order as array.
|
* Returns all pads in alphabetical order as array.
|
||||||
* @returns {Promise<string[]>} A promise that resolves to an array of pad IDs.
|
* @returns {Promise<string[]>} A promise that resolves to an array of pad IDs.
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
if (!this._cachedList) this._cachedList = [...this._list].sort();
|
if (!this._cachedList) this._cachedList = [...this._list].sort();
|
||||||
return this._cachedList;
|
return this._cachedList;
|
||||||
}
|
}
|
||||||
|
|
||||||
addPad(name: string) {
|
addPad(name: string) {
|
||||||
if (this._list.has(name)) return;
|
if (this._list.has(name)) return;
|
||||||
this._list.add(name);
|
this._list.add(name);
|
||||||
this._cachedList = null;
|
this._cachedList = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
removePad(name: string) {
|
removePad(name: string) {
|
||||||
if (!this._list.has(name)) return;
|
if (!this._list.has(name)) return;
|
||||||
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,57 +103,58 @@ 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 (
|
||||||
// check if this is a valid padId
|
id: string,
|
||||||
if (!exports.isValidPadId(id)) {
|
text?: string | null,
|
||||||
throw new CustomError(`${id} is not a valid padId`, 'apierror');
|
authorId: string | null = "",
|
||||||
}
|
): Promise<PadType> => {
|
||||||
|
// check if this is a valid padId
|
||||||
|
if (!exports.isValidPadId(id)) {
|
||||||
|
throw new CustomError(`${id} is not a valid padId`, "apierror");
|
||||||
|
}
|
||||||
|
|
||||||
// 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pad = globalPads.get(id);
|
let pad = globalPads.get(id);
|
||||||
|
|
||||||
// return pad if it's already loaded
|
// return pad if it's already loaded
|
||||||
if (pad != null) {
|
if (pad != null) {
|
||||||
return pad;
|
return pad;
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to load pad
|
// try to load pad
|
||||||
pad = new Pad.Pad(id);
|
pad = new Pad.Pad(id);
|
||||||
|
|
||||||
// initialize the pad
|
// initialize the pad
|
||||||
await pad.init(text, authorId);
|
await pad.init(text, authorId);
|
||||||
globalPads.set(id, pad);
|
globalPads.set(id, pad);
|
||||||
padList.addPad(id);
|
padList.addPad(id);
|
||||||
|
|
||||||
return pad;
|
return pad;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.listAllPads = async () => {
|
exports.listAllPads = async () => {
|
||||||
const padIDs = await padList.getPads();
|
const padIDs = await padList.getPads();
|
||||||
|
|
||||||
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,44 +165,45 @@ 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
|
||||||
exports.sanitizePadId = async (padId: string) => {
|
exports.sanitizePadId = async (padId: string) => {
|
||||||
for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
|
for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
|
||||||
const exists = await exports.doesPadExist(padId);
|
const exists = await exports.doesPadExist(padId);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return padId;
|
return padId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [from, to] = padIdTransforms[i];
|
const [from, to] = padIdTransforms[i];
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
padId = padId.replace(from, to);
|
padId = padId.replace(from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.lowerCasePadIds) padId = padId.toLowerCase();
|
if (settings.lowerCasePadIds) padId = padId.toLowerCase();
|
||||||
|
|
||||||
// we're out of possible transformations, so just return it
|
// we're out of possible transformations, so just return it
|
||||||
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.
|
||||||
*/
|
*/
|
||||||
exports.removePad = async (padId: string) => {
|
exports.removePad = async (padId: string) => {
|
||||||
const p = db.remove(`pad:${padId}`);
|
const p = db.remove(`pad:${padId}`);
|
||||||
exports.unloadPad(padId);
|
exports.unloadPad(padId);
|
||||||
padList.removePad(padId);
|
padList.removePad(padId);
|
||||||
await p;
|
await p;
|
||||||
};
|
};
|
||||||
|
|
||||||
// removes a pad from the cache
|
// removes a pad from the cache
|
||||||
exports.unloadPad = (padId: string) => {
|
exports.unloadPad = (padId: string) => {
|
||||||
globalPads.remove(padId);
|
globalPads.remove(padId);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* The ReadOnlyManager manages the database and rendering releated to read only pads
|
* The ReadOnlyManager manages the database and rendering releated to read only pads
|
||||||
*/
|
*/
|
||||||
|
@ -19,37 +18,35 @@
|
||||||
* 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
|
||||||
* @param {String} padId the id of the pad
|
* @param {String} padId the id of the pad
|
||||||
* @return {String} the read only id
|
* @return {String} the read only id
|
||||||
*/
|
*/
|
||||||
exports.getReadOnlyId = async (padId:string) => {
|
exports.getReadOnlyId = async (padId: string) => {
|
||||||
// check if there is a pad2readonly entry
|
// check if there is a pad2readonly entry
|
||||||
let readOnlyId = await db.get(`pad2readonly:${padId}`);
|
let readOnlyId = await db.get(`pad2readonly:${padId}`);
|
||||||
|
|
||||||
// there is no readOnly Entry in the database, let's create one
|
// there is no readOnly Entry in the database, let's create one
|
||||||
if (readOnlyId == null) {
|
if (readOnlyId == null) {
|
||||||
readOnlyId = `r.${randomString(16)}`;
|
readOnlyId = `r.${randomString(16)}`;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.set(`pad2readonly:${padId}`, readOnlyId),
|
db.set(`pad2readonly:${padId}`, readOnlyId),
|
||||||
db.set(`readonly2pad:${readOnlyId}`, padId),
|
db.set(`readonly2pad:${readOnlyId}`, padId),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readOnlyId;
|
return readOnlyId;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,19 +54,20 @@ 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
|
||||||
* @param {String} id read only id or real pad id
|
* @param {String} id read only id or real pad id
|
||||||
* @return {Object} an object with the padId and readonlyPadId
|
* @return {Object} an object with the padId and readonlyPadId
|
||||||
*/
|
*/
|
||||||
exports.getIds = async (id:string) => {
|
exports.getIds = async (id: string) => {
|
||||||
const readonly = exports.isReadOnlyId(id);
|
const readonly = exports.isReadOnlyId(id);
|
||||||
|
|
||||||
// Might be null, if this is an unknown read-only id
|
// Might be null, if this is an unknown read-only id
|
||||||
const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id);
|
const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id);
|
||||||
const padId = readonly ? await exports.getPadId(id) : id;
|
const padId = readonly ? await exports.getPadId(id) : id;
|
||||||
|
|
||||||
return {readOnlyPadId, padId, readonly};
|
return { readOnlyPadId, padId, readonly };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* Controls the security of pad access
|
* Controls the security of pad access
|
||||||
*/
|
*/
|
||||||
|
@ -19,20 +18,20 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {UserSettingsObject} from "../types/UserSettingsObject";
|
import type { UserSettingsObject } from "../types/UserSettingsObject";
|
||||||
|
|
||||||
const authorManager = require('./AuthorManager');
|
const authorManager = require("./AuthorManager");
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require("../../static/js/pluginfw/hooks.js");
|
||||||
const padManager = require('./PadManager');
|
const padManager = require("./PadManager");
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
const readOnlyManager = require("./ReadOnlyManager");
|
||||||
const sessionManager = require('./SessionManager');
|
const sessionManager = require("./SessionManager");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const webaccess = require('../hooks/express/webaccess');
|
const webaccess = require("../hooks/express/webaccess");
|
||||||
const log4js = require('log4js');
|
const log4js = require("log4js");
|
||||||
const authLogger = log4js.getLogger('auth');
|
const authLogger = log4js.getLogger("auth");
|
||||||
const {padutils} = require('../../static/js/pad_utils');
|
const { padutils } = require("../../static/js/pad_utils");
|
||||||
|
|
||||||
const DENY = Object.freeze({accessStatus: 'deny'});
|
const DENY = Object.freeze({ accessStatus: "deny" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the user can access a pad.
|
* Determines whether the user can access a pad.
|
||||||
|
@ -57,94 +56,123 @@ 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 (
|
||||||
if (!padID) {
|
padID: string,
|
||||||
authLogger.debug('access denied: missing padID');
|
sessionCookie: string,
|
||||||
return DENY;
|
token: string,
|
||||||
}
|
userSettings: UserSettingsObject,
|
||||||
|
) => {
|
||||||
|
if (!padID) {
|
||||||
|
authLogger.debug("access denied: missing padID");
|
||||||
|
return DENY;
|
||||||
|
}
|
||||||
|
|
||||||
let canCreate = !settings.editOnly;
|
let canCreate = !settings.editOnly;
|
||||||
|
|
||||||
if (readOnlyManager.isReadOnlyId(padID)) {
|
if (readOnlyManager.isReadOnlyId(padID)) {
|
||||||
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(
|
||||||
return DENY;
|
"access denied: read-only pad ID for a pad that does not exist",
|
||||||
}
|
);
|
||||||
}
|
return DENY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
);
|
||||||
if (userSettings == null) {
|
} else if (settings.requireAuthentication) {
|
||||||
authLogger.debug('access denied: authentication is required');
|
if (userSettings == null) {
|
||||||
return DENY;
|
authLogger.debug("access denied: authentication is required");
|
||||||
}
|
return DENY;
|
||||||
if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false;
|
}
|
||||||
if (userSettings.readOnly) canCreate = false;
|
if (userSettings.canCreate != null && !userSettings.canCreate)
|
||||||
// Note: userSettings.padAuthorizations should still be populated even if
|
canCreate = false;
|
||||||
// settings.requireAuthorization is false.
|
if (userSettings.readOnly) canCreate = false;
|
||||||
const padAuthzs = userSettings.padAuthorizations || {};
|
// Note: userSettings.padAuthorizations should still be populated even if
|
||||||
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
|
// settings.requireAuthorization is false.
|
||||||
if (!level) {
|
const padAuthzs = userSettings.padAuthorizations || {};
|
||||||
authLogger.debug('access denied: unauthorized');
|
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
|
||||||
return DENY;
|
if (!level) {
|
||||||
}
|
authLogger.debug("access denied: unauthorized");
|
||||||
if (level !== 'create') canCreate = false;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
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
|
||||||
return DENY;
|
.callAll("onAccessCheck", { padID, token, sessionCookie })
|
||||||
}
|
.some(isFalse)
|
||||||
|
) {
|
||||||
|
authLogger.debug(
|
||||||
|
"access denied: an onAccessCheck hook function returned false",
|
||||||
|
);
|
||||||
|
return DENY;
|
||||||
|
}
|
||||||
|
|
||||||
const padExists = await padManager.doesPadExist(padID);
|
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(
|
||||||
return DENY;
|
"access denied: user attempted to create a pad, which is prohibited",
|
||||||
}
|
);
|
||||||
|
return DENY;
|
||||||
|
}
|
||||||
|
|
||||||
const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie);
|
const sessionAuthorID = await sessionManager.findAuthorID(
|
||||||
if (settings.requireSession && !sessionAuthorID) {
|
padID.split("$")[0],
|
||||||
authLogger.debug('access denied: HTTP API session is required');
|
sessionCookie,
|
||||||
return DENY;
|
);
|
||||||
}
|
if (settings.requireSession && !sessionAuthorID) {
|
||||||
if (!sessionAuthorID && token != null && !padutils.isValidAuthorToken(token)) {
|
authLogger.debug("access denied: HTTP API session is required");
|
||||||
// The author token should be kept secret, so do not log it.
|
return DENY;
|
||||||
authLogger.debug('access denied: invalid author token');
|
}
|
||||||
return DENY;
|
if (
|
||||||
}
|
!sessionAuthorID &&
|
||||||
|
token != null &&
|
||||||
|
!padutils.isValidAuthorToken(token)
|
||||||
|
) {
|
||||||
|
// The author token should be kept secret, so do not log it.
|
||||||
|
authLogger.debug("access denied: invalid author token");
|
||||||
|
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(
|
||||||
return DENY;
|
"access denied: must have an HTTP API session to create a group pad",
|
||||||
}
|
);
|
||||||
// Creating a group pad, so there is no public status to check.
|
return DENY;
|
||||||
return grant;
|
}
|
||||||
}
|
// Creating a group pad, so there is no public status to check.
|
||||||
|
return grant;
|
||||||
|
}
|
||||||
|
|
||||||
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(
|
||||||
return DENY;
|
"access denied: must have an HTTP API session to access private group pads",
|
||||||
}
|
);
|
||||||
|
return DENY;
|
||||||
|
}
|
||||||
|
|
||||||
return grant;
|
return grant;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* The Session Manager provides functions to manage session in the database,
|
* The Session Manager provides functions to manage session in the database,
|
||||||
* it only provides session management for sessions created by the API
|
* it only provides session management for sessions created by the API
|
||||||
|
@ -20,12 +19,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require("../utils/customError");
|
||||||
const promises = require('../utils/promises');
|
const promises = require("../utils/promises");
|
||||||
const randomString = require('../utils/randomstring');
|
const randomString = require("../utils/randomstring");
|
||||||
const db = require('./DB');
|
const db = require("./DB");
|
||||||
const groupManager = require('./GroupManager');
|
const groupManager = require("./GroupManager");
|
||||||
const authorManager = require('./AuthorManager');
|
const authorManager = require("./AuthorManager");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the author ID for a session with matching ID and group.
|
* Finds the author ID for a session with matching ID and group.
|
||||||
|
@ -36,52 +35,59 @@ const authorManager = require('./AuthorManager');
|
||||||
* sessionCookie, and is bound to a group with the given ID, then this returns the author ID
|
* sessionCookie, and is bound to a group with the given ID, then this returns the author ID
|
||||||
* bound to the session. Otherwise, returns undefined.
|
* bound to the session. Otherwise, returns undefined.
|
||||||
*/
|
*/
|
||||||
exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
exports.findAuthorID = async (groupID: string, sessionCookie: string) => {
|
||||||
if (!sessionCookie) return undefined;
|
if (!sessionCookie) return undefined;
|
||||||
/*
|
/*
|
||||||
* Sometimes, RFC 6265-compliant web servers may send back a cookie whose
|
* Sometimes, RFC 6265-compliant web servers may send back a cookie whose
|
||||||
* value is enclosed in double quotes, such as:
|
* value is enclosed in double quotes, such as:
|
||||||
*
|
*
|
||||||
* Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,
|
* Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,
|
||||||
* s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard
|
* s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard
|
||||||
*
|
*
|
||||||
* Where the double quotes at the start and the end of the header value are
|
* Where the double quotes at the start and the end of the header value are
|
||||||
* just delimiters. This is perfectly legal: Etherpad parsing logic should
|
* just delimiters. This is perfectly legal: Etherpad parsing logic should
|
||||||
* cope with that, and remove the quotes early in the request phase.
|
* cope with that, and remove the quotes early in the request phase.
|
||||||
*
|
*
|
||||||
* Somehow, this does not happen, and in such cases the actual value that
|
* Somehow, this does not happen, and in such cases the actual value that
|
||||||
* sessionCookie ends up having is:
|
* sessionCookie ends up having is:
|
||||||
*
|
*
|
||||||
* sessionCookie = '"s.37cf5299fbf981e14121fba3a588c02b,s.2b21517bf50729d8130ab85736a11346"'
|
* sessionCookie = '"s.37cf5299fbf981e14121fba3a588c02b,s.2b21517bf50729d8130ab85736a11346"'
|
||||||
*
|
*
|
||||||
* As quick measure, let's strip the double quotes (when present).
|
* As quick measure, let's strip the double quotes (when present).
|
||||||
* Note that here we are being minimal, limiting ourselves to just removing
|
* Note that here we are being minimal, limiting ourselves to just removing
|
||||||
* quotes at the start and the end of the string.
|
* quotes at the start and the end of the 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(
|
||||||
} else {
|
`SessionManager getAuthorID: no session exists with ID ${id}`,
|
||||||
throw err;
|
);
|
||||||
}
|
} else {
|
||||||
}
|
throw err;
|
||||||
return undefined;
|
}
|
||||||
});
|
}
|
||||||
const now = Math.floor(Date.now() / 1000);
|
return undefined;
|
||||||
const isMatch = (si: {
|
});
|
||||||
groupID: string;
|
const now = Math.floor(Date.now() / 1000);
|
||||||
validUntil: number;
|
const isMatch = (
|
||||||
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
si: {
|
||||||
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
|
groupID: string;
|
||||||
if (sessionInfo == null) return undefined;
|
validUntil: number;
|
||||||
return sessionInfo.authorID;
|
} | null,
|
||||||
|
) => si != null && si.groupID === groupID && now < si.validUntil;
|
||||||
|
const sessionInfo = await promises.firstSatisfies(
|
||||||
|
sessionInfoPromises,
|
||||||
|
isMatch,
|
||||||
|
);
|
||||||
|
if (sessionInfo == null) return undefined;
|
||||||
|
return sessionInfo.authorID;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,9 +96,9 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
* @return {Promise<boolean>} Resolves to true if the session exists
|
* @return {Promise<boolean>} Resolves to true if the session exists
|
||||||
*/
|
*/
|
||||||
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,60 +108,64 @@ 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 (
|
||||||
// check if the group exists
|
groupID: string,
|
||||||
const groupExists = await groupManager.doesGroupExist(groupID);
|
authorID: string,
|
||||||
if (!groupExists) {
|
validUntil: number,
|
||||||
throw new CustomError('groupID does not exist', 'apierror');
|
) => {
|
||||||
}
|
// check if the group exists
|
||||||
|
const groupExists = await groupManager.doesGroupExist(groupID);
|
||||||
|
if (!groupExists) {
|
||||||
|
throw new CustomError("groupID does not exist", "apierror");
|
||||||
|
}
|
||||||
|
|
||||||
// check if the author exists
|
// check if the author exists
|
||||||
const authorExists = await authorManager.doesAuthorExist(authorID);
|
const authorExists = await authorManager.doesAuthorExist(authorID);
|
||||||
if (!authorExists) {
|
if (!authorExists) {
|
||||||
throw new CustomError('authorID does not exist', 'apierror');
|
throw new CustomError("authorID does not exist", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to parse validUntil if it's not a number
|
// try to parse validUntil if it's not a number
|
||||||
if (typeof validUntil !== 'number') {
|
if (typeof validUntil !== "number") {
|
||||||
validUntil = parseInt(validUntil);
|
validUntil = Number.parseInt(validUntil);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check it's a valid number
|
// check it's a valid number
|
||||||
if (isNaN(validUntil)) {
|
if (isNaN(validUntil)) {
|
||||||
throw new CustomError('validUntil is not a number', 'apierror');
|
throw new CustomError("validUntil is not a number", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure this is not a negative number
|
// ensure this is not a negative number
|
||||||
if (validUntil < 0) {
|
if (validUntil < 0) {
|
||||||
throw new CustomError('validUntil is a negative number', 'apierror');
|
throw new CustomError("validUntil is a negative number", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure this is not a float value
|
// ensure this is not a float value
|
||||||
if (!isInt(validUntil)) {
|
if (!isInt(validUntil)) {
|
||||||
throw new CustomError('validUntil is a float value', 'apierror');
|
throw new CustomError("validUntil is a float value", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if validUntil is in the future
|
// check if validUntil is in the future
|
||||||
if (validUntil < Math.floor(Date.now() / 1000)) {
|
if (validUntil < Math.floor(Date.now() / 1000)) {
|
||||||
throw new CustomError('validUntil is in the past', 'apierror');
|
throw new CustomError("validUntil is in the past", "apierror");
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate sessionID
|
// generate sessionID
|
||||||
const sessionID = `s.${randomString(16)}`;
|
const sessionID = `s.${randomString(16)}`;
|
||||||
|
|
||||||
// set the session into the database
|
// set the session into the database
|
||||||
await db.set(`session:${sessionID}`, {groupID, authorID, validUntil});
|
await db.set(`session:${sessionID}`, { groupID, authorID, validUntil });
|
||||||
|
|
||||||
// Add the session ID to the group2sessions and author2sessions records after creating the session
|
// Add the session ID to the group2sessions and author2sessions records after creating the session
|
||||||
// so that the state is consistent.
|
// so that the state is consistent.
|
||||||
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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,17 +173,17 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu
|
||||||
* @param {String} sessionID The id of the session
|
* @param {String} sessionID The id of the session
|
||||||
* @return {Promise<Object>} the sessioninfos
|
* @return {Promise<Object>} the sessioninfos
|
||||||
*/
|
*/
|
||||||
exports.getSessionInfo = async (sessionID:string) => {
|
exports.getSessionInfo = 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}`);
|
||||||
|
|
||||||
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
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -181,28 +191,36 @@ exports.getSessionInfo = async (sessionID:string) => {
|
||||||
* @param {String} sessionID The id of the session
|
* @param {String} sessionID The id of the session
|
||||||
* @return {Promise<void>} Resolves when the session is deleted
|
* @return {Promise<void>} Resolves when the session is deleted
|
||||||
*/
|
*/
|
||||||
exports.deleteSession = async (sessionID:string) => {
|
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
|
||||||
const groupID = session.groupID;
|
const groupID = session.groupID;
|
||||||
const authorID = session.authorID;
|
const authorID = session.authorID;
|
||||||
|
|
||||||
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. 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
|
||||||
// is consistent.
|
// is consistent.
|
||||||
await db.remove(`session:${sessionID}`);
|
await db.remove(`session:${sessionID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -211,14 +229,14 @@ exports.deleteSession = async (sessionID:string) => {
|
||||||
* @return {Promise<Object>} The sessioninfos of all sessions of this group
|
* @return {Promise<Object>} The sessioninfos of all sessions of this group
|
||||||
*/
|
*/
|
||||||
exports.listSessionsOfGroup = async (groupID: string) => {
|
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}`);
|
||||||
return sessions;
|
return sessions;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -227,13 +245,13 @@ exports.listSessionsOfGroup = async (groupID: string) => {
|
||||||
* @return {Promise<Object>} The sessioninfos of all sessions of this author
|
* @return {Promise<Object>} The sessioninfos of all sessions of this author
|
||||||
*/
|
*/
|
||||||
exports.listSessionsOfAuthor = async (authorID: string) => {
|
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}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common
|
// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common
|
||||||
|
@ -244,32 +262,32 @@ exports.listSessionsOfAuthor = async (authorID: string) => {
|
||||||
* @return {Promise<*>}
|
* @return {Promise<*>}
|
||||||
*/
|
*/
|
||||||
const listSessionsWithDBKey = async (dbkey: string) => {
|
const listSessionsWithDBKey = async (dbkey: string) => {
|
||||||
// get the group2sessions entry
|
// get the group2sessions entry
|
||||||
const sessionObject = await db.get(dbkey);
|
const sessionObject = await db.get(dbkey);
|
||||||
const sessions = sessionObject ? sessionObject.sessionIDs : null;
|
const sessions = sessionObject ? sessionObject.sessionIDs : null;
|
||||||
|
|
||||||
// iterate through the sessions and get the sessioninfos
|
// iterate through the sessions and get the sessioninfos
|
||||||
for (const sessionID of Object.keys(sessions || {})) {
|
for (const sessionID of Object.keys(sessions || {})) {
|
||||||
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 {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessions;
|
return sessions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if a number is an int
|
* checks if a number is an int
|
||||||
* @param {number|string} value
|
* @param {number|string} value
|
||||||
* @return {boolean} If the value is an integer
|
* @return {boolean} If the value is an integer
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const isInt = (value:number|string): boolean => (parseFloat(value) === parseInt(value)) && !isNaN(value);
|
const isInt = (value: number | string): boolean =>
|
||||||
|
Number.parseFloat(value) === Number.parseInt(value) && !isNaN(value);
|
||||||
|
|
|
@ -1,114 +1,119 @@
|
||||||
'use strict';
|
const DB = require("./DB");
|
||||||
|
const Store = require("express-session").Store;
|
||||||
|
const log4js = require("log4js");
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
const DB = require('./DB');
|
const logger = log4js.getLogger("SessionStore");
|
||||||
const Store = require('express-session').Store;
|
|
||||||
const log4js = require('log4js');
|
|
||||||
const util = require('util');
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('SessionStore');
|
|
||||||
|
|
||||||
class SessionStore extends Store {
|
class SessionStore extends Store {
|
||||||
/**
|
/**
|
||||||
* @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's
|
* @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's
|
||||||
* database record with the cookie's latest expiration time. If the difference between the
|
* database record with the cookie's latest expiration time. If the difference between the
|
||||||
* value saved in the database and the actual value is greater than this amount, the database
|
* value saved in the database and the actual value is greater than this amount, the database
|
||||||
* record will be updated to reflect the actual value. Use this to avoid continual database
|
* record will be updated to reflect the actual value. Use this to avoid continual database
|
||||||
* writes caused by express-session's rolling=true feature (see
|
* writes caused by express-session's rolling=true feature (see
|
||||||
* https://github.com/expressjs/session#rolling). A good value is high enough to keep query
|
* https://github.com/expressjs/session#rolling). A good value is high enough to keep query
|
||||||
* rate low but low enough to avoid annoying premature logouts (session invalidation) if
|
* rate low but low enough to avoid annoying premature logouts (session invalidation) if
|
||||||
* Etherpad is restarted. Use `null` to prevent `touch()` from ever updating the record.
|
* Etherpad is restarted. Use `null` to prevent `touch()` from ever updating the record.
|
||||||
* Ignored if the cookie does not expire.
|
* Ignored if the cookie does not expire.
|
||||||
*/
|
*/
|
||||||
constructor(refresh = null) {
|
constructor(refresh = null) {
|
||||||
super();
|
super();
|
||||||
this._refresh = refresh;
|
this._refresh = refresh;
|
||||||
// Maps session ID to an object with the following properties:
|
// Maps session ID to an object with the following properties:
|
||||||
// - `db`: Session expiration as recorded in the database (ms since epoch, not a Date).
|
// - `db`: Session expiration as recorded in the database (ms since epoch, not a Date).
|
||||||
// - `real`: Actual session expiration (ms since epoch, not a Date). Always greater than or
|
// - `real`: Actual session expiration (ms since epoch, not a Date). Always greater than or
|
||||||
// equal to `db`.
|
// equal to `db`.
|
||||||
// - `timeout`: Timeout ID for a timeout that will clean up the database record.
|
// - `timeout`: Timeout ID for a timeout that will clean up the database record.
|
||||||
this._expirations = new Map();
|
this._expirations = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
for (const {timeout} of this._expirations.values()) clearTimeout(timeout);
|
for (const { timeout } of this._expirations.values()) clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateExpirations(sid: string, sess: any, updateDbExp = true) {
|
async _updateExpirations(sid: string, sess: any, updateDbExp = true) {
|
||||||
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 {
|
||||||
if (expires) {
|
cookie: { expires } = {},
|
||||||
const sessExp = new Date(expires).getTime();
|
} = sess || {};
|
||||||
if (updateDbExp) exp.db = sessExp;
|
if (expires) {
|
||||||
exp.real = Math.max(exp.real || 0, exp.db || 0, sessExp);
|
const sessExp = new Date(expires).getTime();
|
||||||
const now = Date.now();
|
if (updateDbExp) exp.db = sessExp;
|
||||||
if (exp.real <= now) return await this._destroy(sid);
|
exp.real = Math.max(exp.real || 0, exp.db || 0, sessExp);
|
||||||
// If reading from the database, update the expiration with the latest value from touch() so
|
const now = Date.now();
|
||||||
// that touch() appears to write to the database every time even though it doesn't.
|
if (exp.real <= now) return await this._destroy(sid);
|
||||||
if (typeof expires === 'string') sess.cookie.expires = new Date(exp.real).toJSON();
|
// If reading from the database, update the expiration with the latest value from touch() so
|
||||||
// Use this._get(), not this._destroy(), to destroy the DB record for the expired session.
|
// that touch() appears to write to the database every time even though it doesn't.
|
||||||
// This is done in case multiple Etherpad instances are sharing the same database and users
|
if (typeof expires === "string")
|
||||||
// are bouncing between the instances. By using this._get(), this instance will query the DB
|
sess.cookie.expires = new Date(exp.real).toJSON();
|
||||||
// for the latest expiration time written by any of the instances, ensuring that the record
|
// Use this._get(), not this._destroy(), to destroy the DB record for the expired session.
|
||||||
// isn't prematurely deleted if the expiration time was updated by a different Etherpad
|
// This is done in case multiple Etherpad instances are sharing the same database and users
|
||||||
// instance. (Important caveat: Client-side database caching, which ueberdb does by default,
|
// are bouncing between the instances. By using this._get(), this instance will query the DB
|
||||||
// could still cause the record to be prematurely deleted because this instance might get a
|
// for the latest expiration time written by any of the instances, ensuring that the record
|
||||||
// stale expiration time from cache.)
|
// isn't prematurely deleted if the expiration time was updated by a different Etherpad
|
||||||
exp.timeout = setTimeout(() => this._get(sid), exp.real - now);
|
// instance. (Important caveat: Client-side database caching, which ueberdb does by default,
|
||||||
this._expirations.set(sid, exp);
|
// could still cause the record to be prematurely deleted because this instance might get a
|
||||||
} else {
|
// stale expiration time from cache.)
|
||||||
this._expirations.delete(sid);
|
exp.timeout = setTimeout(() => this._get(sid), exp.real - now);
|
||||||
}
|
this._expirations.set(sid, exp);
|
||||||
return sess;
|
} else {
|
||||||
}
|
this._expirations.delete(sid);
|
||||||
|
}
|
||||||
|
return sess;
|
||||||
|
}
|
||||||
|
|
||||||
async _write(sid: string, sess: any) {
|
async _write(sid: string, sess: any) {
|
||||||
await DB.set(`sessionstorage:${sid}`, sess);
|
await DB.set(`sessionstorage:${sid}`, sess);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _get(sid: string) {
|
async _get(sid: string) {
|
||||||
logger.debug(`GET ${sid}`);
|
logger.debug(`GET ${sid}`);
|
||||||
const s = await DB.get(`sessionstorage:${sid}`);
|
const s = await DB.get(`sessionstorage:${sid}`);
|
||||||
return await this._updateExpirations(sid, s);
|
return await this._updateExpirations(sid, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _set(sid: string, sess:any) {
|
async _set(sid: string, sess: any) {
|
||||||
logger.debug(`SET ${sid}`);
|
logger.debug(`SET ${sid}`);
|
||||||
sess = await this._updateExpirations(sid, sess);
|
sess = await this._updateExpirations(sid, sess);
|
||||||
if (sess != null) await this._write(sid, sess);
|
if (sess != null) await this._write(sid, sess);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _destroy(sid:string) {
|
async _destroy(sid: string) {
|
||||||
logger.debug(`DESTROY ${sid}`);
|
logger.debug(`DESTROY ${sid}`);
|
||||||
clearTimeout((this._expirations.get(sid) || {}).timeout);
|
clearTimeout((this._expirations.get(sid) || {}).timeout);
|
||||||
this._expirations.delete(sid);
|
this._expirations.delete(sid);
|
||||||
await DB.remove(`sessionstorage:${sid}`);
|
await DB.remove(`sessionstorage:${sid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: express-session might call touch() before it calls set() for the first time. Ideally this
|
// Note: express-session might call touch() before it calls set() for the first time. Ideally this
|
||||||
// would behave like set() in that case but it's OK if it doesn't -- express-session will call
|
// would behave like set() in that case but it's OK if it doesn't -- express-session will call
|
||||||
// set() soon enough.
|
// set() soon enough.
|
||||||
async _touch(sid: string, sess:any) {
|
async _touch(sid: string, sess: any) {
|
||||||
logger.debug(`TOUCH ${sid}`);
|
logger.debug(`TOUCH ${sid}`);
|
||||||
sess = await this._updateExpirations(sid, sess, false);
|
sess = await this._updateExpirations(sid, sess, false);
|
||||||
if (sess == null) return; // Already expired.
|
if (sess == null) return; // Already expired.
|
||||||
const exp = this._expirations.get(sid);
|
const exp = this._expirations.get(sid);
|
||||||
// If the session doesn't expire, don't do anything. Ideally we would write the session to the
|
// If the session doesn't expire, don't do anything. Ideally we would write the session to the
|
||||||
// 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 (
|
||||||
await this._write(sid, sess);
|
exp.db != null &&
|
||||||
exp.db = new Date(sess.cookie.expires).getTime();
|
(this._refresh == null || exp.real < exp.db + this._refresh)
|
||||||
}
|
)
|
||||||
|
return;
|
||||||
|
await this._write(sid, sess);
|
||||||
|
exp.db = new Date(sess.cookie.expires).getTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SessionStore;
|
module.exports = SessionStore;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
|
* Copyright (c) 2011 RedHog (Egil Möller) <egil.moller@freecode.no>
|
||||||
*
|
*
|
||||||
|
@ -20,94 +19,106 @@
|
||||||
* 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();
|
||||||
|
|
||||||
exports.info = {
|
exports.info = {
|
||||||
__output_stack: [],
|
__output_stack: [],
|
||||||
block_stack: [],
|
block_stack: [],
|
||||||
file_stack: [],
|
file_stack: [],
|
||||||
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);
|
||||||
exports.info.__output = b;
|
exports.info.__output = b;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports._exit = (b:any, recursive:boolean) => {
|
exports._exit = (b: any, recursive: boolean) => {
|
||||||
exports.info.__output = exports.info.__output_stack.pop();
|
exports.info.__output = exports.info.__output_stack.pop();
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = () => {
|
||||||
const name = exports.info.block_stack.pop();
|
const name = exports.info.block_stack.pop();
|
||||||
const renderContext = exports.info.args[exports.info.args.length - 1];
|
const renderContext = exports.info.args[exports.info.args.length - 1];
|
||||||
const content = exports.info.__output.get();
|
const content = exports.info.__output.get();
|
||||||
exports.info.__output.set(exports.info.__output_stack.pop());
|
exports.info.__output.set(exports.info.__output_stack.pop());
|
||||||
const args = {content, renderContext};
|
const args = { content, renderContext };
|
||||||
hooks.callAll(`eejsBlock_${name}`, args);
|
hooks.callAll(`eejsBlock_${name}`, args);
|
||||||
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: {
|
||||||
if (args == null) args = {};
|
filename: string;
|
||||||
|
paths: string[];
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (args == null) args = {};
|
||||||
|
|
||||||
let basedir = __dirname;
|
let basedir = __dirname;
|
||||||
let paths:string[] = [];
|
let paths: string[] = [];
|
||||||
|
|
||||||
if (exports.info.file_stack.length) {
|
if (exports.info.file_stack.length) {
|
||||||
basedir = path.dirname(getCurrentFile().path);
|
basedir = path.dirname(getCurrentFile().path);
|
||||||
}
|
}
|
||||||
if (mod) {
|
if (mod) {
|
||||||
basedir = path.dirname(mod.filename);
|
basedir = path.dirname(mod.filename);
|
||||||
paths = mod.paths;
|
paths = mod.paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)) ||
|
||||||
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
|
ejs.compile(
|
||||||
{filename: ejspath});
|
"<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>" +
|
||||||
if (cache) templateCache.set(ejspath, template);
|
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
|
||||||
|
{ filename: ejspath },
|
||||||
|
);
|
||||||
|
if (cache) templateCache.set(ejspath, template);
|
||||||
|
|
||||||
exports.info.args.push(args);
|
exports.info.args.push(args);
|
||||||
exports.info.file_stack.push({path: ejspath});
|
exports.info.file_stack.push({ path: ejspath });
|
||||||
const res = template(args);
|
const res = template(args);
|
||||||
exports.info.file_stack.pop();
|
exports.info.file_stack.pop();
|
||||||
exports.info.args.pop();
|
exports.info.args.pop();
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* The API Handler handles all API http requests
|
* The API Handler handles all API http requests
|
||||||
*/
|
*/
|
||||||
|
@ -19,140 +18,139 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MapArrayType} from "../types/MapType";
|
import type { MapArrayType } from "../types/MapType";
|
||||||
|
|
||||||
const api = require('../db/API');
|
const api = require("../db/API");
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require("../db/PadManager");
|
||||||
import createHTTPError from 'http-errors';
|
import type { Http2ServerRequest, Http2ServerResponse } from "node:http2";
|
||||||
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
|
import createHTTPError from "http-errors";
|
||||||
import {publicKeyExported} from "../security/OAuth2Provider";
|
import { jwtVerify } from "jose";
|
||||||
import {jwtVerify} from "jose";
|
import { publicKeyExported } from "../security/OAuth2Provider";
|
||||||
|
|
||||||
// a list of all functions
|
// a list of all functions
|
||||||
const version:MapArrayType<any> = {};
|
const version: MapArrayType<any> = {};
|
||||||
|
|
||||||
version['1'] = {
|
version["1"] = {
|
||||||
createGroup: [],
|
createGroup: [],
|
||||||
createGroupIfNotExistsFor: ['groupMapper'],
|
createGroupIfNotExistsFor: ["groupMapper"],
|
||||||
deleteGroup: ['groupID'],
|
deleteGroup: ["groupID"],
|
||||||
listPads: ['groupID'],
|
listPads: ["groupID"],
|
||||||
createPad: ['padID', 'text'],
|
createPad: ["padID", "text"],
|
||||||
createGroupPad: ['groupID', 'padName', 'text'],
|
createGroupPad: ["groupID", "padName", "text"],
|
||||||
createAuthor: ['name'],
|
createAuthor: ["name"],
|
||||||
createAuthorIfNotExistsFor: ['authorMapper', 'name'],
|
createAuthorIfNotExistsFor: ["authorMapper", "name"],
|
||||||
listPadsOfAuthor: ['authorID'],
|
listPadsOfAuthor: ["authorID"],
|
||||||
createSession: ['groupID', 'authorID', 'validUntil'],
|
createSession: ["groupID", "authorID", "validUntil"],
|
||||||
deleteSession: ['sessionID'],
|
deleteSession: ["sessionID"],
|
||||||
getSessionInfo: ['sessionID'],
|
getSessionInfo: ["sessionID"],
|
||||||
listSessionsOfGroup: ['groupID'],
|
listSessionsOfGroup: ["groupID"],
|
||||||
listSessionsOfAuthor: ['authorID'],
|
listSessionsOfAuthor: ["authorID"],
|
||||||
getText: ['padID', 'rev'],
|
getText: ["padID", "rev"],
|
||||||
setText: ['padID', 'text'],
|
setText: ["padID", "text"],
|
||||||
getHTML: ['padID', 'rev'],
|
getHTML: ["padID", "rev"],
|
||||||
setHTML: ['padID', 'html'],
|
setHTML: ["padID", "html"],
|
||||||
getRevisionsCount: ['padID'],
|
getRevisionsCount: ["padID"],
|
||||||
getLastEdited: ['padID'],
|
getLastEdited: ["padID"],
|
||||||
deletePad: ['padID'],
|
deletePad: ["padID"],
|
||||||
getReadOnlyID: ['padID'],
|
getReadOnlyID: ["padID"],
|
||||||
setPublicStatus: ['padID', 'publicStatus'],
|
setPublicStatus: ["padID", "publicStatus"],
|
||||||
getPublicStatus: ['padID'],
|
getPublicStatus: ["padID"],
|
||||||
listAuthorsOfPad: ['padID'],
|
listAuthorsOfPad: ["padID"],
|
||||||
padUsersCount: ['padID'],
|
padUsersCount: ["padID"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.1'] = {
|
version["1.1"] = {
|
||||||
...version['1'],
|
...version["1"],
|
||||||
getAuthorName: ['authorID'],
|
getAuthorName: ["authorID"],
|
||||||
padUsers: ['padID'],
|
padUsers: ["padID"],
|
||||||
sendClientsMessage: ['padID', 'msg'],
|
sendClientsMessage: ["padID", "msg"],
|
||||||
listAllGroups: [],
|
listAllGroups: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2'] = {
|
version["1.2"] = {
|
||||||
...version['1.1'],
|
...version["1.1"],
|
||||||
checkToken: [],
|
checkToken: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.1'] = {
|
version["1.2.1"] = {
|
||||||
...version['1.2'],
|
...version["1.2"],
|
||||||
listAllPads: [],
|
listAllPads: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.7'] = {
|
version["1.2.7"] = {
|
||||||
...version['1.2.1'],
|
...version["1.2.1"],
|
||||||
createDiffHTML: ['padID', 'startRev', 'endRev'],
|
createDiffHTML: ["padID", "startRev", "endRev"],
|
||||||
getChatHistory: ['padID', 'start', 'end'],
|
getChatHistory: ["padID", "start", "end"],
|
||||||
getChatHead: ['padID'],
|
getChatHead: ["padID"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.8'] = {
|
version["1.2.8"] = {
|
||||||
...version['1.2.7'],
|
...version["1.2.7"],
|
||||||
getAttributePool: ['padID'],
|
getAttributePool: ["padID"],
|
||||||
getRevisionChangeset: ['padID', 'rev'],
|
getRevisionChangeset: ["padID", "rev"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.9'] = {
|
version["1.2.9"] = {
|
||||||
...version['1.2.8'],
|
...version["1.2.8"],
|
||||||
copyPad: ['sourceID', 'destinationID', 'force'],
|
copyPad: ["sourceID", "destinationID", "force"],
|
||||||
movePad: ['sourceID', 'destinationID', 'force'],
|
movePad: ["sourceID", "destinationID", "force"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.10'] = {
|
version["1.2.10"] = {
|
||||||
...version['1.2.9'],
|
...version["1.2.9"],
|
||||||
getPadID: ['roID'],
|
getPadID: ["roID"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.11'] = {
|
version["1.2.11"] = {
|
||||||
...version['1.2.10'],
|
...version["1.2.10"],
|
||||||
getSavedRevisionsCount: ['padID'],
|
getSavedRevisionsCount: ["padID"],
|
||||||
listSavedRevisions: ['padID'],
|
listSavedRevisions: ["padID"],
|
||||||
saveRevision: ['padID', 'rev'],
|
saveRevision: ["padID", "rev"],
|
||||||
restoreRevision: ['padID', 'rev'],
|
restoreRevision: ["padID", "rev"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.12'] = {
|
version["1.2.12"] = {
|
||||||
...version['1.2.11'],
|
...version["1.2.11"],
|
||||||
appendChatMessage: ['padID', 'text', 'authorID', 'time'],
|
appendChatMessage: ["padID", "text", "authorID", "time"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.13'] = {
|
version["1.2.13"] = {
|
||||||
...version['1.2.12'],
|
...version["1.2.12"],
|
||||||
appendText: ['padID', 'text'],
|
appendText: ["padID", "text"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.14'] = {
|
version["1.2.14"] = {
|
||||||
...version['1.2.13'],
|
...version["1.2.13"],
|
||||||
getStats: [],
|
getStats: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.2.15'] = {
|
version["1.2.15"] = {
|
||||||
...version['1.2.14'],
|
...version["1.2.14"],
|
||||||
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force'],
|
copyPadWithoutHistory: ["sourceID", "destinationID", "force"],
|
||||||
};
|
};
|
||||||
|
|
||||||
version['1.3.0'] = {
|
version["1.3.0"] = {
|
||||||
...version['1.2.15'],
|
...version["1.2.15"],
|
||||||
appendText: ['padID', 'text', 'authorId'],
|
appendText: ["padID", "text", "authorId"],
|
||||||
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId'],
|
copyPadWithoutHistory: ["sourceID", "destinationID", "force", "authorId"],
|
||||||
createGroupPad: ['groupID', 'padName', 'text', 'authorId'],
|
createGroupPad: ["groupID", "padName", "text", "authorId"],
|
||||||
createPad: ['padID', 'text', 'authorId'],
|
createPad: ["padID", "text", "authorId"],
|
||||||
restoreRevision: ['padID', 'rev', 'authorId'],
|
restoreRevision: ["padID", "rev", "authorId"],
|
||||||
setHTML: ['padID', 'html', 'authorId'],
|
setHTML: ["padID", "html", "authorId"],
|
||||||
setText: ['padID', 'text', 'authorId'],
|
setText: ["padID", "text", "authorId"],
|
||||||
};
|
};
|
||||||
|
|
||||||
// set the latest available API version here
|
// set the latest available API version here
|
||||||
exports.latestApiVersion = '1.3.0';
|
exports.latestApiVersion = "1.3.0";
|
||||||
|
|
||||||
// exports the versions so it can be used by the new Swagger endpoint
|
// exports the versions so it can be used by the new Swagger endpoint
|
||||||
exports.version = version;
|
exports.version = version;
|
||||||
|
|
||||||
|
|
||||||
type APIFields = {
|
type APIFields = {
|
||||||
api_key: string;
|
api_key: string;
|
||||||
padID: string;
|
padID: string;
|
||||||
padName: string;
|
padName: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles an HTTP API call
|
* Handles an HTTP API call
|
||||||
|
@ -162,46 +160,54 @@ 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 (
|
||||||
// say goodbye if this is an unknown API version
|
apiVersion: string,
|
||||||
if (!(apiVersion in version)) {
|
functionName: string,
|
||||||
throw new createHTTPError.NotFound('no such api version');
|
fields: APIFields,
|
||||||
}
|
req: Http2ServerRequest,
|
||||||
|
res: Http2ServerResponse,
|
||||||
|
) {
|
||||||
|
// say goodbye if this is an unknown API version
|
||||||
|
if (!(apiVersion in version)) {
|
||||||
|
throw new createHTTPError.NotFound("no such api version");
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
throw new createHTTPError.Unauthorized("no or wrong API Key");
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
// sanitize any padIDs before continuing
|
||||||
throw new createHTTPError.Unauthorized('no or wrong API Key');
|
if (fields.padID) {
|
||||||
}
|
fields.padID = await padManager.sanitizePadId(fields.padID);
|
||||||
|
}
|
||||||
|
// there was an 'else' here before - removed it to ensure
|
||||||
|
// that this sanitize step can't be circumvented by forcing
|
||||||
|
// the first branch to be taken
|
||||||
|
if (fields.padName) {
|
||||||
|
fields.padName = await padManager.sanitizePadId(fields.padName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// put the function parameters in an array
|
||||||
|
// @ts-ignore
|
||||||
|
const functionParams = version[apiVersion][functionName].map(
|
||||||
|
(field) => fields[field],
|
||||||
|
);
|
||||||
|
|
||||||
|
// call the api function
|
||||||
// sanitize any padIDs before continuing
|
return api[functionName].apply(this, functionParams);
|
||||||
if (fields.padID) {
|
|
||||||
fields.padID = await padManager.sanitizePadId(fields.padID);
|
|
||||||
}
|
|
||||||
// there was an 'else' here before - removed it to ensure
|
|
||||||
// that this sanitize step can't be circumvented by forcing
|
|
||||||
// the first branch to be taken
|
|
||||||
if (fields.padName) {
|
|
||||||
fields.padName = await padManager.sanitizePadId(fields.padName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// put the function parameters in an array
|
|
||||||
// @ts-ignore
|
|
||||||
const functionParams = version[apiVersion][functionName].map((field) => fields[field]);
|
|
||||||
|
|
||||||
// call the api function
|
|
||||||
return api[functionName].apply(this, functionParams);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* Handles the export requests
|
* Handles the export requests
|
||||||
*/
|
*/
|
||||||
|
@ -20,15 +19,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const exporthtml = require('../utils/ExportHtml');
|
const exporthtml = require("../utils/ExportHtml");
|
||||||
const exporttxt = require('../utils/ExportTxt');
|
const exporttxt = require("../utils/ExportTxt");
|
||||||
const exportEtherpad = require('../utils/ExportEtherpad');
|
const exportEtherpad = require("../utils/ExportEtherpad");
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
import os from 'os';
|
import os from "os";
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require("../../static/js/pluginfw/hooks");
|
||||||
import util from 'util';
|
import util from "util";
|
||||||
const { checkValidRev } = require('../utils/checkValidRev');
|
const { checkValidRev } = require("../utils/checkValidRev");
|
||||||
|
|
||||||
const fsp_writeFile = util.promisify(fs.writeFile);
|
const fsp_writeFile = util.promisify(fs.writeFile);
|
||||||
const fsp_unlink = util.promisify(fs.unlink);
|
const fsp_unlink = util.promisify(fs.unlink);
|
||||||
|
@ -43,84 +42,101 @@ 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 (
|
||||||
// avoid naming the read-only file as the original pad's id
|
req: any,
|
||||||
let fileName = readOnlyId ? readOnlyId : padId;
|
res: any,
|
||||||
|
padId: string,
|
||||||
|
readOnlyId: string,
|
||||||
|
type: string,
|
||||||
|
) => {
|
||||||
|
// avoid naming the read-only file as the original pad's id
|
||||||
|
let fileName = readOnlyId ? readOnlyId : padId;
|
||||||
|
|
||||||
// allow fileName to be overwritten by a hook, the type type is kept static for security reasons
|
// 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) {
|
||||||
fileName = hookFileName;
|
fileName = hookFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// tell the browser that this is a downloadable file
|
// tell the browser that this is a downloadable file
|
||||||
res.attachment(`${fileName}.${type}`);
|
res.attachment(`${fileName}.${type}`);
|
||||||
|
|
||||||
if (req.params.rev !== undefined) {
|
if (req.params.rev !== undefined) {
|
||||||
// ensure revision is a number
|
// ensure revision is a number
|
||||||
// modify req, as we use it in a later call to exportConvert
|
// modify req, as we use it in a later call to exportConvert
|
||||||
req.params.rev = checkValidRev(req.params.rev);
|
req.params.rev = checkValidRev(req.params.rev);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
// ensure html can be collected by the garbage collector
|
// ensure html can be collected by the garbage collector
|
||||||
html = null;
|
html = null;
|
||||||
|
|
||||||
// send the convert job to the converter (abiword, libreoffice, ..)
|
// send the convert job to the converter (abiword, libreoffice, ..)
|
||||||
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", {
|
||||||
if (result.length > 0) {
|
srcFile,
|
||||||
// console.log("export handled by plugin", destFile);
|
destFile,
|
||||||
} else {
|
req,
|
||||||
const converter =
|
res,
|
||||||
settings.soffice != null ? require('../utils/LibreOffice')
|
});
|
||||||
: settings.abiword != null ? require('../utils/Abiword')
|
if (result.length > 0) {
|
||||||
: null;
|
// console.log("export handled by plugin", destFile);
|
||||||
await converter.convertFile(srcFile, destFile, type);
|
} else {
|
||||||
}
|
const converter =
|
||||||
|
settings.soffice != null
|
||||||
|
? require("../utils/LibreOffice")
|
||||||
|
: settings.abiword != null
|
||||||
|
? require("../utils/Abiword")
|
||||||
|
: null;
|
||||||
|
await converter.convertFile(srcFile, destFile, type);
|
||||||
|
}
|
||||||
|
|
||||||
// send the file
|
// send the file
|
||||||
await res.sendFile(destFile, null);
|
await res.sendFile(destFile, null);
|
||||||
|
|
||||||
// clean up temporary files
|
// clean up temporary files
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp_unlink(destFile);
|
await fsp_unlink(destFile);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* Handles the import requests
|
* Handles the import requests
|
||||||
*/
|
*/
|
||||||
|
@ -21,53 +20,53 @@
|
||||||
* 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 {
|
||||||
status: string;
|
status: string;
|
||||||
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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rm = async (path: string) => {
|
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,163 +78,193 @@ 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 (
|
||||||
// pipe to a file
|
req: any,
|
||||||
// convert file to html via abiword or soffice
|
res: any,
|
||||||
// set html in the pad
|
padId: string,
|
||||||
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
|
authorId: string,
|
||||||
|
) => {
|
||||||
|
// pipe to a file
|
||||||
|
// convert file to html via abiword or soffice
|
||||||
|
// set html in the pad
|
||||||
|
const randNum = Math.floor(Math.random() * 0xffffffff);
|
||||||
|
|
||||||
// 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,
|
||||||
uploadDir: tmpDirectory,
|
uploadDir: tmpDirectory,
|
||||||
maxFileSize: settings.importMaxFileSize,
|
maxFileSize: settings.importMaxFileSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
let srcFile;
|
let srcFile;
|
||||||
let files;
|
let files;
|
||||||
let fields;
|
let fields;
|
||||||
try {
|
try {
|
||||||
[fields, files] = await form.parse(req);
|
[fields, files] = await form.parse(req);
|
||||||
} 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
if (settings.allowUnknownFileEnds === true) {
|
if (settings.allowUnknownFileEnds === true) {
|
||||||
// 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(
|
||||||
await fs.rename(oldSrcFile, srcFile);
|
path.dirname(srcFile),
|
||||||
} else {
|
`${path.basename(srcFile, fileEnding)}.txt`,
|
||||||
logger.warn(`Not allowing unknown file type to be imported: ${fileEnding}`);
|
);
|
||||||
throw new ImportError('uploadFailed');
|
await fs.rename(oldSrcFile, srcFile);
|
||||||
}
|
} else {
|
||||||
}
|
logger.warn(
|
||||||
|
`Not allowing unknown file type to be imported: ${fileEnding}`,
|
||||||
|
);
|
||||||
|
throw new ImportError("uploadFailed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`);
|
const destFile = path.join(
|
||||||
const context = {srcFile, destFile, fileEnding, padId, ImportError};
|
tmpDirectory,
|
||||||
const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x:string) => x);
|
`etherpad_import_${randNum}.${exportExtension}`,
|
||||||
const fileIsEtherpad = (fileEnding === '.etherpad');
|
);
|
||||||
const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm');
|
const context = { srcFile, destFile, fileEnding, padId, ImportError };
|
||||||
const fileIsTXT = (fileEnding === '.txt');
|
const importHandledByPlugin = (await hooks.aCallAll("import", context)).some(
|
||||||
|
(x: string) => x,
|
||||||
|
);
|
||||||
|
const fileIsEtherpad = fileEnding === ".etherpad";
|
||||||
|
const fileIsHTML = fileEnding === ".html" || fileEnding === ".htm";
|
||||||
|
const fileIsTXT = fileEnding === ".txt";
|
||||||
|
|
||||||
let directDatabaseAccess = false;
|
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",
|
||||||
}
|
);
|
||||||
const text = await fs.readFile(srcFile, 'utf8');
|
throw new ImportError("padHasData");
|
||||||
directDatabaseAccess = true;
|
}
|
||||||
await importEtherpad.setPadRaw(padId, text, authorId);
|
const text = await fs.readFile(srcFile, "utf8");
|
||||||
}
|
directDatabaseAccess = true;
|
||||||
|
await importEtherpad.setPadRaw(padId, text, authorId);
|
||||||
|
}
|
||||||
|
|
||||||
// convert file to html if necessary
|
// convert file to html if necessary
|
||||||
if (!importHandledByPlugin && !directDatabaseAccess) {
|
if (!importHandledByPlugin && !directDatabaseAccess) {
|
||||||
if (fileIsTXT) {
|
if (fileIsTXT) {
|
||||||
// Don't use converter for text files
|
// Don't use converter for text files
|
||||||
useConverter = false;
|
useConverter = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/ether/etherpad-lite/issues/2572
|
// See https://github.com/ether/etherpad-lite/issues/2572
|
||||||
if (fileIsHTML || !useConverter) {
|
if (fileIsHTML || !useConverter) {
|
||||||
// if no converter only rename
|
// if no converter only rename
|
||||||
await fs.rename(srcFile, destFile);
|
await fs.rename(srcFile, destFile);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useConverter && !directDatabaseAccess) {
|
if (!useConverter && !directDatabaseAccess) {
|
||||||
// Read the file with no encoding for raw buffer access.
|
// Read the file with no encoding for raw buffer access.
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// change text of the pad and broadcast the changeset
|
// change text of the pad and broadcast the changeset
|
||||||
if (!directDatabaseAccess) {
|
if (!directDatabaseAccess) {
|
||||||
if (importHandledByPlugin || useConverter || fileIsHTML) {
|
if (importHandledByPlugin || useConverter || fileIsHTML) {
|
||||||
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: ${
|
||||||
} else {
|
err.stack || err
|
||||||
await pad.setText(text, authorId);
|
}`,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
await pad.setText(text, authorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
// updated pad data.
|
// updated pad data.
|
||||||
if (directDatabaseAccess) return true;
|
if (directDatabaseAccess) return true;
|
||||||
|
|
||||||
// tell clients to update
|
// tell clients to update
|
||||||
await padMessageHandler.updatePadClients(pad);
|
await padMessageHandler.updatePadClients(pad);
|
||||||
|
|
||||||
// clean up temporary files
|
// clean up temporary files
|
||||||
rm(srcFile);
|
rm(srcFile);
|
||||||
rm(destFile);
|
rm(destFile);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -246,19 +275,22 @@ const doImport = async (req:any, res:any, padId:string, authorId:string) => {
|
||||||
* @param {String} authorId the author id to use for the import
|
* @param {String} authorId the author id to use for the import
|
||||||
* @return {Promise<void>} a promise
|
* @return {Promise<void>} a promise
|
||||||
*/
|
*/
|
||||||
exports.doImport = async (req:any, res:any, padId:string, authorId:string = '') => {
|
exports.doImport = async (req: any, res: any, padId: string, authorId = "") => {
|
||||||
let httpStatus = 200;
|
let httpStatus = 200;
|
||||||
let code = 0;
|
let code = 0;
|
||||||
let message = 'ok';
|
let message = "ok";
|
||||||
let directDatabaseAccess;
|
let directDatabaseAccess;
|
||||||
try {
|
try {
|
||||||
directDatabaseAccess = await doImport(req, res, padId, authorId);
|
directDatabaseAccess = await doImport(req, res, padId, authorId);
|
||||||
} catch (err:any) {
|
} catch (err: any) {
|
||||||
const known = err instanceof ImportError && err.status;
|
const known = err instanceof ImportError && err.status;
|
||||||
if (!known) logger.error(`Internal error during import: ${err.stack || err}`);
|
if (!known)
|
||||||
httpStatus = known ? 400 : 500;
|
logger.error(`Internal error during import: ${err.stack || err}`);
|
||||||
code = known ? 1 : 2;
|
httpStatus = known ? 400 : 500;
|
||||||
message = known ? err.status : 'internalError';
|
code = known ? 1 : 2;
|
||||||
}
|
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,3 @@
|
||||||
'use strict';
|
|
||||||
/**
|
/**
|
||||||
* This is the Socket.IO Router. It routes the Messages between the
|
* This is the Socket.IO Router. It routes the Messages between the
|
||||||
* components of the Server. The components are at the moment: pad and timeslider
|
* components of the Server. The components are at the moment: pad and timeslider
|
||||||
|
@ -20,87 +19,98 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MapArrayType} from "../types/MapType";
|
import type { MapArrayType } from "../types/MapType";
|
||||||
import {SocketModule} from "../types/SocketModule";
|
import type { SocketModule } from "../types/SocketModule";
|
||||||
const log4js = require('log4js');
|
const log4js = require("log4js");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const stats = require('../../node/stats')
|
const stats = require("../../node/stats");
|
||||||
|
|
||||||
const logger = log4js.getLogger('socket.io');
|
const logger = log4js.getLogger("socket.io");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves all components
|
* Saves all components
|
||||||
* key is the component name
|
* key is the component name
|
||||||
* value is the component module
|
* value is the component module
|
||||||
*/
|
*/
|
||||||
const components:MapArrayType<any> = {};
|
const components: MapArrayType<any> = {};
|
||||||
|
|
||||||
let io:any;
|
let io: any;
|
||||||
|
|
||||||
/** adds a component
|
/** adds a component
|
||||||
* @param {string} moduleName
|
* @param {string} moduleName
|
||||||
* @param {Module} module
|
* @param {Module} module
|
||||||
*/
|
*/
|
||||||
exports.addComponent = (moduleName: string, module: SocketModule) => {
|
exports.addComponent = (moduleName: string, module: SocketModule) => {
|
||||||
if (module == null) return exports.deleteComponent(moduleName);
|
if (module == null) return exports.deleteComponent(moduleName);
|
||||||
components[moduleName] = module;
|
components[moduleName] = module;
|
||||||
module.setSocketIO(io);
|
module.setSocketIO(io);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* @param {Object} _io the socket.io instance
|
* @param {Object} _io the socket.io instance
|
||||||
*/
|
*/
|
||||||
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
|
||||||
socket._send = socket.send;
|
socket._send = socket.send;
|
||||||
socket.send = (message: string) => {
|
socket.send = (message: string) => {
|
||||||
logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`);
|
logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`);
|
||||||
socket._send(message);
|
socket._send(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
// tell all components about this connect
|
// tell all components about this connect
|
||||||
for (const i of Object.keys(components)) {
|
for (const i of Object.keys(components)) {
|
||||||
components[i].handleConnect(socket);
|
components[i].handleConnect(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('message', (message: any, ack: any = () => {}) => (async () => {
|
socket.on("message", (message: any, ack: any = () => {}) =>
|
||||||
if (!message.component || !components[message.component]) {
|
(async () => {
|
||||||
throw new Error(`unknown message component: ${message.component}`);
|
if (!message.component || !components[message.component]) {
|
||||||
}
|
throw new Error(`unknown message component: ${message.component}`);
|
||||||
logger.debug(`from ${socket.id}:`, message);
|
}
|
||||||
return await components[message.component].handleMessage(socket, message);
|
logger.debug(`from ${socket.id}:`, message);
|
||||||
})().then(
|
return await components[message.component].handleMessage(
|
||||||
(val) => ack(null, val),
|
socket,
|
||||||
(err) => {
|
message,
|
||||||
logger.error(
|
);
|
||||||
`Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`);
|
})().then(
|
||||||
ack({name: err.name, message: err.message}); // socket.io can't handle Error objects.
|
(val) => ack(null, val),
|
||||||
}));
|
(err) => {
|
||||||
|
logger.error(
|
||||||
|
`Error handling ${message.component} message from ${socket.id}: ${
|
||||||
|
err.stack || err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
ack({ name: err.name, message: err.message }); // socket.io can't handle Error objects.
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
socket.on('disconnect', (reason: string) => {
|
socket.on("disconnect", (reason: string) => {
|
||||||
logger.debug(`${socket.id} disconnected: ${reason}`);
|
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,261 +1,294 @@
|
||||||
'use strict';
|
import type { Socket } from "node:net";
|
||||||
|
import type { MapArrayType } from "../types/MapType";
|
||||||
|
|
||||||
import {Socket} from "node:net";
|
import events from "events";
|
||||||
import type {MapArrayType} from "../types/MapType";
|
import fs from "fs";
|
||||||
|
|
||||||
import _ from 'underscore';
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from "cookie-parser";
|
||||||
import events from 'events';
|
import express from "express";
|
||||||
import express from 'express';
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import expressSession from 'express-session';
|
import expressSession from "express-session";
|
||||||
import fs from 'fs';
|
import _ from "underscore";
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require("../../static/js/pluginfw/hooks");
|
||||||
import log4js from 'log4js';
|
import log4js from "log4js";
|
||||||
const SessionStore = require('../db/SessionStore');
|
const SessionStore = require("../db/SessionStore");
|
||||||
const settings = require('../utils/Settings');
|
const settings = require("../utils/Settings");
|
||||||
const stats = require('../stats')
|
const stats = require("../stats");
|
||||||
import util from 'util';
|
import util from "util";
|
||||||
const webaccess = require('./express/webaccess');
|
const webaccess = require("./express/webaccess");
|
||||||
|
|
||||||
import SecretRotator from '../security/SecretRotator';
|
import SecretRotator from "../security/SecretRotator";
|
||||||
|
|
||||||
let secretRotator: SecretRotator|null = null;
|
let secretRotator: SecretRotator | null = null;
|
||||||
const logger = log4js.getLogger('http');
|
const logger = log4js.getLogger("http");
|
||||||
let serverName:string;
|
let serverName: string;
|
||||||
let sessionStore: { shutdown: () => void; } | null;
|
let sessionStore: { shutdown: () => void } | null;
|
||||||
const sockets:Set<Socket> = new Set();
|
const sockets: Set<Socket> = new Set();
|
||||||
const socketsEvents = new events.EventEmitter();
|
const socketsEvents = new events.EventEmitter();
|
||||||
const startTime = stats.settableGauge('httpStartTime');
|
const startTime = stats.settableGauge("httpStartTime");
|
||||||
|
|
||||||
exports.server = null;
|
exports.server = null;
|
||||||
|
|
||||||
const closeServer = async () => {
|
const closeServer = async () => {
|
||||||
if (exports.server != null) {
|
if (exports.server != null) {
|
||||||
logger.info('Closing HTTP server...');
|
logger.info("Closing HTTP server...");
|
||||||
// Call exports.server.close() to reject new connections but don't await just yet because the
|
// Call exports.server.close() to reject new connections but don't await just yet because the
|
||||||
// Promise won't resolve until all preexisting connections are closed.
|
// Promise won't resolve until all preexisting connections are closed.
|
||||||
const p = util.promisify(exports.server.close.bind(exports.server))();
|
const p = util.promisify(exports.server.close.bind(exports.server))();
|
||||||
await hooks.aCallAll('expressCloseServer');
|
await hooks.aCallAll("expressCloseServer");
|
||||||
// Give existing connections some time to close on their own before forcibly terminating. The
|
// Give existing connections some time to close on their own before forcibly terminating. The
|
||||||
// time should be long enough to avoid interrupting most preexisting transmissions but short
|
// time should be long enough to avoid interrupting most preexisting transmissions but short
|
||||||
// enough to avoid a noticeable outage.
|
// enough to avoid a noticeable outage.
|
||||||
const timeout = setTimeout(async () => {
|
const timeout = setTimeout(async () => {
|
||||||
logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`);
|
logger.info(
|
||||||
for (const socket of sockets) socket.destroy(new Error('HTTP server is closing'));
|
`Forcibly terminating remaining ${sockets.size} HTTP connections...`,
|
||||||
}, 5000);
|
);
|
||||||
let lastLogged = 0;
|
for (const socket of sockets)
|
||||||
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
socket.destroy(new Error("HTTP server is closing"));
|
||||||
if (Date.now() - lastLogged > 1000) { // Rate limit to avoid filling logs.
|
}, 5000);
|
||||||
logger.info(`Waiting for ${sockets.size} HTTP clients to disconnect...`);
|
let lastLogged = 0;
|
||||||
lastLogged = Date.now();
|
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
||||||
}
|
if (Date.now() - lastLogged > 1000) {
|
||||||
await events.once(socketsEvents, 'updated');
|
// Rate limit to avoid filling logs.
|
||||||
}
|
logger.info(
|
||||||
await p;
|
`Waiting for ${sockets.size} HTTP clients to disconnect...`,
|
||||||
clearTimeout(timeout);
|
);
|
||||||
exports.server = null;
|
lastLogged = Date.now();
|
||||||
startTime.setValue(0);
|
}
|
||||||
logger.info('HTTP server closed');
|
await events.once(socketsEvents, "updated");
|
||||||
}
|
}
|
||||||
if (sessionStore) sessionStore.shutdown();
|
await p;
|
||||||
sessionStore = null;
|
clearTimeout(timeout);
|
||||||
if (secretRotator) secretRotator.stop();
|
exports.server = null;
|
||||||
secretRotator = null;
|
startTime.setValue(0);
|
||||||
|
logger.info("HTTP server closed");
|
||||||
|
}
|
||||||
|
if (sessionStore) sessionStore.shutdown();
|
||||||
|
sessionStore = null;
|
||||||
|
if (secretRotator) secretRotator.stop();
|
||||||
|
secretRotator = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
||||||
} else {
|
`You can access your Etherpad instance using the Unix socket at ${settings.port}`,
|
||||||
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`);
|
);
|
||||||
}
|
} else {
|
||||||
|
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(
|
||||||
} else {
|
`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`,
|
||||||
console.warn('Admin username and password not set in settings.json. ' +
|
);
|
||||||
'To access admin please uncomment and edit "users" in settings.json');
|
} else {
|
||||||
}
|
console.warn(
|
||||||
|
"Admin username and password not set in settings.json. " +
|
||||||
|
'To access admin please uncomment and edit "users" in settings.json',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const env = process.env.NODE_ENV || 'development';
|
const env = process.env.NODE_ENV || "development";
|
||||||
|
|
||||||
if (env !== 'production') {
|
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",
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.restartServer = async () => {
|
exports.restartServer = async () => {
|
||||||
await closeServer();
|
await closeServer();
|
||||||
|
|
||||||
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),
|
||||||
cert: fs.readFileSync(settings.ssl.cert),
|
cert: fs.readFileSync(settings.ssl.cert),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (settings.ssl.ca) {
|
if (settings.ssl.ca) {
|
||||||
options.ca = [];
|
options.ca = [];
|
||||||
for (let i = 0; i < settings.ssl.ca.length; i++) {
|
for (let i = 0; i < settings.ssl.ca.length; i++) {
|
||||||
const caFileName = settings.ssl.ca[i];
|
const caFileName = settings.ssl.ca[i];
|
||||||
options.ca.push(fs.readFileSync(caFileName));
|
options.ca.push(fs.readFileSync(caFileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
// 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.
|
||||||
// It's still a last bastion of referrer security. External URLs should be
|
// It's still a last bastion of referrer security. External URLs should be
|
||||||
// already marked with rel="noreferer" and user-generated content pages are already
|
// already marked with rel="noreferer" and user-generated content pages are already
|
||||||
// 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (settings.trustProxy) {
|
if (settings.trustProxy) {
|
||||||
/*
|
/*
|
||||||
* If 'trust proxy' === true, the client’s IP address in req.ip will be the
|
* If 'trust proxy' === true, the client’s IP address in req.ip will be the
|
||||||
* left-most entry in the X-Forwarded-* header.
|
* left-most entry in the X-Forwarded-* header.
|
||||||
*
|
*
|
||||||
* 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) => {
|
||||||
next();
|
stopWatch.end();
|
||||||
});
|
return sendFn(...args);
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// If the log level specified in the config file is WARN or ERROR the application server never
|
// If the log level specified in the config file is WARN or ERROR the application server never
|
||||||
// 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(
|
||||||
level: log4js.levels.DEBUG.levelStr,
|
log4js.connectLogger(logger, {
|
||||||
format: ':status, :method :url',
|
level: log4js.levels.DEBUG.levelStr,
|
||||||
}));
|
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",
|
||||||
await secretRotator.start();
|
keyRotationInterval,
|
||||||
secret = secretRotator.secrets;
|
sessionLifetime,
|
||||||
}
|
settings.sessionKey,
|
||||||
if (!secret) throw new Error('missing cookie signing secret');
|
);
|
||||||
|
await secretRotator.start();
|
||||||
|
secret = secretRotator.secrets;
|
||||||
|
}
|
||||||
|
if (!secret) throw new Error("missing cookie signing secret");
|
||||||
|
|
||||||
app.use(cookieParser(secret, {}));
|
app.use(cookieParser(secret, {}));
|
||||||
|
|
||||||
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
|
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
|
||||||
exports.sessionMiddleware = expressSession({
|
exports.sessionMiddleware = expressSession({
|
||||||
propagateTouch: true,
|
propagateTouch: true,
|
||||||
rolling: true,
|
rolling: true,
|
||||||
secret,
|
secret,
|
||||||
store: sessionStore,
|
store: sessionStore,
|
||||||
resave: false,
|
resave: false,
|
||||||
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,
|
||||||
|
|
||||||
// The automatic express-session mechanism for determining if the application is being served
|
// The automatic express-session mechanism for determining if the application is being served
|
||||||
// over ssl is similar to the one used for setting the language cookie, which check if one of
|
// over ssl is similar to the one used for setting the language cookie, which check if one of
|
||||||
// these conditions is true:
|
// these conditions is true:
|
||||||
//
|
//
|
||||||
// 1. we are directly serving the nodejs application over SSL, using the "ssl" options in
|
// 1. we are directly serving the nodejs application over SSL, using the "ssl" options in
|
||||||
// settings.json
|
// settings.json
|
||||||
//
|
//
|
||||||
// 2. we are serving the nodejs application in plaintext, but we are using a reverse proxy
|
// 2. we are serving the nodejs application in plaintext, but we are using a reverse proxy
|
||||||
// that terminates SSL for us. In this case, the user has to set trustProxy = true in
|
// that terminates SSL for us. In this case, the user has to set trustProxy = true in
|
||||||
// settings.json, and the information wheter the application is over SSL or not will be
|
// settings.json, and the information wheter the application is over SSL or not will be
|
||||||
// extracted from the X-Forwarded-Proto HTTP header
|
// extracted from the X-Forwarded-Proto HTTP header
|
||||||
//
|
//
|
||||||
// Please note that this will not be compatible with applications being served over http and
|
// Please note that this will not be compatible with applications being served over http and
|
||||||
// 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)(
|
||||||
startTime.setValue(Date.now());
|
settings.port,
|
||||||
logger.info('HTTP server listening for connections');
|
settings.ip,
|
||||||
|
);
|
||||||
|
startTime.setValue(Date.now());
|
||||||
|
logger.info("HTTP server listening for connections");
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.shutdown = async (hookName:string, context: any) => {
|
exports.shutdown = async (hookName: string, context: any) => {
|
||||||
await closeServer();
|
await closeServer();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
'use strict';
|
|
||||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
import type { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
|
||||||
const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the admin navigation link
|
* Add the admin navigation link
|
||||||
|
@ -14,13 +9,24 @@ const ADMIN_PATH = path.join(settings.root, 'src', 'templates', 'admin');
|
||||||
* @param {Function} cb the callback function
|
* @param {Function} cb the callback function
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
|
exports.expressCreateServer = (
|
||||||
args.app.use('/admin/', express.static(path.join(__dirname, '../../../templates/admin'), {maxAge: 1000 * 60 * 60 * 24}));
|
hookName: string,
|
||||||
args.app.get('/admin/*', (_request:any, response:any)=>{
|
args: ArgsExpressType,
|
||||||
response.sendFile(path.resolve(__dirname,'../../../templates/admin', 'index.html'));
|
cb: Function,
|
||||||
} )
|
): any => {
|
||||||
args.app.get('/admin', (req:any, res:any, next:Function) => {
|
args.app.use(
|
||||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
"/admin/",
|
||||||
})
|
express.static(path.join(__dirname, "../../../templates/admin"), {
|
||||||
return cb();
|
maxAge: 1000 * 60 * 60 * 24,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
args.app.get("/admin/*", (_request: any, response: any) => {
|
||||||
|
response.sendFile(
|
||||||
|
path.resolve(__dirname, "../../../templates/admin", "index.html"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
args.app.get("/admin", (req: any, res: any, next: Function) => {
|
||||||
|
if ("/" !== req.path[req.path.length - 1]) return res.redirect("./admin/");
|
||||||
|
});
|
||||||
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,101 +1,116 @@
|
||||||
'use strict';
|
import type { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
import type { ErrorCaused } from "../../types/ErrorCaused";
|
||||||
|
import type { QueryType } from "../../types/QueryType";
|
||||||
|
|
||||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
import {
|
||||||
import {ErrorCaused} from "../../types/ErrorCaused";
|
getAvailablePlugins,
|
||||||
import {QueryType} from "../../types/QueryType";
|
install,
|
||||||
|
search,
|
||||||
|
uninstall,
|
||||||
|
} from "../../../static/js/pluginfw/installer";
|
||||||
|
import type { PackageData } from "../../types/PackageInfo";
|
||||||
|
|
||||||
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer";
|
const pluginDefs = require("../../../static/js/pluginfw/plugin_defs");
|
||||||
import {PackageData} from "../../types/PackageInfo";
|
import semver from "semver";
|
||||||
|
|
||||||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
exports.socketio = (hookName: string, args: ArgsExpressType, cb: Function) => {
|
||||||
import semver from 'semver';
|
const io = args.io.of("/pluginfw/installer");
|
||||||
|
io.on("connection", (socket: any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const {
|
||||||
|
session: {
|
||||||
|
user: { is_admin: isAdmin } = {},
|
||||||
|
} = {},
|
||||||
|
} = socket.conn.request;
|
||||||
|
if (!isAdmin) return;
|
||||||
|
|
||||||
|
socket.on("getInstalled", (query: string) => {
|
||||||
|
// send currently installed plugins
|
||||||
|
const installed = Object.keys(pluginDefs.plugins).map(
|
||||||
|
(plugin) => pluginDefs.plugins[plugin].package,
|
||||||
|
);
|
||||||
|
|
||||||
exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
socket.emit("results:installed", { installed });
|
||||||
const io = args.io.of('/pluginfw/installer');
|
});
|
||||||
io.on('connection', (socket:any) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
|
||||||
if (!isAdmin) return;
|
|
||||||
|
|
||||||
socket.on('getInstalled', (query:string) => {
|
socket.on("checkUpdates", async () => {
|
||||||
// send currently installed plugins
|
// Check plugins for updates
|
||||||
const installed =
|
try {
|
||||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
|
||||||
|
|
||||||
socket.emit('results:installed', {installed});
|
const updatable = Object.keys(pluginDefs.plugins).filter((plugin) => {
|
||||||
});
|
if (!results[plugin]) return false;
|
||||||
|
|
||||||
socket.on('checkUpdates', async () => {
|
const latestVersion = results[plugin].version;
|
||||||
// Check plugins for updates
|
const currentVersion = pluginDefs.plugins[plugin].package.version;
|
||||||
try {
|
|
||||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
|
|
||||||
|
|
||||||
const updatable = Object.keys(pluginDefs.plugins).filter((plugin) => {
|
return semver.gt(latestVersion, currentVersion);
|
||||||
if (!results[plugin]) return false;
|
});
|
||||||
|
|
||||||
const latestVersion = results[plugin].version;
|
socket.emit("results:updatable", { updatable });
|
||||||
const currentVersion = pluginDefs.plugins[plugin].package.version;
|
} catch (err) {
|
||||||
|
const errc = err as ErrorCaused;
|
||||||
|
console.warn(errc.stack || errc.toString());
|
||||||
|
|
||||||
return semver.gt(latestVersion, currentVersion);
|
socket.emit("results:updatable", { updatable: {} });
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.emit('results:updatable', {updatable});
|
socket.on("getAvailable", async (query: string) => {
|
||||||
} catch (err) {
|
try {
|
||||||
const errc = err as ErrorCaused
|
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
||||||
console.warn(errc.stack || errc.toString());
|
socket.emit("results:available", results);
|
||||||
|
} catch (er) {
|
||||||
|
console.error(er);
|
||||||
|
socket.emit("results:available", {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.emit('results:updatable', {updatable: {}});
|
socket.on("search", async (query: QueryType) => {
|
||||||
}
|
try {
|
||||||
});
|
const results = await search(
|
||||||
|
query.searchTerm,
|
||||||
|
/* maxCacheAge:*/ 60 * 10,
|
||||||
|
);
|
||||||
|
let res = Object.keys(results)
|
||||||
|
.map((pluginName) => results[pluginName])
|
||||||
|
.filter((plugin) => !pluginDefs.plugins[plugin.name]);
|
||||||
|
res = sortPluginList(res, query.sortBy, query.sortDir).slice(
|
||||||
|
query.offset,
|
||||||
|
query.offset + query.limit,
|
||||||
|
);
|
||||||
|
socket.emit("results:search", { results: res, query });
|
||||||
|
} catch (er) {
|
||||||
|
console.error(er);
|
||||||
|
|
||||||
socket.on('getAvailable', async (query:string) => {
|
socket.emit("results:search", { results: {}, query });
|
||||||
try {
|
}
|
||||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
});
|
||||||
socket.emit('results:available', results);
|
|
||||||
} catch (er) {
|
|
||||||
console.error(er);
|
|
||||||
socket.emit('results:available', {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('search', async (query: QueryType) => {
|
socket.on("install", (pluginName: string) => {
|
||||||
try {
|
install(pluginName, (err: ErrorCaused) => {
|
||||||
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
|
if (err) console.warn(err.stack || err.toString());
|
||||||
let res = Object.keys(results)
|
|
||||||
.map((pluginName) => results[pluginName])
|
|
||||||
.filter((plugin) => !pluginDefs.plugins[plugin.name]);
|
|
||||||
res = sortPluginList(res, query.sortBy, query.sortDir)
|
|
||||||
.slice(query.offset, query.offset + query.limit);
|
|
||||||
socket.emit('results:search', {results: res, query});
|
|
||||||
} catch (er) {
|
|
||||||
console.error(er);
|
|
||||||
|
|
||||||
socket.emit('results:search', {results: {}, query});
|
socket.emit("finished:install", {
|
||||||
}
|
plugin: pluginName,
|
||||||
});
|
code: err ? err.code : null,
|
||||||
|
error: err ? err.message : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('install', (pluginName: string) => {
|
socket.on("uninstall", (pluginName: string) => {
|
||||||
install(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:install', {
|
socket.emit("finished:uninstall", {
|
||||||
plugin: pluginName,
|
plugin: pluginName,
|
||||||
code: err ? err.code : null,
|
error: err ? err.message : null,
|
||||||
error: err ? err.message : null,
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return cb();
|
||||||
socket.on('uninstall', (pluginName:string) => {
|
|
||||||
uninstall(pluginName, (err:ErrorCaused) => {
|
|
||||||
if (err) console.warn(err.stack || err.toString());
|
|
||||||
|
|
||||||
socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,17 +120,22 @@ 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 = (
|
||||||
// @ts-ignore
|
plugins: PackageData[],
|
||||||
if (a[property] < b[property]) {
|
property: string,
|
||||||
return dir ? -1 : 1;
|
/* ASC?*/ dir: string,
|
||||||
}
|
): PackageData[] =>
|
||||||
|
plugins.sort((a, b) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (a[property] < b[property]) {
|
||||||
|
return dir ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (a[property] > b[property]) {
|
if (a[property] > b[property]) {
|
||||||
return dir ? 1 : -1;
|
return dir ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// a must be equal to b
|
// a must be equal to b
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,211 +1,226 @@
|
||||||
'use strict';
|
import type {
|
||||||
|
PadQueryResult,
|
||||||
|
PadSearchQuery,
|
||||||
import {PadQueryResult, PadSearchQuery} from "../../types/PadSearchQuery";
|
} from "../../types/PadSearchQuery";
|
||||||
import {PadType} from "../../types/PadType";
|
import { PadType } from "../../types/PadType";
|
||||||
|
|
||||||
const eejs = require('../../eejs');
|
|
||||||
const fsp = require('fs').promises;
|
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
|
||||||
const plugins = require('../../../static/js/pluginfw/plugins');
|
|
||||||
const settings = require('../../utils/Settings');
|
|
||||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
|
||||||
const padManager = require('../../db/PadManager');
|
|
||||||
const api = require('../../db/API');
|
|
||||||
|
|
||||||
|
const eejs = require("../../eejs");
|
||||||
|
const fsp = require("fs").promises;
|
||||||
|
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||||
|
const plugins = require("../../../static/js/pluginfw/plugins");
|
||||||
|
const settings = require("../../utils/Settings");
|
||||||
|
const UpdateCheck = require("../../utils/UpdateCheck");
|
||||||
|
const padManager = require("../../db/PadManager");
|
||||||
|
const api = require("../../db/API");
|
||||||
|
|
||||||
const queryPadLimit = 12;
|
const queryPadLimit = 12;
|
||||||
|
|
||||||
|
exports.socketio = (hookName: string, { io }: any) => {
|
||||||
|
io.of("/settings").on("connection", (socket: any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const {
|
||||||
|
session: {
|
||||||
|
user: { is_admin: isAdmin } = {},
|
||||||
|
} = {},
|
||||||
|
} = socket.conn.request;
|
||||||
|
if (!isAdmin) return;
|
||||||
|
|
||||||
exports.socketio = (hookName:string, {io}:any) => {
|
socket.on("load", async (query: string): Promise<any> => {
|
||||||
io.of('/settings').on('connection', (socket: any ) => {
|
let data;
|
||||||
// @ts-ignore
|
try {
|
||||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
data = await fsp.readFile(settings.settingsFilename, "utf8");
|
||||||
if (!isAdmin) return;
|
} catch (err) {
|
||||||
|
return console.log(err);
|
||||||
|
}
|
||||||
|
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
|
||||||
|
if (settings.showSettingsInAdminPage === false) {
|
||||||
|
socket.emit("settings", { results: "NOT_ALLOWED" });
|
||||||
|
} else {
|
||||||
|
socket.emit("settings", { results: data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('load', async (query:string):Promise<any> => {
|
socket.on("saveSettings", async (newSettings: string) => {
|
||||||
let data;
|
console.log(
|
||||||
try {
|
"Admin request to save settings through a socket on /admin/settings",
|
||||||
data = await fsp.readFile(settings.settingsFilename, 'utf8');
|
);
|
||||||
} catch (err) {
|
await fsp.writeFile(settings.settingsFilename, newSettings);
|
||||||
return console.log(err);
|
socket.emit("saveprogress", "saved");
|
||||||
}
|
});
|
||||||
// if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result
|
|
||||||
if (settings.showSettingsInAdminPage === false) {
|
|
||||||
socket.emit('settings', {results: 'NOT_ALLOWED'});
|
|
||||||
} else {
|
|
||||||
socket.emit('settings', {results: data});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('saveSettings', async (newSettings:string) => {
|
socket.on("help", () => {
|
||||||
console.log('Admin request to save settings through a socket on /admin/settings');
|
const gitCommit = settings.getGitCommit();
|
||||||
await fsp.writeFile(settings.settingsFilename, newSettings);
|
const epVersion = settings.getEpVersion();
|
||||||
socket.emit('saveprogress', 'saved');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const hooks: Map<string, Map<string, string>> = plugins.getHooks(
|
||||||
|
"hooks",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const clientHooks: Map<string, Map<string, string>> = plugins.getHooks(
|
||||||
|
"client_hooks",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
socket.on('help', ()=> {
|
function mapToObject(map: Map<string, any>) {
|
||||||
const gitCommit = settings.getGitCommit();
|
const obj = Object.create(null);
|
||||||
const epVersion = settings.getEpVersion();
|
for (const [k, v] of map) {
|
||||||
|
if (v instanceof Map) {
|
||||||
|
obj[k] = mapToObject(v);
|
||||||
|
} else {
|
||||||
|
obj[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
const hooks:Map<string, Map<string,string>> = plugins.getHooks('hooks', false);
|
socket.emit("reply:help", {
|
||||||
const clientHooks:Map<string, Map<string,string>> = plugins.getHooks('client_hooks', false);
|
gitCommit,
|
||||||
|
epVersion,
|
||||||
|
installedPlugins: plugins.getPlugins(),
|
||||||
|
installedParts: plugins.getParts(),
|
||||||
|
installedServerHooks: mapToObject(hooks),
|
||||||
|
installedClientHooks: mapToObject(clientHooks),
|
||||||
|
latestVersion: UpdateCheck.getLatestVersion(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function mapToObject(map: Map<string,any>) {
|
socket.on("padLoad", async (query: PadSearchQuery) => {
|
||||||
let obj = Object.create(null);
|
const { padIDs } = await padManager.listAllPads();
|
||||||
for (let [k,v] of map) {
|
|
||||||
if(v instanceof Map) {
|
|
||||||
obj[k] = mapToObject(v);
|
|
||||||
} else {
|
|
||||||
obj[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit('reply:help', {
|
const data: {
|
||||||
gitCommit,
|
total: number;
|
||||||
epVersion,
|
results?: PadQueryResult[];
|
||||||
installedPlugins: plugins.getPlugins(),
|
} = {
|
||||||
installedParts: plugins.getParts(),
|
total: padIDs.length,
|
||||||
installedServerHooks: mapToObject(hooks),
|
};
|
||||||
installedClientHooks: mapToObject(clientHooks),
|
let result: string[] = padIDs;
|
||||||
latestVersion: UpdateCheck.getLatestVersion(),
|
let maxResult;
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Filter out matches
|
||||||
|
if (query.pattern) {
|
||||||
|
result = result.filter((padName: string) =>
|
||||||
|
padName.includes(query.pattern),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('padLoad', async (query: PadSearchQuery) => {
|
data.total = result.length;
|
||||||
const {padIDs} = await padManager.listAllPads();
|
|
||||||
|
|
||||||
const data:{
|
maxResult = result.length - 1;
|
||||||
total: number,
|
if (maxResult < 0) {
|
||||||
results?: PadQueryResult[]
|
maxResult = 0;
|
||||||
} = {
|
}
|
||||||
total: padIDs.length,
|
|
||||||
};
|
|
||||||
let result: string[] = padIDs;
|
|
||||||
let maxResult;
|
|
||||||
|
|
||||||
// Filter out matches
|
if (query.offset && query.offset < 0) {
|
||||||
if (query.pattern) {
|
query.offset = 0;
|
||||||
result = result.filter((padName: string) => padName.includes(query.pattern));
|
} else if (query.offset > maxResult) {
|
||||||
}
|
query.offset = maxResult;
|
||||||
|
}
|
||||||
|
|
||||||
data.total = result.length;
|
if (query.limit && query.limit < 0) {
|
||||||
|
query.limit = 0;
|
||||||
|
} else if (query.limit > queryPadLimit) {
|
||||||
|
query.limit = queryPadLimit;
|
||||||
|
}
|
||||||
|
|
||||||
maxResult = result.length - 1;
|
if (query.sortBy === "padName") {
|
||||||
if (maxResult < 0) {
|
result = result
|
||||||
maxResult = 0;
|
.sort((a, b) => {
|
||||||
}
|
if (a < b) return query.ascending ? -1 : 1;
|
||||||
|
if (a > b) return query.ascending ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.slice(query.offset, query.offset + query.limit);
|
||||||
|
|
||||||
if (query.offset && query.offset < 0) {
|
data.results = await Promise.all(
|
||||||
query.offset = 0;
|
result.map(async (padName: string) => {
|
||||||
} else if (query.offset > maxResult) {
|
const pad = await padManager.getPad(padName);
|
||||||
query.offset = maxResult;
|
const revisionNumber = pad.getHeadRevisionNumber();
|
||||||
}
|
const userCount = api.padUsersCount(padName).padUsersCount;
|
||||||
|
const lastEdited = await pad.getLastEdit();
|
||||||
|
|
||||||
if (query.limit && query.limit < 0) {
|
return {
|
||||||
query.limit = 0;
|
padName,
|
||||||
} else if (query.limit > queryPadLimit) {
|
lastEdited,
|
||||||
query.limit = queryPadLimit;
|
userCount,
|
||||||
}
|
revisionNumber,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const currentWinners: PadQueryResult[] = [];
|
||||||
|
let queryOffsetCounter = 0;
|
||||||
|
for (const res of result) {
|
||||||
|
const pad = await padManager.getPad(res);
|
||||||
|
const padType = {
|
||||||
|
padName: res,
|
||||||
|
lastEdited: await pad.getLastEdit(),
|
||||||
|
userCount: api.padUsersCount(res).padUsersCount,
|
||||||
|
revisionNumber: pad.getHeadRevisionNumber(),
|
||||||
|
};
|
||||||
|
|
||||||
if (query.sortBy === 'padName') {
|
if (currentWinners.length < query.limit) {
|
||||||
result = result.sort((a,b)=>{
|
if (queryOffsetCounter < query.offset) {
|
||||||
if(a < b) return query.ascending ? -1 : 1;
|
queryOffsetCounter++;
|
||||||
if(a > b) return query.ascending ? 1 : -1;
|
continue;
|
||||||
return 0;
|
}
|
||||||
}).slice(query.offset, query.offset + query.limit);
|
currentWinners.push({
|
||||||
|
padName: res,
|
||||||
|
lastEdited: await pad.getLastEdit(),
|
||||||
|
userCount: api.padUsersCount(res).padUsersCount,
|
||||||
|
revisionNumber: pad.getHeadRevisionNumber(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Kick out worst pad and replace by current pad
|
||||||
|
const worstPad = currentWinners.sort((a, b) => {
|
||||||
|
if (a[query.sortBy] < b[query.sortBy])
|
||||||
|
return query.ascending ? -1 : 1;
|
||||||
|
if (a[query.sortBy] > b[query.sortBy])
|
||||||
|
return query.ascending ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
worstPad[0] &&
|
||||||
|
worstPad[0][query.sortBy] < padType[query.sortBy]
|
||||||
|
) {
|
||||||
|
if (queryOffsetCounter < query.offset) {
|
||||||
|
queryOffsetCounter++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1);
|
||||||
|
currentWinners.push({
|
||||||
|
padName: res,
|
||||||
|
lastEdited: await pad.getLastEdit(),
|
||||||
|
userCount: api.padUsersCount(res).padUsersCount,
|
||||||
|
revisionNumber: pad.getHeadRevisionNumber(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.results = currentWinners;
|
||||||
|
}
|
||||||
|
|
||||||
data.results = await Promise.all(result.map(async (padName: string) => {
|
socket.emit("results:padLoad", data);
|
||||||
const pad = await padManager.getPad(padName);
|
});
|
||||||
const revisionNumber = pad.getHeadRevisionNumber()
|
|
||||||
const userCount = api.padUsersCount(padName).padUsersCount;
|
|
||||||
const lastEdited = await pad.getLastEdit();
|
|
||||||
|
|
||||||
return {
|
socket.on("deletePad", async (padId: string) => {
|
||||||
padName,
|
const padExists = await padManager.doesPadExists(padId);
|
||||||
lastEdited,
|
if (padExists) {
|
||||||
userCount,
|
const pad = await padManager.getPad(padId);
|
||||||
revisionNumber
|
await pad.remove();
|
||||||
}}));
|
socket.emit("results:deletePad", padId);
|
||||||
} else {
|
}
|
||||||
const currentWinners: PadQueryResult[] = []
|
});
|
||||||
let queryOffsetCounter = 0
|
|
||||||
for (let res of result) {
|
|
||||||
|
|
||||||
const pad = await padManager.getPad(res);
|
socket.on("restartServer", async () => {
|
||||||
const padType = {
|
console.log(
|
||||||
padName: res,
|
"Admin request to restart server through a socket on /admin/settings",
|
||||||
lastEdited: await pad.getLastEdit(),
|
);
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
settings.reloadSettings();
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
await plugins.update();
|
||||||
};
|
await hooks.aCallAll("loadSettings", { settings });
|
||||||
|
await hooks.aCallAll("restartServer");
|
||||||
if (currentWinners.length < query.limit) {
|
});
|
||||||
if(queryOffsetCounter < query.offset){
|
});
|
||||||
queryOffsetCounter++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
currentWinners.push({
|
|
||||||
padName: res,
|
|
||||||
lastEdited: await pad.getLastEdit(),
|
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Kick out worst pad and replace by current pad
|
|
||||||
let worstPad = currentWinners.sort((a, b) => {
|
|
||||||
if (a[query.sortBy] < b[query.sortBy]) return query.ascending ? -1 : 1;
|
|
||||||
if (a[query.sortBy] > b[query.sortBy]) return query.ascending ? 1 : -1;
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
if(worstPad[0]&&worstPad[0][query.sortBy] < padType[query.sortBy]){
|
|
||||||
if(queryOffsetCounter < query.offset){
|
|
||||||
queryOffsetCounter++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
currentWinners.splice(currentWinners.indexOf(worstPad[0]), 1)
|
|
||||||
currentWinners.push({
|
|
||||||
padName: res,
|
|
||||||
lastEdited: await pad.getLastEdit(),
|
|
||||||
userCount: api.padUsersCount(res).padUsersCount,
|
|
||||||
revisionNumber: pad.getHeadRevisionNumber()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.results = currentWinners;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit('results:padLoad', data);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
socket.on('deletePad', async (padId: string) => {
|
|
||||||
const padExists = await padManager.doesPadExists(padId);
|
|
||||||
if (padExists) {
|
|
||||||
const pad = await padManager.getPad(padId);
|
|
||||||
await pad.remove();
|
|
||||||
socket.emit('results:deletePad', padId);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('restartServer', async () => {
|
|
||||||
console.log('Admin request to restart server through a socket on /admin/settings');
|
|
||||||
settings.reloadSettings();
|
|
||||||
await plugins.update();
|
|
||||||
await hooks.aCallAll('loadSettings', {settings});
|
|
||||||
await hooks.aCallAll('restartServer');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchPad = async (query: PadSearchQuery) => {};
|
||||||
|
|
||||||
const searchPad = async (query:PadSearchQuery) => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,44 +1,45 @@
|
||||||
'use strict';
|
const log4js = require("log4js");
|
||||||
|
const clientLogger = log4js.getLogger("client");
|
||||||
|
const { Formidable } = require("formidable");
|
||||||
|
const apiHandler = require("../../handler/APIHandler");
|
||||||
|
const util = require("util");
|
||||||
|
|
||||||
const log4js = require('log4js');
|
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||||
const clientLogger = log4js.getLogger('client');
|
// The Etherpad client side sends information about how a disconnect happened
|
||||||
const {Formidable} = require('formidable');
|
app.post("/ep/pad/connection-diagnostic-info", async (req: any, res: any) => {
|
||||||
const apiHandler = require('../../handler/APIHandler');
|
const [fields, files] = await new Formidable({}).parse(req);
|
||||||
const util = require('util');
|
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
||||||
|
res.end("OK");
|
||||||
|
});
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
const parseJserrorForm = async (req: any) => {
|
||||||
// The Etherpad client side sends information about how a disconnect happened
|
const form = new Formidable({
|
||||||
app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => {
|
maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used.
|
||||||
const [fields, files] = await (new Formidable({})).parse(req);
|
});
|
||||||
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
const [fields, files] = await form.parse(req);
|
||||||
res.end('OK');
|
return fields.errorInfo;
|
||||||
});
|
};
|
||||||
|
|
||||||
const parseJserrorForm = async (req:any) => {
|
// The Etherpad client side sends information about client side javscript errors
|
||||||
const form = new Formidable({
|
app.post("/jserror", (req: any, res: any, next: Function) => {
|
||||||
maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used.
|
(async () => {
|
||||||
});
|
const data = JSON.parse(await parseJserrorForm(req));
|
||||||
const [fields, files] = await form.parse(req);
|
clientLogger.warn(`${data.msg} --`, {
|
||||||
return fields.errorInfo;
|
[util.inspect.custom]: (depth: number, options: any) => {
|
||||||
};
|
// Depth is forced to infinity to ensure that all of the provided data is logged.
|
||||||
|
options = Object.assign({}, options, {
|
||||||
|
depth: Number.POSITIVE_INFINITY,
|
||||||
|
colors: true,
|
||||||
|
});
|
||||||
|
return util.inspect(data, options);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.end("OK");
|
||||||
|
})().catch((err) => next(err || new Error(err)));
|
||||||
|
});
|
||||||
|
|
||||||
// The Etherpad client side sends information about client side javscript errors
|
// Provide a possibility to query the latest available API version
|
||||||
app.post('/jserror', (req:any, res:any, next:Function) => {
|
app.get("/api", (req: any, res: any) => {
|
||||||
(async () => {
|
res.json({ currentVersion: apiHandler.latestApiVersion });
|
||||||
const data = JSON.parse(await parseJserrorForm(req));
|
});
|
||||||
clientLogger.warn(`${data.msg} --`, {
|
|
||||||
[util.inspect.custom]: (depth: number, options:any) => {
|
|
||||||
// Depth is forced to infinity to ensure that all of the provided data is logged.
|
|
||||||
options = Object.assign({}, options, {depth: Infinity, colors: true});
|
|
||||||
return util.inspect(data, options);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.end('OK');
|
|
||||||
})().catch((err) => next(err || new Error(err)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Provide a possibility to query the latest available API version
|
|
||||||
app.get('/api', (req:any, res:any) => {
|
|
||||||
res.json({currentVersion: apiHandler.latestApiVersion});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
'use strict';
|
import type { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
import type { ErrorCaused } from "../../types/ErrorCaused";
|
||||||
|
|
||||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
const stats = require("../../stats");
|
||||||
import {ErrorCaused} from "../../types/ErrorCaused";
|
|
||||||
|
|
||||||
const stats = require('../../stats')
|
exports.expressCreateServer = (
|
||||||
|
hook_name: string,
|
||||||
|
args: ArgsExpressType,
|
||||||
|
cb: Function,
|
||||||
|
) => {
|
||||||
|
exports.app = args.app;
|
||||||
|
|
||||||
exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => {
|
// Handle errors
|
||||||
exports.app = args.app;
|
args.app.use((err: ErrorCaused, req: any, res: any, next: Function) => {
|
||||||
|
// if an error occurs Connect will pass it down
|
||||||
|
// through these "error-handling" middleware
|
||||||
|
// allowing you to respond however you like
|
||||||
|
res.status(500).send({ error: "Sorry, something bad happened!" });
|
||||||
|
console.error(err.stack ? err.stack : err.toString());
|
||||||
|
stats.meter("http500").mark();
|
||||||
|
});
|
||||||
|
|
||||||
// Handle errors
|
return cb();
|
||||||
args.app.use((err:ErrorCaused, req:any, res:any, next:Function) => {
|
|
||||||
// if an error occurs Connect will pass it down
|
|
||||||
// through these "error-handling" middleware
|
|
||||||
// allowing you to respond however you like
|
|
||||||
res.status(500).send({error: 'Sorry, something bad happened!'});
|
|
||||||
console.error(err.stack ? err.stack : err.toString());
|
|
||||||
stats.meter('http500').mark();
|
|
||||||
});
|
|
||||||
|
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,89 +1,122 @@
|
||||||
'use strict';
|
import type { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
|
||||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
const hasPadAccess = require("../../padaccess");
|
||||||
|
const settings = require("../../utils/Settings");
|
||||||
|
const exportHandler = require("../../handler/ExportHandler");
|
||||||
|
const importHandler = require("../../handler/ImportHandler");
|
||||||
|
const padManager = require("../../db/PadManager");
|
||||||
|
const readOnlyManager = require("../../db/ReadOnlyManager");
|
||||||
|
const rateLimit = require("express-rate-limit");
|
||||||
|
const securityManager = require("../../db/SecurityManager");
|
||||||
|
const webaccess = require("./webaccess");
|
||||||
|
|
||||||
const hasPadAccess = require('../../padaccess');
|
exports.expressCreateServer = (
|
||||||
const settings = require('../../utils/Settings');
|
hookName: string,
|
||||||
const exportHandler = require('../../handler/ExportHandler');
|
args: ArgsExpressType,
|
||||||
const importHandler = require('../../handler/ImportHandler');
|
cb: Function,
|
||||||
const padManager = require('../../db/PadManager');
|
) => {
|
||||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
const limiter = rateLimit({
|
||||||
const rateLimit = require('express-rate-limit');
|
...settings.importExportRateLimiting,
|
||||||
const securityManager = require('../../db/SecurityManager');
|
handler: (request: any) => {
|
||||||
const webaccess = require('./webaccess');
|
if (request.rateLimit.current === request.rateLimit.limit + 1) {
|
||||||
|
// when the rate limiter triggers, write a warning in the logs
|
||||||
|
console.warn(
|
||||||
|
"Import/Export rate limiter triggered on " +
|
||||||
|
`"${request.originalUrl}" for IP address ${request.ip}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
// handle export requests
|
||||||
const limiter = rateLimit({
|
args.app.use("/p/:pad/:rev?/export/:type", limiter);
|
||||||
...settings.importExportRateLimiting,
|
args.app.get(
|
||||||
handler: (request:any) => {
|
"/p/:pad/:rev?/export/:type",
|
||||||
if (request.rateLimit.current === request.rateLimit.limit + 1) {
|
(req: any, res: any, next: Function) => {
|
||||||
// when the rate limiter triggers, write a warning in the logs
|
(async () => {
|
||||||
console.warn('Import/Export rate limiter triggered on ' +
|
const types = ["pdf", "doc", "txt", "html", "odt", "etherpad"];
|
||||||
`"${request.originalUrl}" for IP address ${request.ip}`);
|
// send a 404 if we don't support this filetype
|
||||||
}
|
if (types.indexOf(req.params.type) === -1) {
|
||||||
},
|
return next();
|
||||||
});
|
}
|
||||||
|
|
||||||
// handle export requests
|
// if abiword is disabled, and this is a format we only support with abiword, output a message
|
||||||
args.app.use('/p/:pad/:rev?/export/:type', limiter);
|
if (
|
||||||
args.app.get('/p/:pad/:rev?/export/:type', (req:any, res:any, next:Function) => {
|
settings.exportAvailable() === "no" &&
|
||||||
(async () => {
|
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1
|
||||||
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
|
) {
|
||||||
// send a 404 if we don't support this filetype
|
console.error(
|
||||||
if (types.indexOf(req.params.type) === -1) {
|
`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
|
||||||
return next();
|
" There is no converter configured",
|
||||||
}
|
);
|
||||||
|
|
||||||
// if abiword is disabled, and this is a format we only support with abiword, output a message
|
// ACHTUNG: do not include req.params.type in res.send() because there is
|
||||||
if (settings.exportAvailable() === 'no' &&
|
// no HTML escaping and it would lead to an XSS
|
||||||
['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) {
|
res.send(
|
||||||
console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` +
|
"This export is not enabled at this Etherpad instance. Set the path to Abiword" +
|
||||||
' There is no converter configured');
|
" or soffice (LibreOffice) in settings.json to enable this feature",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ACHTUNG: do not include req.params.type in res.send() because there is
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
// no HTML escaping and it would lead to an XSS
|
|
||||||
res.send('This export is not enabled at this Etherpad instance. Set the path to Abiword' +
|
|
||||||
' or soffice (LibreOffice) in settings.json to enable this feature');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
if (await hasPadAccess(req, res)) {
|
||||||
|
let padId = req.params.pad;
|
||||||
|
|
||||||
if (await hasPadAccess(req, res)) {
|
let readOnlyId = null;
|
||||||
let padId = req.params.pad;
|
if (readOnlyManager.isReadOnlyId(padId)) {
|
||||||
|
readOnlyId = padId;
|
||||||
|
padId = await readOnlyManager.getPadId(readOnlyId);
|
||||||
|
}
|
||||||
|
|
||||||
let readOnlyId = null;
|
const exists = await padManager.doesPadExists(padId);
|
||||||
if (readOnlyManager.isReadOnlyId(padId)) {
|
if (!exists) {
|
||||||
readOnlyId = padId;
|
console.warn(
|
||||||
padId = await readOnlyManager.getPadId(readOnlyId);
|
`Someone tried to export a pad that doesn't exist (${padId})`,
|
||||||
}
|
);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
const exists = await padManager.doesPadExists(padId);
|
console.log(
|
||||||
if (!exists) {
|
`Exporting pad "${req.params.pad}" in ${req.params.type} format`,
|
||||||
console.warn(`Someone tried to export a pad that doesn't exist (${padId})`);
|
);
|
||||||
return next();
|
await exportHandler.doExport(
|
||||||
}
|
req,
|
||||||
|
res,
|
||||||
|
padId,
|
||||||
|
readOnlyId,
|
||||||
|
req.params.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})().catch((err) => next(err || new Error(err)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
|
// handle import requests
|
||||||
await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
|
args.app.use("/p/:pad/import", limiter);
|
||||||
}
|
args.app.post("/p/:pad/import", (req: any, res: any, next: Function) => {
|
||||||
})().catch((err) => next(err || new Error(err)));
|
(async () => {
|
||||||
});
|
// @ts-ignore
|
||||||
|
const {
|
||||||
|
session: { user } = {},
|
||||||
|
} = req;
|
||||||
|
const { accessStatus, authorID: authorId } =
|
||||||
|
await securityManager.checkAccess(
|
||||||
|
req.params.pad,
|
||||||
|
req.cookies.sessionID,
|
||||||
|
req.cookies.token,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
accessStatus !== "grant" ||
|
||||||
|
!webaccess.userCanModify(req.params.pad, req)
|
||||||
|
) {
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
}
|
||||||
|
await importHandler.doImport(req, res, req.params.pad, authorId);
|
||||||
|
})().catch((err) => next(err || new Error(err)));
|
||||||
|
});
|
||||||
|
|
||||||
// handle import requests
|
return cb();
|
||||||
args.app.use('/p/:pad/import', limiter);
|
|
||||||
args.app.post('/p/:pad/import', (req:any, res:any, next:Function) => {
|
|
||||||
(async () => {
|
|
||||||
// @ts-ignore
|
|
||||||
const {session: {user} = {}} = req;
|
|
||||||
const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
|
|
||||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
|
||||||
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
|
|
||||||
return res.status(403).send('Forbidden');
|
|
||||||
}
|
|
||||||
await importHandler.doImport(req, res, req.params.pad, authorId);
|
|
||||||
})().catch((err) => next(err || new Error(err)));
|
|
||||||
});
|
|
||||||
|
|
||||||
return cb();
|
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,32 +1,39 @@
|
||||||
'use strict';
|
import type { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
|
||||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
const padManager = require("../../db/PadManager");
|
||||||
|
|
||||||
const padManager = require('../../db/PadManager');
|
exports.expressCreateServer = (
|
||||||
|
hookName: string,
|
||||||
|
args: ArgsExpressType,
|
||||||
|
cb: Function,
|
||||||
|
) => {
|
||||||
|
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
|
||||||
|
args.app.param("pad", (req: any, res: any, next: Function, padId: string) => {
|
||||||
|
(async () => {
|
||||||
|
// ensure the padname is valid and the url doesn't end with a /
|
||||||
|
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
|
||||||
|
res.status(404).send("Such a padname is forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
const sanitizedPadId = await padManager.sanitizePadId(padId);
|
||||||
// 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) => {
|
|
||||||
(async () => {
|
|
||||||
// ensure the padname is valid and the url doesn't end with a /
|
|
||||||
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
|
|
||||||
res.status(404).send('Such a padname is forbidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitizedPadId = await padManager.sanitizePadId(padId);
|
if (sanitizedPadId === padId) {
|
||||||
|
// the pad id was fine, so just render it
|
||||||
if (sanitizedPadId === padId) {
|
next();
|
||||||
// the pad id was fine, so just render it
|
} else {
|
||||||
next();
|
// the pad id was sanitized, so we redirect to the sanitized version
|
||||||
} else {
|
const realURL =
|
||||||
// the pad id was sanitized, so we redirect to the sanitized version
|
encodeURIComponent(sanitizedPadId) +
|
||||||
const realURL =
|
new URL(req.url, "http://invalid.invalid").search;
|
||||||
encodeURIComponent(sanitizedPadId) + new URL(req.url, 'http://invalid.invalid').search;
|
res.header("Location", realURL);
|
||||||
res.header('Location', realURL);
|
res
|
||||||
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
|
.status(302)
|
||||||
}
|
.send(
|
||||||
})().catch((err) => next(err || new Error(err)));
|
`You should be redirected to <a href="${realURL}">${realURL}</a>`,
|
||||||
});
|
);
|
||||||
return cb();
|
}
|
||||||
|
})().catch((err) => next(err || new Error(err)));
|
||||||
|
});
|
||||||
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,142 +1,146 @@
|
||||||
'use strict';
|
import type { ArgsExpressType } from "../../types/ArgsExpressType";
|
||||||
|
|
||||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
import events from "events";
|
||||||
|
const express = require("../express");
|
||||||
|
import log4js from "log4js";
|
||||||
|
const proxyaddr = require("proxy-addr");
|
||||||
|
const settings = require("../../utils/Settings");
|
||||||
|
import { Server, type Socket } from "socket.io";
|
||||||
|
const socketIORouter = require("../../handler/SocketIORouter");
|
||||||
|
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||||
|
const padMessageHandler = require("../../handler/PadMessageHandler");
|
||||||
|
|
||||||
import events from 'events';
|
let io: any;
|
||||||
const express = require('../express');
|
const logger = log4js.getLogger("socket.io");
|
||||||
import log4js from 'log4js';
|
|
||||||
const proxyaddr = require('proxy-addr');
|
|
||||||
const settings = require('../../utils/Settings');
|
|
||||||
import {Server, Socket} from 'socket.io'
|
|
||||||
const socketIORouter = require('../../handler/SocketIORouter');
|
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
|
||||||
const padMessageHandler = require('../../handler/PadMessageHandler');
|
|
||||||
|
|
||||||
let io:any;
|
|
||||||
const logger = log4js.getLogger('socket.io');
|
|
||||||
const 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
|
||||||
// objects is undocumented, but I don't see any other way to shut down socket.io without also
|
// objects is undocumented, but I don't see any other way to shut down socket.io without also
|
||||||
// closing the HTTP server.
|
// closing the HTTP server.
|
||||||
io.engine.close();
|
io.engine.close();
|
||||||
// Closing the socket.io engine should disconnect all clients but it is not documented. Wait for
|
// Closing the socket.io engine should disconnect all clients but it is not documented. Wait for
|
||||||
// all of the connections to close to make sure, and log the progress so that we can troubleshoot
|
// all of the connections to close to make sure, and log the progress so that we can troubleshoot
|
||||||
// if socket.io's behavior ever changes.
|
// if socket.io's behavior ever changes.
|
||||||
//
|
//
|
||||||
// Note: `io.sockets.clients()` should not be used here to track the remaining clients.
|
// Note: `io.sockets.clients()` should not be used here to track the remaining clients.
|
||||||
// `io.sockets.clients()` works with socket.io 2.x, but not with 3.x: With socket.io 2.x all
|
// `io.sockets.clients()` works with socket.io 2.x, but not with 3.x: With socket.io 2.x all
|
||||||
// clients are always added to the default namespace (`io.sockets`) even if they specified a
|
// clients are always added to the default namespace (`io.sockets`) even if they specified a
|
||||||
// different namespace upon connection, but with socket.io 3.x clients are NOT added to the
|
// different namespace upon connection, but with socket.io 3.x clients are NOT added to the
|
||||||
// default namespace if they have specified a different namespace. With socket.io 3.x there does
|
// default namespace if they have specified a different namespace. With socket.io 3.x there does
|
||||||
// not appear to be a way to get all clients across all namespaces without tracking them
|
// not appear to be a way to get all clients across all namespaces without tracking them
|
||||||
// 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.
|
||||||
lastLogged = Date.now();
|
logger.info(
|
||||||
}
|
`Waiting for ${sockets.size} socket.io clients to disconnect...`,
|
||||||
await events.once(socketsEvents, 'updated');
|
);
|
||||||
}
|
lastLogged = Date.now();
|
||||||
logger.info('All socket.io clients have disconnected');
|
}
|
||||||
|
await events.once(socketsEvents, "updated");
|
||||||
|
}
|
||||||
|
logger.info("All socket.io clients have disconnected");
|
||||||
};
|
};
|
||||||
|
|
||||||
const socketSessionMiddleware = (args: any) => (socket: any, next: Function) => {
|
const socketSessionMiddleware =
|
||||||
const req = socket.request;
|
(args: any) => (socket: any, next: Function) => {
|
||||||
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
|
const req = socket.request;
|
||||||
if (req.ip == null) {
|
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
|
||||||
if (settings.trustProxy) {
|
if (req.ip == null) {
|
||||||
req.ip = proxyaddr(req, args.app.get('trust proxy fn'));
|
if (settings.trustProxy) {
|
||||||
} else {
|
req.ip = proxyaddr(req, args.app.get("trust proxy fn"));
|
||||||
req.ip = socket.handshake.address;
|
} else {
|
||||||
}
|
req.ip = socket.handshake.address;
|
||||||
}
|
}
|
||||||
if (!req.headers.cookie) {
|
}
|
||||||
// socketio.js-client on node.js doesn't support cookies, so pass them via a query parameter.
|
if (!req.headers.cookie) {
|
||||||
req.headers.cookie = socket.handshake.query.cookie;
|
// socketio.js-client on node.js doesn't support cookies, so pass them via a query parameter.
|
||||||
}
|
req.headers.cookie = socket.handshake.query.cookie;
|
||||||
express.sessionMiddleware(req, {}, next);
|
}
|
||||||
};
|
express.sessionMiddleware(req, {}, next);
|
||||||
|
};
|
||||||
export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|
||||||
// init socket.io and redirect all requests to the MessageHandler
|
export const expressCreateServer = (
|
||||||
// there shouldn't be a browser that isn't compatible to all
|
hookName: string,
|
||||||
// transports in this list at once
|
args: ArgsExpressType,
|
||||||
// e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling
|
cb: Function,
|
||||||
io = new Server(args.server,{
|
) => {
|
||||||
transports: settings.socketTransportProtocols,
|
// init socket.io and redirect all requests to the MessageHandler
|
||||||
cookie: false,
|
// there shouldn't be a browser that isn't compatible to all
|
||||||
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
// transports in this list at once
|
||||||
})
|
// e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling
|
||||||
|
io = new Server(args.server, {
|
||||||
|
transports: settings.socketTransportProtocols,
|
||||||
const handleConnection = (socket:Socket) => {
|
cookie: false,
|
||||||
sockets.add(socket);
|
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
||||||
socketsEvents.emit('updated');
|
});
|
||||||
// https://socket.io/docs/v3/faq/index.html
|
|
||||||
// @ts-ignore
|
const handleConnection = (socket: Socket) => {
|
||||||
const session = socket.request.session;
|
sockets.add(socket);
|
||||||
session.connections++;
|
socketsEvents.emit("updated");
|
||||||
session.save();
|
// https://socket.io/docs/v3/faq/index.html
|
||||||
socket.on('disconnect', () => {
|
// @ts-ignore
|
||||||
sockets.delete(socket);
|
const session = socket.request.session;
|
||||||
socketsEvents.emit('updated');
|
session.connections++;
|
||||||
});
|
session.save();
|
||||||
}
|
socket.on("disconnect", () => {
|
||||||
|
sockets.delete(socket);
|
||||||
const renewSession = (socket:any, next:Function) => {
|
socketsEvents.emit("updated");
|
||||||
socket.conn.on('packet', (packet:string) => {
|
});
|
||||||
// Tell express-session that the session is still active. The session store can use these
|
};
|
||||||
// touch events to defer automatic session cleanup, and if express-session is configured with
|
|
||||||
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not
|
const renewSession = (socket: any, next: Function) => {
|
||||||
// have a standard mechanism for periodically updating the browser's cookies, so the browser
|
socket.conn.on("packet", (packet: string) => {
|
||||||
// will not see the new cookie expiration time unless it makes a new HTTP request or the new
|
// Tell express-session that the session is still active. The session store can use these
|
||||||
// cookie value is sent to the client in a custom socket.io message.)
|
// touch events to defer automatic session cleanup, and if express-session is configured with
|
||||||
if (socket.request.session != null) socket.request.session.touch();
|
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not
|
||||||
});
|
// have a standard mechanism for periodically updating the browser's cookies, so the browser
|
||||||
next();
|
// will not see the new cookie expiration time unless it makes a new HTTP request or the new
|
||||||
}
|
// cookie value is sent to the client in a custom socket.io message.)
|
||||||
|
if (socket.request.session != null) socket.request.session.touch();
|
||||||
|
});
|
||||||
io.on('connection', handleConnection);
|
next();
|
||||||
|
};
|
||||||
io.use(socketSessionMiddleware(args));
|
|
||||||
|
io.on("connection", handleConnection);
|
||||||
// Temporary workaround so all clients go through middleware and handle connection
|
|
||||||
io.of('/pluginfw/installer')
|
io.use(socketSessionMiddleware(args));
|
||||||
.on('connection',handleConnection)
|
|
||||||
.use(socketSessionMiddleware(args))
|
// Temporary workaround so all clients go through middleware and handle connection
|
||||||
.use(renewSession)
|
io.of("/pluginfw/installer")
|
||||||
io.of('/settings')
|
.on("connection", handleConnection)
|
||||||
.on('connection',handleConnection)
|
.use(socketSessionMiddleware(args))
|
||||||
.use(socketSessionMiddleware(args))
|
.use(renewSession);
|
||||||
.use(renewSession)
|
io.of("/settings")
|
||||||
|
.on("connection", handleConnection)
|
||||||
io.use(renewSession);
|
.use(socketSessionMiddleware(args))
|
||||||
|
.use(renewSession);
|
||||||
// var socketIOLogger = log4js.getLogger("socket.io");
|
|
||||||
// Debug logging now has to be set at an environment level, this is stupid.
|
io.use(renewSession);
|
||||||
// https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0
|
|
||||||
// This debug logging environment is set in Settings.js
|
// var socketIOLogger = log4js.getLogger("socket.io");
|
||||||
|
// Debug logging now has to be set at an environment level, this is stupid.
|
||||||
// minify socket.io javascript
|
// https://github.com/Automattic/socket.io/wiki/Migrating-to-1.0
|
||||||
// Due to a shitty decision by the SocketIO team minification is
|
// This debug logging environment is set in Settings.js
|
||||||
// no longer available, details available at:
|
|
||||||
// http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0
|
// minify socket.io javascript
|
||||||
// if(settings.minify) io.enable('browser client minification');
|
// Due to a shitty decision by the SocketIO team minification is
|
||||||
|
// no longer available, details available at:
|
||||||
// Initialize the Socket.IO Router
|
// http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0
|
||||||
socketIORouter.setSocketIO(io);
|
// if(settings.minify) io.enable('browser client minification');
|
||||||
socketIORouter.addComponent('pad', padMessageHandler);
|
|
||||||
|
// Initialize the Socket.IO Router
|
||||||
hooks.callAll('socketio', {app: args.app, io, server: args.server});
|
socketIORouter.setSocketIO(io);
|
||||||
|
socketIORouter.addComponent("pad", padMessageHandler);
|
||||||
return cb();
|
|
||||||
|
hooks.callAll("socketio", { app: args.app, io, server: args.server });
|
||||||
|
|
||||||
|
return cb();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,121 +1,139 @@
|
||||||
'use strict';
|
const path = require("path");
|
||||||
|
const eejs = require("../../eejs");
|
||||||
const path = require('path');
|
const fs = require("fs");
|
||||||
const eejs = require('../../eejs');
|
|
||||||
const fs = require('fs');
|
|
||||||
const fsp = fs.promises;
|
const fsp = fs.promises;
|
||||||
const toolbar = require('../../utils/toolbar');
|
const toolbar = require("../../utils/toolbar");
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
const hooks = require("../../../static/js/pluginfw/hooks");
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require("../../utils/Settings");
|
||||||
const util = require('util');
|
const util = require("util");
|
||||||
const webaccess = require('./webaccess');
|
const webaccess = require("./webaccess");
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||||
// This endpoint is intended to conform to:
|
// This endpoint is intended to conform to:
|
||||||
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
||||||
app.get('/health', (req:any, res:any) => {
|
app.get("/health", (req: any, res: any) => {
|
||||||
res.set('Content-Type', 'application/health+json');
|
res.set("Content-Type", "application/health+json");
|
||||||
res.json({
|
res.json({
|
||||||
status: 'pass',
|
status: "pass",
|
||||||
releaseId: settings.getEpVersion(),
|
releaseId: settings.getEpVersion(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/stats', (req:any, res:any) => {
|
app.get("/stats", (req: any, res: any) => {
|
||||||
res.json(require('../../stats').toJSON());
|
res.json(require("../../stats").toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/javascript', (req:any, res:any) => {
|
app.get("/javascript", (req: any, res: any) => {
|
||||||
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
|
res.send(
|
||||||
});
|
eejs.require("ep_etherpad-lite/templates/javascript.html", { req }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/robots.txt', (req:any, res:any) => {
|
app.get("/robots.txt", (req: any, res: any) => {
|
||||||
let filePath =
|
let filePath = path.join(
|
||||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
|
settings.root,
|
||||||
res.sendFile(filePath, (err:any) => {
|
"src",
|
||||||
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
"static",
|
||||||
if (err) {
|
"skins",
|
||||||
filePath = path.join(settings.root, 'src', 'static', 'robots.txt');
|
settings.skinName,
|
||||||
res.sendFile(filePath);
|
"robots.txt",
|
||||||
}
|
);
|
||||||
});
|
res.sendFile(filePath, (err: any) => {
|
||||||
});
|
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
||||||
|
if (err) {
|
||||||
|
filePath = path.join(settings.root, "src", "static", "robots.txt");
|
||||||
|
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
|
||||||
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
|
? [path.resolve(settings.root, settings.favicon)]
|
||||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
|
: []),
|
||||||
path.join(settings.root, 'src', 'static', 'favicon.ico'),
|
path.join(
|
||||||
];
|
settings.root,
|
||||||
for (const fn of fns) {
|
"src",
|
||||||
try {
|
"static",
|
||||||
await fsp.access(fn, fs.constants.R_OK);
|
"skins",
|
||||||
} catch (err) {
|
settings.skinName,
|
||||||
continue;
|
"favicon.ico",
|
||||||
}
|
),
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
path.join(settings.root, "src", "static", "favicon.ico"),
|
||||||
await util.promisify(res.sendFile.bind(res))(fn);
|
];
|
||||||
return;
|
for (const fn of fns) {
|
||||||
}
|
try {
|
||||||
next();
|
await fsp.access(fn, fs.constants.R_OK);
|
||||||
})().catch((err) => next(err || new Error(err)));
|
} catch (err) {
|
||||||
});
|
continue;
|
||||||
|
}
|
||||||
|
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||||
|
await util.promisify(res.sendFile.bind(res))(fn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
})().catch((err) => next(err || new Error(err)));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
||||||
req,
|
eejs.require("ep_etherpad-lite/templates/pad.html", {
|
||||||
toolbar,
|
req,
|
||||||
isReadOnly,
|
toolbar,
|
||||||
}));
|
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(
|
||||||
req,
|
eejs.require("ep_etherpad-lite/templates/timeslider.html", {
|
||||||
toolbar,
|
req,
|
||||||
}));
|
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,81 +1,94 @@
|
||||||
'use strict';
|
import type { MapArrayType } from "../../types/MapType";
|
||||||
|
import type { PartType } from "../../types/PartType";
|
||||||
|
|
||||||
import {MapArrayType} from "../../types/MapType";
|
const fs = require("fs").promises;
|
||||||
import {PartType} from "../../types/PartType";
|
const minify = require("../../utils/Minify");
|
||||||
|
const path = require("path");
|
||||||
const fs = require('fs').promises;
|
const plugins = require("../../../static/js/pluginfw/plugin_defs");
|
||||||
const minify = require('../../utils/Minify');
|
const settings = require("../../utils/Settings");
|
||||||
const path = require('path');
|
import CachingMiddleware from "../../utils/caching_middleware";
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
const Yajsml = require("etherpad-yajsml");
|
||||||
const settings = require('../../utils/Settings');
|
|
||||||
import CachingMiddleware from '../../utils/caching_middleware';
|
|
||||||
const Yajsml = require('etherpad-yajsml');
|
|
||||||
|
|
||||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
||||||
const getTar = async () => {
|
const getTar = async () => {
|
||||||
const prefixLocalLibraryPath = (path:string) => {
|
const prefixLocalLibraryPath = (path: string) => {
|
||||||
if (path.charAt(0) === '$') {
|
if (path.charAt(0) === "$") {
|
||||||
return path.slice(1);
|
return path.slice(1);
|
||||||
} else {
|
} else {
|
||||||
return `ep_etherpad-lite/static/js/${path}`;
|
return `ep_etherpad-lite/static/js/${path}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
|
const tarJson = await fs.readFile(
|
||||||
const tar:MapArrayType<string[]> = {};
|
path.join(settings.root, "src/node/utils/tar.json"),
|
||||||
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [string, string[]][]) {
|
"utf8",
|
||||||
const files = relativeFiles.map(prefixLocalLibraryPath);
|
);
|
||||||
tar[prefixLocalLibraryPath(key)] = files
|
const tar: MapArrayType<string[]> = {};
|
||||||
.concat(files.map((p) => p.replace(/\.js$/, '')))
|
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [
|
||||||
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
|
string,
|
||||||
}
|
string[],
|
||||||
return tar;
|
][]) {
|
||||||
|
const files = relativeFiles.map(prefixLocalLibraryPath);
|
||||||
|
tar[prefixLocalLibraryPath(key)] = files
|
||||||
|
.concat(files.map((p) => p.replace(/\.js$/, "")))
|
||||||
|
.concat(files.map((p) => `${p.replace(/\.js$/, "")}/index.js`));
|
||||||
|
}
|
||||||
|
return tar;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
exports.expressPreSession = async (hookName: string, { app }: any) => {
|
||||||
// Cache both minified and static.
|
// Cache both minified and static.
|
||||||
const assetCache = new CachingMiddleware();
|
const assetCache = new CachingMiddleware();
|
||||||
// Cache static assets
|
// Cache static assets
|
||||||
app.all(/\/js\/(.*)/, assetCache.handle.bind(assetCache));
|
app.all(/\/js\/(.*)/, assetCache.handle.bind(assetCache));
|
||||||
app.all(/\/css\/(.*)/, assetCache.handle.bind(assetCache));
|
app.all(/\/css\/(.*)/, assetCache.handle.bind(assetCache));
|
||||||
|
|
||||||
// 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(
|
||||||
const associator = new StaticAssociator(associations);
|
await getTar(),
|
||||||
jsServer.setAssociator(associator);
|
);
|
||||||
|
const associator = new StaticAssociator(associations);
|
||||||
|
jsServer.setAssociator(associator);
|
||||||
|
|
||||||
app.use(jsServer.handle.bind(jsServer));
|
app.use(jsServer.handle.bind(jsServer));
|
||||||
|
|
||||||
// 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",
|
||||||
const clientPlugins:MapArrayType<string> = {};
|
(req: any, res: any, next: Function) => {
|
||||||
for (const name of new Set(clientParts.map((part: PartType) => part.plugin))) {
|
const clientParts = plugins.parts.filter(
|
||||||
// @ts-ignore
|
(part: PartType) => part.client_hooks != null,
|
||||||
clientPlugins[name] = {...plugins.plugins[name]};
|
);
|
||||||
// @ts-ignore
|
const clientPlugins: MapArrayType<string> = {};
|
||||||
delete clientPlugins[name].package;
|
for (const name of new Set(
|
||||||
}
|
clientParts.map((part: PartType) => part.plugin),
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
)) {
|
||||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
// @ts-ignore
|
||||||
res.write(JSON.stringify({plugins: clientPlugins, parts: clientParts}));
|
clientPlugins[name] = { ...plugins.plugins[name] };
|
||||||
res.end();
|
// @ts-ignore
|
||||||
});
|
delete clientPlugins[name].package;
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
|
res.setHeader("Cache-Control", `public, max-age=${settings.maxAge}`);
|
||||||
|
res.write(JSON.stringify({ plugins: clientPlugins, parts: clientParts }));
|
||||||
|
res.end();
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue