mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-20 15:36:16 -04:00
Added rewrite.
This commit is contained in:
parent
fa2d6d15a9
commit
f8175a6433
76 changed files with 3150 additions and 2453 deletions
379
pnpm-lock.yaml
generated
379
pnpm-lock.yaml
generated
|
@ -143,6 +143,9 @@ importers:
|
||||||
'@etherpad/express-session':
|
'@etherpad/express-session':
|
||||||
specifier: ^1.18.2
|
specifier: ^1.18.2
|
||||||
version: 1.18.2
|
version: 1.18.2
|
||||||
|
'@types/cookie-parser':
|
||||||
|
specifier: ^1.4.7
|
||||||
|
version: 1.4.7
|
||||||
async:
|
async:
|
||||||
specifier: ^3.2.5
|
specifier: ^3.2.5
|
||||||
version: 3.2.5
|
version: 3.2.5
|
||||||
|
@ -282,6 +285,12 @@ importers:
|
||||||
'@types/async':
|
'@types/async':
|
||||||
specifier: ^3.2.24
|
specifier: ^3.2.24
|
||||||
version: 3.2.24
|
version: 3.2.24
|
||||||
|
'@types/cross-spawn':
|
||||||
|
specifier: ^6.0.6
|
||||||
|
version: 6.0.6
|
||||||
|
'@types/ejs':
|
||||||
|
specifier: ^3.1.5
|
||||||
|
version: 3.1.5
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
@ -300,6 +309,9 @@ importers:
|
||||||
'@types/jsdom':
|
'@types/jsdom':
|
||||||
specifier: ^21.1.7
|
specifier: ^21.1.7
|
||||||
version: 21.1.7
|
version: 21.1.7
|
||||||
|
'@types/jsonminify':
|
||||||
|
specifier: ^0.4.3
|
||||||
|
version: 0.4.3
|
||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: ^9.0.6
|
specifier: ^9.0.6
|
||||||
version: 9.0.6
|
version: 9.0.6
|
||||||
|
@ -312,6 +324,9 @@ importers:
|
||||||
'@types/oidc-provider':
|
'@types/oidc-provider':
|
||||||
specifier: ^8.5.1
|
specifier: ^8.5.1
|
||||||
version: 8.5.1
|
version: 8.5.1
|
||||||
|
'@types/resolve':
|
||||||
|
specifier: ^1.20.6
|
||||||
|
version: 1.20.6
|
||||||
'@types/semver':
|
'@types/semver':
|
||||||
specifier: ^7.5.8
|
specifier: ^7.5.8
|
||||||
version: 7.5.8
|
version: 7.5.8
|
||||||
|
@ -321,6 +336,9 @@ importers:
|
||||||
'@types/supertest':
|
'@types/supertest':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
|
'@types/tinycon':
|
||||||
|
specifier: ^0.6.5
|
||||||
|
version: 0.6.5
|
||||||
'@types/underscore':
|
'@types/underscore':
|
||||||
specifier: ^1.11.15
|
specifier: ^1.11.15
|
||||||
version: 1.11.15
|
version: 1.11.15
|
||||||
|
@ -366,6 +384,9 @@ importers:
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.3
|
specifier: ^5.5.3
|
||||||
version: 5.5.3
|
version: 5.5.3
|
||||||
|
vitest:
|
||||||
|
specifier: ^2.0.3
|
||||||
|
version: 2.0.3(@types/node@20.14.11)(jsdom@24.1.0)
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -1459,6 +1480,9 @@ packages:
|
||||||
'@types/content-disposition@0.5.8':
|
'@types/content-disposition@0.5.8':
|
||||||
resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==}
|
resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==}
|
||||||
|
|
||||||
|
'@types/cookie-parser@1.4.7':
|
||||||
|
resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==}
|
||||||
|
|
||||||
'@types/cookie@0.4.1':
|
'@types/cookie@0.4.1':
|
||||||
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
||||||
|
|
||||||
|
@ -1471,9 +1495,15 @@ packages:
|
||||||
'@types/cors@2.8.17':
|
'@types/cors@2.8.17':
|
||||||
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
|
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
|
||||||
|
|
||||||
|
'@types/cross-spawn@6.0.6':
|
||||||
|
resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==}
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
|
'@types/ejs@3.1.5':
|
||||||
|
resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
|
||||||
|
|
||||||
'@types/estree@1.0.5':
|
'@types/estree@1.0.5':
|
||||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||||
|
|
||||||
|
@ -1516,6 +1546,9 @@ packages:
|
||||||
'@types/json5@0.0.29':
|
'@types/json5@0.0.29':
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
|
|
||||||
|
'@types/jsonminify@0.4.3':
|
||||||
|
resolution: {integrity: sha512-+oz7EbPz1Nwmn/sr3UztgXpRhdFpvFrjGi5ictEYxUri5ZvQMTcdTi36MTfD/gCb1A5xhJKdH8Hwz2uz5k6s9A==}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.6':
|
'@types/jsonwebtoken@9.0.6':
|
||||||
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
|
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
|
||||||
|
|
||||||
|
@ -1579,6 +1612,9 @@ packages:
|
||||||
'@types/react@18.3.3':
|
'@types/react@18.3.3':
|
||||||
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
|
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
|
||||||
|
|
||||||
|
'@types/resolve@1.20.6':
|
||||||
|
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
|
||||||
|
|
||||||
'@types/semver@7.5.8':
|
'@types/semver@7.5.8':
|
||||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||||
|
|
||||||
|
@ -1606,6 +1642,9 @@ packages:
|
||||||
'@types/tar@6.1.13':
|
'@types/tar@6.1.13':
|
||||||
resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==}
|
resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==}
|
||||||
|
|
||||||
|
'@types/tinycon@0.6.5':
|
||||||
|
resolution: {integrity: sha512-RrZzmMXr1P+7NJKQsiTxAxbt87lNMgX6luT0q5Ni96wpvRunOYXUWVStumTnt6ew6oEDkPHQE6o04jUMBTb4Sg==}
|
||||||
|
|
||||||
'@types/tough-cookie@4.0.5':
|
'@types/tough-cookie@4.0.5':
|
||||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||||
|
|
||||||
|
@ -1697,6 +1736,24 @@ packages:
|
||||||
vite: ^5.0.0
|
vite: ^5.0.0
|
||||||
vue: ^3.2.25
|
vue: ^3.2.25
|
||||||
|
|
||||||
|
'@vitest/expect@2.0.3':
|
||||||
|
resolution: {integrity: sha512-X6AepoOYePM0lDNUPsGXTxgXZAl3EXd0GYe/MZyVE4HzkUqyUVC6S3PrY5mClDJ6/7/7vALLMV3+xD/Ko60Hqg==}
|
||||||
|
|
||||||
|
'@vitest/pretty-format@2.0.3':
|
||||||
|
resolution: {integrity: sha512-URM4GLsB2xD37nnTyvf6kfObFafxmycCL8un3OC9gaCs5cti2u+5rJdIflZ2fUJUen4NbvF6jCufwViAFLvz1g==}
|
||||||
|
|
||||||
|
'@vitest/runner@2.0.3':
|
||||||
|
resolution: {integrity: sha512-EmSP4mcjYhAcuBWwqgpjR3FYVeiA4ROzRunqKltWjBfLNs1tnMLtF+qtgd5ClTwkDP6/DGlKJTNa6WxNK0bNYQ==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@2.0.3':
|
||||||
|
resolution: {integrity: sha512-6OyA6v65Oe3tTzoSuRPcU6kh9m+mPL1vQ2jDlPdn9IQoUxl8rXhBnfICNOC+vwxWY684Vt5UPgtcA2aPFBb6wg==}
|
||||||
|
|
||||||
|
'@vitest/spy@2.0.3':
|
||||||
|
resolution: {integrity: sha512-sfqyAw/ypOXlaj4S+w8689qKM1OyPOqnonqOc9T91DsoHbfN5mU7FdifWWv3MtQFf0lEUstEwR9L/q/M390C+A==}
|
||||||
|
|
||||||
|
'@vitest/utils@2.0.3':
|
||||||
|
resolution: {integrity: sha512-c/UdELMuHitQbbc/EVctlBaxoYAwQPQdSNwv7z/vHyBKy2edYZaFgptE27BRueZB7eW8po+cllotMNTDpL3HWg==}
|
||||||
|
|
||||||
'@vue/compiler-core@3.4.31':
|
'@vue/compiler-core@3.4.31':
|
||||||
resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==}
|
resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==}
|
||||||
|
|
||||||
|
@ -1881,6 +1938,10 @@ packages:
|
||||||
asap@2.0.6:
|
asap@2.0.6:
|
||||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||||
|
|
||||||
|
assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
ast-types@0.13.4:
|
ast-types@0.13.4:
|
||||||
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
|
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -1961,6 +2022,10 @@ packages:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
cac@6.7.14:
|
||||||
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
cache-content-type@1.0.1:
|
cache-content-type@1.0.1:
|
||||||
resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==}
|
resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
@ -1991,6 +2056,10 @@ packages:
|
||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
|
chai@5.1.1:
|
||||||
|
resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
chalk@2.4.2:
|
chalk@2.4.2:
|
||||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -2005,6 +2074,10 @@ packages:
|
||||||
character-entities-legacy@3.0.0:
|
character-entities-legacy@3.0.0:
|
||||||
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
|
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
|
||||||
|
|
||||||
|
check-error@2.1.1:
|
||||||
|
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
@ -2180,6 +2253,10 @@ packages:
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
deep-eql@5.0.2:
|
||||||
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
deep-equal@1.0.1:
|
deep-equal@1.0.1:
|
||||||
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
|
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
|
||||||
|
|
||||||
|
@ -2513,6 +2590,9 @@ packages:
|
||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
esutils@2.0.3:
|
esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -2530,6 +2610,10 @@ packages:
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
execa@8.0.1:
|
||||||
|
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||||
|
engines: {node: '>=16.17'}
|
||||||
|
|
||||||
express-rate-limit@7.3.1:
|
express-rate-limit@7.3.1:
|
||||||
resolution: {integrity: sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==}
|
resolution: {integrity: sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
|
@ -2691,6 +2775,9 @@ packages:
|
||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
|
get-func-name@2.0.2:
|
||||||
|
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||||
|
|
||||||
get-intrinsic@1.2.4:
|
get-intrinsic@1.2.4:
|
||||||
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -2703,6 +2790,10 @@ packages:
|
||||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
get-stream@8.0.1:
|
||||||
|
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
get-symbol-description@1.0.2:
|
get-symbol-description@1.0.2:
|
||||||
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
|
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -2868,6 +2959,10 @@ packages:
|
||||||
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
|
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
human-signals@5.0.0:
|
||||||
|
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||||
|
engines: {node: '>=16.17.0'}
|
||||||
|
|
||||||
i18next-browser-languagedetector@8.0.0:
|
i18next-browser-languagedetector@8.0.0:
|
||||||
resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==}
|
resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==}
|
||||||
|
|
||||||
|
@ -3011,6 +3106,10 @@ packages:
|
||||||
resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==}
|
resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-stream@3.0.0:
|
||||||
|
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
is-string@1.0.7:
|
is-string@1.0.7:
|
||||||
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
|
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -3220,6 +3319,9 @@ packages:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
loupe@3.1.1:
|
||||||
|
resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==}
|
||||||
|
|
||||||
lower-case@2.0.2:
|
lower-case@2.0.2:
|
||||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||||
|
|
||||||
|
@ -3263,6 +3365,9 @@ packages:
|
||||||
merge-descriptors@1.0.1:
|
merge-descriptors@1.0.1:
|
||||||
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||||
|
|
||||||
|
merge-stream@2.0.0:
|
||||||
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||||
|
|
||||||
merge2@1.4.1:
|
merge2@1.4.1:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
@ -3308,6 +3413,10 @@ packages:
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
mimic-fn@4.0.0:
|
||||||
|
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
mimic-response@3.1.0:
|
mimic-response@3.1.0:
|
||||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -3426,6 +3535,10 @@ packages:
|
||||||
resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
|
resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
npm-run-path@5.3.0:
|
||||||
|
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
nwsapi@2.2.10:
|
nwsapi@2.2.10:
|
||||||
resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==}
|
resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==}
|
||||||
|
|
||||||
|
@ -3481,6 +3594,10 @@ packages:
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
onetime@6.0.0:
|
||||||
|
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
only@0.0.2:
|
only@0.0.2:
|
||||||
resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==}
|
resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==}
|
||||||
|
|
||||||
|
@ -3547,6 +3664,10 @@ packages:
|
||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-key@4.0.0:
|
||||||
|
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
path-parse@1.0.7:
|
path-parse@1.0.7:
|
||||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||||
|
|
||||||
|
@ -3560,6 +3681,13 @@ packages:
|
||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
pathe@1.1.2:
|
||||||
|
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||||
|
|
||||||
|
pathval@2.0.0:
|
||||||
|
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
|
||||||
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
perfect-debounce@1.0.0:
|
perfect-debounce@1.0.0:
|
||||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
|
@ -3882,9 +4010,16 @@ packages:
|
||||||
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
signal-exit@3.0.7:
|
signal-exit@3.0.7:
|
||||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||||
|
|
||||||
|
signal-exit@4.1.0:
|
||||||
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
sinon@18.0.0:
|
sinon@18.0.0:
|
||||||
resolution: {integrity: sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==}
|
resolution: {integrity: sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==}
|
||||||
|
|
||||||
|
@ -3943,6 +4078,9 @@ packages:
|
||||||
sprintf-js@1.1.3:
|
sprintf-js@1.1.3:
|
||||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
statuses@1.5.0:
|
statuses@1.5.0:
|
||||||
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
|
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -3951,6 +4089,9 @@ packages:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
std-env@3.7.0:
|
||||||
|
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
|
||||||
|
|
||||||
streamroller@3.1.5:
|
streamroller@3.1.5:
|
||||||
resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==}
|
resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
@ -3981,6 +4122,10 @@ packages:
|
||||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
strip-final-newline@3.0.0:
|
||||||
|
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
strip-json-comments@3.1.1:
|
strip-json-comments@3.1.1:
|
||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -4047,9 +4192,24 @@ packages:
|
||||||
tiny-worker@2.3.0:
|
tiny-worker@2.3.0:
|
||||||
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
|
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
|
||||||
|
|
||||||
|
tinybench@2.8.0:
|
||||||
|
resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==}
|
||||||
|
|
||||||
tinycon@0.6.8:
|
tinycon@0.6.8:
|
||||||
resolution: {integrity: sha512-bF8Lxm4JUXF6Cw0XlZdugJ44GV575OinZ0Pt8vQPr8ooNqd2yyNkoFdCHzmdpHlgoqfSLfcyk4HDP1EyllT+ug==}
|
resolution: {integrity: sha512-bF8Lxm4JUXF6Cw0XlZdugJ44GV575OinZ0Pt8vQPr8ooNqd2yyNkoFdCHzmdpHlgoqfSLfcyk4HDP1EyllT+ug==}
|
||||||
|
|
||||||
|
tinypool@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|
||||||
|
tinyrainbow@1.2.0:
|
||||||
|
resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
tinyspy@3.0.0:
|
||||||
|
resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
to-fast-properties@2.0.0:
|
to-fast-properties@2.0.0:
|
||||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -4246,6 +4406,11 @@ packages:
|
||||||
vfile@6.0.1:
|
vfile@6.0.1:
|
||||||
resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==}
|
resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==}
|
||||||
|
|
||||||
|
vite-node@2.0.3:
|
||||||
|
resolution: {integrity: sha512-14jzwMx7XTcMB+9BhGQyoEAmSl0eOr3nrnn+Z12WNERtOvLN+d2scbRUvyni05rT3997Bg+rZb47NyP4IQPKXg==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
vite-plugin-static-copy@1.0.6:
|
vite-plugin-static-copy@1.0.6:
|
||||||
resolution: {integrity: sha512-3uSvsMwDVFZRitqoWHj0t4137Kz7UynnJeq1EZlRW7e25h2068fyIZX4ORCCOAkfp1FklGxJNVJBkBOD+PZIew==}
|
resolution: {integrity: sha512-3uSvsMwDVFZRitqoWHj0t4137Kz7UynnJeq1EZlRW7e25h2068fyIZX4ORCCOAkfp1FklGxJNVJBkBOD+PZIew==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
@ -4297,6 +4462,31 @@ packages:
|
||||||
postcss:
|
postcss:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@2.0.3:
|
||||||
|
resolution: {integrity: sha512-o3HRvU93q6qZK4rI2JrhKyZMMuxg/JRt30E6qeQs6ueaiz5hr1cPj+Sk2kATgQzMMqsa2DiNI0TIK++1ULx8Jw==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@types/node': ^18.0.0 || >=20.0.0
|
||||||
|
'@vitest/browser': 2.0.3
|
||||||
|
'@vitest/ui': 2.0.3
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
void-elements@3.1.0:
|
void-elements@3.1.0:
|
||||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -4359,6 +4549,11 @@ packages:
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
word-wrap@1.2.5:
|
word-wrap@1.2.5:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -5424,6 +5619,10 @@ snapshots:
|
||||||
|
|
||||||
'@types/content-disposition@0.5.8': {}
|
'@types/content-disposition@0.5.8': {}
|
||||||
|
|
||||||
|
'@types/cookie-parser@1.4.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
|
||||||
'@types/cookie@0.4.1': {}
|
'@types/cookie@0.4.1': {}
|
||||||
|
|
||||||
'@types/cookiejar@2.1.5': {}
|
'@types/cookiejar@2.1.5': {}
|
||||||
|
@ -5439,10 +5638,16 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.14.11
|
'@types/node': 20.14.11
|
||||||
|
|
||||||
|
'@types/cross-spawn@6.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.14.11
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 0.7.34
|
'@types/ms': 0.7.34
|
||||||
|
|
||||||
|
'@types/ejs@3.1.5': {}
|
||||||
|
|
||||||
'@types/estree@1.0.5': {}
|
'@types/estree@1.0.5': {}
|
||||||
|
|
||||||
'@types/express-serve-static-core@4.19.3':
|
'@types/express-serve-static-core@4.19.3':
|
||||||
|
@ -5493,6 +5698,8 @@ snapshots:
|
||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
|
|
||||||
|
'@types/jsonminify@0.4.3': {}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.6':
|
'@types/jsonwebtoken@9.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.14.11
|
'@types/node': 20.14.11
|
||||||
|
@ -5566,6 +5773,8 @@ snapshots:
|
||||||
'@types/prop-types': 15.7.12
|
'@types/prop-types': 15.7.12
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
|
'@types/resolve@1.20.6': {}
|
||||||
|
|
||||||
'@types/semver@7.5.8': {}
|
'@types/semver@7.5.8': {}
|
||||||
|
|
||||||
'@types/send@0.17.4':
|
'@types/send@0.17.4':
|
||||||
|
@ -5603,6 +5812,8 @@ snapshots:
|
||||||
'@types/node': 20.14.11
|
'@types/node': 20.14.11
|
||||||
minipass: 4.2.8
|
minipass: 4.2.8
|
||||||
|
|
||||||
|
'@types/tinycon@0.6.5': {}
|
||||||
|
|
||||||
'@types/tough-cookie@4.0.5': {}
|
'@types/tough-cookie@4.0.5': {}
|
||||||
|
|
||||||
'@types/underscore@1.11.15': {}
|
'@types/underscore@1.11.15': {}
|
||||||
|
@ -5710,6 +5921,39 @@ snapshots:
|
||||||
vite: 5.3.4(@types/node@20.14.11)
|
vite: 5.3.4(@types/node@20.14.11)
|
||||||
vue: 3.4.31(typescript@5.5.3)
|
vue: 3.4.31(typescript@5.5.3)
|
||||||
|
|
||||||
|
'@vitest/expect@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 2.0.3
|
||||||
|
'@vitest/utils': 2.0.3
|
||||||
|
chai: 5.1.1
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
|
'@vitest/pretty-format@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
|
'@vitest/runner@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 2.0.3
|
||||||
|
pathe: 1.1.2
|
||||||
|
|
||||||
|
'@vitest/snapshot@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 2.0.3
|
||||||
|
magic-string: 0.30.10
|
||||||
|
pathe: 1.1.2
|
||||||
|
|
||||||
|
'@vitest/spy@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
tinyspy: 3.0.0
|
||||||
|
|
||||||
|
'@vitest/utils@2.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 2.0.3
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
loupe: 3.1.1
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
'@vue/compiler-core@3.4.31':
|
'@vue/compiler-core@3.4.31':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.24.7
|
'@babel/parser': 7.24.7
|
||||||
|
@ -5943,6 +6187,8 @@ snapshots:
|
||||||
|
|
||||||
asap@2.0.6: {}
|
asap@2.0.6: {}
|
||||||
|
|
||||||
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
ast-types@0.13.4:
|
ast-types@0.13.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.3
|
tslib: 2.6.3
|
||||||
|
@ -6028,6 +6274,8 @@ snapshots:
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
|
cac@6.7.14: {}
|
||||||
|
|
||||||
cache-content-type@1.0.1:
|
cache-content-type@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
@ -6061,6 +6309,14 @@ snapshots:
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
|
chai@5.1.1:
|
||||||
|
dependencies:
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
check-error: 2.1.1
|
||||||
|
deep-eql: 5.0.2
|
||||||
|
loupe: 3.1.1
|
||||||
|
pathval: 2.0.0
|
||||||
|
|
||||||
chalk@2.4.2:
|
chalk@2.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 3.2.1
|
ansi-styles: 3.2.1
|
||||||
|
@ -6076,6 +6332,8 @@ snapshots:
|
||||||
|
|
||||||
character-entities-legacy@3.0.0: {}
|
character-entities-legacy@3.0.0: {}
|
||||||
|
|
||||||
|
check-error@2.1.1: {}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
|
@ -6233,6 +6491,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-response: 3.1.0
|
mimic-response: 3.1.0
|
||||||
|
|
||||||
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
deep-equal@1.0.1: {}
|
deep-equal@1.0.1: {}
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
|
@ -6726,6 +6986,10 @@ snapshots:
|
||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.5
|
||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
eta@3.4.0: {}
|
eta@3.4.0: {}
|
||||||
|
@ -6742,6 +7006,18 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
execa@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
cross-spawn: 7.0.3
|
||||||
|
get-stream: 8.0.1
|
||||||
|
human-signals: 5.0.0
|
||||||
|
is-stream: 3.0.0
|
||||||
|
merge-stream: 2.0.0
|
||||||
|
npm-run-path: 5.3.0
|
||||||
|
onetime: 6.0.0
|
||||||
|
signal-exit: 4.1.0
|
||||||
|
strip-final-newline: 3.0.0
|
||||||
|
|
||||||
express-rate-limit@7.3.1(express@4.19.2):
|
express-rate-limit@7.3.1(express@4.19.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
express: 4.19.2
|
express: 4.19.2
|
||||||
|
@ -6937,6 +7213,8 @@ snapshots:
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
|
get-func-name@2.0.2: {}
|
||||||
|
|
||||||
get-intrinsic@1.2.4:
|
get-intrinsic@1.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
@ -6949,6 +7227,8 @@ snapshots:
|
||||||
|
|
||||||
get-stream@6.0.1: {}
|
get-stream@6.0.1: {}
|
||||||
|
|
||||||
|
get-stream@8.0.1: {}
|
||||||
|
|
||||||
get-symbol-description@1.0.2:
|
get-symbol-description@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
|
@ -7194,6 +7474,8 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
human-signals@5.0.0: {}
|
||||||
|
|
||||||
i18next-browser-languagedetector@8.0.0:
|
i18next-browser-languagedetector@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.24.7
|
'@babel/runtime': 7.24.7
|
||||||
|
@ -7322,6 +7604,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
|
|
||||||
|
is-stream@3.0.0: {}
|
||||||
|
|
||||||
is-string@1.0.7:
|
is-string@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
@ -7580,6 +7864,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
loupe@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
get-func-name: 2.0.2
|
||||||
|
|
||||||
lower-case@2.0.2:
|
lower-case@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.3
|
tslib: 2.6.3
|
||||||
|
@ -7625,6 +7913,8 @@ snapshots:
|
||||||
|
|
||||||
merge-descriptors@1.0.1: {}
|
merge-descriptors@1.0.1: {}
|
||||||
|
|
||||||
|
merge-stream@2.0.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
methods@1.1.2: {}
|
methods@1.1.2: {}
|
||||||
|
@ -7661,6 +7951,8 @@ snapshots:
|
||||||
|
|
||||||
mime@2.6.0: {}
|
mime@2.6.0: {}
|
||||||
|
|
||||||
|
mimic-fn@4.0.0: {}
|
||||||
|
|
||||||
mimic-response@3.1.0: {}
|
mimic-response@3.1.0: {}
|
||||||
|
|
||||||
mimic-response@4.0.0: {}
|
mimic-response@4.0.0: {}
|
||||||
|
@ -7774,6 +8066,10 @@ snapshots:
|
||||||
|
|
||||||
normalize-url@8.0.1: {}
|
normalize-url@8.0.1: {}
|
||||||
|
|
||||||
|
npm-run-path@5.3.0:
|
||||||
|
dependencies:
|
||||||
|
path-key: 4.0.0
|
||||||
|
|
||||||
nwsapi@2.2.10: {}
|
nwsapi@2.2.10: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
@ -7842,6 +8138,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
onetime@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
mimic-fn: 4.0.0
|
||||||
|
|
||||||
only@0.0.2: {}
|
only@0.0.2: {}
|
||||||
|
|
||||||
openapi-backend@5.10.6:
|
openapi-backend@5.10.6:
|
||||||
|
@ -7932,6 +8232,8 @@ snapshots:
|
||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
path-key@4.0.0: {}
|
||||||
|
|
||||||
path-parse@1.0.7: {}
|
path-parse@1.0.7: {}
|
||||||
|
|
||||||
path-to-regexp@0.1.7: {}
|
path-to-regexp@0.1.7: {}
|
||||||
|
@ -7940,6 +8242,10 @@ snapshots:
|
||||||
|
|
||||||
path-type@4.0.0: {}
|
path-type@4.0.0: {}
|
||||||
|
|
||||||
|
pathe@1.1.2: {}
|
||||||
|
|
||||||
|
pathval@2.0.0: {}
|
||||||
|
|
||||||
perfect-debounce@1.0.0: {}
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
picocolors@1.0.1: {}
|
picocolors@1.0.1: {}
|
||||||
|
@ -8292,8 +8598,12 @@ snapshots:
|
||||||
get-intrinsic: 1.2.4
|
get-intrinsic: 1.2.4
|
||||||
object-inspect: 1.13.1
|
object-inspect: 1.13.1
|
||||||
|
|
||||||
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
signal-exit@3.0.7: {}
|
signal-exit@3.0.7: {}
|
||||||
|
|
||||||
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
sinon@18.0.0:
|
sinon@18.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sinonjs/commons': 3.0.1
|
'@sinonjs/commons': 3.0.1
|
||||||
|
@ -8379,10 +8689,14 @@ snapshots:
|
||||||
|
|
||||||
sprintf-js@1.1.3: {}
|
sprintf-js@1.1.3: {}
|
||||||
|
|
||||||
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
statuses@1.5.0: {}
|
statuses@1.5.0: {}
|
||||||
|
|
||||||
statuses@2.0.1: {}
|
statuses@2.0.1: {}
|
||||||
|
|
||||||
|
std-env@3.7.0: {}
|
||||||
|
|
||||||
streamroller@3.1.5:
|
streamroller@3.1.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
date-format: 4.0.14
|
date-format: 4.0.14
|
||||||
|
@ -8427,6 +8741,8 @@ snapshots:
|
||||||
|
|
||||||
strip-bom@3.0.0: {}
|
strip-bom@3.0.0: {}
|
||||||
|
|
||||||
|
strip-final-newline@3.0.0: {}
|
||||||
|
|
||||||
strip-json-comments@3.1.1: {}
|
strip-json-comments@3.1.1: {}
|
||||||
|
|
||||||
superagent@8.1.2:
|
superagent@8.1.2:
|
||||||
|
@ -8520,8 +8836,16 @@ snapshots:
|
||||||
esm: 3.2.25
|
esm: 3.2.25
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
tinybench@2.8.0: {}
|
||||||
|
|
||||||
tinycon@0.6.8: {}
|
tinycon@0.6.8: {}
|
||||||
|
|
||||||
|
tinypool@1.0.0: {}
|
||||||
|
|
||||||
|
tinyrainbow@1.2.0: {}
|
||||||
|
|
||||||
|
tinyspy@3.0.0: {}
|
||||||
|
|
||||||
to-fast-properties@2.0.0: {}
|
to-fast-properties@2.0.0: {}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
|
@ -8730,6 +9054,23 @@ snapshots:
|
||||||
unist-util-stringify-position: 4.0.0
|
unist-util-stringify-position: 4.0.0
|
||||||
vfile-message: 4.0.2
|
vfile-message: 4.0.2
|
||||||
|
|
||||||
|
vite-node@2.0.3(@types/node@20.14.11):
|
||||||
|
dependencies:
|
||||||
|
cac: 6.7.14
|
||||||
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
|
pathe: 1.1.2
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
vite: 5.3.4(@types/node@20.14.11)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- sass
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
|
||||||
vite-plugin-static-copy@1.0.6(vite@5.3.4(@types/node@20.14.11)):
|
vite-plugin-static-copy@1.0.6(vite@5.3.4(@types/node@20.14.11)):
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
|
@ -8805,6 +9146,39 @@ snapshots:
|
||||||
- typescript
|
- typescript
|
||||||
- universal-cookie
|
- universal-cookie
|
||||||
|
|
||||||
|
vitest@2.0.3(@types/node@20.14.11)(jsdom@24.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
'@vitest/expect': 2.0.3
|
||||||
|
'@vitest/pretty-format': 2.0.3
|
||||||
|
'@vitest/runner': 2.0.3
|
||||||
|
'@vitest/snapshot': 2.0.3
|
||||||
|
'@vitest/spy': 2.0.3
|
||||||
|
'@vitest/utils': 2.0.3
|
||||||
|
chai: 5.1.1
|
||||||
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
|
execa: 8.0.1
|
||||||
|
magic-string: 0.30.10
|
||||||
|
pathe: 1.1.2
|
||||||
|
std-env: 3.7.0
|
||||||
|
tinybench: 2.8.0
|
||||||
|
tinypool: 1.0.0
|
||||||
|
tinyrainbow: 1.2.0
|
||||||
|
vite: 5.3.4(@types/node@20.14.11)
|
||||||
|
vite-node: 2.0.3(@types/node@20.14.11)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 20.14.11
|
||||||
|
jsdom: 24.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- sass
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
|
||||||
void-elements@3.1.0: {}
|
void-elements@3.1.0: {}
|
||||||
|
|
||||||
vue-demi@0.14.8(vue@3.4.31(typescript@5.5.3)):
|
vue-demi@0.14.8(vue@3.4.31(typescript@5.5.3)):
|
||||||
|
@ -8862,6 +9236,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
workerpool@6.5.1: {}
|
workerpool@6.5.1: {}
|
||||||
|
|
|
@ -19,59 +19,61 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps} from '../../static/js/Changeset';
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
import ChatMessage from '../../static/js/ChatMessage';
|
||||||
const CustomError = require('../utils/customError');
|
import CustomError from '../utils/customError';
|
||||||
const padManager = require('./PadManager');
|
import * as padManager from './PadManager';
|
||||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
import * as padMessageHandler from '../handler/PadMessageHandler';
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
import {getPadId, getReadOnlyId} from './ReadOnlyManager';
|
||||||
const groupManager = require('./GroupManager');
|
import * as groupManager from './GroupManager';
|
||||||
const authorManager = require('./AuthorManager');
|
import * as authorManager from './AuthorManager';
|
||||||
const sessionManager = require('./SessionManager');
|
import * as sessionManager from './SessionManager';
|
||||||
const exportHtml = require('../utils/ExportHtml');
|
import {getPadHTML} from '../utils/ExportHtml';
|
||||||
const exportTxt = require('../utils/ExportTxt');
|
import {getTXTFromAtext} from '../utils/ExportTxt';
|
||||||
const importHtml = require('../utils/ImportHtml');
|
import {setPadHTML} from '../utils/ImportHtml';
|
||||||
const cleanText = require('./Pad').cleanText;
|
import {cleanText} from './Pad'
|
||||||
const PadDiff = require('../utils/padDiff');
|
import PadDiff from '../utils/padDiff';
|
||||||
const {checkValidRev, isInt} = require('../utils/checkValidRev');
|
import {checkValidRev, isInt} from '../utils/checkValidRev';
|
||||||
|
import {Builder} from "../../static/js/Builder";
|
||||||
|
import {Attribute} from "../../static/js/types/Attribute";
|
||||||
|
|
||||||
/* ********************
|
/* ********************
|
||||||
* GROUP FUNCTIONS ****
|
* GROUP FUNCTIONS ****
|
||||||
******************** */
|
******************** */
|
||||||
|
|
||||||
exports.listAllGroups = groupManager.listAllGroups;
|
export const listAllGroups = groupManager.listAllGroups;
|
||||||
exports.createGroup = groupManager.createGroup;
|
export const createGroup = groupManager.createGroup;
|
||||||
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
|
export const createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
|
||||||
exports.deleteGroup = groupManager.deleteGroup;
|
export const deleteGroup = groupManager.deleteGroup;
|
||||||
exports.listPads = groupManager.listPads;
|
export const listPads = groupManager.listPads;
|
||||||
exports.createGroupPad = groupManager.createGroupPad;
|
export const createGroupPad = groupManager.createGroupPad;
|
||||||
|
|
||||||
/* ********************
|
/* ********************
|
||||||
* PADLIST FUNCTION ***
|
* PADLIST FUNCTION ***
|
||||||
******************** */
|
******************** */
|
||||||
|
|
||||||
exports.listAllPads = padManager.listAllPads;
|
export const listAllPads = padManager.listAllPads;
|
||||||
|
|
||||||
/* ********************
|
/* ********************
|
||||||
* AUTHOR FUNCTIONS ***
|
* AUTHOR FUNCTIONS ***
|
||||||
******************** */
|
******************** */
|
||||||
|
|
||||||
exports.createAuthor = authorManager.createAuthor;
|
export const createAuthor = authorManager.createAuthor;
|
||||||
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
|
export const createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
|
||||||
exports.getAuthorName = authorManager.getAuthorName;
|
export const getAuthorName = authorManager.getAuthorName;
|
||||||
exports.listPadsOfAuthor = authorManager.listPadsOfAuthor;
|
export const listPadsOfAuthor = authorManager.listPadsOfAuthor;
|
||||||
exports.padUsers = padMessageHandler.padUsers;
|
export const padUsers = padMessageHandler.padUsers;
|
||||||
exports.padUsersCount = padMessageHandler.padUsersCount;
|
export const padUsersCount = padMessageHandler.padUsersCount;
|
||||||
|
|
||||||
/* ********************
|
/* ********************
|
||||||
* SESSION FUNCTIONS **
|
* SESSION FUNCTIONS **
|
||||||
******************** */
|
******************** */
|
||||||
|
|
||||||
exports.createSession = sessionManager.createSession;
|
export const createSession = sessionManager.createSession;
|
||||||
exports.deleteSession = sessionManager.deleteSession;
|
export const deleteSession = sessionManager.deleteSession;
|
||||||
exports.getSessionInfo = sessionManager.getSessionInfo;
|
export const getSessionInfo = sessionManager.getSessionInfo;
|
||||||
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup;
|
export const listSessionsOfGroup = sessionManager.listSessionsOfGroup;
|
||||||
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
|
export const listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
|
||||||
|
|
||||||
/* ***********************
|
/* ***********************
|
||||||
* PAD CONTENT FUNCTIONS *
|
* PAD CONTENT FUNCTIONS *
|
||||||
|
@ -104,7 +106,7 @@ Example returns:
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
exports.getAttributePool = async (padID: string) => {
|
export const getAttributePool = async (padID: string) => {
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
return {pool: pad.pool};
|
return {pool: pad.pool};
|
||||||
};
|
};
|
||||||
|
@ -122,7 +124,7 @@ Example returns:
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
exports.getRevisionChangeset = async (padID: string, rev: string) => {
|
export const getRevisionChangeset = async (padID: string, rev: number) => {
|
||||||
// try to parse the revision number
|
// try to parse the revision number
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
rev = checkValidRev(rev);
|
rev = checkValidRev(rev);
|
||||||
|
@ -155,7 +157,7 @@ Example returns:
|
||||||
{code: 0, message:"ok", data: {text:"Welcome Text"}}
|
{code: 0, message:"ok", data: {text:"Welcome Text"}}
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
*/
|
*/
|
||||||
exports.getText = async (padID: string, rev: string) => {
|
export const getText = async (padID: string, rev: number) => {
|
||||||
// try to parse the revision number
|
// try to parse the revision number
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
rev = checkValidRev(rev);
|
rev = checkValidRev(rev);
|
||||||
|
@ -180,7 +182,7 @@ exports.getText = async (padID: string, rev: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// the client wants the latest text, lets return it to him
|
// the client wants the latest text, lets return it to him
|
||||||
const text = exportTxt.getTXTFromAtext(pad, pad.atext);
|
const text = getTXTFromAtext(pad, pad.atext);
|
||||||
return {text};
|
return {text};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -200,7 +202,7 @@ Example returns:
|
||||||
* @param {String} authorId the id of the author, defaulting to empty string
|
* @param {String} authorId the id of the author, defaulting to empty string
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise<void> => {
|
export const setText = async (padID: string, text?: string, authorId: string = ''): Promise<void> => {
|
||||||
// text is required
|
// text is required
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== 'string') {
|
||||||
throw new CustomError('text is not a string', 'apierror');
|
throw new CustomError('text is not a string', 'apierror');
|
||||||
|
@ -225,7 +227,7 @@ Example returns:
|
||||||
@param {String} text the text of the pad
|
@param {String} text the text of the pad
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.appendText = async (padID:string, text?: string, authorId:string = '') => {
|
export const appendText = async (padID:string, text?: string, authorId:string = '') => {
|
||||||
// text is required
|
// text is required
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== 'string') {
|
||||||
throw new CustomError('text is not a string', 'apierror');
|
throw new CustomError('text is not a string', 'apierror');
|
||||||
|
@ -247,7 +249,7 @@ Example returns:
|
||||||
@param {String} rev the revision number, defaulting to the latest revision
|
@param {String} rev the revision number, defaulting to the latest revision
|
||||||
@return {Promise<{html: string}>} the html of the pad
|
@return {Promise<{html: string}>} the html of the pad
|
||||||
*/
|
*/
|
||||||
exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }> => {
|
export const getHTML = async (padID: string, rev: number): Promise<{ html: string; }> => {
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
rev = checkValidRev(rev);
|
rev = checkValidRev(rev);
|
||||||
}
|
}
|
||||||
|
@ -264,7 +266,7 @@ exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }>
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the html of this revision
|
// get the html of this revision
|
||||||
let html = await exportHtml.getPadHTML(pad, rev);
|
let html = await getPadHTML(pad, rev);
|
||||||
|
|
||||||
// wrap the HTML
|
// wrap the HTML
|
||||||
html = `<!DOCTYPE HTML><html><body>${html}</body></html>`;
|
html = `<!DOCTYPE HTML><html><body>${html}</body></html>`;
|
||||||
|
@ -283,7 +285,7 @@ Example returns:
|
||||||
@param {String} html the html of the pad
|
@param {String} html the html of the pad
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.setHTML = async (padID: string, html:string|object, authorId = '') => {
|
export const setHTML = async (padID: string, html:string|object, authorId = '') => {
|
||||||
// html string is required
|
// html string is required
|
||||||
if (typeof html !== 'string') {
|
if (typeof html !== 'string') {
|
||||||
throw new CustomError('html is not a string', 'apierror');
|
throw new CustomError('html is not a string', 'apierror');
|
||||||
|
@ -294,13 +296,13 @@ exports.setHTML = async (padID: string, html:string|object, authorId = '') => {
|
||||||
|
|
||||||
// add a new changeset with the new html to the pad
|
// add a new changeset with the new html to the pad
|
||||||
try {
|
try {
|
||||||
await importHtml.setPadHTML(pad, cleanText(html), authorId);
|
await setPadHTML(pad, cleanText(html), authorId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new CustomError('HTML is malformed', 'apierror');
|
throw new CustomError('HTML is malformed', 'apierror');
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the clients on the pad
|
// update the clients on the pad
|
||||||
padMessageHandler.updatePadClients(pad);
|
await padMessageHandler.updatePadClients(pad);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ****************
|
/* ****************
|
||||||
|
@ -324,7 +326,7 @@ Example returns:
|
||||||
@param {Number} start the start point of the chat-history
|
@param {Number} start the start point of the chat-history
|
||||||
@param {Number} end the end point of the chat-history
|
@param {Number} end the end point of the chat-history
|
||||||
*/
|
*/
|
||||||
exports.getChatHistory = async (padID: string, start:number, end:number) => {
|
export const getChatHistory = async (padID: string, start:number, end:number) => {
|
||||||
if (start && end) {
|
if (start && end) {
|
||||||
if (start < 0) {
|
if (start < 0) {
|
||||||
throw new CustomError('start is below zero', 'apierror');
|
throw new CustomError('start is below zero', 'apierror');
|
||||||
|
@ -374,7 +376,7 @@ Example returns:
|
||||||
@param {String} authorID the id of the author
|
@param {String} authorID the id of the author
|
||||||
@param {Number} time the timestamp of the chat-message
|
@param {Number} time the timestamp of the chat-message
|
||||||
*/
|
*/
|
||||||
exports.appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => {
|
export const appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => {
|
||||||
// text is required
|
// text is required
|
||||||
if (typeof text !== 'string') {
|
if (typeof text !== 'string') {
|
||||||
throw new CustomError('text is not a string', 'apierror');
|
throw new CustomError('text is not a string', 'apierror');
|
||||||
|
@ -404,7 +406,7 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.getRevisionsCount = async (padID: string) => {
|
export const getRevisionsCount = async (padID: string) => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
return {revisions: pad.getHeadRevisionNumber()};
|
return {revisions: pad.getHeadRevisionNumber()};
|
||||||
|
@ -419,7 +421,7 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.getSavedRevisionsCount = async (padID: string) => {
|
export const getSavedRevisionsCount = async (padID: string) => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
return {savedRevisions: pad.getSavedRevisionsNumber()};
|
return {savedRevisions: pad.getSavedRevisionsNumber()};
|
||||||
|
@ -434,7 +436,7 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.listSavedRevisions = async (padID: string) => {
|
export const listSavedRevisions = async (padID: string) => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
return {savedRevisions: pad.getSavedRevisionsList()};
|
return {savedRevisions: pad.getSavedRevisionsList()};
|
||||||
|
@ -450,7 +452,7 @@ Example returns:
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
@param {Number} rev the revision number, defaulting to the latest revision
|
@param {Number} rev the revision number, defaulting to the latest revision
|
||||||
*/
|
*/
|
||||||
exports.saveRevision = async (padID: string, rev: number) => {
|
export const saveRevision = async (padID: string, rev: number) => {
|
||||||
// check if rev is a number
|
// check if rev is a number
|
||||||
if (rev !== undefined) {
|
if (rev !== undefined) {
|
||||||
rev = checkValidRev(rev);
|
rev = checkValidRev(rev);
|
||||||
|
@ -483,7 +485,7 @@ Example returns:
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
@return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad
|
@return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad
|
||||||
*/
|
*/
|
||||||
exports.getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => {
|
export const getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
const lastEdited = await pad.getLastEdit();
|
const lastEdited = await pad.getLastEdit();
|
||||||
|
@ -501,7 +503,7 @@ Example returns:
|
||||||
@param {String} text the initial text of the pad
|
@param {String} text the initial text of the pad
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.createPad = async (padID: string, text: string, authorId = '') => {
|
export const createPad = async (padID: string, text: string, authorId = '') => {
|
||||||
if (padID) {
|
if (padID) {
|
||||||
// ensure there is no $ in the padID
|
// ensure there is no $ in the padID
|
||||||
if (padID.indexOf('$') !== -1) {
|
if (padID.indexOf('$') !== -1) {
|
||||||
|
@ -527,7 +529,7 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.deletePad = async (padID: string) => {
|
export const deletePad = async (padID: string) => {
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
await pad.remove();
|
await pad.remove();
|
||||||
};
|
};
|
||||||
|
@ -543,7 +545,7 @@ exports.deletePad = async (padID: string) => {
|
||||||
@param {Number} rev the revision number, defaulting to the latest revision
|
@param {Number} rev the revision number, defaulting to the latest revision
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
export const restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
// check if rev is a number
|
// check if rev is a number
|
||||||
if (rev === undefined) {
|
if (rev === undefined) {
|
||||||
throw new CustomError('rev is not defined', 'apierror');
|
throw new CustomError('rev is not defined', 'apierror');
|
||||||
|
@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
const oldText = pad.text();
|
const oldText = pad.text();
|
||||||
atext.text += '\n';
|
atext.text += '\n';
|
||||||
|
|
||||||
const eachAttribRun = (attribs: string[], func:Function) => {
|
const eachAttribRun = (attribs: string, func:Function) => {
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = 0;
|
const newTextStart = 0;
|
||||||
const newTextEnd = atext.text.length;
|
const newTextEnd = atext.text.length;
|
||||||
for (const op of Changeset.deserializeOps(attribs)) {
|
for (const op of deserializeOps(attribs)) {
|
||||||
const nextIndex = textIndex + op.chars;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
||||||
|
@ -577,10 +579,10 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// create a new changeset with a helper builder object
|
// create a new changeset with a helper builder object
|
||||||
const builder = Changeset.builder(oldText.length);
|
const builder = new Builder(oldText.length);
|
||||||
|
|
||||||
// assemble each line into the builder
|
// assemble each line into the builder
|
||||||
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
|
eachAttribRun(atext.attribs, (start: number, end: number, attribs: Attribute[]) => {
|
||||||
builder.insert(atext.text.substring(start, end), attribs);
|
builder.insert(atext.text.substring(start, end), attribs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -588,7 +590,7 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
if (lastNewlinePos < 0) {
|
if (lastNewlinePos < 0) {
|
||||||
builder.remove(oldText.length - 1, 0);
|
builder.remove(oldText.length - 1, 0);
|
||||||
} else {
|
} else {
|
||||||
builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1);
|
builder.remove(lastNewlinePos, oldText.match(/\n/g)!.length - 1);
|
||||||
builder.remove(oldText.length - lastNewlinePos - 1, 0);
|
builder.remove(oldText.length - lastNewlinePos - 1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -610,7 +612,7 @@ Example returns:
|
||||||
@param {String} destinationID the id of the destination pad
|
@param {String} destinationID the id of the destination pad
|
||||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||||
*/
|
*/
|
||||||
exports.copyPad = async (sourceID: string, destinationID: string, force: boolean) => {
|
export const copyPad = async (sourceID: string, destinationID: string, force: boolean) => {
|
||||||
const pad = await getPadSafe(sourceID, true);
|
const pad = await getPadSafe(sourceID, true);
|
||||||
await pad.copy(destinationID, force);
|
await pad.copy(destinationID, force);
|
||||||
};
|
};
|
||||||
|
@ -628,7 +630,7 @@ Example returns:
|
||||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||||
@param {String} authorId the id of the author, defaulting to empty string
|
@param {String} authorId the id of the author, defaulting to empty string
|
||||||
*/
|
*/
|
||||||
exports.copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId = '') => {
|
export const copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId: string = '') => {
|
||||||
const pad = await getPadSafe(sourceID, true);
|
const pad = await getPadSafe(sourceID, true);
|
||||||
await pad.copyPadWithoutHistory(destinationID, force, authorId);
|
await pad.copyPadWithoutHistory(destinationID, force, authorId);
|
||||||
};
|
};
|
||||||
|
@ -645,7 +647,7 @@ Example returns:
|
||||||
@param {String} destinationID the id of the destination pad
|
@param {String} destinationID the id of the destination pad
|
||||||
@param {Boolean} force whether to overwrite the destination pad if it exists
|
@param {Boolean} force whether to overwrite the destination pad if it exists
|
||||||
*/
|
*/
|
||||||
exports.movePad = async (sourceID: string, destinationID: string, force:boolean) => {
|
export const movePad = async (sourceID: string, destinationID: string, force:boolean) => {
|
||||||
const pad = await getPadSafe(sourceID, true);
|
const pad = await getPadSafe(sourceID, true);
|
||||||
await pad.copy(destinationID, force);
|
await pad.copy(destinationID, force);
|
||||||
await pad.remove();
|
await pad.remove();
|
||||||
|
@ -660,12 +662,12 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.getReadOnlyID = async (padID: string) => {
|
export const getReadOnlyID = async (padID: string) => {
|
||||||
// we don't need the pad object, but this function does all the security stuff for us
|
// we don't need the pad object, but this function does all the security stuff for us
|
||||||
await getPadSafe(padID, true);
|
await getPadSafe(padID, true);
|
||||||
|
|
||||||
// get the readonlyId
|
// get the readonlyId
|
||||||
const readOnlyID = await readOnlyManager.getReadOnlyId(padID);
|
const readOnlyID = await getReadOnlyId(padID);
|
||||||
|
|
||||||
return {readOnlyID};
|
return {readOnlyID};
|
||||||
};
|
};
|
||||||
|
@ -679,9 +681,9 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} roID the readonly id of the pad
|
@param {String} roID the readonly id of the pad
|
||||||
*/
|
*/
|
||||||
exports.getPadID = async (roID: string) => {
|
export const getPadID = async (roID: string) => {
|
||||||
// get the PadId
|
// get the PadId
|
||||||
const padID = await readOnlyManager.getPadId(roID);
|
const padID = await getPadId(roID);
|
||||||
if (padID == null) {
|
if (padID == null) {
|
||||||
throw new CustomError('padID does not exist', 'apierror');
|
throw new CustomError('padID does not exist', 'apierror');
|
||||||
}
|
}
|
||||||
|
@ -699,7 +701,7 @@ Example returns:
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
@param {Boolean} publicStatus the public status of the pad
|
@param {Boolean} publicStatus the public status of the pad
|
||||||
*/
|
*/
|
||||||
exports.setPublicStatus = async (padID: string, publicStatus: boolean|string) => {
|
export const setPublicStatus = async (padID: string, publicStatus: boolean|string) => {
|
||||||
// ensure this is a group pad
|
// ensure this is a group pad
|
||||||
checkGroupPad(padID, 'publicStatus');
|
checkGroupPad(padID, 'publicStatus');
|
||||||
|
|
||||||
|
@ -723,7 +725,7 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.getPublicStatus = async (padID: string) => {
|
export const getPublicStatus = async (padID: string) => {
|
||||||
// ensure this is a group pad
|
// ensure this is a group pad
|
||||||
checkGroupPad(padID, 'publicStatus');
|
checkGroupPad(padID, 'publicStatus');
|
||||||
|
|
||||||
|
@ -741,7 +743,7 @@ Example returns:
|
||||||
{code: 1, message:"padID does not exist", data: null}
|
{code: 1, message:"padID does not exist", data: null}
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
*/
|
*/
|
||||||
exports.listAuthorsOfPad = async (padID: string) => {
|
export const listAuthorsOfPad = async (padID: string) => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
const authorIDs = pad.getAllAuthors();
|
const authorIDs = pad.getAllAuthors();
|
||||||
|
@ -773,7 +775,7 @@ Example returns:
|
||||||
@param {String} msg the message to send
|
@param {String} msg the message to send
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.sendClientsMessage = async (padID: string, msg: string) => {
|
export const sendClientsMessage = async (padID: string, msg: string) => {
|
||||||
await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
|
await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
|
||||||
padMessageHandler.handleCustomMessage(padID, msg);
|
padMessageHandler.handleCustomMessage(padID, msg);
|
||||||
};
|
};
|
||||||
|
@ -786,7 +788,7 @@ Example returns:
|
||||||
{"code":0,"message":"ok","data":null}
|
{"code":0,"message":"ok","data":null}
|
||||||
{"code":4,"message":"no or wrong API Key","data":null}
|
{"code":4,"message":"no or wrong API Key","data":null}
|
||||||
*/
|
*/
|
||||||
exports.checkToken = async () => {
|
export const checkToken = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -799,7 +801,7 @@ Example returns:
|
||||||
@param {String} padID the id of the pad
|
@param {String} padID the id of the pad
|
||||||
@return {Promise<{chatHead: number}>} the chatHead of the pad
|
@return {Promise<{chatHead: number}>} the chatHead of the pad
|
||||||
*/
|
*/
|
||||||
exports.getChatHead = async (padID:string): Promise<{ chatHead: number; }> => {
|
export const getChatHead = async (padID:string): Promise<{ chatHead: number; }> => {
|
||||||
// get the pad
|
// get the pad
|
||||||
const pad = await getPadSafe(padID, true);
|
const pad = await getPadSafe(padID, true);
|
||||||
return {chatHead: pad.chatHead};
|
return {chatHead: pad.chatHead};
|
||||||
|
@ -825,7 +827,7 @@ Example returns:
|
||||||
@param {Number} startRev the start revision number
|
@param {Number} startRev the start revision number
|
||||||
@param {Number} endRev the end revision number
|
@param {Number} endRev the end revision number
|
||||||
*/
|
*/
|
||||||
exports.createDiffHTML = async (padID: string, startRev: number, endRev: number) => {
|
export const createDiffHTML = async (padID: string, startRev: number, endRev: number) => {
|
||||||
// check if startRev is a number
|
// check if startRev is a number
|
||||||
if (startRev !== undefined) {
|
if (startRev !== undefined) {
|
||||||
startRev = checkValidRev(startRev);
|
startRev = checkValidRev(startRev);
|
||||||
|
@ -901,7 +903,7 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the pad exists
|
// check if the pad exists
|
||||||
const exists = await padManager.doesPadExists(padID);
|
const exists = await padManager.doesPadExist(padID);
|
||||||
|
|
||||||
if (!exists && shouldExist) {
|
if (!exists && shouldExist) {
|
||||||
// does not exist, but should
|
// does not exist, but should
|
||||||
|
|
|
@ -19,12 +19,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const db = require('./DB');
|
import {get, getSub, set, setSub} from './DB';
|
||||||
const CustomError = require('../utils/customError');
|
import CustomError from '../utils/customError';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
import {aCallFirst} from '../../static/js/pluginfw/hooks';
|
||||||
import {padUtils, randomString} from '../../static/js/pad_utils'
|
import {padUtils, randomString} from '../../static/js/pad_utils'
|
||||||
|
|
||||||
exports.getColorPalette = () => [
|
export const getColorPalette = () => [
|
||||||
'#ffc7c7',
|
'#ffc7c7',
|
||||||
'#fff1c7',
|
'#fff1c7',
|
||||||
'#e3ffc7',
|
'#e3ffc7',
|
||||||
|
@ -95,8 +95,8 @@ exports.getColorPalette = () => [
|
||||||
* Checks if the author exists
|
* Checks if the author exists
|
||||||
* @param {String} authorID The id of the author
|
* @param {String} authorID The id of the author
|
||||||
*/
|
*/
|
||||||
exports.doesAuthorExist = async (authorID: string) => {
|
export const doesAuthorExist = async (authorID: string) => {
|
||||||
const author = await db.get(`globalAuthor:${authorID}`);
|
const author = await get(`globalAuthor:${authorID}`);
|
||||||
|
|
||||||
return author != null;
|
return author != null;
|
||||||
};
|
};
|
||||||
|
@ -105,7 +105,6 @@ exports.doesAuthorExist = async (authorID: string) => {
|
||||||
exported for backwards compatibility
|
exported for backwards compatibility
|
||||||
@param {String} authorID The id of the author
|
@param {String} authorID The id of the author
|
||||||
*/
|
*/
|
||||||
exports.doesAuthorExists = exports.doesAuthorExist;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,14 +115,14 @@ exports.doesAuthorExists = exports.doesAuthorExist;
|
||||||
*/
|
*/
|
||||||
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 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 set(`${mapperkey}:${mapper}`, author.authorID);
|
||||||
|
|
||||||
// return the author
|
// return the author
|
||||||
return author;
|
return author;
|
||||||
|
@ -131,7 +130,8 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
|
||||||
|
|
||||||
// there is an author with this mapper
|
// there is an author with this mapper
|
||||||
// update the timestamp of this author
|
// update the timestamp of this author
|
||||||
await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now());
|
// @ts-ignore
|
||||||
|
await db!.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now());
|
||||||
|
|
||||||
// return the author
|
// return the author
|
||||||
return {authorID: author};
|
return {authorID: author};
|
||||||
|
@ -142,7 +142,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
|
||||||
* @param {String} token The token of the author
|
* @param {String} token The token of the author
|
||||||
* @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
|
||||||
|
@ -155,9 +155,9 @@ const getAuthor4Token = async (token: string) => {
|
||||||
* @param {Object} user
|
* @param {Object} user
|
||||||
* @return {Promise<*>}
|
* @return {Promise<*>}
|
||||||
*/
|
*/
|
||||||
exports.getAuthorId = async (token: string, user: object) => {
|
export const 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 aCallFirst('getAuthorId', context);
|
||||||
if (!authorId) authorId = await getAuthor4Token(context.dbKey);
|
if (!authorId) authorId = await getAuthor4Token(context.dbKey);
|
||||||
return authorId;
|
return authorId;
|
||||||
};
|
};
|
||||||
|
@ -168,10 +168,10 @@ exports.getAuthorId = async (token: string, user: object) => {
|
||||||
* @deprecated Use `getAuthorId` instead.
|
* @deprecated Use `getAuthorId` instead.
|
||||||
* @param {String} token The token
|
* @param {String} token The token
|
||||||
*/
|
*/
|
||||||
exports.getAuthor4Token = async (token: string) => {
|
export const getAuthor4Token = async (token: string) => {
|
||||||
padUtils.warnDeprecated(
|
padUtils.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,12 +179,12 @@ 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) => {
|
export const createAuthorIfNotExistsFor = async (authorMapper: string, name: string) => {
|
||||||
const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
|
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 setAuthorName(author.authorID, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return author;
|
return author;
|
||||||
|
@ -195,19 +195,20 @@ exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string)
|
||||||
* 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) => {
|
export const 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() * (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);
|
// @ts-ignore
|
||||||
|
await db!.set(`globalAuthor:${author}`, authorObj);
|
||||||
|
|
||||||
return {authorID: author};
|
return {authorID: author};
|
||||||
};
|
};
|
||||||
|
@ -216,48 +217,49 @@ exports.createAuthor = async (name: string) => {
|
||||||
* Returns the Author Obj of the author
|
* Returns the Author Obj of the author
|
||||||
* @param {String} author The id of the author
|
* @param {String} author The id of the author
|
||||||
*/
|
*/
|
||||||
exports.getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`);
|
export const getAuthor = async (author: string) => await 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']);
|
export const getAuthorColorId = async (author: string) => await getSub(`globalAuthor:${author}`, ['colorId']) as number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(
|
export const setAuthorColorId = async (author: string, colorId: string) => await setSub(
|
||||||
|
// @ts-ignore
|
||||||
`globalAuthor:${author}`, ['colorId'], colorId);
|
`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']);
|
export const getAuthorName = async (author: string) => await 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(
|
export const setAuthorName = async (author: string, name: string) => await setSub(
|
||||||
`globalAuthor:${author}`, ['name'], name);
|
`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) => {
|
export const 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 get(`globalAuthor:${authorID}`);
|
||||||
|
|
||||||
if (author == null) {
|
if (author == null) {
|
||||||
// author does not exist
|
// author does not exist
|
||||||
|
@ -275,9 +277,9 @@ exports.listPadsOfAuthor = async (authorID: string) => {
|
||||||
* @param {String} authorID The id of the author
|
* @param {String} authorID The id of the author
|
||||||
* @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) => {
|
export const addPad = async (authorID: string, padID: string) => {
|
||||||
// get the entry
|
// get the entry
|
||||||
const author = await db.get(`globalAuthor:${authorID}`);
|
const author = await get(`globalAuthor:${authorID}`);
|
||||||
|
|
||||||
if (author == null) return;
|
if (author == null) return;
|
||||||
|
|
||||||
|
@ -294,7 +296,7 @@ exports.addPad = async (authorID: string, padID: string) => {
|
||||||
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 set(`globalAuthor:${authorID}`, author);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -302,14 +304,14 @@ exports.addPad = async (authorID: string, padID: string) => {
|
||||||
* @param {String} authorID The id of the author
|
* @param {String} authorID The id of the author
|
||||||
* @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) => {
|
export const removePad = async (authorID: string, padID?: string) => {
|
||||||
const author = await db.get(`globalAuthor:${authorID}`);
|
const author = await 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 set(`globalAuthor:${authorID}`, author);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,37 +24,66 @@
|
||||||
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')
|
import {measuredCollection} from '../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
|
||||||
*/
|
*/
|
||||||
exports.db = null;
|
let db: ueberDB.Database|null = 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 () => {
|
export const init = async () => {
|
||||||
exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger);
|
db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger);
|
||||||
await exports.db.init();
|
await db.init();
|
||||||
if (exports.db.metrics != null) {
|
if (db.metrics != null) {
|
||||||
for (const [metric, value] of Object.entries(exports.db.metrics)) {
|
for (const [metric, value] of Object.entries(db.metrics)) {
|
||||||
if (typeof value !== 'number') continue;
|
if (typeof value !== 'number') continue;
|
||||||
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
|
measuredCollection.gauge(`ueberdb_${metric}`, () => db!.metrics[metric]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) => {
|
export const get = async (key: string) => {
|
||||||
|
if (db == null) throw new Error('Database not initialized');
|
||||||
|
return await db.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const set = async (key: string, value: any) => {
|
||||||
|
if (db == null) throw new Error('Database not initialized');
|
||||||
|
return await db.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findKeys = async (key: string, notKey:string|null, callback?: Function) => {
|
||||||
|
if (db == null) throw new Error('Database not initialized');
|
||||||
|
// @ts-ignore
|
||||||
|
return await db.findKeys(key, notKey, callback as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSub = async (key: string, field: string[], callback?: Function) => {
|
||||||
|
if (db == null) throw new Error('Database not initialized');
|
||||||
|
// @ts-ignore
|
||||||
|
return await db.getSub(key, field, callback as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSub = async (key: string, field: string[], value: any, callback?: any, deprecated?: any) => {
|
||||||
|
if (db == null) throw new Error('Database not initialized');
|
||||||
|
// @ts-ignore
|
||||||
|
return await db.setSub(key, field, value, callback, deprecated);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remove = async (key: string, callback?: null) => {
|
||||||
|
if (db == null) throw new Error('Database not initialized');
|
||||||
|
return await db.remove(key, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const 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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,18 +19,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
import CustomError from '../utils/customError';
|
||||||
const randomString = require('../../static/js/pad_utils').randomString;
|
const randomString = require('../../static/js/pad_utils').randomString;
|
||||||
const db = require('./DB');
|
import {get, getSub, remove, set, setSub} from './DB';
|
||||||
const padManager = require('./PadManager');
|
import {doesPadExist, getPad} from './PadManager';
|
||||||
const sessionManager = require('./SessionManager');
|
import {deleteSession} from './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 () => {
|
export const listAllGroups = async (): Promise<{ groupIDs: string[]; }> => {
|
||||||
let groups = await db.get('groups');
|
let groups = await get('groups');
|
||||||
groups = groups || {};
|
groups = groups || {};
|
||||||
|
|
||||||
const groupIDs = Object.keys(groups);
|
const groupIDs = Object.keys(groups);
|
||||||
|
@ -42,8 +42,8 @@ exports.listAllGroups = async () => {
|
||||||
* @param {String} groupID The id of the group
|
* @param {String} groupID The id of the group
|
||||||
* @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> => {
|
export const deleteGroup = async (groupID: string): Promise<void> => {
|
||||||
const group = await db.get(`group:${groupID}`);
|
const group = await get(`group:${groupID}`);
|
||||||
|
|
||||||
// ensure group exists
|
// ensure group exists
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
|
@ -53,28 +53,28 @@ exports.deleteGroup = async (groupID: string): Promise<void> => {
|
||||||
|
|
||||||
// iterate through all pads of this group and delete them (in parallel)
|
// iterate through all pads of this group and delete them (in parallel)
|
||||||
await Promise.all(Object.keys(group.pads).map(async (padId) => {
|
await Promise.all(Object.keys(group.pads).map(async (padId) => {
|
||||||
const pad = await padManager.getPad(padId);
|
const pad = await getPad(padId);
|
||||||
await pad.remove();
|
await pad.remove();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Delete associated sessions in parallel. This should be done before deleting the group2sessions
|
// Delete associated sessions in parallel. This should be done before deleting the group2sessions
|
||||||
// record because deleting a session updates the group2sessions record.
|
// record because deleting a session updates the group2sessions record.
|
||||||
const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {};
|
const {sessionIDs = {}} = await get(`group2sessions:${groupID}`) || {};
|
||||||
await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => {
|
await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => {
|
||||||
await sessionManager.deleteSession(sessionId);
|
await deleteSession(sessionId);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.remove(`group2sessions:${groupID}`),
|
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),
|
setSub('groups', [groupID], undefined),
|
||||||
...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)),
|
...Object.keys(group.mappings || {}).map(async (m) => await 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 remove(`group:${groupID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,9 +82,9 @@ exports.deleteGroup = async (groupID: string): Promise<void> => {
|
||||||
* @param {String} groupID the id of the group to delete
|
* @param {String} groupID the id of the group to delete
|
||||||
* @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) => {
|
export const 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 get(`group:${groupID}`);
|
||||||
|
|
||||||
return (group != null);
|
return (group != null);
|
||||||
};
|
};
|
||||||
|
@ -93,13 +93,13 @@ exports.doesGroupExist = async (groupID: string) => {
|
||||||
* Creates a new group
|
* Creates a new group
|
||||||
* @return {Promise<{groupID: string}>} the id of the new group
|
* @return {Promise<{groupID: string}>} the id of the new group
|
||||||
*/
|
*/
|
||||||
exports.createGroup = async () => {
|
export const createGroup = async (): Promise<{ groupID: string; }> => {
|
||||||
const groupID = `g.${randomString(16)}`;
|
const groupID = `g.${randomString(16)}`;
|
||||||
await db.set(`group:${groupID}`, {pads: {}, mappings: {}});
|
await 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 setSub('groups', [groupID], 1);
|
||||||
return {groupID};
|
return {groupID};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,20 +108,20 @@ 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) => {
|
export const 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 get(`mapper2group:${groupMapper}`) as string;
|
||||||
if (groupID && await exports.doesGroupExist(groupID)) return {groupID};
|
if (groupID && await doesGroupExist(groupID)) return {groupID};
|
||||||
const result = await exports.createGroup();
|
const result = await createGroup();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.set(`mapper2group:${groupMapper}`, result.groupID),
|
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),
|
setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1),
|
||||||
]);
|
]);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@ -134,19 +134,19 @@ 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; }> => {
|
export const createGroupPad = async (groupID: string, padName: string, text: string, authorId: string = ''): Promise<{ padID: string; }> => {
|
||||||
// create the padID
|
// create the padID
|
||||||
const padID = `${groupID}$${padName}`;
|
const padID = `${groupID}$${padName}`;
|
||||||
|
|
||||||
// ensure group exists
|
// ensure group exists
|
||||||
const groupExists = await exports.doesGroupExist(groupID);
|
const groupExists = await 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 doesPadExist(padID);
|
||||||
|
|
||||||
if (padExists) {
|
if (padExists) {
|
||||||
// pad exists already
|
// pad exists already
|
||||||
|
@ -154,10 +154,10 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the pad
|
// create the pad
|
||||||
await padManager.getPad(padID, text, authorId);
|
await 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 setSub(`group:${groupID}`, ['pads', padID], 1);
|
||||||
|
|
||||||
return {padID};
|
return {padID};
|
||||||
};
|
};
|
||||||
|
@ -167,8 +167,8 @@ 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[]; }> => {
|
export const listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => {
|
||||||
const exists = await exports.doesGroupExist(groupID);
|
const exists = await doesGroupExist(groupID);
|
||||||
|
|
||||||
// ensure the group exists
|
// ensure the group exists
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
|
@ -176,7 +176,7 @@ exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 getSub(`group:${groupID}`, ['pads']);
|
||||||
const padIDs = Object.keys(result);
|
const padIDs = Object.keys(result);
|
||||||
|
|
||||||
return {padIDs};
|
return {padIDs};
|
||||||
|
|
|
@ -8,23 +8,26 @@ import {MapArrayType} from "../types/MapType";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {applyToAText, checkRep, copyAText, deserializeOps, makeAText, makeSplice, opsFromAText, pack, unpack} from '../../static/js/Changeset';
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
import ChatMessage from '../../static/js/ChatMessage';
|
||||||
import AttributePool from '../../static/js/AttributePool';
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
const Stream = require('../utils/Stream');
|
import Stream from '../utils/Stream';
|
||||||
const assert = require('assert').strict;
|
const assert = require('assert').strict;
|
||||||
const db = require('./DB');
|
import {get, set, setSub, remove} from './DB';
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
const authorManager = require('./AuthorManager');
|
import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager';
|
||||||
const padManager = require('./PadManager');
|
import {doesPadExist, getPad} from './PadManager';
|
||||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
import {kickSessionsFromPad} from '../handler/PadMessageHandler';
|
||||||
const groupManager = require('./GroupManager');
|
import {doesGroupExist} from './GroupManager';
|
||||||
const CustomError = require('../utils/customError');
|
import CustomError from '../utils/customError';
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
import {getReadOnlyId} from './ReadOnlyManager';
|
||||||
const randomString = require('../utils/randomstring');
|
import {randomString} from '../utils/randomstring';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
import {aCallAll} from '../../static/js/pluginfw/hooks';
|
||||||
import {padUtils} from "../../static/js/pad_utils";
|
import {padUtils} from "../../static/js/pad_utils";
|
||||||
const promises = require('../utils/promises');
|
import {PadRevision} from "../../static/js/types/PadRevision";
|
||||||
|
import {} from '../utils/promises';
|
||||||
|
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
|
||||||
|
import {timesLimit} from "async";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
||||||
|
@ -32,19 +35,18 @@ const promises = require('../utils/promises');
|
||||||
* @param {String} txt The text to clean
|
* @param {String} txt The text to clean
|
||||||
* @returns {String} The cleaned text
|
* @returns {String} The cleaned text
|
||||||
*/
|
*/
|
||||||
exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
|
export const cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
|
||||||
.replace(/\r/g, '\n')
|
.replace(/\r/g, '\n')
|
||||||
.replace(/\t/g, ' ')
|
.replace(/\t/g, ' ')
|
||||||
.replace(/\xa0/g, ' ');
|
.replace(/\xa0/g, ' ');
|
||||||
|
|
||||||
class Pad {
|
class Pad {
|
||||||
private db: Database;
|
atext: AText;
|
||||||
private atext: AText;
|
pool: AttributePool;
|
||||||
private pool: AttributePool;
|
head: number;
|
||||||
private head: number;
|
chatHead: number;
|
||||||
private chatHead: number;
|
|
||||||
private publicStatus: boolean;
|
private publicStatus: boolean;
|
||||||
private id: string;
|
id: string;
|
||||||
private savedRevisions: any[];
|
private savedRevisions: any[];
|
||||||
/**
|
/**
|
||||||
* @param id
|
* @param id
|
||||||
|
@ -54,9 +56,8 @@ class Pad {
|
||||||
* can be used to shard pad storage across multiple database backends, to put each pad in its
|
* can be used to shard pad storage across multiple database backends, to put each pad in its
|
||||||
* own database table, or to validate imported pad data before it is written to the database.
|
* own database table, or to validate imported pad data before it is written to the database.
|
||||||
*/
|
*/
|
||||||
constructor(id:string, database = db) {
|
constructor(id:string) {
|
||||||
this.db = database;
|
this.atext = makeAText('\n');
|
||||||
this.atext = Changeset.makeAText('\n');
|
|
||||||
this.pool = new AttributePool();
|
this.pool = new AttributePool();
|
||||||
this.head = -1;
|
this.head = -1;
|
||||||
this.chatHead = -1;
|
this.chatHead = -1;
|
||||||
|
@ -93,13 +94,13 @@ class Pad {
|
||||||
* @param {String} authorId The id of the author
|
* @param {String} authorId The id of the author
|
||||||
* @return {Promise<number|string>}
|
* @return {Promise<number|string>}
|
||||||
*/
|
*/
|
||||||
async appendRevision(aChangeset:AChangeSet, authorId = '') {
|
async appendRevision(aChangeset:string, authorId: string = ''): Promise<number | string> {
|
||||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
const newAText = applyToAText(aChangeset, this.atext, this.pool);
|
||||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
||||||
this.head !== -1) {
|
this.head !== -1) {
|
||||||
return this.head;
|
return this.head;
|
||||||
}
|
}
|
||||||
Changeset.copyAText(newAText, this.atext);
|
copyAText(newAText, this.atext);
|
||||||
|
|
||||||
const newRev = ++this.head;
|
const newRev = ++this.head;
|
||||||
|
|
||||||
|
@ -121,16 +122,17 @@ class Pad {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.saveToDatabase(),
|
this.saveToDatabase(),
|
||||||
authorId && authorManager.addPad(authorId, this.id),
|
authorId && addPad(authorId, this.id),
|
||||||
hooks.aCallAll(hook, {
|
aCallAll(hook, {
|
||||||
pad: this,
|
pad: this,
|
||||||
authorId,
|
authorId,
|
||||||
get author() {
|
get author() {
|
||||||
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||||
return this.authorId;
|
return authorId;
|
||||||
},
|
},
|
||||||
set author(authorId) {
|
set author(authorId) {
|
||||||
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||||
|
// @ts-ignore
|
||||||
this.authorId = authorId;
|
this.authorId = authorId;
|
||||||
},
|
},
|
||||||
...this.head === 0 ? {} : {
|
...this.head === 0 ? {} : {
|
||||||
|
@ -192,8 +194,8 @@ class Pad {
|
||||||
* Returns all authors that worked on this pad
|
* Returns all authors that worked on this pad
|
||||||
* @return {[String]} The id of authors who contributed to this pad
|
* @return {[String]} The id of authors who contributed to this pad
|
||||||
*/
|
*/
|
||||||
getAllAuthors() {
|
getAllAuthors(): string[] {
|
||||||
const authorIds = [];
|
const authorIds: string[] = [];
|
||||||
|
|
||||||
for (const key in this.pool.numToAttrib) {
|
for (const key in this.pool.numToAttrib) {
|
||||||
if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') {
|
if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') {
|
||||||
|
@ -215,22 +217,23 @@ class Pad {
|
||||||
]);
|
]);
|
||||||
const apool = this.apool();
|
const apool = this.apool();
|
||||||
let atext = keyAText;
|
let atext = keyAText;
|
||||||
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
|
for (const cs of changesets) atext = applyToAText(cs, atext, apool);
|
||||||
return atext;
|
return atext;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRevision(revNum: number) {
|
async getRevision(revNum: number) {
|
||||||
return await this.db.get(`pad:${this.id}:revs:${revNum}`);
|
return await get(`pad:${this.id}:revs:${revNum}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllAuthorColors() {
|
async getAllAuthorColors() {
|
||||||
const authorIds = this.getAllAuthors();
|
const authorIds = this.getAllAuthors();
|
||||||
const returnTable:MapArrayType<string> = {};
|
const returnTable:MapArrayType<number> = {};
|
||||||
const colorPalette = authorManager.getColorPalette();
|
const colorPalette = getColorPalette();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId:string) => {
|
authorIds.map((authorId) => getAuthorColorId(authorId).then((colorId) => {
|
||||||
// colorId might be a hex color or an number out of the palette
|
// colorId might be a hex color or an number out of the palette
|
||||||
|
// @ts-ignore
|
||||||
returnTable[authorId] = colorPalette[colorId] || colorId;
|
returnTable[authorId] = colorPalette[colorId] || colorId;
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
@ -293,7 +296,7 @@ class Pad {
|
||||||
(!ins && start > 0 && orig[start - 1] === '\n');
|
(!ins && start > 0 && orig[start - 1] === '\n');
|
||||||
if (!willEndWithNewline) ins += '\n';
|
if (!willEndWithNewline) ins += '\n';
|
||||||
if (ndel === 0 && ins.length === 0) return;
|
if (ndel === 0 && ins.length === 0) return;
|
||||||
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
|
const changeset = makeSplice(orig, start, ndel, ins);
|
||||||
await this.appendRevision(changeset, authorId);
|
await this.appendRevision(changeset, authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,7 +319,7 @@ class Pad {
|
||||||
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
* @param {string} [authorId] - The author ID of the user that initiated the change, if
|
||||||
* applicable.
|
* applicable.
|
||||||
*/
|
*/
|
||||||
async appendText(newText:string, authorId = '') {
|
async appendText(newText:string, authorId: string = '') {
|
||||||
await this.spliceText(this.text().length - 1, 0, newText, authorId);
|
await this.spliceText(this.text().length - 1, 0, newText, authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +333,7 @@ class Pad {
|
||||||
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
||||||
* `msgOrText.time` instead.
|
* `msgOrText.time` instead.
|
||||||
*/
|
*/
|
||||||
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
|
async appendChatMessage(msgOrText: string| ChatMessage, authorId = null, time = null) {
|
||||||
const msg =
|
const msg =
|
||||||
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
||||||
this.chatHead++;
|
this.chatHead++;
|
||||||
|
@ -338,6 +341,7 @@ class Pad {
|
||||||
// Don't save the display name in the database because the user can change it at any time. The
|
// Don't save the display name in the database because the user can change it at any time. The
|
||||||
// `displayName` property will be populated with the current value when the message is read
|
// `displayName` property will be populated with the current value when the message is read
|
||||||
// from the database.
|
// from the database.
|
||||||
|
// @ts-ignore
|
||||||
this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}),
|
this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}),
|
||||||
this.saveToDatabase(),
|
this.saveToDatabase(),
|
||||||
]);
|
]);
|
||||||
|
@ -347,11 +351,11 @@ class Pad {
|
||||||
* @param {number} entryNum - ID of the desired chat message.
|
* @param {number} entryNum - ID of the desired chat message.
|
||||||
* @returns {?ChatMessage}
|
* @returns {?ChatMessage}
|
||||||
*/
|
*/
|
||||||
async getChatMessage(entryNum: number) {
|
async getChatMessage(entryNum: number): Promise<ChatMessage | null> {
|
||||||
const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`);
|
const entry = await get(`pad:${this.id}:chat:${entryNum}`);
|
||||||
if (entry == null) return null;
|
if (entry == null) return null;
|
||||||
const message = ChatMessage.fromObject(entry);
|
const message = ChatMessage.fromObject(entry);
|
||||||
message.displayName = await authorManager.getAuthorName(message.authorId);
|
message.displayName = await getAuthorName(message.authorId!);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,7 +366,7 @@ class Pad {
|
||||||
* (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open
|
* (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open
|
||||||
* interval as is typical in code.
|
* interval as is typical in code.
|
||||||
*/
|
*/
|
||||||
async getChatMessages(start: string, end: number) {
|
async getChatMessages(start: number, end: number) {
|
||||||
const entries =
|
const entries =
|
||||||
await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this)));
|
await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this)));
|
||||||
|
|
||||||
|
@ -378,9 +382,9 @@ class Pad {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(text:string, authorId = '') {
|
async init(text:string|null, authorId = '') {
|
||||||
// try to load the pad
|
// try to load the pad
|
||||||
const value = await this.db.get(`pad:${this.id}`);
|
const value = await get(`pad:${this.id}`);
|
||||||
|
|
||||||
// if this pad exists, load it
|
// if this pad exists, load it
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
@ -389,14 +393,14 @@ class Pad {
|
||||||
} else {
|
} else {
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
|
const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
|
||||||
await hooks.aCallAll('padDefaultContent', context);
|
await aCallAll('padDefaultContent', context);
|
||||||
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
||||||
text = exports.cleanText(context.content);
|
text = exports.cleanText(context.content);
|
||||||
}
|
}
|
||||||
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
|
const firstChangeset = makeSplice('\n', 0, 0, text);
|
||||||
await this.appendRevision(firstChangeset, authorId);
|
await this.appendRevision(firstChangeset, authorId);
|
||||||
}
|
}
|
||||||
await hooks.aCallAll('padLoad', {pad: this});
|
await aCallAll('padLoad', {pad: this});
|
||||||
}
|
}
|
||||||
|
|
||||||
async copy(destinationID: string, force: boolean) {
|
async copy(destinationID: string, force: boolean) {
|
||||||
|
@ -415,8 +419,8 @@ class Pad {
|
||||||
await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
|
await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
|
||||||
|
|
||||||
const copyRecord = async (keySuffix: string) => {
|
const copyRecord = async (keySuffix: string) => {
|
||||||
const val = await this.db.get(`pad:${this.id}${keySuffix}`);
|
const val = await get(`pad:${this.id}${keySuffix}`);
|
||||||
await db.set(`pad:${destinationID}${keySuffix}`, val);
|
await set(`pad:${destinationID}${keySuffix}`, val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises = (function* () {
|
const promises = (function* () {
|
||||||
|
@ -427,22 +431,24 @@ class Pad {
|
||||||
yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`));
|
yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`));
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
yield this.copyAuthorInfoToDestinationPad(destinationID);
|
yield this.copyAuthorInfoToDestinationPad(destinationID);
|
||||||
if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
|
if (destGroupID) yield setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
|
||||||
}).call(this);
|
}).call(this);
|
||||||
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
|
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
|
||||||
|
|
||||||
// Initialize the new pad (will update the listAllPads cache)
|
// Initialize the new pad (will update the listAllPads cache)
|
||||||
const dstPad = await padManager.getPad(destinationID, null);
|
const dstPad = await getPad(destinationID, null);
|
||||||
|
|
||||||
// let the plugins know the pad was copied
|
// let the plugins know the pad was copied
|
||||||
await hooks.aCallAll('padCopy', {
|
await aCallAll('padCopy', {
|
||||||
get originalPad() {
|
get originalPad() {
|
||||||
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||||
|
// @ts-ignore
|
||||||
return this.srcPad;
|
return this.srcPad;
|
||||||
},
|
},
|
||||||
get destinationID() {
|
get destinationID() {
|
||||||
padUtils.warnDeprecated(
|
padUtils.warnDeprecated(
|
||||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||||
|
// @ts-ignore
|
||||||
return this.dstPad.id;
|
return this.dstPad.id;
|
||||||
},
|
},
|
||||||
srcPad: this,
|
srcPad: this,
|
||||||
|
@ -457,7 +463,7 @@ class Pad {
|
||||||
|
|
||||||
if (destinationID.indexOf('$') >= 0) {
|
if (destinationID.indexOf('$') >= 0) {
|
||||||
destGroupID = destinationID.split('$')[0];
|
destGroupID = destinationID.split('$')[0];
|
||||||
const groupExists = await groupManager.doesGroupExist(destGroupID);
|
const groupExists = await doesGroupExist(destGroupID);
|
||||||
|
|
||||||
// group does not exist
|
// group does not exist
|
||||||
if (!groupExists) {
|
if (!groupExists) {
|
||||||
|
@ -469,7 +475,7 @@ class Pad {
|
||||||
|
|
||||||
async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) {
|
async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) {
|
||||||
// if the pad exists, we should abort, unless forced.
|
// if the pad exists, we should abort, unless forced.
|
||||||
const exists = await padManager.doesPadExist(destinationID);
|
const exists = await doesPadExist(destinationID);
|
||||||
|
|
||||||
// allow force to be a string
|
// allow force to be a string
|
||||||
if (typeof force === 'string') {
|
if (typeof force === 'string') {
|
||||||
|
@ -485,7 +491,7 @@ class Pad {
|
||||||
}
|
}
|
||||||
|
|
||||||
// exists and forcing
|
// exists and forcing
|
||||||
const pad = await padManager.getPad(destinationID);
|
const pad = await getPad(destinationID);
|
||||||
await pad.remove();
|
await pad.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -493,7 +499,7 @@ class Pad {
|
||||||
async copyAuthorInfoToDestinationPad(destinationID: string) {
|
async copyAuthorInfoToDestinationPad(destinationID: string) {
|
||||||
// add the new sourcePad to all authors who contributed to the old one
|
// add the new sourcePad to all authors who contributed to the old one
|
||||||
await Promise.all(this.getAllAuthors().map(
|
await Promise.all(this.getAllAuthors().map(
|
||||||
(authorID) => authorManager.addPad(authorID, destinationID)));
|
(authorID) => addPad(authorID, destinationID)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') {
|
async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') {
|
||||||
|
@ -510,18 +516,18 @@ class Pad {
|
||||||
|
|
||||||
// Group pad? Add it to the group's list
|
// Group pad? Add it to the group's list
|
||||||
if (destGroupID) {
|
if (destGroupID) {
|
||||||
await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
|
await setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize the pad with a new line to avoid getting the defaultText
|
// initialize the pad with a new line to avoid getting the defaultText
|
||||||
const dstPad = await padManager.getPad(destinationID, '\n', authorId);
|
const dstPad = await getPad(destinationID, '\n', authorId);
|
||||||
dstPad.pool = this.pool.clone();
|
dstPad.pool = this.pool.clone();
|
||||||
|
|
||||||
const oldAText = this.atext;
|
const oldAText = this.atext;
|
||||||
|
|
||||||
// based on Changeset.makeSplice
|
// based on Changeset.makeSplice
|
||||||
const assem = Changeset.smartOpAssembler();
|
const assem = new SmartOpAssembler();
|
||||||
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
|
for (const op of opsFromAText(oldAText)) assem.append(op);
|
||||||
assem.endDocument();
|
assem.endDocument();
|
||||||
|
|
||||||
// although we have instantiated the dstPad with '\n', an additional '\n' is
|
// although we have instantiated the dstPad with '\n', an additional '\n' is
|
||||||
|
@ -533,17 +539,19 @@ class Pad {
|
||||||
|
|
||||||
// create a changeset that removes the previous text and add the newText with
|
// create a changeset that removes the previous text and add the newText with
|
||||||
// all atributes present on the source pad
|
// all atributes present on the source pad
|
||||||
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
|
const changeset = pack(oldLength, newLength, assem.toString(), newText);
|
||||||
dstPad.appendRevision(changeset, authorId);
|
await dstPad.appendRevision(changeset, authorId);
|
||||||
|
|
||||||
await hooks.aCallAll('padCopy', {
|
await aCallAll('padCopy', {
|
||||||
get originalPad() {
|
get originalPad() {
|
||||||
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||||
|
// @ts-ignore
|
||||||
return this.srcPad;
|
return this.srcPad;
|
||||||
},
|
},
|
||||||
get destinationID() {
|
get destinationID() {
|
||||||
padUtils.warnDeprecated(
|
padUtils.warnDeprecated(
|
||||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||||
|
// @ts-ignore
|
||||||
return this.dstPad.id;
|
return this.dstPad.id;
|
||||||
},
|
},
|
||||||
srcPad: this,
|
srcPad: this,
|
||||||
|
@ -558,7 +566,7 @@ class Pad {
|
||||||
const p = [];
|
const p = [];
|
||||||
|
|
||||||
// kick everyone from this pad
|
// kick everyone from this pad
|
||||||
padMessageHandler.kickSessionsFromPad(padID);
|
kickSessionsFromPad(padID);
|
||||||
|
|
||||||
// delete all relations - the original code used async.parallel but
|
// delete all relations - the original code used async.parallel but
|
||||||
// none of the operations except getting the group depended on callbacks
|
// none of the operations except getting the group depended on callbacks
|
||||||
|
@ -569,41 +577,44 @@ class Pad {
|
||||||
if (padID.indexOf('$') >= 0) {
|
if (padID.indexOf('$') >= 0) {
|
||||||
// it is a group pad
|
// it is a group pad
|
||||||
const groupID = padID.substring(0, padID.indexOf('$'));
|
const groupID = padID.substring(0, padID.indexOf('$'));
|
||||||
const group = await db.get(`group:${groupID}`);
|
const group = await get(`group:${groupID}`);
|
||||||
|
|
||||||
// remove the pad entry
|
// remove the pad entry
|
||||||
delete group.pads[padID];
|
delete group.pads[padID];
|
||||||
|
|
||||||
// set the new value
|
// set the new value
|
||||||
p.push(db.set(`group:${groupID}`, group));
|
p.push(set(`group:${groupID}`, group));
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the readonly entries
|
// remove the readonly entries
|
||||||
p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => {
|
p.push(getReadOnlyId(padID).then(async (readonlyID: string) => {
|
||||||
await db.remove(`readonly2pad:${readonlyID}`);
|
await remove(`readonly2pad:${readonlyID}`);
|
||||||
}));
|
}));
|
||||||
p.push(db.remove(`pad2readonly:${padID}`));
|
p.push(remove(`pad2readonly:${padID}`));
|
||||||
|
|
||||||
// delete all chat messages
|
// delete all chat messages
|
||||||
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
// @ts-ignore
|
||||||
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
|
p.push(timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
||||||
|
await remove(`pad:${this.id}:chat:${i}`, null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// delete all revisions
|
// delete all revisions
|
||||||
p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => {
|
// @ts-ignore
|
||||||
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
|
p.push(timesLimit(this.head + 1, 500, async (i: string) => {
|
||||||
|
await remove(`pad:${this.id}:revs:${i}`, null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// remove pad from all authors who contributed
|
// remove pad from all authors who contributed
|
||||||
this.getAllAuthors().forEach((authorId) => {
|
this.getAllAuthors().forEach((authorId) => {
|
||||||
p.push(authorManager.removePad(authorId, padID));
|
p.push(removePad(authorId, padID));
|
||||||
});
|
});
|
||||||
|
|
||||||
// delete the pad entry and delete pad from padManager
|
// delete the pad entry and delete pad from padManager
|
||||||
p.push(padManager.removePad(padID));
|
p.push(removePad(padID));
|
||||||
p.push(hooks.aCallAll('padRemove', {
|
p.push(aCallAll('padRemove', {
|
||||||
get padID() {
|
get padID() {
|
||||||
padUtils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
padUtils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||||
|
// @ts-ignore
|
||||||
return this.pad.id;
|
return this.pad.id;
|
||||||
},
|
},
|
||||||
pad: this,
|
pad: this,
|
||||||
|
@ -617,7 +628,7 @@ class Pad {
|
||||||
await this.saveToDatabase();
|
await this.saveToDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSavedRevision(revNum: string, savedById: string, label: string) {
|
async addSavedRevision(revNum: number, savedById: string, label?: string) {
|
||||||
// if this revision is already saved, return silently
|
// if this revision is already saved, return silently
|
||||||
for (const i in this.savedRevisions) {
|
for (const i in this.savedRevisions) {
|
||||||
if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) {
|
if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) {
|
||||||
|
@ -626,12 +637,13 @@ class Pad {
|
||||||
}
|
}
|
||||||
|
|
||||||
// build the saved revision object
|
// build the saved revision object
|
||||||
const savedRevision:MapArrayType<any> = {};
|
const savedRevision:PadRevision = {
|
||||||
savedRevision.revNum = revNum;
|
revNum,
|
||||||
savedRevision.savedById = savedById;
|
savedById,
|
||||||
savedRevision.label = label || `Revision ${revNum}`;
|
label: label || `Revision ${revNum}`,
|
||||||
savedRevision.timestamp = Date.now();
|
timestamp: Date.now(),
|
||||||
savedRevision.id = randomString(10);
|
id: randomString(10),
|
||||||
|
};
|
||||||
|
|
||||||
// save this new saved revision
|
// save this new saved revision
|
||||||
this.savedRevisions.push(savedRevision);
|
this.savedRevisions.push(savedRevision);
|
||||||
|
@ -706,7 +718,7 @@ class Pad {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.batch(100).buffer(99);
|
.batch(100).buffer(99);
|
||||||
let atext = Changeset.makeAText('\n');
|
let atext = makeAText('\n');
|
||||||
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
||||||
try {
|
try {
|
||||||
assert(authorId != null);
|
assert(authorId != null);
|
||||||
|
@ -717,10 +729,10 @@ class Pad {
|
||||||
assert(timestamp > 0);
|
assert(timestamp > 0);
|
||||||
assert(changeset != null);
|
assert(changeset != null);
|
||||||
assert.equal(typeof changeset, 'string');
|
assert.equal(typeof changeset, 'string');
|
||||||
Changeset.checkRep(changeset);
|
checkRep(changeset);
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
let text = atext.text;
|
let text = atext.text;
|
||||||
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
for (const op of deserializeOps(unpacked.ops)) {
|
||||||
if (['=', '-'].includes(op.opcode)) {
|
if (['=', '-'].includes(op.opcode)) {
|
||||||
assert(text.length >= op.chars);
|
assert(text.length >= op.chars);
|
||||||
const consumed = text.slice(0, op.chars);
|
const consumed = text.slice(0, op.chars);
|
||||||
|
@ -731,7 +743,7 @@ class Pad {
|
||||||
}
|
}
|
||||||
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
|
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
|
||||||
}
|
}
|
||||||
atext = Changeset.applyToAText(changeset, atext, pool);
|
atext = applyToAText(changeset, atext, pool);
|
||||||
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
||||||
} catch (err:any) {
|
} catch (err:any) {
|
||||||
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
||||||
|
@ -759,7 +771,7 @@ class Pad {
|
||||||
.batch(100).buffer(99);
|
.batch(100).buffer(99);
|
||||||
for (const p of chats) await p;
|
for (const p of chats) await p;
|
||||||
|
|
||||||
await hooks.aCallAll('padCheck', {pad: this});
|
await aCallAll('padCheck', {pad: this});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.Pad = Pad;
|
export default Pad
|
||||||
|
|
|
@ -20,11 +20,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
import {PadType} from "../types/PadType";
|
import CustomError from '../utils/customError';
|
||||||
|
import Pad from '../db/Pad';
|
||||||
const CustomError = require('../utils/customError');
|
import {findKeys, get, remove} from './DB';
|
||||||
const Pad = require('../db/Pad');
|
|
||||||
const db = require('./DB');
|
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,7 +72,7 @@ const padList = new class {
|
||||||
async getPads() {
|
async getPads() {
|
||||||
if (!this._loaded) {
|
if (!this._loaded) {
|
||||||
this._loaded = (async () => {
|
this._loaded = (async () => {
|
||||||
const dbData = await db.findKeys('pad:*', '*:*:*');
|
const dbData = await 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:/, ''));
|
||||||
})();
|
})();
|
||||||
|
@ -106,9 +104,9 @@ 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> => {
|
export const getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise<Pad> => {
|
||||||
// check if this is a valid padId
|
// check if this is a valid padId
|
||||||
if (!exports.isValidPadId(id)) {
|
if (!isValidPadId(id)) {
|
||||||
throw new CustomError(`${id} is not a valid padId`, 'apierror');
|
throw new CustomError(`${id} is not a valid padId`, 'apierror');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +131,7 @@ exports.getPad = async (id: string, text?: string|null, authorId:string|null = '
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to load pad
|
// try to load pad
|
||||||
pad = new Pad.Pad(id);
|
pad = new Pad(id);
|
||||||
|
|
||||||
// initialize the pad
|
// initialize the pad
|
||||||
await pad.init(text, authorId);
|
await pad.init(text, authorId);
|
||||||
|
@ -143,7 +141,7 @@ exports.getPad = async (id: string, text?: string|null, authorId:string|null = '
|
||||||
return pad;
|
return pad;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.listAllPads = async () => {
|
export const listAllPads = async () => {
|
||||||
const padIDs = await padList.getPads();
|
const padIDs = await padList.getPads();
|
||||||
|
|
||||||
return {padIDs};
|
return {padIDs};
|
||||||
|
@ -153,14 +151,13 @@ exports.listAllPads = async () => {
|
||||||
|
|
||||||
|
|
||||||
// checks if a pad exists
|
// checks if a pad exists
|
||||||
exports.doesPadExist = async (padId: string) => {
|
// alias for backwards compatibility
|
||||||
const value = await db.get(`pad:${padId}`);
|
export const doesPadExist = async (padId: string) => {
|
||||||
|
const value = await get(`pad:${padId}`);
|
||||||
|
|
||||||
return (value != null && value.atext);
|
return (value != null && value.atext);
|
||||||
};
|
};
|
||||||
|
|
||||||
// alias for backwards compatibility
|
|
||||||
exports.doesPadExists = exports.doesPadExist;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of padId transformations. These represent changes in pad name policy over
|
* An array of padId transformations. These represent changes in pad name policy over
|
||||||
|
@ -172,9 +169,9 @@ const padIdTransforms = [
|
||||||
];
|
];
|
||||||
|
|
||||||
// returns a sanitized padId, respecting legacy pad id formats
|
// returns a sanitized padId, respecting legacy pad id formats
|
||||||
exports.sanitizePadId = async (padId: string) => {
|
export const 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 doesPadExist(padId);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return padId;
|
return padId;
|
||||||
|
@ -192,19 +189,19 @@ exports.sanitizePadId = async (padId: string) => {
|
||||||
return padId;
|
return padId;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.isValidPadId = (padId: string) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
|
export const 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) => {
|
export const removePad = async (padId: string) => {
|
||||||
const p = db.remove(`pad:${padId}`);
|
const p = await remove(`pad:${padId}`);
|
||||||
exports.unloadPad(padId);
|
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) => {
|
export const unloadPad = (padId: string) => {
|
||||||
globalPads.remove(padId);
|
globalPads.remove(padId);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
const db = require('./DB');
|
import {get, set} from './DB';
|
||||||
const randomString = require('../utils/randomstring');
|
import {randomString} from '../utils/randomstring';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,23 +29,23 @@ const randomString = require('../utils/randomstring');
|
||||||
* @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.');
|
export const isReadOnlyId = (id:string): boolean => 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) => {
|
export const getReadOnlyId = async (padId:string): Promise<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 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),
|
set(`pad2readonly:${padId}`, readOnlyId),
|
||||||
db.set(`readonly2pad:${readOnlyId}`, padId),
|
set(`readonly2pad:${readOnlyId}`, padId),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,19 +57,23 @@ 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}`);
|
export const getPadId = async (readOnlyId:string): Promise<string> => await get(`readonly2pad:${readOnlyId}`) as string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) => {
|
export const getIds = async (id:string): Promise<{
|
||||||
const readonly = exports.isReadOnlyId(id);
|
readOnlyPadId: string,
|
||||||
|
padId: string,
|
||||||
|
readonly: boolean
|
||||||
|
}> => {
|
||||||
|
const readonly = 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 getReadOnlyId(id);
|
||||||
const padId = readonly ? await exports.getPadId(id) : id;
|
const padId = readonly ? await getPadId(id) : id;
|
||||||
|
|
||||||
return {readOnlyPadId, padId, readonly};
|
return {readOnlyPadId, padId, readonly};
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,13 +21,13 @@
|
||||||
|
|
||||||
import {UserSettingsObject} from "../types/UserSettingsObject";
|
import {UserSettingsObject} from "../types/UserSettingsObject";
|
||||||
|
|
||||||
const authorManager = require('./AuthorManager');
|
import {getAuthorId} from './AuthorManager';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
import {callAll} from '../../static/js/pluginfw/hooks.js';
|
||||||
const padManager = require('./PadManager');
|
import {doesPadExist, getPad} from './PadManager';
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
import {getPadId, isReadOnlyId} from './ReadOnlyManager';
|
||||||
const sessionManager = require('./SessionManager');
|
import {findAuthorID} from './SessionManager';
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
const webaccess = require('../hooks/express/webaccess');
|
import {normalizeAuthzLevel} from '../hooks/express/webaccess';
|
||||||
const log4js = require('log4js');
|
const log4js = require('log4js');
|
||||||
const authLogger = log4js.getLogger('auth');
|
const authLogger = log4js.getLogger('auth');
|
||||||
import {padUtils as padutils} from '../../static/js/pad_utils';
|
import {padUtils as padutils} from '../../static/js/pad_utils';
|
||||||
|
@ -57,7 +57,14 @@ const DENY = Object.freeze({accessStatus: 'deny'});
|
||||||
* @param {Object} userSettings
|
* @param {Object} userSettings
|
||||||
* @return {DENY|{accessStatus: String, authorID: String}}
|
* @return {DENY|{accessStatus: String, authorID: String}}
|
||||||
*/
|
*/
|
||||||
exports.checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject) => {
|
|
||||||
|
|
||||||
|
type CheckAccessStat = {
|
||||||
|
accessStatus: string;
|
||||||
|
authorID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject): Promise<CheckAccessStat> => {
|
||||||
if (!padID) {
|
if (!padID) {
|
||||||
authLogger.debug('access denied: missing padID');
|
authLogger.debug('access denied: missing padID');
|
||||||
return DENY;
|
return DENY;
|
||||||
|
@ -65,9 +72,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
|
|
||||||
let canCreate = !settings.editOnly;
|
let canCreate = !settings.editOnly;
|
||||||
|
|
||||||
if (readOnlyManager.isReadOnlyId(padID)) {
|
if (isReadOnlyId(padID)) {
|
||||||
canCreate = false;
|
canCreate = false;
|
||||||
padID = await readOnlyManager.getPadId(padID);
|
padID = await getPadId(padID);
|
||||||
if (padID == null) {
|
if (padID == null) {
|
||||||
authLogger.debug('access denied: read-only pad ID for a pad that does not exist');
|
authLogger.debug('access denied: read-only pad ID for a pad that does not exist');
|
||||||
return DENY;
|
return DENY;
|
||||||
|
@ -88,7 +95,7 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
// Note: userSettings.padAuthorizations should still be populated even if
|
// Note: userSettings.padAuthorizations should still be populated even if
|
||||||
// settings.requireAuthorization is false.
|
// settings.requireAuthorization is false.
|
||||||
const padAuthzs = userSettings.padAuthorizations || {};
|
const padAuthzs = userSettings.padAuthorizations || {};
|
||||||
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
|
const level = normalizeAuthzLevel(padAuthzs[padID]);
|
||||||
if (!level) {
|
if (!level) {
|
||||||
authLogger.debug('access denied: unauthorized');
|
authLogger.debug('access denied: unauthorized');
|
||||||
return DENY;
|
return DENY;
|
||||||
|
@ -98,18 +105,18 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
|
|
||||||
// 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 (callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
|
||||||
authLogger.debug('access denied: an onAccessCheck hook function returned false');
|
authLogger.debug('access denied: an onAccessCheck hook function returned false');
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const padExists = await padManager.doesPadExist(padID);
|
const padExists = await doesPadExist(padID);
|
||||||
if (!padExists && !canCreate) {
|
if (!padExists && !canCreate) {
|
||||||
authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
|
authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
|
||||||
return DENY;
|
return DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie);
|
const sessionAuthorID = await findAuthorID(padID.split('$')[0], sessionCookie);
|
||||||
if (settings.requireSession && !sessionAuthorID) {
|
if (settings.requireSession && !sessionAuthorID) {
|
||||||
authLogger.debug('access denied: HTTP API session is required');
|
authLogger.debug('access denied: HTTP API session is required');
|
||||||
return DENY;
|
return DENY;
|
||||||
|
@ -122,7 +129,7 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
|
|
||||||
const grant = {
|
const grant = {
|
||||||
accessStatus: 'grant',
|
accessStatus: 'grant',
|
||||||
authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings),
|
authorID: sessionAuthorID || await getAuthorId(token, userSettings),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!padID.includes('$')) {
|
if (!padID.includes('$')) {
|
||||||
|
@ -139,7 +146,7 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u
|
||||||
return grant;
|
return grant;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pad = await padManager.getPad(padID);
|
const pad = await getPad(padID);
|
||||||
|
|
||||||
if (!pad.getPublicStatus() && sessionAuthorID == null) {
|
if (!pad.getPublicStatus() && sessionAuthorID == null) {
|
||||||
authLogger.debug('access denied: must have an HTTP API session to access private group pads');
|
authLogger.debug('access denied: must have an HTTP API session to access private group pads');
|
||||||
|
|
|
@ -20,12 +20,25 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
import CustomError from '../utils/customError';
|
||||||
const promises = require('../utils/promises');
|
import {firstSatisfies} from '../utils/promises';
|
||||||
const randomString = require('../utils/randomstring');
|
import {randomString} from '../utils/randomstring';
|
||||||
const db = require('./DB');
|
import {get, remove, set, setSub} from './DB';
|
||||||
const groupManager = require('./GroupManager');
|
import {doesGroupExist} from './GroupManager';
|
||||||
const authorManager = require('./AuthorManager');
|
import {doesAuthorExist} from './AuthorManager';
|
||||||
|
|
||||||
|
|
||||||
|
type Session = {
|
||||||
|
groupID: string;
|
||||||
|
authorID: string;
|
||||||
|
validUntil: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Author2Sessions = {
|
||||||
|
sessionIDs: {
|
||||||
|
[key: string]: Session;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,7 +49,7 @@ 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) => {
|
export const 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
|
||||||
|
@ -64,7 +77,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
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 getSessionInfo(id);
|
||||||
} catch (err:any) {
|
} catch (err:any) {
|
||||||
if (err.message === 'sessionID does not exist') {
|
if (err.message === 'sessionID does not exist') {
|
||||||
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
|
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
|
||||||
|
@ -79,7 +92,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
groupID: string;
|
groupID: string;
|
||||||
validUntil: number;
|
validUntil: number;
|
||||||
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
||||||
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
|
const sessionInfo = await firstSatisfies(sessionInfoPromises, isMatch) as Session;
|
||||||
if (sessionInfo == null) return undefined;
|
if (sessionInfo == null) return undefined;
|
||||||
return sessionInfo.authorID;
|
return sessionInfo.authorID;
|
||||||
};
|
};
|
||||||
|
@ -89,9 +102,9 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
* @param {String} sessionID The id of the session
|
* @param {String} sessionID The id of the session
|
||||||
* @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) => {
|
export const 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 get(`session:${sessionID}`) as Session;
|
||||||
return (session != null);
|
return (session != null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,15 +115,15 @@ 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) => {
|
export const createSession = async (groupID: string, authorID: string, validUntil: number) => {
|
||||||
// check if the group exists
|
// check if the group exists
|
||||||
const groupExists = await groupManager.doesGroupExist(groupID);
|
const groupExists = await doesGroupExist(groupID);
|
||||||
if (!groupExists) {
|
if (!groupExists) {
|
||||||
throw new CustomError('groupID does not exist', 'apierror');
|
throw new CustomError('groupID does not exist', 'apierror');
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the author exists
|
// check if the author exists
|
||||||
const authorExists = await authorManager.doesAuthorExist(authorID);
|
const authorExists = await doesAuthorExist(authorID);
|
||||||
if (!authorExists) {
|
if (!authorExists) {
|
||||||
throw new CustomError('authorID does not exist', 'apierror');
|
throw new CustomError('authorID does not exist', 'apierror');
|
||||||
}
|
}
|
||||||
|
@ -144,15 +157,15 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu
|
||||||
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 set(`session:${sessionID}`, {groupID, authorID, validUntil} satisfies Session);
|
||||||
|
|
||||||
// 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),
|
setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1),
|
||||||
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1),
|
setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {sessionID};
|
return {sessionID};
|
||||||
|
@ -163,9 +176,9 @@ 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) => {
|
export const getSessionInfo = async (sessionID:string): Promise<Session> => {
|
||||||
// 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 get(`session:${sessionID}`) as Session;
|
||||||
|
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
// session does not exist
|
// session does not exist
|
||||||
|
@ -181,9 +194,9 @@ 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) => {
|
export const deleteSession = async (sessionID:string): Promise<void> => {
|
||||||
// ensure that the session exists
|
// ensure that the session exists
|
||||||
const session = await db.get(`session:${sessionID}`);
|
const session = await get(`session:${sessionID}`) as Session;
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
throw new CustomError('sessionID does not exist', 'apierror');
|
throw new CustomError('sessionID does not exist', 'apierror');
|
||||||
}
|
}
|
||||||
|
@ -196,13 +209,13 @@ exports.deleteSession = async (sessionID:string) => {
|
||||||
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
||||||
// property, and writes the result. Setting a property to `undefined` deletes that property
|
// property, and writes the result. Setting a property to `undefined` deletes that property
|
||||||
// (JSON.stringify() ignores such properties).
|
// (JSON.stringify() ignores such properties).
|
||||||
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined),
|
setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined),
|
||||||
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined),
|
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 remove(`session:${sessionID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,15 +223,14 @@ exports.deleteSession = async (sessionID:string) => {
|
||||||
* @param {String} groupID The id of the group
|
* @param {String} groupID The id of the group
|
||||||
* @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) => {
|
export const listSessionsOfGroup = async (groupID: string) => {
|
||||||
// check that the group exists
|
// check that the group exists
|
||||||
const exists = await groupManager.doesGroupExist(groupID);
|
const exists = await 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}`);
|
return await listSessionsWithDBKey(`group2sessions:${groupID}`);
|
||||||
return sessions;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,9 +238,9 @@ exports.listSessionsOfGroup = async (groupID: string) => {
|
||||||
* @param {String} authorID The id of the author
|
* @param {String} authorID The id of the author
|
||||||
* @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) => {
|
export const listSessionsOfAuthor = async (authorID: string): Promise<object> => {
|
||||||
// check that the author exists
|
// check that the author exists
|
||||||
const exists = await authorManager.doesAuthorExist(authorID);
|
const exists = await doesAuthorExist(authorID);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new CustomError('authorID does not exist', 'apierror');
|
throw new CustomError('authorID does not exist', 'apierror');
|
||||||
}
|
}
|
||||||
|
@ -243,15 +255,15 @@ exports.listSessionsOfAuthor = async (authorID: string) => {
|
||||||
* @param {String} dbkey The db key to use to get the sessions
|
* @param {String} dbkey The db key to use to get the sessions
|
||||||
* @return {Promise<*>}
|
* @return {Promise<*>}
|
||||||
*/
|
*/
|
||||||
const listSessionsWithDBKey = async (dbkey: string) => {
|
const listSessionsWithDBKey = async (dbkey: string): Promise<any> => {
|
||||||
// get the group2sessions entry
|
// get the group2sessions entry
|
||||||
const sessionObject = await db.get(dbkey);
|
const sessionObject = await 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 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}`);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const DB = require('./DB');
|
import {get, remove, set} from './DB';
|
||||||
const Store = require('@etherpad/express-session').Store;
|
const Store = require('@etherpad/express-session').Store;
|
||||||
const log4js = require('log4js');
|
import log4js from 'log4js';
|
||||||
const util = require('util');
|
import util from 'util';
|
||||||
|
|
||||||
const logger = log4js.getLogger('SessionStore');
|
const logger = log4js.getLogger('SessionStore');
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class SessionStore extends Store {
|
||||||
* 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: number | null = 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:
|
||||||
|
@ -65,12 +65,12 @@ class SessionStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _write(sid: string, sess: any) {
|
async _write(sid: string, sess: any) {
|
||||||
await DB.set(`sessionstorage:${sid}`, sess);
|
await 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 get(`sessionstorage:${sid}`);
|
||||||
return await this._updateExpirations(sid, s);
|
return await this._updateExpirations(sid, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class SessionStore extends Store {
|
||||||
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 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
|
||||||
|
@ -111,4 +111,4 @@ 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;
|
export default SessionStore
|
||||||
|
|
|
@ -20,54 +20,61 @@
|
||||||
* require("./index").require("./path/to/template.ejs")
|
* require("./index").require("./path/to/template.ejs")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ejs = require('ejs');
|
import ejs from 'ejs';
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
import {callAll} from '../../static/js/pluginfw/hooks.js';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const resolve = require('resolve');
|
import resolve from '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 = {
|
export const info: {
|
||||||
|
__output_stack: any[],
|
||||||
|
block_stack: string[],
|
||||||
|
file_stack: {path: string}[],
|
||||||
|
args: any[],
|
||||||
|
__output: any
|
||||||
|
} = {
|
||||||
__output_stack: [],
|
__output_stack: [],
|
||||||
block_stack: [],
|
block_stack: [],
|
||||||
file_stack: [],
|
file_stack: [],
|
||||||
args: [],
|
args: [],
|
||||||
|
__output: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1];
|
const getCurrentFile = () => info.file_stack[info.file_stack.length - 1];
|
||||||
|
|
||||||
exports._init = (b: any, recursive: boolean) => {
|
export const _init = (b: any, recursive: boolean) => {
|
||||||
exports.info.__output_stack.push(exports.info.__output);
|
info.__output_stack.push(info.__output);
|
||||||
exports.info.__output = b;
|
info.__output = b;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports._exit = (b:any, recursive:boolean) => {
|
export const _exit = (b:any, recursive:boolean) => {
|
||||||
exports.info.__output = exports.info.__output_stack.pop();
|
info.__output = info.__output_stack.pop();
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.begin_block = (name:string) => {
|
export const begin_block = (name:string) => {
|
||||||
exports.info.block_stack.push(name);
|
info.block_stack.push(name);
|
||||||
exports.info.__output_stack.push(exports.info.__output.get());
|
info.__output_stack.push(info.__output.get());
|
||||||
exports.info.__output.set('');
|
info.__output.set('');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.end_block = () => {
|
export const end_block = () => {
|
||||||
const name = exports.info.block_stack.pop();
|
const name = info.block_stack.pop();
|
||||||
const renderContext = exports.info.args[exports.info.args.length - 1];
|
const renderContext = info.args[info.args.length - 1];
|
||||||
const content = exports.info.__output.get();
|
const content = info.__output.get();
|
||||||
exports.info.__output.set(exports.info.__output_stack.pop());
|
info.__output.set(info.__output_stack.pop());
|
||||||
const args = {content, renderContext};
|
const args = {content, renderContext};
|
||||||
hooks.callAll(`eejsBlock_${name}`, args);
|
callAll(`eejsBlock_${name}`, args);
|
||||||
exports.info.__output.set(exports.info.__output.get().concat(args.content));
|
info.__output.set(info.__output.get().concat(args.content));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.require = (name:string, args:{
|
export const requireP = (name:string, args:{
|
||||||
e?: Function,
|
e?: Function,
|
||||||
require?: Function,
|
require?: string,
|
||||||
}, mod:{
|
}, mod?:{
|
||||||
filename:string,
|
filename:string,
|
||||||
paths:string[],
|
paths:string[],
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -76,7 +83,7 @@ exports.require = (name:string, args:{
|
||||||
let basedir = __dirname;
|
let basedir = __dirname;
|
||||||
let paths:string[] = [];
|
let paths:string[] = [];
|
||||||
|
|
||||||
if (exports.info.file_stack.length) {
|
if (info.file_stack.length) {
|
||||||
basedir = path.dirname(getCurrentFile().path);
|
basedir = path.dirname(getCurrentFile().path);
|
||||||
}
|
}
|
||||||
if (mod) {
|
if (mod) {
|
||||||
|
@ -94,20 +101,21 @@ exports.require = (name:string, args:{
|
||||||
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;
|
// @ts-ignore
|
||||||
|
args.require = requireP;
|
||||||
|
|
||||||
const cache = settings.maxAge !== 0;
|
const cache: boolean = settings.maxAge !== 0;
|
||||||
const template = cache && templateCache.get(ejspath) || ejs.compile(
|
const template = cache && templateCache.get(ejspath) || ejs.compile(
|
||||||
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
|
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
|
||||||
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
|
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
|
||||||
{filename: ejspath});
|
{filename: ejspath});
|
||||||
if (cache) templateCache.set(ejspath, template);
|
if (cache) templateCache.set(ejspath, template);
|
||||||
|
|
||||||
exports.info.args.push(args);
|
info.args.push(args);
|
||||||
exports.info.file_stack.push({path: ejspath});
|
info.file_stack.push({path: ejspath});
|
||||||
const res = template(args);
|
const res = template(args);
|
||||||
exports.info.file_stack.pop();
|
info.file_stack.pop();
|
||||||
exports.info.args.pop();
|
info.args.pop();
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,34 +22,38 @@
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const padManager = require('../db/PadManager');
|
import {doesPadExist, getPad, sanitizePadId} from '../db/PadManager';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {checkRep, cloneAText, compose, deserializeOps, follow, identity, inverse, makeAText, makeSplice, moveOpsToNewPool, mutateTextLines, oldLen, prepareForWire, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
import ChatMessage from '../../static/js/ChatMessage';
|
||||||
import AttributePool from '../../static/js/AttributePool';
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
import AttributeManager from '../../static/js/AttributeManager';
|
import AttributeManager from '../../static/js/AttributeManager';
|
||||||
const authorManager = require('../db/AuthorManager');
|
import {getAuthor, getAuthorColorId, getAuthorName, getColorPalette, setAuthorColorId, setAuthorName} from '../db/AuthorManager';
|
||||||
const {padutils} = require('../../static/js/pad_utils');
|
import {padUtils} from '../../static/js/pad_utils';
|
||||||
const readOnlyManager = require('../db/ReadOnlyManager');
|
import {getIds} from '../db/ReadOnlyManager';
|
||||||
const settings = require('../utils/Settings');
|
import settings from '../utils/Settings';
|
||||||
const securityManager = require('../db/SecurityManager');
|
import {checkAccess} from '../db/SecurityManager';
|
||||||
const plugins = require('../../static/js/pluginfw/plugin_defs.js');
|
import {pluginDefs} from '../../static/js/pluginfw/plugin_defs.js';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const messageLogger = log4js.getLogger('message');
|
const messageLogger = log4js.getLogger('message');
|
||||||
const accessLogger = log4js.getLogger('access');
|
const accessLogger = log4js.getLogger('access');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
import {aCallAll, deprecationNotices} from '../../static/js/pluginfw/hooks.js';
|
||||||
const stats = require('../stats')
|
import {measuredCollection} from '../stats';
|
||||||
const assert = require('assert').strict;
|
const assert = require('assert').strict;
|
||||||
import {RateLimiterMemory} from 'rate-limiter-flexible';
|
import {RateLimiterMemory} from 'rate-limiter-flexible';
|
||||||
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
|
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
|
||||||
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
|
import {AText, PadAuthor, PadType} from "../types/PadType";
|
||||||
import {ChangeSet} from "../types/ChangeSet";
|
import {ChangeSet} from "../types/ChangeSet";
|
||||||
const webaccess = require('../hooks/express/webaccess');
|
import Pad from "../db/Pad";
|
||||||
const { checkValidRev } = require('../utils/checkValidRev');
|
import { userCanModify} from '../hooks/express/webaccess';
|
||||||
|
import {checkValidRev} from '../utils/checkValidRev';
|
||||||
|
import {AttributePoolWire, ChangesetRequestMessage, ChatMessageMessage, ClientReadyMessage, ClientSendMessages, ClientUserChangesMessage, ClientVarMessage, ClientVarPayload, HistoricalAuthorData, UserChanges, UserSuggestUserName} from "../../static/js/types/SocketIOMessage";
|
||||||
|
import {AttributionLinesMutator} from "../../static/js/AttributionLinesMutator";
|
||||||
|
import {Builder} from "../../static/js/Builder";
|
||||||
|
|
||||||
let rateLimiter:any;
|
let rateLimiter:any;
|
||||||
let socketio: any = null;
|
let _socketio: any = null;
|
||||||
|
|
||||||
hooks.deprecationNotices.clientReady = 'use the userJoin hook instead';
|
deprecationNotices.clientReady = 'use the userJoin hook instead';
|
||||||
|
|
||||||
const addContextToError = (err:any, pfx:string) => {
|
const addContextToError = (err:any, pfx:string) => {
|
||||||
const newErr = new Error(`${pfx}${err.message}`, {cause: err});
|
const newErr = new Error(`${pfx}${err.message}`, {cause: err});
|
||||||
|
@ -60,7 +64,7 @@ const addContextToError = (err:any, pfx:string) => {
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.socketio = () => {
|
export const socketio = () => {
|
||||||
// The rate limiter is created in this hook so that restarting the server resets the limiter. The
|
// The rate limiter is created in this hook so that restarting the server resets the limiter. The
|
||||||
// settings.commitRateLimiting object is passed directly to the rate limiter so that the limits
|
// settings.commitRateLimiting object is passed directly to the rate limiter so that the limits
|
||||||
// can be dynamically changed during runtime by modifying its properties.
|
// can be dynamically changed during runtime by modifying its properties.
|
||||||
|
@ -85,11 +89,10 @@ exports.socketio = () => {
|
||||||
* - readonly: Whether the client has read-only access (true) or read/write access (false).
|
* - readonly: Whether the client has read-only access (true) or read/write access (false).
|
||||||
* - rev: The last revision that was sent to the client.
|
* - rev: The last revision that was sent to the client.
|
||||||
*/
|
*/
|
||||||
const sessioninfos:MapArrayType<any> = {};
|
export const sessioninfos:MapArrayType<any> = {};
|
||||||
exports.sessioninfos = sessioninfos;
|
|
||||||
|
|
||||||
stats.gauge('totalUsers', () => socketio ? socketio.engine.clientsCount : 0);
|
measuredCollection.gauge('totalUsers', () => _socketio ? _socketio.engine.clientsCount : 0);
|
||||||
stats.gauge('activePads', () => {
|
measuredCollection.gauge('activePads', () => {
|
||||||
const padIds = new Set();
|
const padIds = new Set();
|
||||||
for (const {padId} of Object.values(sessioninfos)) {
|
for (const {padId} of Object.values(sessioninfos)) {
|
||||||
if (!padId) continue;
|
if (!padId) continue;
|
||||||
|
@ -108,7 +111,7 @@ class Channels {
|
||||||
* @param {(ch, task) => any} [exec] - Task executor. If omitted, tasks are assumed to be
|
* @param {(ch, task) => any} [exec] - Task executor. If omitted, tasks are assumed to be
|
||||||
* functions that will be executed with the channel as the only argument.
|
* functions that will be executed with the channel as the only argument.
|
||||||
*/
|
*/
|
||||||
constructor(exec = (ch: string, task:any) => task(ch)) {
|
constructor(exec: (ch:any, task:any) => any = (ch: string, task:any) => task(ch)) {
|
||||||
this._exec = exec;
|
this._exec = exec;
|
||||||
this._promiseChains = new Map();
|
this._promiseChains = new Map();
|
||||||
}
|
}
|
||||||
|
@ -143,16 +146,16 @@ const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(so
|
||||||
* This Method is called by server.ts to tell the message handler on which socket it should send
|
* This Method is called by server.ts to tell the message handler on which socket it should send
|
||||||
* @param socket_io The Socket
|
* @param socket_io The Socket
|
||||||
*/
|
*/
|
||||||
exports.setSocketIO = (socket_io:any) => {
|
export const setSocketIO = (socket_io:any) => {
|
||||||
socketio = socket_io;
|
_socketio = socket_io;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the connection of a new user
|
* Handles the connection of a new user
|
||||||
* @param socket the socket.io Socket object for the new connection from the client
|
* @param socket the socket.io Socket object for the new connection from the client
|
||||||
*/
|
*/
|
||||||
exports.handleConnect = (socket:any) => {
|
export const handleConnect = (socket:any) => {
|
||||||
stats.meter('connects').mark();
|
measuredCollection.meter('connects').mark();
|
||||||
|
|
||||||
// Initialize sessioninfos for this new session
|
// Initialize sessioninfos for this new session
|
||||||
sessioninfos[socket.id] = {};
|
sessioninfos[socket.id] = {};
|
||||||
|
@ -161,23 +164,23 @@ exports.handleConnect = (socket:any) => {
|
||||||
/**
|
/**
|
||||||
* Kicks all sessions from a pad
|
* Kicks all sessions from a pad
|
||||||
*/
|
*/
|
||||||
exports.kickSessionsFromPad = (padID: string) => {
|
export const kickSessionsFromPad = (padID: string) => {
|
||||||
|
|
||||||
if(socketio.sockets == null) return;
|
if(_socketio.sockets == null) return;
|
||||||
|
|
||||||
// skip if there is nobody on this pad
|
// skip if there is nobody on this pad
|
||||||
if (_getRoomSockets(padID).length === 0) return;
|
if (_getRoomSockets(padID).length === 0) return;
|
||||||
|
|
||||||
// disconnect everyone from this pad
|
// disconnect everyone from this pad
|
||||||
socketio.in(padID).emit('message', {disconnect: 'deleted'});
|
_socketio.in(padID).emit('message', {disconnect: 'deleted'});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the disconnection of a user
|
* Handles the disconnection of a user
|
||||||
* @param socket the socket.io Socket object for the client
|
* @param socket the socket.io Socket object for the client
|
||||||
*/
|
*/
|
||||||
exports.handleDisconnect = async (socket:any) => {
|
export const handleDisconnect = async (socket:any) => {
|
||||||
stats.meter('disconnects').mark();
|
measuredCollection.meter('disconnects').mark();
|
||||||
const session = sessioninfos[socket.id];
|
const session = sessioninfos[socket.id];
|
||||||
delete sessioninfos[socket.id];
|
delete sessioninfos[socket.id];
|
||||||
// session.padId can be nullish if the user disconnects before sending CLIENT_READY.
|
// session.padId can be nullish if the user disconnects before sending CLIENT_READY.
|
||||||
|
@ -196,12 +199,12 @@ exports.handleDisconnect = async (socket:any) => {
|
||||||
data: {
|
data: {
|
||||||
type: 'USER_LEAVE',
|
type: 'USER_LEAVE',
|
||||||
userInfo: {
|
userInfo: {
|
||||||
colorId: await authorManager.getAuthorColorId(session.author),
|
colorId: await getAuthorColorId(session.author),
|
||||||
userId: session.author,
|
userId: session.author,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await hooks.aCallAll('userLeave', {
|
await aCallAll('userLeave', {
|
||||||
...session, // For backwards compatibility.
|
...session, // For backwards compatibility.
|
||||||
authorId: session.author,
|
authorId: session.author,
|
||||||
readOnly: session.readonly,
|
readOnly: session.readonly,
|
||||||
|
@ -214,7 +217,7 @@ exports.handleDisconnect = async (socket:any) => {
|
||||||
* @param socket the socket.io Socket object for the client
|
* @param socket the socket.io Socket object for the client
|
||||||
* @param message the message from the client
|
* @param message the message from the client
|
||||||
*/
|
*/
|
||||||
exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
export const handleMessage = async (socket:any, message: ClientVarMessage) => {
|
||||||
const env = process.env.NODE_ENV || 'development';
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
if (env === 'production') {
|
if (env === 'production') {
|
||||||
|
@ -223,7 +226,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` +
|
messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` +
|
||||||
'limiting that happens edit the rateLimit values in settings.json');
|
'limiting that happens edit the rateLimit values in settings.json');
|
||||||
stats.meter('rateLimited').mark();
|
measuredCollection.meter('rateLimited').mark();
|
||||||
socket.emit('message', {disconnect: 'rateLimited'});
|
socket.emit('message', {disconnect: 'rateLimited'});
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -246,14 +249,14 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pad does not exist, so we need to sanitize the id
|
// Pad does not exist, so we need to sanitize the id
|
||||||
if (!(await padManager.doesPadExist(thisSession.auth.padID))) {
|
if (!(await doesPadExist(thisSession.auth.padID))) {
|
||||||
thisSession.auth.padID = await padManager.sanitizePadId(thisSession.auth.padID);
|
thisSession.auth.padID = await sanitizePadId(thisSession.auth.padID);
|
||||||
}
|
}
|
||||||
const padIds = await readOnlyManager.getIds(thisSession.auth.padID);
|
const padIds = await getIds(thisSession.auth.padID);
|
||||||
thisSession.padId = padIds.padId;
|
thisSession.padId = padIds.padId;
|
||||||
thisSession.readOnlyPadId = padIds.readOnlyPadId;
|
thisSession.readOnlyPadId = padIds.readOnlyPadId;
|
||||||
thisSession.readonly =
|
thisSession.readonly =
|
||||||
padIds.readonly || !webaccess.userCanModify(thisSession.auth.padID, socket.client.request);
|
padIds.readonly || !userCanModify(thisSession.auth.padID, socket.client.request);
|
||||||
}
|
}
|
||||||
// Outside of the checks done by this function, message.padId must not be accessed because it is
|
// Outside of the checks done by this function, message.padId must not be accessed because it is
|
||||||
// too easy to introduce a security vulnerability that allows malicious users to read or modify
|
// too easy to introduce a security vulnerability that allows malicious users to read or modify
|
||||||
|
@ -273,7 +276,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
|
|
||||||
const {session: {user} = {}} = socket.client.request as SocketClientRequest;
|
const {session: {user} = {}} = socket.client.request as SocketClientRequest;
|
||||||
const {accessStatus, authorID} =
|
const {accessStatus, authorID} =
|
||||||
await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user);
|
await checkAccess(auth.padID, auth.sessionID, auth.token, user);
|
||||||
if (accessStatus !== 'grant') {
|
if (accessStatus !== 'grant') {
|
||||||
socket.emit('message', {accessStatus});
|
socket.emit('message', {accessStatus});
|
||||||
throw new Error('access denied');
|
throw new Error('access denied');
|
||||||
|
@ -303,16 +306,16 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
},
|
},
|
||||||
socket,
|
socket,
|
||||||
get client() {
|
get client() {
|
||||||
padutils.warnDeprecated(
|
padUtils.warnDeprecated(
|
||||||
'the `client` context property for the handleMessageSecurity and handleMessage hooks ' +
|
'the `client` context property for the handleMessageSecurity and handleMessage hooks ' +
|
||||||
'is deprecated; use the `socket` property instead');
|
'is deprecated; use the `socket` property instead');
|
||||||
return this.socket;
|
return this.socket;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
for (const res of await hooks.aCallAll('handleMessageSecurity', context)) {
|
for (const res of await aCallAll('handleMessageSecurity', context)) {
|
||||||
switch (res) {
|
switch (res) {
|
||||||
case true:
|
case true:
|
||||||
padutils.warnDeprecated(
|
padUtils.warnDeprecated(
|
||||||
'returning `true` from a `handleMessageSecurity` hook function is deprecated; ' +
|
'returning `true` from a `handleMessageSecurity` hook function is deprecated; ' +
|
||||||
'return "permitOnce" instead');
|
'return "permitOnce" instead');
|
||||||
thisSession.readonly = false;
|
thisSession.readonly = false;
|
||||||
|
@ -327,7 +330,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call handleMessage hook. If a plugin returns null, the message will be dropped.
|
// Call handleMessage hook. If a plugin returns null, the message will be dropped.
|
||||||
if ((await hooks.aCallAll('handleMessage', context)).some((m: null|string) => m == null)) {
|
if ((await aCallAll('handleMessage', context)).some((m: null|string) => m == null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,7 +348,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
try {
|
try {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'USER_CHANGES':
|
case 'USER_CHANGES':
|
||||||
stats.counter('pendingEdits').inc();
|
measuredCollection.counter('pendingEdits').inc();
|
||||||
await padChannels.enqueue(thisSession.padId, {socket, message});
|
await padChannels.enqueue(thisSession.padId, {socket, message});
|
||||||
break;
|
break;
|
||||||
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
|
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
|
||||||
|
@ -386,7 +389,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
*/
|
*/
|
||||||
const handleSaveRevisionMessage = async (socket:any, message: string) => {
|
const handleSaveRevisionMessage = async (socket:any, message: string) => {
|
||||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||||
const pad = await padManager.getPad(padId, null, authorId);
|
const pad = await getPad(padId, null, authorId);
|
||||||
await pad.addSavedRevision(pad.head, authorId);
|
await pad.addSavedRevision(pad.head, authorId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -397,14 +400,14 @@ const handleSaveRevisionMessage = async (socket:any, message: string) => {
|
||||||
* @param msg {Object} the message we're sending
|
* @param msg {Object} the message we're sending
|
||||||
* @param sessionID {string} the socketIO session to which we're sending this message
|
* @param sessionID {string} the socketIO session to which we're sending this message
|
||||||
*/
|
*/
|
||||||
exports.handleCustomObjectMessage = (msg: typeof ChatMessage, sessionID: string) => {
|
export const handleCustomObjectMessage = (msg: ClientVarMessage, sessionID: string) => {
|
||||||
if (msg.data.type === 'CUSTOM') {
|
if ("data" in msg && msg.type != 'CLIENT_VARS' && msg.data.type === 'CUSTOM') {
|
||||||
if (sessionID) {
|
if (sessionID) {
|
||||||
// a sessionID is targeted: directly to this sessionID
|
// a sessionID is targeted: directly to this sessionID
|
||||||
socketio.sockets.socket(sessionID).emit('message', msg);
|
_socketio.sockets.socket(sessionID).emit('message', msg);
|
||||||
} else {
|
} else {
|
||||||
// broadcast to all clients on this pad
|
// broadcast to all clients on this pad
|
||||||
socketio.sockets.in(msg.data.payload.padId).emit('message', msg);
|
_socketio.sockets.in(msg.data.payload.padId).emit('message', msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -415,7 +418,7 @@ exports.handleCustomObjectMessage = (msg: typeof ChatMessage, sessionID: string)
|
||||||
* @param padID {Pad} the pad to which we're sending this message
|
* @param padID {Pad} the pad to which we're sending this message
|
||||||
* @param msgString {String} the message we're sending
|
* @param msgString {String} the message we're sending
|
||||||
*/
|
*/
|
||||||
exports.handleCustomMessage = (padID: string, msgString:string) => {
|
export const handleCustomMessage = (padID: string, msgString:string) => {
|
||||||
const time = Date.now();
|
const time = Date.now();
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'COLLABROOM',
|
type: 'COLLABROOM',
|
||||||
|
@ -424,7 +427,7 @@ exports.handleCustomMessage = (padID: string, msgString:string) => {
|
||||||
time,
|
time,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
socketio.sockets.in(padID).emit('message', msg);
|
_socketio.sockets.in(padID).emit('message', msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -432,13 +435,13 @@ exports.handleCustomMessage = (padID: string, msgString:string) => {
|
||||||
* @param socket the socket.io Socket object for the client
|
* @param socket the socket.io Socket object for the client
|
||||||
* @param message the message from the client
|
* @param message the message from the client
|
||||||
*/
|
*/
|
||||||
const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
|
const handleChatMessage = async (socket:any, message: ChatMessageMessage) => {
|
||||||
const chatMessage = ChatMessage.fromObject(message.data.message);
|
const chatMessage = ChatMessage.fromObject(message.data.message);
|
||||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||||
// Don't trust the user-supplied values.
|
// Don't trust the user-supplied values.
|
||||||
chatMessage.time = Date.now();
|
chatMessage.time = Date.now();
|
||||||
chatMessage.authorId = authorId;
|
chatMessage.authorId = authorId;
|
||||||
await exports.sendChatMessageToPadClients(chatMessage, padId);
|
await sendChatMessageToPadClients(chatMessage, padId);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -452,16 +455,16 @@ const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
|
||||||
* @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message
|
* @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message
|
||||||
* object as the first argument and the destination pad ID as the second argument instead.
|
* object as the first argument and the destination pad ID as the second argument instead.
|
||||||
*/
|
*/
|
||||||
exports.sendChatMessageToPadClients = async (mt: typeof ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
|
export const sendChatMessageToPadClients = async (mt: ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
|
||||||
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
|
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
|
||||||
padId = mt instanceof ChatMessage ? puId : padId;
|
padId = mt instanceof ChatMessage ? puId : padId;
|
||||||
const pad = await padManager.getPad(padId, null, message.authorId);
|
const pad = await getPad(padId!, null, message.authorId);
|
||||||
await hooks.aCallAll('chatNewMessage', {message, pad, padId});
|
await aCallAll('chatNewMessage', {message, pad, padId});
|
||||||
// pad.appendChatMessage() ignores the displayName property so we don't need to wait for
|
// pad.appendChatMessage() ignores the displayName property so we don't need to wait for
|
||||||
// authorManager.getAuthorName() to resolve before saving the message to the database.
|
// authorManager.getAuthorName() to resolve before saving the message to the database.
|
||||||
const promise = pad.appendChatMessage(message);
|
const promise = pad.appendChatMessage(message);
|
||||||
message.displayName = await authorManager.getAuthorName(message.authorId);
|
message.displayName = await getAuthorName(message.authorId!);
|
||||||
socketio.sockets.in(padId).emit('message', {
|
_socketio.sockets.in(padId).emit('message', {
|
||||||
type: 'COLLABROOM',
|
type: 'COLLABROOM',
|
||||||
data: {type: 'CHAT_MESSAGE', message},
|
data: {type: 'CHAT_MESSAGE', message},
|
||||||
});
|
});
|
||||||
|
@ -479,7 +482,7 @@ const handleGetChatMessages = async (socket:any, {data: {start, end}}:any) => {
|
||||||
const count = end - start;
|
const count = end - start;
|
||||||
if (count < 0 || count > 100) throw new Error(`invalid number of messages: ${count}`);
|
if (count < 0 || count > 100) throw new Error(`invalid number of messages: ${count}`);
|
||||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||||
const pad = await padManager.getPad(padId, null, authorId);
|
const pad = await getPad(padId, null, authorId);
|
||||||
|
|
||||||
const chatMessages = await pad.getChatMessages(start, end);
|
const chatMessages = await pad.getChatMessages(start, end);
|
||||||
const infoMsg = {
|
const infoMsg = {
|
||||||
|
@ -499,7 +502,7 @@ const handleGetChatMessages = async (socket:any, {data: {start, end}}:any) => {
|
||||||
* @param socket the socket.io Socket object for the client
|
* @param socket the socket.io Socket object for the client
|
||||||
* @param message the message from the client
|
* @param message the message from the client
|
||||||
*/
|
*/
|
||||||
const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => {
|
const handleSuggestUserName = (socket:any, message: UserSuggestUserName) => {
|
||||||
const {newName, unnamedId} = message.data.payload;
|
const {newName, unnamedId} = message.data.payload;
|
||||||
if (newName == null) throw new Error('missing newName');
|
if (newName == null) throw new Error('missing newName');
|
||||||
if (unnamedId == null) throw new Error('missing unnamedId');
|
if (unnamedId == null) throw new Error('missing unnamedId');
|
||||||
|
@ -531,8 +534,8 @@ const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId
|
||||||
|
|
||||||
// Tell the authorManager about the new attributes
|
// Tell the authorManager about the new attributes
|
||||||
const p = Promise.all([
|
const p = Promise.all([
|
||||||
authorManager.setAuthorColorId(author, colorId),
|
setAuthorColorId(author, colorId),
|
||||||
authorManager.setAuthorName(author, name),
|
setAuthorName(author, name!),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const padId = session.padId;
|
const padId = session.padId;
|
||||||
|
@ -567,9 +570,9 @@ const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId
|
||||||
* @param socket the socket.io Socket object for the client
|
* @param socket the socket.io Socket object for the client
|
||||||
* @param message the message from the client
|
* @param message the message from the client
|
||||||
*/
|
*/
|
||||||
const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
const handleUserChanges = async (socket:any, message: UserChanges) => {
|
||||||
// This one's no longer pending, as we're gonna process it now
|
// This one's no longer pending, as we're gonna process it now
|
||||||
stats.counter('pendingEdits').dec();
|
measuredCollection.counter('pendingEdits').dec();
|
||||||
|
|
||||||
// The client might disconnect between our callbacks. We should still
|
// The client might disconnect between our callbacks. We should still
|
||||||
// finish processing the changeset, so keep a reference to the session.
|
// finish processing the changeset, so keep a reference to the session.
|
||||||
|
@ -581,20 +584,20 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||||
if (!thisSession) throw new Error('client disconnected');
|
if (!thisSession) throw new Error('client disconnected');
|
||||||
|
|
||||||
// Measure time to process edit
|
// Measure time to process edit
|
||||||
const stopWatch = stats.timer('edits').start();
|
const stopWatch = measuredCollection.timer('edits').start();
|
||||||
try {
|
try {
|
||||||
const {data: {baseRev, apool, changeset}} = message;
|
const {data: {baseRev, apool, changeset}} = message;
|
||||||
if (baseRev == null) throw new Error('missing baseRev');
|
if (baseRev == null) throw new Error('missing baseRev');
|
||||||
if (apool == null) throw new Error('missing apool');
|
if (apool == null) throw new Error('missing apool');
|
||||||
if (changeset == null) throw new Error('missing changeset');
|
if (changeset == null) throw new Error('missing changeset');
|
||||||
const wireApool = (new AttributePool()).fromJsonable(apool);
|
const wireApool = (new AttributePool()).fromJsonable(apool);
|
||||||
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
|
const pad = await getPad(thisSession.padId, null, thisSession.author);
|
||||||
|
|
||||||
// Verify that the changeset has valid syntax and is in canonical form
|
// Verify that the changeset has valid syntax and is in canonical form
|
||||||
Changeset.checkRep(changeset);
|
checkRep(changeset);
|
||||||
|
|
||||||
// Validate all added 'author' attribs to be the same value as the current user
|
// Validate all added 'author' attribs to be the same value as the current user
|
||||||
for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) {
|
for (const op of deserializeOps(unpack(changeset).ops)) {
|
||||||
// + can add text with attribs
|
// + can add text with attribs
|
||||||
// = can change or add attribs
|
// = can change or add attribs
|
||||||
// - can have attribs, but they are discarded and don't show up in the attribs -
|
// - can have attribs, but they are discarded and don't show up in the attribs -
|
||||||
|
@ -613,7 +616,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||||
// ex. adoptChangesetAttribs
|
// ex. adoptChangesetAttribs
|
||||||
|
|
||||||
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
|
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
|
||||||
let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
|
let rebasedChangeset = moveOpsToNewPool(changeset, wireApool, pad.pool);
|
||||||
|
|
||||||
// ex. applyUserChanges
|
// ex. applyUserChanges
|
||||||
let r = baseRev;
|
let r = baseRev;
|
||||||
|
@ -626,24 +629,24 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||||
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
|
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
|
||||||
if (changeset === c && thisSession.author === authorId) {
|
if (changeset === c && thisSession.author === authorId) {
|
||||||
// Assume this is a retransmission of an already applied changeset.
|
// Assume this is a retransmission of an already applied changeset.
|
||||||
rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen);
|
rebasedChangeset = identity(unpack(changeset).oldLen);
|
||||||
}
|
}
|
||||||
// At this point, both "c" (from the pad) and "changeset" (from the
|
// At this point, both "c" (from the pad) and "changeset" (from the
|
||||||
// client) are relative to revision r - 1. The follow function
|
// client) are relative to revision r - 1. The follow function
|
||||||
// rebases "changeset" so that it is relative to revision r
|
// rebases "changeset" so that it is relative to revision r
|
||||||
// and can be applied after "c".
|
// and can be applied after "c".
|
||||||
rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool);
|
rebasedChangeset = follow(c, rebasedChangeset, false, pad.pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevText = pad.text();
|
const prevText = pad.text();
|
||||||
|
|
||||||
if (Changeset.oldLen(rebasedChangeset) !== prevText.length) {
|
if (oldLen(rebasedChangeset) !== prevText.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
|
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
|
||||||
`${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
|
`${oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author);
|
const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author) as number;
|
||||||
// The head revision will either stay the same or increase by 1 depending on whether the
|
// The head revision will either stay the same or increase by 1 depending on whether the
|
||||||
// changeset has a net effect.
|
// changeset has a net effect.
|
||||||
assert([r, r + 1].includes(newRev));
|
assert([r, r + 1].includes(newRev));
|
||||||
|
@ -655,7 +658,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||||
|
|
||||||
// Make sure the pad always ends with an empty line.
|
// Make sure the pad always ends with an empty line.
|
||||||
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
|
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
|
||||||
const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
|
const nlChangeset = makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
|
||||||
await pad.appendRevision(nlChangeset, thisSession.author);
|
await pad.appendRevision(nlChangeset, thisSession.author);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,10 +668,10 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||||
socket.emit('message', {type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}});
|
socket.emit('message', {type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}});
|
||||||
thisSession.rev = newRev;
|
thisSession.rev = newRev;
|
||||||
if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev);
|
if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev);
|
||||||
await exports.updatePadClients(pad);
|
await updatePadClients(pad);
|
||||||
} catch (err:any) {
|
} catch (err:any) {
|
||||||
socket.emit('message', {disconnect: 'badChangeset'});
|
socket.emit('message', {disconnect: 'badChangeset'});
|
||||||
stats.meter('failedChangesets').mark();
|
measuredCollection.meter('failedChangesets').mark();
|
||||||
messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` +
|
messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` +
|
||||||
`(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`);
|
`(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -676,7 +679,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.updatePadClients = async (pad: PadType) => {
|
export const updatePadClients = async (pad: Pad) => {
|
||||||
// skip this if no-one is on this pad
|
// skip this if no-one is on this pad
|
||||||
const roomSockets = _getRoomSockets(pad.id);
|
const roomSockets = _getRoomSockets(pad.id);
|
||||||
if (roomSockets.length === 0) return;
|
if (roomSockets.length === 0) return;
|
||||||
|
@ -710,7 +713,7 @@ exports.updatePadClients = async (pad: PadType) => {
|
||||||
const revChangeset = revision.changeset;
|
const revChangeset = revision.changeset;
|
||||||
const currentTime = revision.meta.timestamp;
|
const currentTime = revision.meta.timestamp;
|
||||||
|
|
||||||
const forWire = Changeset.prepareForWire(revChangeset, pad.pool);
|
const forWire = prepareForWire(revChangeset, pad.pool);
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'COLLABROOM',
|
type: 'COLLABROOM',
|
||||||
data: {
|
data: {
|
||||||
|
@ -745,7 +748,7 @@ const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||||
// that aren't at the start of a line
|
// that aren't at the start of a line
|
||||||
const badMarkers = [];
|
const badMarkers = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
for (const op of Changeset.deserializeOps(atext.attribs)) {
|
for (const op of deserializeOps(atext.attribs)) {
|
||||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
|
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
|
||||||
if (hasMarker) {
|
if (hasMarker) {
|
||||||
|
@ -767,7 +770,7 @@ const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||||
// create changeset that removes these bad markers
|
// create changeset that removes these bad markers
|
||||||
offset = 0;
|
offset = 0;
|
||||||
|
|
||||||
const builder = Changeset.builder(text.length);
|
const builder = new Builder(text.length);
|
||||||
|
|
||||||
badMarkers.forEach((pos) => {
|
badMarkers.forEach((pos) => {
|
||||||
builder.keepText(text.substring(offset, pos));
|
builder.keepText(text.substring(offset, pos));
|
||||||
|
@ -785,26 +788,29 @@ const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||||
* @param socket the socket.io Socket object for the client
|
* @param socket the socket.io Socket object for the client
|
||||||
* @param message the message from the client
|
* @param message the message from the client
|
||||||
*/
|
*/
|
||||||
const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
|
||||||
const sessionInfo = sessioninfos[socket.id];
|
const sessionInfo = sessioninfos[socket.id];
|
||||||
if (sessionInfo == null) throw new Error('client disconnected');
|
if (sessionInfo == null) throw new Error('client disconnected');
|
||||||
assert(sessionInfo.author);
|
assert(sessionInfo.author);
|
||||||
|
|
||||||
await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context.
|
await aCallAll('clientReady', message); // Deprecated due to awkward context.
|
||||||
|
|
||||||
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
|
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
|
||||||
|
// @ts-ignore
|
||||||
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) {
|
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) {
|
||||||
messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`);
|
messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`);
|
||||||
|
// @ts-ignore
|
||||||
authorColorId = null;
|
authorColorId = null;
|
||||||
}
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
authorName && authorManager.setAuthorName(sessionInfo.author, authorName),
|
authorName && setAuthorName(sessionInfo.author, authorName),
|
||||||
authorColorId && authorManager.setAuthorColorId(sessionInfo.author, authorColorId),
|
// @ts-ignore
|
||||||
|
authorColorId && setAuthorColorId(sessionInfo.author, authorColorId),
|
||||||
]);
|
]);
|
||||||
({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author));
|
({colorId: authorColorId, name: authorName} = await getAuthor(sessionInfo.author));
|
||||||
|
|
||||||
// load the pad-object from the database
|
// load the pad-object from the database
|
||||||
const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author);
|
const pad = await getPad(sessionInfo.padId, null, sessionInfo.author);
|
||||||
|
|
||||||
// these db requests all need the pad object (timestamp of latest revision, author data)
|
// these db requests all need the pad object (timestamp of latest revision, author data)
|
||||||
const authors = pad.getAllAuthors();
|
const authors = pad.getAllAuthors();
|
||||||
|
@ -813,12 +819,9 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber());
|
const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber());
|
||||||
|
|
||||||
// get all author data out of the database (in parallel)
|
// get all author data out of the database (in parallel)
|
||||||
const historicalAuthorData:MapArrayType<{
|
const historicalAuthorData: HistoricalAuthorData = {};
|
||||||
name: string;
|
|
||||||
colorId: string;
|
|
||||||
}> = {};
|
|
||||||
await Promise.all(authors.map(async (authorId: string) => {
|
await Promise.all(authors.map(async (authorId: string) => {
|
||||||
const author = await authorManager.getAuthor(authorId);
|
const author = await getAuthor(authorId);
|
||||||
if (!author) {
|
if (!author) {
|
||||||
messageLogger.error(`There is no author for authorId: ${authorId}. ` +
|
messageLogger.error(`There is no author for authorId: ${authorId}. ` +
|
||||||
'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802');
|
'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802');
|
||||||
|
@ -872,7 +875,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
const revisionsNeeded = [];
|
const revisionsNeeded = [];
|
||||||
const changesets:MapArrayType<any> = {};
|
const changesets:MapArrayType<any> = {};
|
||||||
|
|
||||||
let startNum = message.client_rev + 1;
|
let startNum = message.client_rev! + 1;
|
||||||
let endNum = pad.getHeadRevisionNumber() + 1;
|
let endNum = pad.getHeadRevisionNumber() + 1;
|
||||||
|
|
||||||
const headNum = pad.getHeadRevisionNumber();
|
const headNum = pad.getHeadRevisionNumber();
|
||||||
|
@ -901,7 +904,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
|
|
||||||
// return pending changesets
|
// return pending changesets
|
||||||
for (const r of revisionsNeeded) {
|
for (const r of revisionsNeeded) {
|
||||||
const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool);
|
const forWire = prepareForWire(changesets[r].changeset, pad.pool);
|
||||||
const wireMsg = {type: 'COLLABROOM',
|
const wireMsg = {type: 'COLLABROOM',
|
||||||
data: {type: 'CLIENT_RECONNECT',
|
data: {type: 'CLIENT_RECONNECT',
|
||||||
headRev: pad.getHeadRevisionNumber(),
|
headRev: pad.getHeadRevisionNumber(),
|
||||||
|
@ -923,11 +926,11 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
} else {
|
} else {
|
||||||
// This is a normal first connect
|
// This is a normal first connect
|
||||||
let atext;
|
let atext;
|
||||||
let apool;
|
let apool: AttributePoolWire;
|
||||||
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
|
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
|
||||||
try {
|
try {
|
||||||
atext = Changeset.cloneAText(pad.atext);
|
atext = cloneAText(pad.atext);
|
||||||
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
const attribsForWire = prepareForWire(atext.attribs, pad.pool);
|
||||||
apool = attribsForWire.pool.toJsonable();
|
apool = attribsForWire.pool.toJsonable();
|
||||||
atext.attribs = attribsForWire.translated;
|
atext.attribs = attribsForWire.translated;
|
||||||
} catch (e:any) {
|
} catch (e:any) {
|
||||||
|
@ -938,10 +941,10 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
|
|
||||||
// Warning: never ever send sessionInfo.padId to the client. If the client is read only you
|
// Warning: never ever send sessionInfo.padId to the client. If the client is read only you
|
||||||
// would open a security hole 1 swedish mile wide...
|
// would open a security hole 1 swedish mile wide...
|
||||||
const clientVars:MapArrayType<any> = {
|
const clientVars:ClientVarPayload = {
|
||||||
skinName: settings.skinName,
|
skinName: settings.skinName!,
|
||||||
skinVariants: settings.skinVariants,
|
skinVariants: settings.skinVariants!,
|
||||||
randomVersionString: settings.randomVersionString,
|
randomVersionString: settings.randomVersionString!,
|
||||||
accountPrivs: {
|
accountPrivs: {
|
||||||
maxRevisions: 100,
|
maxRevisions: 100,
|
||||||
},
|
},
|
||||||
|
@ -958,7 +961,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
rev: pad.getHeadRevisionNumber(),
|
rev: pad.getHeadRevisionNumber(),
|
||||||
time: currentTime,
|
time: currentTime,
|
||||||
},
|
},
|
||||||
colorPalette: authorManager.getColorPalette(),
|
colorPalette: getColorPalette(),
|
||||||
clientIp: '127.0.0.1',
|
clientIp: '127.0.0.1',
|
||||||
userColor: authorColorId,
|
userColor: authorColorId,
|
||||||
padId: sessionInfo.auth.padID,
|
padId: sessionInfo.auth.padID,
|
||||||
|
@ -979,8 +982,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
sofficeAvailable: settings.sofficeAvailable(),
|
sofficeAvailable: settings.sofficeAvailable(),
|
||||||
exportAvailable: settings.exportAvailable(),
|
exportAvailable: settings.exportAvailable(),
|
||||||
plugins: {
|
plugins: {
|
||||||
plugins: plugins.plugins,
|
plugins: pluginDefs.getPlugins(),
|
||||||
parts: plugins.parts,
|
parts: pluginDefs.getParts(),
|
||||||
},
|
},
|
||||||
indentationOnNewLine: settings.indentationOnNewLine,
|
indentationOnNewLine: settings.indentationOnNewLine,
|
||||||
scrollWhenFocusLineIsOutOfViewport: {
|
scrollWhenFocusLineIsOutOfViewport: {
|
||||||
|
@ -997,7 +1000,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
|
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
|
||||||
},
|
},
|
||||||
initialChangesets: [], // FIXME: REMOVE THIS SHIT,
|
initialChangesets: [], // FIXME: REMOVE THIS SHIT,
|
||||||
mode: process.env.NODE_ENV
|
mode: process.env.NODE_ENV!
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a username to the clientVars if one avaiable
|
// Add a username to the clientVars if one avaiable
|
||||||
|
@ -1006,7 +1009,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// call the clientVars-hook so plugins can modify them before they get sent to the client
|
// call the clientVars-hook so plugins can modify them before they get sent to the client
|
||||||
const messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket});
|
const messages = await aCallAll('clientVars', {clientVars, pad, socket});
|
||||||
|
|
||||||
// combine our old object with the new attributes from the hook
|
// combine our old object with the new attributes from the hook
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
|
@ -1052,7 +1055,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
if (authorId == null) return;
|
if (authorId == null) return;
|
||||||
|
|
||||||
// reuse previously created cache of author's data
|
// reuse previously created cache of author's data
|
||||||
const authorInfo = historicalAuthorData[authorId] || await authorManager.getAuthor(authorId);
|
const authorInfo = historicalAuthorData[authorId] || await getAuthor(authorId);
|
||||||
if (authorInfo == null) {
|
if (authorInfo == null) {
|
||||||
messageLogger.error(
|
messageLogger.error(
|
||||||
`Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` +
|
`Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` +
|
||||||
|
@ -1077,7 +1080,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
socket.emit('message', msg);
|
socket.emit('message', msg);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await hooks.aCallAll('userJoin', {
|
await aCallAll('userJoin', {
|
||||||
authorId: sessionInfo.author,
|
authorId: sessionInfo.author,
|
||||||
displayName: authorName,
|
displayName: authorName,
|
||||||
padId: sessionInfo.padId,
|
padId: sessionInfo.padId,
|
||||||
|
@ -1090,7 +1093,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
/**
|
/**
|
||||||
* Handles a request for a rough changeset, the timeslider client needs it
|
* Handles a request for a rough changeset, the timeslider client needs it
|
||||||
*/
|
*/
|
||||||
const handleChangesetRequest = async (socket:any, {data: {granularity, start, requestID}}: ChangesetRequest) => {
|
const handleChangesetRequest = async (socket:any, {data: {granularity, start, requestID}}: ChangesetRequestMessage) => {
|
||||||
if (granularity == null) throw new Error('missing granularity');
|
if (granularity == null) throw new Error('missing granularity');
|
||||||
if (!Number.isInteger(granularity)) throw new Error('granularity is not an integer');
|
if (!Number.isInteger(granularity)) throw new Error('granularity is not an integer');
|
||||||
if (start == null) throw new Error('missing start');
|
if (start == null) throw new Error('missing start');
|
||||||
|
@ -1098,7 +1101,7 @@ const handleChangesetRequest = async (socket:any, {data: {granularity, start, re
|
||||||
if (requestID == null) throw new Error('mising requestID');
|
if (requestID == null) throw new Error('mising requestID');
|
||||||
const end = start + (100 * granularity);
|
const end = start + (100 * granularity);
|
||||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||||
const pad = await padManager.getPad(padId, null, authorId);
|
const pad = await getPad(padId, null, authorId);
|
||||||
const headRev = pad.getHeadRevisionNumber();
|
const headRev = pad.getHeadRevisionNumber();
|
||||||
if (start > headRev)
|
if (start > headRev)
|
||||||
start = headRev;
|
start = headRev;
|
||||||
|
@ -1111,7 +1114,7 @@ const handleChangesetRequest = async (socket:any, {data: {granularity, start, re
|
||||||
* Tries to rebuild the getChangestInfo function of the original Etherpad
|
* Tries to rebuild the getChangestInfo function of the original Etherpad
|
||||||
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
|
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144
|
||||||
*/
|
*/
|
||||||
const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, granularity: number) => {
|
const getChangesetInfo = async (pad: Pad, startNum: number, endNum:number, granularity: number) => {
|
||||||
const headRevision = pad.getHeadRevisionNumber();
|
const headRevision = pad.getHeadRevisionNumber();
|
||||||
|
|
||||||
// calculate the last full endnum
|
// calculate the last full endnum
|
||||||
|
@ -1163,13 +1166,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
||||||
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
|
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
|
||||||
|
|
||||||
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
|
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
|
||||||
const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
|
const backwards = inverse(forwards, lines.textlines, lines.alines, pad.apool());
|
||||||
|
|
||||||
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
|
new AttributionLinesMutator(forwards, lines.alines, pad.apool());
|
||||||
Changeset.mutateTextLines(forwards, lines.textlines);
|
mutateTextLines(forwards, lines.textlines);
|
||||||
|
|
||||||
const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
|
const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool);
|
||||||
const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
|
const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool);
|
||||||
|
|
||||||
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
|
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
|
||||||
const t2 = revisionDate[compositeEnd - 1];
|
const t2 = revisionDate[compositeEnd - 1];
|
||||||
|
@ -1188,19 +1191,19 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
||||||
* Tries to rebuild the getPadLines function of the original Etherpad
|
* Tries to rebuild the getPadLines function of the original Etherpad
|
||||||
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263
|
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263
|
||||||
*/
|
*/
|
||||||
const getPadLines = async (pad: PadType, revNum: number) => {
|
const getPadLines = async (pad: Pad, revNum: number) => {
|
||||||
// get the atext
|
// get the atext
|
||||||
let atext;
|
let atext;
|
||||||
|
|
||||||
if (revNum >= 0) {
|
if (revNum >= 0) {
|
||||||
atext = await pad.getInternalRevisionAText(revNum);
|
atext = await pad.getInternalRevisionAText(revNum);
|
||||||
} else {
|
} else {
|
||||||
atext = Changeset.makeAText('\n');
|
atext = makeAText('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
textlines: Changeset.splitTextLines(atext.text),
|
textlines: splitTextLines(atext.text),
|
||||||
alines: Changeset.splitAttributionLines(atext.attribs, atext.text),
|
alines: splitAttributionLines(atext.attribs, atext.text),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1208,7 +1211,7 @@ const getPadLines = async (pad: PadType, revNum: number) => {
|
||||||
* Tries to rebuild the composePadChangeset function of the original Etherpad
|
* Tries to rebuild the composePadChangeset function of the original Etherpad
|
||||||
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
|
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
|
||||||
*/
|
*/
|
||||||
const composePadChangesets = async (pad: PadType, startNum: number, endNum: number) => {
|
const composePadChangesets = async (pad: Pad, startNum: number, endNum: number) => {
|
||||||
// fetch all changesets we need
|
// fetch all changesets we need
|
||||||
const headNum = pad.getHeadRevisionNumber();
|
const headNum = pad.getHeadRevisionNumber();
|
||||||
endNum = Math.min(endNum, headNum + 1);
|
endNum = Math.min(endNum, headNum + 1);
|
||||||
|
@ -1235,7 +1238,7 @@ const composePadChangesets = async (pad: PadType, startNum: number, endNum: numb
|
||||||
|
|
||||||
for (r = startNum + 1; r < endNum; r++) {
|
for (r = startNum + 1; r < endNum; r++) {
|
||||||
const cs = changesets[r];
|
const cs = changesets[r];
|
||||||
changeset = Changeset.compose(changeset, cs, pool);
|
changeset = compose(changeset, cs, pool);
|
||||||
}
|
}
|
||||||
return changeset;
|
return changeset;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1247,7 +1250,7 @@ const composePadChangesets = async (pad: PadType, startNum: number, endNum: numb
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getRoomSockets = (padID: string) => {
|
const _getRoomSockets = (padID: string) => {
|
||||||
const ns = socketio.sockets; // Default namespace.
|
const ns = _socketio.sockets; // Default namespace.
|
||||||
// We could call adapter.clients(), but that method is unnecessarily asynchronous. Replicate what
|
// We could call adapter.clients(), but that method is unnecessarily asynchronous. Replicate what
|
||||||
// it does here, but synchronously to avoid a race condition. This code will have to change when
|
// it does here, but synchronously to avoid a race condition. This code will have to change when
|
||||||
// we update to socket.io v3.
|
// we update to socket.io v3.
|
||||||
|
@ -1263,21 +1266,21 @@ const _getRoomSockets = (padID: string) => {
|
||||||
/**
|
/**
|
||||||
* Get the number of users in a pad
|
* Get the number of users in a pad
|
||||||
*/
|
*/
|
||||||
exports.padUsersCount = (padID:string) => ({
|
export const padUsersCount = (padID:string) => ({
|
||||||
padUsersCount: _getRoomSockets(padID).length,
|
padUsersCount: _getRoomSockets(padID).length,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of users in a pad
|
* Get the list of users in a pad
|
||||||
*/
|
*/
|
||||||
exports.padUsers = async (padID: string) => {
|
export const padUsers = async (padID: string) => {
|
||||||
const padUsers:PadAuthor[] = [];
|
const padUsers:PadAuthor[] = [];
|
||||||
|
|
||||||
// iterate over all clients (in parallel)
|
// iterate over all clients (in parallel)
|
||||||
await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => {
|
await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => {
|
||||||
const s = sessioninfos[roomSocket.id];
|
const s = sessioninfos[roomSocket.id];
|
||||||
if (s) {
|
if (s) {
|
||||||
const author = await authorManager.getAuthor(s.author);
|
const author = await getAuthor(s.author);
|
||||||
// Fixes: https://github.com/ether/etherpad-lite/issues/4120
|
// Fixes: https://github.com/ether/etherpad-lite/issues/4120
|
||||||
// On restart author might not be populated?
|
// On restart author might not be populated?
|
||||||
if (author) {
|
if (author) {
|
||||||
|
@ -1289,5 +1292,3 @@ exports.padUsers = async (padID: string) => {
|
||||||
|
|
||||||
return {padUsers};
|
return {padUsers};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.sessioninfos = sessioninfos;
|
|
||||||
|
|
|
@ -1,49 +1,50 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {Socket} from "node:net";
|
|
||||||
import type {MapArrayType} from "../types/MapType";
|
import type {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
// @ts-ignore
|
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import events from 'events';
|
import events from 'events';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import expressSession from '@etherpad/express-session';
|
import expressSession from '@etherpad/express-session';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
import {aCallAll} from '../../static/js/pluginfw/hooks';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const SessionStore = require('../db/SessionStore');
|
import SessionStore from '../db/SessionStore';
|
||||||
const settings = require('../utils/Settings');
|
import settings from '../utils/Settings';
|
||||||
const stats = require('../stats')
|
import {measuredCollection} from '../stats';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
const webaccess = require('./express/webaccess');
|
import {checkAccess} from './express/webaccess';
|
||||||
|
|
||||||
import SecretRotator from '../security/SecretRotator';
|
import SecretRotator from '../security/SecretRotator';
|
||||||
|
import {Server, Socket} from "socket.io";
|
||||||
|
import {DefaultEventsMap} from "socket.io/dist/typed-events";
|
||||||
|
|
||||||
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<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>> = new Set();
|
||||||
const socketsEvents = new events.EventEmitter();
|
const socketsEvents = new events.EventEmitter();
|
||||||
const startTime = stats.settableGauge('httpStartTime');
|
const startTime = measuredCollection.settableGauge('httpStartTime');
|
||||||
|
|
||||||
exports.server = null;
|
|
||||||
|
|
||||||
const closeServer = async () => {
|
const closeServer = async () => {
|
||||||
if (exports.server != null) {
|
if (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 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(server.close.bind(server))();
|
||||||
await hooks.aCallAll('expressCloseServer');
|
await 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(`Forcibly terminating remaining ${sockets.size} HTTP connections...`);
|
||||||
for (const socket of sockets) socket.destroy(new Error('HTTP server is closing'));
|
for (const socket of sockets) { // @ts-ignore
|
||||||
|
socket.destroy(new Error('HTTP server is closing'));
|
||||||
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
let lastLogged = 0;
|
let lastLogged = 0;
|
||||||
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
while (sockets.size > 0 && !settings.enableAdminUITests) {
|
||||||
|
@ -55,7 +56,7 @@ const closeServer = async () => {
|
||||||
}
|
}
|
||||||
await p;
|
await p;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
exports.server = null;
|
server = null;
|
||||||
startTime.setValue(0);
|
startTime.setValue(0);
|
||||||
logger.info('HTTP server closed');
|
logger.info('HTTP server closed');
|
||||||
}
|
}
|
||||||
|
@ -65,14 +66,14 @@ const closeServer = async () => {
|
||||||
secretRotator = null;
|
secretRotator = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.createServer = async () => {
|
export const 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 restartServer();
|
||||||
|
|
||||||
if (settings.ip === '') {
|
if (settings.ip === '') {
|
||||||
// using Unix socket for connectivity
|
// using Unix socket for connectivity
|
||||||
|
@ -97,7 +98,10 @@ exports.createServer = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.restartServer = async () => {
|
export let server: Server|null = null;
|
||||||
|
export let sessionMiddleware:any
|
||||||
|
|
||||||
|
export const restartServer = async () => {
|
||||||
await closeServer();
|
await closeServer();
|
||||||
|
|
||||||
const app = express(); // New syntax for express v3
|
const app = express(); // New syntax for express v3
|
||||||
|
@ -121,10 +125,11 @@ exports.restartServer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
exports.server = https.createServer(options, app);
|
|
||||||
|
server = https.createServer(options, app);
|
||||||
} else {
|
} else {
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
exports.server = http.createServer(app);
|
server = http.createServer(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
|
@ -167,7 +172,7 @@ exports.restartServer = async () => {
|
||||||
|
|
||||||
// Measure response time
|
// Measure response time
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const stopWatch = stats.timer('httpRequests').start();
|
const stopWatch = measuredCollection.timer('httpRequests').start();
|
||||||
const sendFn = res.send.bind(res);
|
const sendFn = res.send.bind(res);
|
||||||
res.send = (...args) => { stopWatch.end(); return sendFn(...args); };
|
res.send = (...args) => { stopWatch.end(); return sendFn(...args); };
|
||||||
next();
|
next();
|
||||||
|
@ -177,7 +182,8 @@ exports.restartServer = async () => {
|
||||||
// starts listening to requests as reported in issue #158. Not installing the log4js connect
|
// starts listening to requests as reported in issue #158. Not installing the log4js connect
|
||||||
// logger when the log level has a higher severity than INFO since it would not log at that level
|
// logger when the log level has a higher severity than INFO since it would not log at that level
|
||||||
// anyway.
|
// anyway.
|
||||||
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
|
// @ts-ignore
|
||||||
|
if (!(loglevel === 'WARN' && loglevel === 'ERROR')) {
|
||||||
app.use(log4js.connectLogger(logger, {
|
app.use(log4js.connectLogger(logger, {
|
||||||
level: log4js.levels.DEBUG.levelStr,
|
level: log4js.levels.DEBUG.levelStr,
|
||||||
format: ':status, :method :url',
|
format: ':status, :method :url',
|
||||||
|
@ -185,7 +191,7 @@ exports.restartServer = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {keyRotationInterval, sessionLifetime} = settings.cookie;
|
const {keyRotationInterval, sessionLifetime} = settings.cookie;
|
||||||
let secret = settings.sessionKey;
|
let secret: string|string[] = settings.sessionKey!;
|
||||||
if (keyRotationInterval && sessionLifetime) {
|
if (keyRotationInterval && sessionLifetime) {
|
||||||
secretRotator = new SecretRotator(
|
secretRotator = new SecretRotator(
|
||||||
'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey);
|
'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey);
|
||||||
|
@ -197,7 +203,7 @@ exports.restartServer = async () => {
|
||||||
app.use(cookieParser(secret, {}));
|
app.use(cookieParser(secret, {}));
|
||||||
|
|
||||||
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
|
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
|
||||||
exports.sessionMiddleware = expressSession({
|
sessionMiddleware = expressSession({
|
||||||
propagateTouch: true,
|
propagateTouch: true,
|
||||||
rolling: true,
|
rolling: true,
|
||||||
secret,
|
secret,
|
||||||
|
@ -234,16 +240,16 @@ exports.restartServer = async () => {
|
||||||
// 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 aCallAll('expressPreSession', {app});
|
||||||
app.use(exports.sessionMiddleware);
|
app.use(sessionMiddleware);
|
||||||
|
|
||||||
app.use(webaccess.checkAccess);
|
app.use(checkAccess);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
hooks.aCallAll('expressConfigure', {app}),
|
aCallAll('expressConfigure', {app}),
|
||||||
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
|
aCallAll('expressCreateServer', {app, server: server}),
|
||||||
]);
|
]);
|
||||||
exports.server.on('connection', (socket:Socket) => {
|
server!.on('connection', (socket) => {
|
||||||
sockets.add(socket);
|
sockets.add(socket);
|
||||||
socketsEvents.emit('updated');
|
socketsEvents.emit('updated');
|
||||||
socket.on('close', () => {
|
socket.on('close', () => {
|
||||||
|
@ -251,11 +257,12 @@ exports.restartServer = async () => {
|
||||||
socketsEvents.emit('updated');
|
socketsEvents.emit('updated');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip);
|
// @ts-ignore
|
||||||
|
await util.promisify(server!.listen).bind(server)(port, ip);
|
||||||
startTime.setValue(Date.now());
|
startTime.setValue(Date.now());
|
||||||
logger.info('HTTP server listening for connections');
|
logger.info('HTTP server listening for connections');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.shutdown = async (hookName:string, context: any) => {
|
export const shutdown = async (hookName:string, context: any) => {
|
||||||
await closeServer();
|
await closeServer();
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
import {Dirent} from "node:fs";
|
import {Dirent} from "node:fs";
|
||||||
import {PluginDef} from "../../types/PartType";
|
import {PluginDef} from "../../types/PartType";
|
||||||
|
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const fsp = require('fs').promises;
|
import {promises as fsp} from 'fs';
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
import {pluginDefs} from '../../../static/js/pluginfw/plugin_defs';
|
||||||
const sanitizePathname = require('../../utils/sanitizePathname');
|
import sanitizePathname from '../../utils/sanitizePathname';
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require('../../utils/Settings');
|
||||||
|
|
||||||
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
|
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
|
||||||
|
@ -32,11 +32,11 @@ const findSpecs = async (specDir: string) => {
|
||||||
return specs;
|
return specs;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
export const expressPreSession = async (hookName:string, {app}:any) => {
|
||||||
app.get('/tests/frontend/frontendTestSpecs.json', (req:any, res:any, next:Function) => {
|
app.get('/tests/frontend/frontendTestSpecs.json', (req:any, res:any, next:Function) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const modules:string[] = [];
|
const modules:string[] = [];
|
||||||
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
|
await Promise.all(Object.entries(pluginDefs.getPlugins()).map(async ([plugin, def]) => {
|
||||||
let {package: {path: pluginPath}} = def as PluginDef;
|
let {package: {path: pluginPath}} = def as PluginDef;
|
||||||
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
|
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
|
||||||
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
|
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
|
||||||
|
|
|
@ -7,21 +7,19 @@ import {WebAccessTypes} from "../../types/WebAccessTypes";
|
||||||
import {SettingsUser} from "../../types/SettingsUser";
|
import {SettingsUser} from "../../types/SettingsUser";
|
||||||
const httpLogger = log4js.getLogger('http');
|
const httpLogger = log4js.getLogger('http');
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require('../../utils/Settings');
|
||||||
const hooks = require('../../../static/js/pluginfw/hooks');
|
import {deprecationNotices, aCallFirst as HookAcall} from '../../../static/js/pluginfw/hooks';
|
||||||
const readOnlyManager = require('../../db/ReadOnlyManager');
|
const readOnlyManager = require('../../db/ReadOnlyManager');
|
||||||
|
|
||||||
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
||||||
|
|
||||||
// Promisified wrapper around hooks.aCallFirst.
|
// Promisified wrapper around hooks.aCallFirst.
|
||||||
const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => {
|
const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => {
|
||||||
hooks.aCallFirst(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred);
|
HookAcall(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred)
|
||||||
});
|
});
|
||||||
|
|
||||||
const aCallFirst0 =
|
const aCallFirst0 = async (hookName: string, context:any, pred = null): Promise<any> => (await aCallFirst(hookName, context, pred)) as any[0];
|
||||||
// @ts-ignore
|
|
||||||
async (hookName: string, context:any, pred = null) => (await aCallFirst(hookName, context, pred))[0];
|
|
||||||
|
|
||||||
exports.normalizeAuthzLevel = (level: string|boolean) => {
|
export const normalizeAuthzLevel = (level: string|boolean) => {
|
||||||
if (!level) return false;
|
if (!level) return false;
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case true:
|
case true:
|
||||||
|
@ -36,20 +34,20 @@ exports.normalizeAuthzLevel = (level: string|boolean) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.userCanModify = (padId: string, req: SocketClientRequest) => {
|
export const userCanModify = (padId: string, req: SocketClientRequest) => {
|
||||||
if (readOnlyManager.isReadOnlyId(padId)) return false;
|
if (readOnlyManager.isReadOnlyId(padId)) return false;
|
||||||
if (!settings.requireAuthentication) return true;
|
if (!settings.requireAuthentication) return true;
|
||||||
const {session: {user} = {}} = req;
|
const {session: {user} = {}} = req;
|
||||||
if (!user || user.readOnly) return false;
|
if (!user || user.readOnly) return false;
|
||||||
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
|
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
|
||||||
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
|
const level = normalizeAuthzLevel(user.padAuthorizations[padId]);
|
||||||
return level && level !== 'readOnly';
|
return level && level !== 'readOnly';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
|
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
|
||||||
exports.authnFailureDelayMs = 1000;
|
export const authnFailureDelayMs = 1000;
|
||||||
|
|
||||||
const checkAccess = async (req:any, res:any, next: Function) => {
|
const _checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth');
|
const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth');
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -93,7 +91,7 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
// authentication is checked and once after (if settings.requireAuthorization is true).
|
// authentication is checked and once after (if settings.requireAuthorization is true).
|
||||||
const authorize = async () => {
|
const authorize = async () => {
|
||||||
const grant = async (level: string|false) => {
|
const grant = async (level: string|false) => {
|
||||||
level = exports.normalizeAuthzLevel(level);
|
level = normalizeAuthzLevel(level);
|
||||||
if (!level) return false;
|
if (!level) return false;
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
if (user == null) return true; // This will happen if authentication is not required.
|
if (user == null) return true; // This will happen if authentication is not required.
|
||||||
|
@ -173,7 +171,7 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
||||||
}
|
}
|
||||||
// Delay the error response for 1s to slow down brute force attacks.
|
// Delay the error response for 1s to slow down brute force attacks.
|
||||||
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
|
await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs));
|
||||||
res.status(401).send('Authentication Required');
|
res.status(401).send('Authentication Required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -213,6 +211,6 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
||||||
* Express middleware to authenticate the user and check authorization. Must be installed after the
|
* Express middleware to authenticate the user and check authorization. Must be installed after the
|
||||||
* express-session middleware.
|
* express-session middleware.
|
||||||
*/
|
*/
|
||||||
exports.checkAccess = (req:any, res:any, next:Function) => {
|
export const checkAccess = (req:any, res:any, next:Function) => {
|
||||||
checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
|
_checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
const securityManager = require('./db/SecurityManager');
|
import {checkAccess} from './db/SecurityManager';
|
||||||
|
|
||||||
// checks for padAccess
|
// checks for padAccess
|
||||||
module.exports = async (req: { params?: any; cookies?: any; session?: any; }, res: { status: (arg0: number) => { (): any; new(): any; send: { (arg0: string): void; new(): any; }; }; }) => {
|
export default async (req: { params?: any; cookies?: any; session?: any; }, res: { status: (arg0: number) => { (): any; new(): any; send: { (arg0: string): void; new(): any; }; }; }) => {
|
||||||
const {session: {user} = {}} = req;
|
const {session: {user} = {}} = req;
|
||||||
const accessObj = await securityManager.checkAccess(
|
const accessObj = await checkAccess(
|
||||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
||||||
|
|
||||||
if (accessObj.accessStatus === 'grant') {
|
if (accessObj.accessStatus === 'grant') {
|
||||||
|
|
|
@ -28,8 +28,7 @@ import log4js from 'log4js';
|
||||||
import pkg from '../package.json';
|
import pkg from '../package.json';
|
||||||
import {checkForMigration} from "../static/js/pluginfw/installer";
|
import {checkForMigration} from "../static/js/pluginfw/installer";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import settings from "./utils/Settings";
|
||||||
const settings = require('./utils/Settings');
|
|
||||||
|
|
||||||
let wtfnode: any;
|
let wtfnode: any;
|
||||||
if (settings.dumpOnUncleanExit) {
|
if (settings.dumpOnUncleanExit) {
|
||||||
|
@ -64,18 +63,18 @@ if (process.env['https_proxy']) {
|
||||||
* early check for version compatibility before calling
|
* early check for version compatibility before calling
|
||||||
* any modules that require newer versions of NodeJS
|
* any modules that require newer versions of NodeJS
|
||||||
*/
|
*/
|
||||||
const NodeVersion = require('./utils/NodeVersion');
|
import {checkDeprecationStatus, enforceMinNodeVersion} from './utils/NodeVersion';
|
||||||
NodeVersion.enforceMinNodeVersion(pkg.engines.node.replace(">=", ""));
|
enforceMinNodeVersion(pkg.engines.node.replace(">=", ""));
|
||||||
NodeVersion.checkDeprecationStatus(pkg.engines.node.replace(">=", ""), '2.1.0');
|
checkDeprecationStatus(pkg.engines.node.replace(">=", ""), '2.1.0');
|
||||||
|
|
||||||
const UpdateCheck = require('./utils/UpdateCheck');
|
import {check} from './utils/UpdateCheck';
|
||||||
const db = require('./db/DB');
|
import {init} from './db/DB';
|
||||||
const express = require('./hooks/express');
|
import {server} from './hooks/express';
|
||||||
const hooks = require('../static/js/pluginfw/hooks');
|
import {aCallAll} from '../static/js/pluginfw/hooks';
|
||||||
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
import {pluginDefs} from '../static/js/pluginfw/plugin_defs';
|
||||||
const plugins = require('../static/js/pluginfw/plugins');
|
import {formatHooks, formatParts, update} from '../static/js/pluginfw/plugins';
|
||||||
const {Gate} = require('./utils/promises');
|
import {Gate} from './utils/promises';
|
||||||
const stats = require('./stats')
|
import {measuredCollection} from './stats';
|
||||||
|
|
||||||
const logger = log4js.getLogger('server');
|
const logger = log4js.getLogger('server');
|
||||||
|
|
||||||
|
@ -100,17 +99,17 @@ const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsLi
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let startDoneGate: { resolve: () => void; }
|
let startDoneGate: Gate<void>
|
||||||
exports.start = async () => {
|
export const start = async (): Promise<any> => {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case State.INITIAL:
|
case State.INITIAL:
|
||||||
break;
|
break;
|
||||||
case State.STARTING:
|
case State.STARTING:
|
||||||
await startDoneGate;
|
await startDoneGate;
|
||||||
// Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED.
|
// Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED.
|
||||||
return await exports.start();
|
return await start();
|
||||||
case State.RUNNING:
|
case State.RUNNING:
|
||||||
return express.server;
|
return server;
|
||||||
case State.STOPPING:
|
case State.STOPPING:
|
||||||
case State.STOPPED:
|
case State.STOPPED:
|
||||||
case State.EXITING:
|
case State.EXITING:
|
||||||
|
@ -121,22 +120,20 @@ exports.start = async () => {
|
||||||
throw new Error(`unknown State: ${state.toString()}`);
|
throw new Error(`unknown State: ${state.toString()}`);
|
||||||
}
|
}
|
||||||
logger.info('Starting Etherpad...');
|
logger.info('Starting Etherpad...');
|
||||||
startDoneGate = new Gate();
|
startDoneGate = new Gate<void>();
|
||||||
state = State.STARTING;
|
state = State.STARTING;
|
||||||
try {
|
try {
|
||||||
// Check if Etherpad version is up-to-date
|
// Check if Etherpad version is up-to-date
|
||||||
UpdateCheck.check();
|
check();
|
||||||
|
|
||||||
// @ts-ignore
|
measuredCollection.gauge('memoryUsage', () => process.memoryUsage().rss);
|
||||||
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
|
measuredCollection.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
|
||||||
// @ts-ignore
|
|
||||||
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
|
|
||||||
|
|
||||||
process.on('uncaughtException', (err: ErrorCaused) => {
|
process.on('uncaughtException', (err: ErrorCaused) => {
|
||||||
logger.debug(`uncaught exception: ${err.stack || err}`);
|
logger.debug(`uncaught exception: ${err.stack || err}`);
|
||||||
|
|
||||||
// eslint-disable-next-line promise/no-promise-in-callback
|
// eslint-disable-next-line promise/no-promise-in-callback
|
||||||
exports.exit(err)
|
exit(err)
|
||||||
.catch((err: ErrorCaused) => {
|
.catch((err: ErrorCaused) => {
|
||||||
logger.error('Error in process exit', err);
|
logger.error('Error in process exit', err);
|
||||||
// eslint-disable-next-line n/no-process-exit
|
// eslint-disable-next-line n/no-process-exit
|
||||||
|
@ -153,12 +150,12 @@ exports.start = async () => {
|
||||||
for (const signal of ['SIGINT', 'SIGTERM'] as NodeJS.Signals[]) {
|
for (const signal of ['SIGINT', 'SIGTERM'] as NodeJS.Signals[]) {
|
||||||
// Forcibly remove other signal listeners to prevent them from terminating node before we are
|
// Forcibly remove other signal listeners to prevent them from terminating node before we are
|
||||||
// done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a
|
// done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a
|
||||||
// problematic listener. This means that exports.exit is solely responsible for performing all
|
// problematic listener. This means that exit is solely responsible for performing all
|
||||||
// necessary cleanup tasks.
|
// necessary cleanup tasks.
|
||||||
for (const listener of process.listeners(signal)) {
|
for (const listener of process.listeners(signal)) {
|
||||||
removeSignalListener(signal, listener);
|
removeSignalListener(signal, listener);
|
||||||
}
|
}
|
||||||
process.on(signal, exports.exit);
|
process.on(signal, exit);
|
||||||
// Prevent signal listeners from being added in the future.
|
// Prevent signal listeners from being added in the future.
|
||||||
process.on('newListener', (event, listener) => {
|
process.on('newListener', (event, listener) => {
|
||||||
if (event !== signal) return;
|
if (event !== signal) return;
|
||||||
|
@ -166,40 +163,40 @@ exports.start = async () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.init();
|
await init();
|
||||||
await checkForMigration();
|
await checkForMigration();
|
||||||
await plugins.update();
|
await update();
|
||||||
const installedPlugins = (Object.values(pluginDefs.plugins) as PluginType[])
|
const installedPlugins = (Object.values(pluginDefs.getPlugins()) as PluginType[])
|
||||||
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
|
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
|
||||||
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
|
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
logger.info(`Installed plugins: ${installedPlugins}`);
|
logger.info(`Installed plugins: ${installedPlugins}`);
|
||||||
logger.debug(`Installed parts:\n${plugins.formatParts()}`);
|
logger.debug(`Installed parts:\n${formatParts()}`);
|
||||||
logger.debug(`Installed server-side hooks:\n${plugins.formatHooks('hooks', false)}`);
|
logger.debug(`Installed server-side hooks:\n${formatHooks('hooks', false)}`);
|
||||||
await hooks.aCallAll('loadSettings', {settings});
|
await aCallAll('loadSettings', {settings});
|
||||||
await hooks.aCallAll('createServer');
|
await aCallAll('createServer');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error occurred while starting Etherpad');
|
logger.error('Error occurred while starting Etherpad');
|
||||||
state = State.STATE_TRANSITION_FAILED;
|
state = State.STATE_TRANSITION_FAILED;
|
||||||
startDoneGate.resolve();
|
startDoneGate.resolve!();
|
||||||
return await exports.exit(err);
|
return await exit(err as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Etherpad is running');
|
logger.info('Etherpad is running');
|
||||||
state = State.RUNNING;
|
state = State.RUNNING;
|
||||||
startDoneGate.resolve();
|
startDoneGate.resolve!();
|
||||||
|
|
||||||
// Return the HTTP server to make it easier to write tests.
|
// Return the HTTP server to make it easier to write tests.
|
||||||
return express.server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopDoneGate = new Gate();
|
const stopDoneGate = new Gate();
|
||||||
exports.stop = async () => {
|
export const stop = async (): Promise<void> => {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case State.STARTING:
|
case State.STARTING:
|
||||||
await exports.start();
|
await start();
|
||||||
// Don't fall through to State.RUNNING in case another caller is also waiting for startup.
|
// Don't fall through to State.RUNNING in case another caller is also waiting for startup.
|
||||||
return await exports.stop();
|
return await stop();
|
||||||
case State.RUNNING:
|
case State.RUNNING:
|
||||||
break;
|
break;
|
||||||
case State.STOPPING:
|
case State.STOPPING:
|
||||||
|
@ -219,7 +216,7 @@ exports.stop = async () => {
|
||||||
try {
|
try {
|
||||||
let timeout: NodeJS.Timeout = null as unknown as NodeJS.Timeout;
|
let timeout: NodeJS.Timeout = null as unknown as NodeJS.Timeout;
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
hooks.aCallAll('shutdown'),
|
aCallAll('shutdown'),
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
|
timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
|
||||||
}),
|
}),
|
||||||
|
@ -228,24 +225,25 @@ exports.stop = async () => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error occurred while stopping Etherpad');
|
logger.error('Error occurred while stopping Etherpad');
|
||||||
state = State.STATE_TRANSITION_FAILED;
|
state = State.STATE_TRANSITION_FAILED;
|
||||||
stopDoneGate.resolve();
|
stopDoneGate.resolve!();
|
||||||
return await exports.exit(err);
|
// @ts-ignore
|
||||||
|
return await exit(err);
|
||||||
}
|
}
|
||||||
logger.info('Etherpad stopped');
|
logger.info('Etherpad stopped');
|
||||||
state = State.STOPPED;
|
state = State.STOPPED;
|
||||||
stopDoneGate.resolve();
|
stopDoneGate.resolve!();
|
||||||
};
|
};
|
||||||
|
|
||||||
let exitGate: any;
|
let exitGate: any;
|
||||||
let exitCalled = false;
|
let exitCalled = false;
|
||||||
exports.exit = async (err: ErrorCaused|string|null = null) => {
|
export const exit = async (err: ErrorCaused|string|null = null): Promise<void> => {
|
||||||
/* eslint-disable no-process-exit */
|
/* eslint-disable no-process-exit */
|
||||||
if (err === 'SIGTERM') {
|
if (err === 'SIGTERM') {
|
||||||
// Termination from SIGTERM is not treated as an abnormal termination.
|
// Termination from SIGTERM is not treated as an abnormal termination.
|
||||||
logger.info('Received SIGTERM signal');
|
logger.info('Received SIGTERM signal');
|
||||||
err = null;
|
err = null;
|
||||||
} else if (typeof err == "object" && err != null) {
|
} else if (typeof err == "object" && err != null) {
|
||||||
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`);
|
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(measuredCollection.toJSON(), null, 2)}`);
|
||||||
logger.error(err.stack || err.toString());
|
logger.error(err.stack || err.toString());
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
if (exitCalled) {
|
if (exitCalled) {
|
||||||
|
@ -259,11 +257,11 @@ exports.exit = async (err: ErrorCaused|string|null = null) => {
|
||||||
case State.STARTING:
|
case State.STARTING:
|
||||||
case State.RUNNING:
|
case State.RUNNING:
|
||||||
case State.STOPPING:
|
case State.STOPPING:
|
||||||
await exports.stop();
|
await stop();
|
||||||
// Don't fall through to State.STOPPED in case another caller is also waiting for stop().
|
// Don't fall through to State.STOPPED in case another caller is also waiting for stop().
|
||||||
// Don't pass err to exports.exit() because this err has already been processed. (If err is
|
// Don't pass err to exports.exit() because this err has already been processed. (If err is
|
||||||
// passed again to exit() then exit() will think that a second error occurred while exiting.)
|
// passed again to exit() then exit() will think that a second error occurred while exiting.)
|
||||||
return await exports.exit();
|
return await exit();
|
||||||
case State.INITIAL:
|
case State.INITIAL:
|
||||||
case State.STOPPED:
|
case State.STOPPED:
|
||||||
case State.STATE_TRANSITION_FAILED:
|
case State.STATE_TRANSITION_FAILED:
|
||||||
|
@ -303,7 +301,7 @@ exports.exit = async (err: ErrorCaused|string|null = null) => {
|
||||||
/* eslint-enable no-process-exit */
|
/* eslint-enable no-process-exit */
|
||||||
};
|
};
|
||||||
|
|
||||||
if (require.main === module) exports.start();
|
if (require.main === module) start();
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (typeof(PhusionPassenger) !== 'undefined') exports.start();
|
if (typeof(PhusionPassenger) !== 'undefined') start();
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
|
|
||||||
const measured = require('measured-core');
|
const measured = require('measured-core');
|
||||||
|
|
||||||
module.exports = measured.createCollection();
|
export const measuredCollection = measured.createCollection();
|
||||||
|
|
||||||
// @ts-ignore
|
export const shutdown = async (hookName: string, context:any) => {
|
||||||
module.exports.shutdown = async (hookName, context) => {
|
measuredCollection.end();
|
||||||
module.exports.end();
|
|
||||||
};
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
export type RunCMDOptions = {
|
export type RunCMDOptions = {
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
stdio?: string[],
|
stdio?: string[]|null[],
|
||||||
env?: NodeJS.ProcessEnv
|
env?: NodeJS.ProcessEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {UserSettingsObject} from "./UserSettingsObject";
|
||||||
|
|
||||||
export type SocketClientRequest = {
|
export type SocketClientRequest = {
|
||||||
session: {
|
session: {
|
||||||
user: {
|
user: {
|
||||||
|
@ -5,6 +7,7 @@ export type SocketClientRequest = {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
padAuthorizations: {
|
padAuthorizations: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
|
user: UserSettingsObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,17 @@
|
||||||
import {ChildProcess} from "node:child_process";
|
import {ChildProcess} from "node:child_process";
|
||||||
import {AsyncQueueTask} from "../types/AsyncQueueTask";
|
import {AsyncQueueTask} from "../types/AsyncQueueTask";
|
||||||
|
|
||||||
const spawn = require('child_process').spawn;
|
import {spawn} from 'child_process'
|
||||||
const async = require('async');
|
import async from 'async';
|
||||||
const settings = require('./Settings');
|
const settings = require('./Settings');
|
||||||
const os = require('os');
|
import os from 'os';
|
||||||
|
|
||||||
|
export let convertFile: (srcFile: string, destFile: string, type: string)=> Promise<void>
|
||||||
|
|
||||||
// on windows we have to spawn a process for each convertion,
|
// on windows we have to spawn a process for each convertion,
|
||||||
// cause the plugin abicommand doesn't exist on this platform
|
// cause the plugin abicommand doesn't exist on this platform
|
||||||
if (os.type().indexOf('Windows') > -1) {
|
if (os.type().indexOf('Windows') > -1) {
|
||||||
exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
|
convertFile = async (srcFile: string, destFile: string, type: string) => {
|
||||||
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
|
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
|
||||||
let stdoutBuffer = '';
|
let stdoutBuffer = '';
|
||||||
abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); });
|
abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); });
|
||||||
|
@ -87,7 +89,7 @@ if (os.type().indexOf('Windows') > -1) {
|
||||||
};
|
};
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
|
convertFile = async (srcFile: string, destFile: string, type: string) => {
|
||||||
await queue.pushAsync({srcFile, destFile, type});
|
await queue.pushAsync({srcFile, destFile, type});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,9 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
const log4js = require('log4js');
|
import log4js from 'log4js';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const _ = require('underscore');
|
import _ from 'underscore';
|
||||||
|
|
||||||
const absPathLogger = log4js.getLogger('AbsolutePaths');
|
const absPathLogger = log4js.getLogger('AbsolutePaths');
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ const popIfEndsWith = (stringArray: string[], lastDesiredElements: string[]): st
|
||||||
* @return {string} The identified absolute base path. If such path cannot be
|
* @return {string} The identified absolute base path. If such path cannot be
|
||||||
* identified, prints a log and exits the application.
|
* identified, prints a log and exits the application.
|
||||||
*/
|
*/
|
||||||
exports.findEtherpadRoot = () => {
|
export const findEtherpadRoot = () => {
|
||||||
if (etherpadRoot != null) {
|
if (etherpadRoot != null) {
|
||||||
return etherpadRoot;
|
return etherpadRoot;
|
||||||
}
|
}
|
||||||
|
@ -130,12 +130,12 @@ exports.findEtherpadRoot = () => {
|
||||||
* it is returned unchanged. Otherwise it is interpreted
|
* it is returned unchanged. Otherwise it is interpreted
|
||||||
* relative to exports.root.
|
* relative to exports.root.
|
||||||
*/
|
*/
|
||||||
exports.makeAbsolute = (somePath: string) => {
|
export const makeAbsolute = (somePath: string) => {
|
||||||
if (path.isAbsolute(somePath)) {
|
if (path.isAbsolute(somePath)) {
|
||||||
return somePath;
|
return somePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath);
|
const rewrittenPath = path.join(findEtherpadRoot(), somePath);
|
||||||
|
|
||||||
absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
|
absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
|
||||||
return rewrittenPath;
|
return rewrittenPath;
|
||||||
|
@ -149,7 +149,7 @@ exports.makeAbsolute = (somePath: string) => {
|
||||||
* a subdirectory of the base one
|
* a subdirectory of the base one
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
exports.isSubdir = (parent: string, arbitraryDir: string): boolean => {
|
export const isSubdir = (parent: string, arbitraryDir: string): boolean => {
|
||||||
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
|
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
|
||||||
const relative = path.relative(parent, arbitraryDir);
|
const relative = path.relative(parent, arbitraryDir);
|
||||||
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
||||||
|
|
|
@ -21,7 +21,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// An object containing the parsed command-line options
|
// An object containing the parsed command-line options
|
||||||
exports.argv = {};
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
|
export const argvP: MapArrayType<any> = {};
|
||||||
|
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
let arg, prevArg;
|
let arg, prevArg;
|
||||||
|
@ -32,22 +34,22 @@ for (let i = 0; i < argv.length; i++) {
|
||||||
|
|
||||||
// Override location of settings.json file
|
// Override location of settings.json file
|
||||||
if (prevArg === '--settings' || prevArg === '-s') {
|
if (prevArg === '--settings' || prevArg === '-s') {
|
||||||
exports.argv.settings = arg;
|
argvP.settings = arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override location of credentials.json file
|
// Override location of credentials.json file
|
||||||
if (prevArg === '--credentials') {
|
if (prevArg === '--credentials') {
|
||||||
exports.argv.credentials = arg;
|
argvP.credentials = arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override location of settings.json file
|
// Override location of settings.json file
|
||||||
if (prevArg === '--sessionkey') {
|
if (prevArg === '--sessionkey') {
|
||||||
exports.argv.sessionkey = arg;
|
argvP.sessionkey = arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override location of APIKEY.txt file
|
// Override location of APIKEY.txt file
|
||||||
if (prevArg === '--apikey') {
|
if (prevArg === '--apikey') {
|
||||||
exports.argv.apikey = arg;
|
argvP.apikey = arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
prevArg = arg;
|
prevArg = arg;
|
||||||
|
|
|
@ -15,35 +15,36 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Stream = require('./Stream');
|
import Stream from './Stream';
|
||||||
const assert = require('assert').strict;
|
import {strict as assert} from 'assert'
|
||||||
const authorManager = require('../db/AuthorManager');
|
import {getAuthor} from '../db/AuthorManager';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
import {aCallAll} from '../../static/js/pluginfw/hooks';
|
||||||
const padManager = require('../db/PadManager');
|
import {getPad} from '../db/PadManager';
|
||||||
|
import {findKeys, get} from "../db/DB";
|
||||||
|
|
||||||
exports.getPadRaw = async (padId:string, readOnlyId:string) => {
|
export const getPadRaw = async (padId:string, readOnlyId:string) => {
|
||||||
const dstPfx = `pad:${readOnlyId || padId}`;
|
const dstPfx = `pad:${readOnlyId || padId}`;
|
||||||
const [pad, customPrefixes] = await Promise.all([
|
const [pad, customPrefixes] = await Promise.all([
|
||||||
padManager.getPad(padId),
|
getPad(padId),
|
||||||
hooks.aCallAll('exportEtherpadAdditionalContent'),
|
aCallAll('exportEtherpadAdditionalContent'),
|
||||||
]);
|
]);
|
||||||
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix:string) => {
|
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix:string) => {
|
||||||
const srcPfx = `${customPrefix}:${padId}`;
|
const srcPfx = `${customPrefix}:${padId}`;
|
||||||
const dstPfx = `${customPrefix}:${readOnlyId || padId}`;
|
const dstPfx = `${customPrefix}:${readOnlyId || padId}`;
|
||||||
assert(!srcPfx.includes('*'));
|
assert(!srcPfx.includes('*'));
|
||||||
const srcKeys = await pad.db.findKeys(`${srcPfx}:*`, null);
|
const srcKeys = await findKeys(`${srcPfx}:*`, null);
|
||||||
return (function* () {
|
return (function* () {
|
||||||
yield [dstPfx, pad.db.get(srcPfx)];
|
yield [dstPfx, get(srcPfx)];
|
||||||
for (const k of srcKeys) {
|
for (const k of srcKeys) {
|
||||||
assert(k.startsWith(`${srcPfx}:`));
|
assert(k.startsWith(`${srcPfx}:`));
|
||||||
yield [`${dstPfx}${k.slice(srcPfx.length)}`, pad.db.get(k)];
|
yield [`${dstPfx}${k.slice(srcPfx.length)}`, get(k)];
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}));
|
}));
|
||||||
const records = (function* () {
|
const records = (function* () {
|
||||||
for (const authorId of pad.getAllAuthors()) {
|
for (const authorId of pad.getAllAuthors()) {
|
||||||
yield [`globalAuthor:${authorId}`, (async () => {
|
yield [`globalAuthor:${authorId}`, (async () => {
|
||||||
const authorEntry = await authorManager.getAuthor(authorId);
|
const authorEntry = await getAuthor(authorId);
|
||||||
if (!authorEntry) return undefined; // Becomes unset when converted to JSON.
|
if (!authorEntry) return undefined; // Becomes unset when converted to JSON.
|
||||||
if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId;
|
if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId;
|
||||||
return authorEntry;
|
return authorEntry;
|
||||||
|
@ -55,7 +56,7 @@ exports.getPadRaw = async (padId:string, readOnlyId:string) => {
|
||||||
})();
|
})();
|
||||||
const data = {[dstPfx]: pad};
|
const data = {[dstPfx]: pad};
|
||||||
for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p;
|
for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p;
|
||||||
await hooks.aCallAll('exportEtherpad', {
|
await aCallAll('exportEtherpad', {
|
||||||
pad,
|
pad,
|
||||||
data,
|
data,
|
||||||
dstPadId: readOnlyId || padId,
|
dstPadId: readOnlyId || padId,
|
||||||
|
|
|
@ -21,17 +21,17 @@
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
import AttributePool from "../../static/js/AttributePool";
|
import AttributePool from "../../static/js/AttributePool";
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||||
const { checkValidRev } = require('./checkValidRev');
|
import {checkValidRev} from './checkValidRev';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This method seems unused in core and no plugins depend on it
|
* This method seems unused in core and no plugins depend on it
|
||||||
*/
|
*/
|
||||||
exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any; atext: any; pool: any; }, revNum: undefined) => {
|
export const getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any; atext: any; pool: any; }, revNum: undefined) => {
|
||||||
const _analyzeLine = exports._analyzeLine;
|
const _analyzeLine = exports._analyzeLine;
|
||||||
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
const textLines = atext.text.slice(0, -1).split('\n');
|
||||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
const attribLines = splitAttributionLines(atext.attribs, atext.text);
|
||||||
const apool = pad.pool;
|
const apool = pad.pool;
|
||||||
|
|
||||||
const pieces = [];
|
const pieces = [];
|
||||||
|
@ -52,14 +52,14 @@ type LineModel = {
|
||||||
[id:string]:string|number|LineModel
|
[id:string]:string|number|LineModel
|
||||||
}
|
}
|
||||||
|
|
||||||
exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) => {
|
export const _analyzeLine = (text:string, aline: string, apool: AttributePool) => {
|
||||||
const line: LineModel = {};
|
const line: LineModel = {};
|
||||||
|
|
||||||
// identify list
|
// identify list
|
||||||
let lineMarker = 0;
|
let lineMarker = 0;
|
||||||
line.listLevel = 0;
|
line.listLevel = 0;
|
||||||
if (aline) {
|
if (aline) {
|
||||||
const [op] = Changeset.deserializeOps(aline);
|
const [op] = deserializeOps(aline);
|
||||||
if (op != null) {
|
if (op != null) {
|
||||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
let listType = attribs.get('list');
|
let listType = attribs.get('list');
|
||||||
|
@ -79,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) =>
|
||||||
}
|
}
|
||||||
if (lineMarker) {
|
if (lineMarker) {
|
||||||
line.text = text.substring(1);
|
line.text = text.substring(1);
|
||||||
line.aline = Changeset.subattribution(aline, 1);
|
line.aline = subattribution(aline, 1);
|
||||||
} else {
|
} else {
|
||||||
line.text = text;
|
line.text = text;
|
||||||
line.aline = aline;
|
line.aline = aline;
|
||||||
|
@ -88,5 +88,5 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports._encodeWhitespace =
|
export const _encodeWhitespace =
|
||||||
(s:string) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
|
(s:string) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
import {AText, PadType} from "../types/PadType";
|
import {AText, PadType} from "../types/PadType";
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
import Pad from "../db/Pad";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc.
|
* Copyright 2009 Google Inc.
|
||||||
|
@ -18,18 +19,19 @@ import {MapArrayType} from "../types/MapType";
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||||
const attributes = require('../../static/js/attributes');
|
import {decodeAttribString} from '../../static/js/attributes';
|
||||||
const padManager = require('../db/PadManager');
|
import {getPad} from '../db/PadManager';
|
||||||
const _ = require('underscore');
|
import _ from 'underscore';
|
||||||
const Security = require('security');
|
const Security = require('security');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
import {aCallAll} from '../../static/js/pluginfw/hooks';
|
||||||
const eejs = require('../eejs');
|
import {requireP} from '../eejs';
|
||||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
import {_analyzeLine, _encodeWhitespace} from './ExportHelper'
|
||||||
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
import {StringIterator} from "../../static/js/StringIterator";
|
||||||
|
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||||
const padutils = require('../../static/js/pad_utils').padutils;
|
const padutils = require('../../static/js/pad_utils').padutils;
|
||||||
|
|
||||||
const getPadHTML = async (pad: PadType, revNum: string) => {
|
export const getPadHTML = async (pad: Pad, revNum: number) => {
|
||||||
let atext = pad.atext;
|
let atext = pad.atext;
|
||||||
|
|
||||||
// fetch revision atext
|
// fetch revision atext
|
||||||
|
@ -41,17 +43,17 @@ const getPadHTML = async (pad: PadType, revNum: string) => {
|
||||||
return await getHTMLFromAtext(pad, atext);
|
return await getHTMLFromAtext(pad, atext);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string[]) => {
|
export const getHTMLFromAtext = async (pad:Pad, atext: AText, authorColors?: string[]) => {
|
||||||
const apool = pad.apool();
|
const apool = pad.apool();
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
const textLines = atext.text.slice(0, -1).split('\n');
|
||||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
const attribLines = splitAttributionLines(atext.attribs, atext.text);
|
||||||
|
|
||||||
const tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
|
const tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
|
||||||
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// prepare tags stored as ['tag', true] to be exported
|
// prepare tags stored as ['tag', true] to be exported
|
||||||
hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps: string[]) => {
|
aCallAll('exportHtmlAdditionalTags', pad).then((newProps: string[]) => {
|
||||||
newProps.forEach((prop) => {
|
newProps.forEach((prop) => {
|
||||||
tags.push(prop);
|
tags.push(prop);
|
||||||
props.push(prop);
|
props.push(prop);
|
||||||
|
@ -59,7 +61,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
}),
|
}),
|
||||||
// prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags
|
// prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags
|
||||||
// like <span data-tag="value">
|
// like <span data-tag="value">
|
||||||
hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps: string[]) => {
|
aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps: string[]) => {
|
||||||
newProps.forEach((prop) => {
|
newProps.forEach((prop) => {
|
||||||
tags.push(`span data-${prop[0]}="${prop[1]}"`);
|
tags.push(`span data-${prop[0]}="${prop[1]}"`);
|
||||||
props.push(prop);
|
props.push(prop);
|
||||||
|
@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
css += '<style>\n';
|
css += '<style>\n';
|
||||||
|
|
||||||
for (const a of Object.keys(apool.numToAttrib)) {
|
for (const a of Object.keys(apool.numToAttrib)) {
|
||||||
|
// @ts-ignore
|
||||||
const attr = apool.numToAttrib[a];
|
const attr = apool.numToAttrib[a];
|
||||||
|
|
||||||
// skip non author attributes
|
// skip non author attributes
|
||||||
|
@ -115,20 +118,21 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
// see hook exportHtmlAdditionalTagsWithData
|
// see hook exportHtmlAdditionalTagsWithData
|
||||||
attrib = propName;
|
attrib = propName;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
const propTrueNum = apool.putAttrib(attrib, true);
|
const propTrueNum = apool.putAttrib(attrib, true);
|
||||||
if (propTrueNum >= 0) {
|
if (propTrueNum >= 0) {
|
||||||
anumMap[propTrueNum] = i;
|
anumMap[propTrueNum] = i;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const getLineHTML = (text: string, attribs: string[]) => {
|
const getLineHTML = (text: string, attribs: string) => {
|
||||||
// Use order of tags (b/i/u) as order of nesting, for simplicity
|
// Use order of tags (b/i/u) as order of nesting, for simplicity
|
||||||
// and decent nesting. For example,
|
// and decent nesting. For example,
|
||||||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
// becomes
|
// becomes
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
const taker = Changeset.stringIterator(text);
|
const taker = new StringIterator(text);
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
const openTags:string[] = [];
|
const openTags:string[] = [];
|
||||||
|
|
||||||
const getSpanClassFor = (i: string) => {
|
const getSpanClassFor = (i: string) => {
|
||||||
|
@ -204,7 +208,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||||
idx += numChars;
|
idx += numChars;
|
||||||
|
|
||||||
// this iterates over every op string and decides which tags to open or to close
|
// this iterates over every op string and decides which tags to open or to close
|
||||||
|
@ -213,7 +217,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
const usedAttribs:string[] = [];
|
const usedAttribs:string[] = [];
|
||||||
|
|
||||||
// mark all attribs as used
|
// mark all attribs as used
|
||||||
for (const a of attributes.decodeAttribString(o.attribs)) {
|
for (const a of decodeAttribString(o.attribs)) {
|
||||||
if (a in anumMap) {
|
if (a in anumMap) {
|
||||||
usedAttribs.push(String(anumMap[a])); // i = 0 => bold, etc.
|
usedAttribs.push(String(anumMap[a])); // i = 0 => bold, etc.
|
||||||
}
|
}
|
||||||
|
@ -307,7 +311,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
for (let i = 0; i < textLines.length; i++) {
|
for (let i = 0; i < textLines.length; i++) {
|
||||||
let context;
|
let context;
|
||||||
const line = _analyzeLine(textLines[i], attribLines[i], apool);
|
const line = _analyzeLine(textLines[i], attribLines[i], apool);
|
||||||
const lineContent = getLineHTML(line.text, line.aline);
|
const lineContent = getLineHTML(line.text as string, line.aline as string);
|
||||||
// If we are inside a list
|
// If we are inside a list
|
||||||
if (line.listLevel) {
|
if (line.listLevel) {
|
||||||
context = {
|
context = {
|
||||||
|
@ -326,7 +330,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
if (i < textLines.length) {
|
if (i < textLines.length) {
|
||||||
nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool);
|
nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool);
|
||||||
}
|
}
|
||||||
await hooks.aCallAll('getLineHTMLForExport', context);
|
await aCallAll('getLineHTMLForExport', context);
|
||||||
// To create list parent elements
|
// To create list parent elements
|
||||||
if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
|
if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
|
||||||
(line.listTypeName !== prevLine.listTypeName)) {
|
(line.listTypeName !== prevLine.listTypeName)) {
|
||||||
|
@ -335,14 +339,14 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
let prevLevel = 0;
|
let prevLevel = 0;
|
||||||
if (prevLine && prevLine.listLevel) {
|
if (prevLine && prevLine.listLevel) {
|
||||||
prevLevel = prevLine.listLevel;
|
prevLevel = prevLine.listLevel as number;
|
||||||
}
|
}
|
||||||
if (prevLine && line.listTypeName !== prevLine.listTypeName) {
|
if (prevLine && line.listTypeName !== prevLine.listTypeName) {
|
||||||
prevLevel = 0;
|
prevLevel = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let diff = prevLevel; diff < line.listLevel; diff++) {
|
for (let diff = prevLevel; diff < (line.listLevel as number); diff++) {
|
||||||
openLists.push({level: diff, type: line.listTypeName});
|
openLists.push({level: diff, type: line.listTypeName as string});
|
||||||
const prevPiece = pieces[pieces.length - 1];
|
const prevPiece = pieces[pieces.length - 1];
|
||||||
|
|
||||||
if (prevPiece.indexOf('<ul') === 0 ||
|
if (prevPiece.indexOf('<ul') === 0 ||
|
||||||
|
@ -372,7 +376,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
// pieces.push("</li>");
|
// pieces.push("</li>");
|
||||||
|
|
||||||
*/
|
*/
|
||||||
if ((nextLine.listTypeName === 'number') && (nextLine.text === '')) {
|
if ((nextLine!.listTypeName === 'number') && (nextLine!.text === '')) {
|
||||||
// is the listTypeName check needed here? null text might be completely fine!
|
// is the listTypeName check needed here? null text might be completely fine!
|
||||||
// TODO Check against Uls
|
// TODO Check against Uls
|
||||||
// don't do anything because the next item is a nested ol openener so
|
// don't do anything because the next item is a nested ol openener so
|
||||||
|
@ -431,13 +435,13 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
(line.listTypeName !== nextLine.listTypeName)) {
|
(line.listTypeName !== nextLine.listTypeName)) {
|
||||||
let nextLevel = 0;
|
let nextLevel = 0;
|
||||||
if (nextLine && nextLine.listLevel) {
|
if (nextLine && nextLine.listLevel) {
|
||||||
nextLevel = nextLine.listLevel;
|
nextLevel = nextLine.listLevel as number;
|
||||||
}
|
}
|
||||||
if (nextLine && line.listTypeName !== nextLine.listTypeName) {
|
if (nextLine && line.listTypeName !== nextLine.listTypeName) {
|
||||||
nextLevel = 0;
|
nextLevel = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let diff = nextLevel; diff < line.listLevel; diff++) {
|
for (let diff = nextLevel; diff < (line.listLevel as number); diff++) {
|
||||||
openLists = openLists.filter((el) => el.level !== diff && el.type !== line.listTypeName);
|
openLists = openLists.filter((el) => el.level !== diff && el.type !== line.listTypeName);
|
||||||
|
|
||||||
if (pieces[pieces.length - 1].indexOf('</ul') === 0 ||
|
if (pieces[pieces.length - 1].indexOf('</ul') === 0 ||
|
||||||
|
@ -463,7 +467,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
padId: pad.id,
|
padId: pad.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
await hooks.aCallAll('getLineHTMLForExport', context);
|
await aCallAll('getLineHTMLForExport', context);
|
||||||
pieces.push(context.lineContent, '<br>');
|
pieces.push(context.lineContent, '<br>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -471,23 +475,24 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
return pieces.join('');
|
return pieces.join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getPadHTMLDocument = async (padId: string, revNum: string, readOnlyId: number) => {
|
export const getPadHTMLDocument = async (padId: string, revNum: number, readOnlyId: number) => {
|
||||||
const pad = await padManager.getPad(padId);
|
const pad = await getPad(padId);
|
||||||
|
|
||||||
// Include some Styles into the Head for Export
|
// Include some Styles into the Head for Export
|
||||||
let stylesForExportCSS = '';
|
let stylesForExportCSS = '';
|
||||||
const stylesForExport: string[] = await hooks.aCallAll('stylesForExport', padId);
|
const stylesForExport: string[] = await aCallAll('stylesForExport', padId);
|
||||||
stylesForExport.forEach((css) => {
|
stylesForExport.forEach((css) => {
|
||||||
stylesForExportCSS += css;
|
stylesForExportCSS += css;
|
||||||
});
|
});
|
||||||
|
|
||||||
let html = await getPadHTML(pad, revNum);
|
let html = await getPadHTML(pad, revNum);
|
||||||
|
|
||||||
for (const hookHtml of await hooks.aCallAll('exportHTMLAdditionalContent', {padId})) {
|
for (const hookHtml of await aCallAll('exportHTMLAdditionalContent', {padId})) {
|
||||||
html += hookHtml;
|
html += hookHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
return eejs.require('ep_etherpad-lite/templates/export_html.html', {
|
return requireP('ep_etherpad-lite/templates/export_html.html', {
|
||||||
|
// @ts-ignore
|
||||||
body: html,
|
body: html,
|
||||||
padId: Security.escapeHTML(readOnlyId || padId),
|
padId: Security.escapeHTML(readOnlyId || padId),
|
||||||
extraCSS: stylesForExportCSS,
|
extraCSS: stylesForExportCSS,
|
||||||
|
@ -543,5 +548,3 @@ const _processSpaces = (s: string) => {
|
||||||
return parts.join('');
|
return parts.join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getPadHTML = getPadHTML;
|
|
||||||
exports.getHTMLFromAtext = getHTMLFromAtext;
|
|
||||||
|
|
|
@ -19,16 +19,19 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AText, PadType} from "../types/PadType";
|
import {AText} from "../types/PadType";
|
||||||
import {MapType} from "../types/MapType";
|
import {MapType} from "../types/MapType";
|
||||||
|
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||||
const attributes = require('../../static/js/attributes');
|
import {decodeAttribString} from '../../static/js/attributes';
|
||||||
const padManager = require('../db/PadManager');
|
import {getPad} from '../db/PadManager';
|
||||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
import {StringIterator} from "../../static/js/StringIterator";
|
||||||
|
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||||
|
import Pad from "../db/Pad";
|
||||||
|
import {_analyzeLine} from "./ExportHelper";
|
||||||
|
|
||||||
// This is slightly different than the HTML method as it passes the output to getTXTFromAText
|
// This is slightly different than the HTML method as it passes the output to getTXTFromAText
|
||||||
const getPadTXT = async (pad: PadType, revNum: string) => {
|
const getPadTXT = async (pad: Pad, revNum: number) => {
|
||||||
let atext = pad.atext;
|
let atext = pad.atext;
|
||||||
|
|
||||||
if (revNum !== undefined) {
|
if (revNum !== undefined) {
|
||||||
|
@ -40,18 +43,19 @@ const getPadTXT = async (pad: PadType, revNum: string) => {
|
||||||
return getTXTFromAtext(pad, atext);
|
return getTXTFromAtext(pad, atext);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is different than the functionality provided in ExportHtml as it provides formatting
|
// This is different from the functionality provided in ExportHtml as it provides formatting
|
||||||
// functionality that is designed specifically for TXT exports
|
// functionality that is designed specifically for TXT exports
|
||||||
const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
export const getTXTFromAtext = (pad: Pad, atext: AText, authorColors?:string) => {
|
||||||
const apool = pad.apool();
|
const apool = pad.apool();
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
const textLines = atext.text.slice(0, -1).split('\n');
|
||||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
const attribLines = splitAttributionLines(atext.attribs, atext.text);
|
||||||
|
|
||||||
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||||
const anumMap: MapType = {};
|
const anumMap: MapType = {};
|
||||||
const css = '';
|
const css = '';
|
||||||
|
|
||||||
props.forEach((propName, i) => {
|
props.forEach((propName, i) => {
|
||||||
|
// @ts-ignore
|
||||||
const propTrueNum = apool.putAttrib([propName, true], true);
|
const propTrueNum = apool.putAttrib([propName, true], true);
|
||||||
if (propTrueNum >= 0) {
|
if (propTrueNum >= 0) {
|
||||||
anumMap[propTrueNum] = i;
|
anumMap[propTrueNum] = i;
|
||||||
|
@ -69,8 +73,8 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
// becomes
|
// becomes
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
const taker = Changeset.stringIterator(text);
|
const taker = new StringIterator(text);
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
|
@ -79,13 +83,13 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||||
idx += numChars;
|
idx += numChars;
|
||||||
|
|
||||||
for (const o of ops) {
|
for (const o of ops) {
|
||||||
let propChanged = false;
|
let propChanged = false;
|
||||||
|
|
||||||
for (const a of attributes.decodeAttribString(o.attribs)) {
|
for (const a of decodeAttribString(o.attribs)) {
|
||||||
if (a in anumMap) {
|
if (a in anumMap) {
|
||||||
const i = anumMap[a] as number; // i = 0 => bold, etc.
|
const i = anumMap[a] as number; // i = 0 => bold, etc.
|
||||||
|
|
||||||
|
@ -197,7 +201,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
|
|
||||||
for (let i = 0; i < textLines.length; i++) {
|
for (let i = 0; i < textLines.length; i++) {
|
||||||
const line = _analyzeLine(textLines[i], attribLines[i], apool);
|
const line = _analyzeLine(textLines[i], attribLines[i], apool);
|
||||||
let lineContent = getLineTXT(line.text, line.aline);
|
let lineContent = getLineTXT(line.text as string, line.aline);
|
||||||
|
|
||||||
if (line.listTypeName === 'bullet') {
|
if (line.listTypeName === 'bullet') {
|
||||||
lineContent = `* ${lineContent}`; // add a bullet
|
lineContent = `* ${lineContent}`; // add a bullet
|
||||||
|
@ -210,11 +214,11 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.listLevel > 0) {
|
if ((line.listLevel as number) > 0) {
|
||||||
for (let j = line.listLevel - 1; j >= 0; j--) {
|
for (let j = (line.listLevel as number) - 1; j >= 0; j--) {
|
||||||
pieces.push('\t'); // tab indent list numbers..
|
pieces.push('\t'); // tab indent list numbers..
|
||||||
if (!listNumbers[line.listLevel]) {
|
if (!listNumbers[(line.listLevel as number)]) {
|
||||||
listNumbers[line.listLevel] = 0;
|
listNumbers[(line.listLevel as number)] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,23 +236,23 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
* To handle going back to 2.1 when prevListLevel is lower number
|
* To handle going back to 2.1 when prevListLevel is lower number
|
||||||
* than current line.listLevel then reset the object value
|
* than current line.listLevel then reset the object value
|
||||||
*/
|
*/
|
||||||
if (line.listLevel < prevListLevel) {
|
if ((line.listLevel as number) < prevListLevel!) {
|
||||||
delete listNumbers[prevListLevel];
|
delete listNumbers[(prevListLevel as number)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
listNumbers[line.listLevel]++;
|
listNumbers[line.listLevel]++;
|
||||||
if (line.listLevel > 1) {
|
if ((line.listLevel as number) > 1) {
|
||||||
let x = 1;
|
let x = 1;
|
||||||
while (x <= line.listLevel - 1) {
|
while (x <= (line.listLevel as number) - 1) {
|
||||||
// if it's undefined to avoid undefined.undefined.1 for 0.0.1
|
// if it's undefined to avoid undefined.undefined.1 for 0.0.1
|
||||||
if (!listNumbers[x]) listNumbers[x] = 0;
|
if (!listNumbers[x]) listNumbers[x] = 0;
|
||||||
pieces.push(`${listNumbers[x]}.`);
|
pieces.push(`${listNumbers[x]}.`);
|
||||||
x++;
|
x++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pieces.push(`${listNumbers[line.listLevel]}. `);
|
pieces.push(`${listNumbers[(line.listLevel as number)]}. `);
|
||||||
prevListLevel = line.listLevel;
|
prevListLevel = line.listLevel as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
pieces.push(lineContent, '\n');
|
pieces.push(lineContent, '\n');
|
||||||
|
@ -260,9 +264,8 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
return pieces.join('');
|
return pieces.join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getTXTFromAtext = getTXTFromAtext;
|
|
||||||
|
|
||||||
exports.getPadTXTDocument = async (padId:string, revNum:string) => {
|
export const getPadTXTDocument = async (padId:string, revNum:number) => {
|
||||||
const pad = await padManager.getPad(padId);
|
const pad = await getPad(padId);
|
||||||
return getPadTXT(pad, revNum);
|
return getPadTXT(pad, revNum);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {APool} from "../types/PadType";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2014 John McLear (Etherpad Foundation / McLear Ltd)
|
* 2014 John McLear (Etherpad Foundation / McLear Ltd)
|
||||||
*
|
*
|
||||||
|
@ -19,29 +17,28 @@ import {APool} from "../types/PadType";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import AttributePool from '../../static/js/AttributePool';
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
const {Pad} = require('../db/Pad');
|
import Pad from '../db/Pad';
|
||||||
const Stream = require('./Stream');
|
import Stream from './Stream';
|
||||||
const authorManager = require('../db/AuthorManager');
|
import {addPad, doesAuthorExist} from '../db/AuthorManager';
|
||||||
const db = require('../db/DB');
|
import {aCallAll, callAll} from '../../static/js/pluginfw/hooks';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const supportedElems = require('../../static/js/contentcollector').supportedElems;
|
const supportedElems = require('../../static/js/contentcollector').supportedElems;
|
||||||
import ueberdb from 'ueberdb2';
|
import ueberdb from 'ueberdb2';
|
||||||
|
|
||||||
const logger = log4js.getLogger('ImportEtherpad');
|
const logger = log4js.getLogger('ImportEtherpad');
|
||||||
|
|
||||||
exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
export const setPadRaw = async (padId: string, r: string, authorId = '') => {
|
||||||
const records = JSON.parse(r);
|
const records = JSON.parse(r);
|
||||||
|
|
||||||
// get supported block Elements from plugins, we will use this later.
|
// get supported block Elements from plugins, we will use this later.
|
||||||
hooks.callAll('ccRegisterBlockElements').forEach((element:any) => {
|
callAll('ccRegisterBlockElements').forEach((element:any) => {
|
||||||
supportedElems.add(element);
|
supportedElems.add(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
// DB key prefixes for pad records. Each key is expected to have the form `${prefix}:${padId}` or
|
// DB key prefixes for pad records. Each key is expected to have the form `${prefix}:${padId}` or
|
||||||
// `${prefix}:${padId}:${otherstuff}`.
|
// `${prefix}:${padId}:${otherstuff}`.
|
||||||
const padKeyPrefixes = [
|
const padKeyPrefixes = [
|
||||||
...await hooks.aCallAll('exportEtherpadAdditionalContent'),
|
...await aCallAll('exportEtherpadAdditionalContent'),
|
||||||
'pad',
|
'pad',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -55,7 +52,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
||||||
// there is a problem with the data.
|
// there is a problem with the data.
|
||||||
|
|
||||||
const data = new Map();
|
const data = new Map();
|
||||||
const existingAuthors = new Set();
|
const existingAuthors = new Set<string>();
|
||||||
const padDb = new ueberdb.Database('memory', {data});
|
const padDb = new ueberdb.Database('memory', {data});
|
||||||
await padDb.init();
|
await padDb.init();
|
||||||
try {
|
try {
|
||||||
|
@ -74,7 +71,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
||||||
throw new TypeError('globalAuthor padIDs subkey is not a string');
|
throw new TypeError('globalAuthor padIDs subkey is not a string');
|
||||||
}
|
}
|
||||||
checkOriginalPadId(value.padIDs);
|
checkOriginalPadId(value.padIDs);
|
||||||
if (await authorManager.doesAuthorExist(id)) {
|
if (await doesAuthorExist(id)) {
|
||||||
existingAuthors.add(id);
|
existingAuthors.add(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -106,9 +103,9 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
||||||
const readOps = new Stream(Object.entries(records)).map(([k, v]) => processRecord(k, v));
|
const readOps = new Stream(Object.entries(records)).map(([k, v]) => processRecord(k, v));
|
||||||
for (const op of readOps.batch(100).buffer(99)) await op;
|
for (const op of readOps.batch(100).buffer(99)) await op;
|
||||||
|
|
||||||
const pad = new Pad(padId, padDb);
|
const pad = new Pad(padId);
|
||||||
await pad.init(null, authorId);
|
await pad.init(null, authorId);
|
||||||
await hooks.aCallAll('importEtherpad', {
|
await aCallAll('importEtherpad', {
|
||||||
pad,
|
pad,
|
||||||
// Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but
|
// Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but
|
||||||
// it's not worth the added complexity.
|
// it's not worth the added complexity.
|
||||||
|
@ -121,8 +118,8 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeOps = (function* () {
|
const writeOps = (function* () {
|
||||||
for (const [k, v] of data) yield db.set(k, v);
|
for (const [k, v] of data) yield padDb.set(k, v);
|
||||||
for (const a of existingAuthors) yield authorManager.addPad(a, padId);
|
for (const a of existingAuthors) yield addPad(a, padId);
|
||||||
})();
|
})();
|
||||||
for (const op of new Stream(writeOps).batch(100).buffer(99)) await op;
|
for (const op of new Stream(writeOps).batch(100).buffer(99)) await op;
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,15 +16,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps} from '../../static/js/Changeset';
|
||||||
const contentcollector = require('../../static/js/contentcollector');
|
import ContentCollector from '../../static/js/contentcollector';
|
||||||
import jsdom from 'jsdom';
|
import jsdom from 'jsdom';
|
||||||
import {PadType} from "../types/PadType";
|
import Pad from "../db/Pad";
|
||||||
|
import {Builder} from "../../static/js/Builder";
|
||||||
|
|
||||||
const apiLogger = log4js.getLogger('ImportHtml');
|
const apiLogger = log4js.getLogger('ImportHtml');
|
||||||
let processor:any;
|
let processor:any;
|
||||||
|
|
||||||
exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
|
export const setPadHTML = async (pad: Pad, html:string, authorId = '') => {
|
||||||
if (processor == null) {
|
if (processor == null) {
|
||||||
const [{rehype}, {default: minifyWhitespace}] =
|
const [{rehype}, {default: minifyWhitespace}] =
|
||||||
await Promise.all([import('rehype'), import('rehype-minify-whitespace')]);
|
await Promise.all([import('rehype'), import('rehype-minify-whitespace')]);
|
||||||
|
@ -43,7 +44,7 @@ exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
|
||||||
|
|
||||||
// Convert a dom tree into a list of lines and attribute liens
|
// Convert a dom tree into a list of lines and attribute liens
|
||||||
// using the content collector object
|
// using the content collector object
|
||||||
const cc = contentcollector.makeContentCollector(true, null, pad.pool);
|
const cc = new ContentCollector(true, null, pad.pool);
|
||||||
try {
|
try {
|
||||||
// we use a try here because if the HTML is bad it will blow up
|
// we use a try here because if the HTML is bad it will blow up
|
||||||
cc.collectContent(document.body);
|
cc.collectContent(document.body);
|
||||||
|
@ -69,13 +70,13 @@ exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
|
||||||
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
|
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
|
||||||
|
|
||||||
// create a new changeset with a helper builder object
|
// create a new changeset with a helper builder object
|
||||||
const builder = Changeset.builder(1);
|
const builder = new Builder(1);
|
||||||
|
|
||||||
// assemble each line into the builder
|
// assemble each line into the builder
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = 0;
|
const newTextStart = 0;
|
||||||
const newTextEnd = newText.length;
|
const newTextEnd = newText.length;
|
||||||
for (const op of Changeset.deserializeOps(newAttribs)) {
|
for (const op of deserializeOps(newAttribs)) {
|
||||||
const nextIndex = textIndex + op.chars;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
const start = Math.max(newTextStart, textIndex);
|
const start = Math.max(newTextStart, textIndex);
|
||||||
|
|
|
@ -17,12 +17,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const async = require('async');
|
import async from 'async';
|
||||||
const fs = require('fs').promises;
|
import {promises as fs} from 'fs'
|
||||||
const log4js = require('log4js');
|
import log4js from 'log4js';
|
||||||
const os = require('os');
|
import os from 'os';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const runCmd = require('./run_cmd');
|
import runCmd from './run_cmd';
|
||||||
const settings = require('./Settings');
|
const settings = require('./Settings');
|
||||||
|
|
||||||
const logger = log4js.getLogger('LibreOffice');
|
const logger = log4js.getLogger('LibreOffice');
|
||||||
|
@ -54,25 +54,25 @@ const doConvertTask = async (task:{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(line) => logger.error(`[${p.child.pid}] stderr: ${line}`),
|
(line) => logger.error(`[${p.child.pid}] stderr: ${line}`),
|
||||||
]});
|
]});
|
||||||
logger.info(`[${p.child.pid}] Converting ${task.srcFile} to ${task.type} in ${tmpDir}`);
|
logger.info(`[${p.child!.pid}] Converting ${task.srcFile} to ${task.type} in ${tmpDir}`);
|
||||||
// Soffice/libreoffice is buggy and often hangs.
|
// Soffice/libreoffice is buggy and often hangs.
|
||||||
// To remedy this we kill the spawned process after a while.
|
// To remedy this we kill the spawned process after a while.
|
||||||
// TODO: Use the timeout option once support for Node.js < v15.13.0 is dropped.
|
// TODO: Use the timeout option once support for Node.js < v15.13.0 is dropped.
|
||||||
const hangTimeout = setTimeout(() => {
|
const hangTimeout = setTimeout(() => {
|
||||||
logger.error(`[${p.child.pid}] Conversion timed out; killing LibreOffice...`);
|
logger.error(`[${p.child!.pid}] Conversion timed out; killing LibreOffice...`);
|
||||||
p.child.kill();
|
p.child!.kill();
|
||||||
}, 120000);
|
}, 120000);
|
||||||
try {
|
try {
|
||||||
await p;
|
await p;
|
||||||
} catch (err:any) {
|
} catch (err:any) {
|
||||||
logger.error(`[${p.child.pid}] Conversion failed: ${err.stack || err}`);
|
logger.error(`[${p.child!.pid}] Conversion failed: ${err.stack || err}`);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(hangTimeout);
|
clearTimeout(hangTimeout);
|
||||||
}
|
}
|
||||||
logger.info(`[${p.child.pid}] Conversion done.`);
|
logger.info(`[${p.child!.pid}] Conversion done.`);
|
||||||
const filename = path.basename(task.srcFile);
|
const filename = path.basename(task.srcFile);
|
||||||
const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
|
const sourceFile = `${filename.substring(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
|
||||||
const sourcePath = path.join(tmpDir, sourceFile);
|
const sourcePath = path.join(tmpDir, sourceFile);
|
||||||
logger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
|
logger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
|
||||||
await fs.rename(sourcePath, task.destFile);
|
await fs.rename(sourcePath, task.destFile);
|
||||||
|
@ -89,7 +89,7 @@ const queue = async.queue(doConvertTask, 1);
|
||||||
* @param {String} type The type to convert into
|
* @param {String} type The type to convert into
|
||||||
* @param {Function} callback Standard callback function
|
* @param {Function} callback Standard callback function
|
||||||
*/
|
*/
|
||||||
exports.convertFile = async (srcFile: string, destFile: string, type:string) => {
|
export const convertFile = async (srcFile: string, destFile: string, type:string) => {
|
||||||
// Used for the moving of the file, not the conversion
|
// Used for the moving of the file, not the conversion
|
||||||
const fileExtension = type;
|
const fileExtension = type;
|
||||||
|
|
||||||
|
|
|
@ -19,14 +19,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const semver = require('semver');
|
import semver from 'semver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quits if Etherpad is not running on a given minimum Node version
|
* Quits if Etherpad is not running on a given minimum Node version
|
||||||
*
|
*
|
||||||
* @param {String} minNodeVersion Minimum required Node version
|
* @param {String} minNodeVersion Minimum required Node version
|
||||||
*/
|
*/
|
||||||
exports.enforceMinNodeVersion = (minNodeVersion: string) => {
|
export const enforceMinNodeVersion = (minNodeVersion: string) => {
|
||||||
const currentNodeVersion = process.version;
|
const currentNodeVersion = process.version;
|
||||||
|
|
||||||
// we cannot use template literals, since we still do not know if we are
|
// we cannot use template literals, since we still do not know if we are
|
||||||
|
@ -49,7 +49,7 @@ exports.enforceMinNodeVersion = (minNodeVersion: string) => {
|
||||||
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated
|
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated
|
||||||
* Node releases
|
* Node releases
|
||||||
*/
|
*/
|
||||||
exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion: string, epRemovalVersion:Function) => {
|
export const checkDeprecationStatus = (lowestNonDeprecatedNodeVersion: string, epRemovalVersion:string) => {
|
||||||
const currentNodeVersion = process.version;
|
const currentNodeVersion = process.version;
|
||||||
|
|
||||||
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {
|
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {
|
||||||
|
|
|
@ -28,123 +28,111 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
import {SettingsNode, SettingsTree} from "./SettingsTree";
|
import {SettingsNode} from "./SettingsTree";
|
||||||
import {coerce} from "semver";
|
import {version} from '../../package.json'
|
||||||
|
import {findEtherpadRoot, isSubdir, makeAbsolute} from './AbsolutePaths';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import {argvP} from "./Cli";
|
||||||
|
import jsonminify from 'jsonminify';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import {randomString} from './randomstring';
|
||||||
|
|
||||||
const absolutePaths = require('./AbsolutePaths');
|
import _ from 'underscore';
|
||||||
const deepEqual = require('fast-deep-equal/es6');
|
|
||||||
const fs = require('fs');
|
|
||||||
const os = require('os');
|
|
||||||
const path = require('path');
|
|
||||||
const argv = require('./Cli').argv;
|
|
||||||
const jsonminify = require('jsonminify');
|
|
||||||
const log4js = require('log4js');
|
|
||||||
const randomString = require('./randomstring');
|
|
||||||
const suppressDisableMsg = ' -- To suppress these warning messages change ' +
|
|
||||||
'suppressErrorsInPadText to true in your settings.json\n';
|
|
||||||
const _ = require('underscore');
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('settings');
|
|
||||||
|
|
||||||
// Exported values that settings.json and credentials.json cannot override.
|
|
||||||
const nonSettings = [
|
|
||||||
'credentialsFilename',
|
|
||||||
'settingsFilename',
|
|
||||||
];
|
|
||||||
|
|
||||||
// This is a function to make it easy to create a new instance. It is important to not reuse a
|
class Settings {
|
||||||
// config object after passing it to log4js.configure() because that method mutates the object. :(
|
|
||||||
const defaultLogConfig = (level: string) => ({
|
|
||||||
appenders: {console: {type: 'console'}},
|
|
||||||
categories: {
|
|
||||||
default: {appenders: ['console'], level},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const defaultLogLevel = 'INFO';
|
|
||||||
|
|
||||||
const initLogging = (config: any) => {
|
|
||||||
// log4js.configure() modifies exports.logconfig so check for equality first.
|
|
||||||
log4js.configure(config);
|
|
||||||
log4js.getLogger('console');
|
|
||||||
|
|
||||||
// Overwrites for console output methods
|
|
||||||
console.debug = logger.debug.bind(logger);
|
|
||||||
console.log = logger.info.bind(logger);
|
|
||||||
console.warn = logger.warn.bind(logger);
|
|
||||||
console.error = logger.error.bind(logger);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized
|
// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized
|
||||||
// with the user's chosen log level and logger config after the settings have been loaded.
|
// with the user's chosen log level and logger config after the settings have been loaded.
|
||||||
initLogging(defaultLogConfig(defaultLogLevel));
|
this.initLogging(this.defaultLogConfig(this.defaultLogLevel));
|
||||||
|
this.logger.info('All relative paths will be interpreted relative to the identified ' +
|
||||||
/* Root path of the installation */
|
`Etherpad base dir: ${this.root}`);
|
||||||
exports.root = absolutePaths.findEtherpadRoot();
|
// initially load settings
|
||||||
logger.info('All relative paths will be interpreted relative to the identified ' +
|
this.reloadSettings();
|
||||||
`Etherpad base dir: ${exports.root}`);
|
}
|
||||||
exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
|
|
||||||
exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The app title, visible e.g. in the browser window
|
* The app title, visible e.g. in the browser window
|
||||||
*/
|
*/
|
||||||
exports.title = 'Etherpad';
|
private title = 'Etherpad';
|
||||||
|
private settingsFilename = makeAbsolute(argvP.settings || 'settings.json');
|
||||||
|
private credentialsFilename = makeAbsolute(argvP.credentials || 'credentials.json');
|
||||||
|
|
||||||
|
private suppressDisableMsg = ' -- To suppress these warning messages change ' +
|
||||||
|
'suppressErrorsInPadText to true in your settings.json\n';
|
||||||
|
private defaultLogLevel = 'INFO';
|
||||||
|
private logger = log4js.getLogger('settings');
|
||||||
|
/* Root path of the installation */
|
||||||
|
private root = findEtherpadRoot();
|
||||||
/**
|
/**
|
||||||
* Pathname of the favicon you want to use. If null, the skin's favicon is
|
* Pathname of the favicon you want to use. If null, the skin's favicon is
|
||||||
* used if one is provided by the skin, otherwise the default Etherpad favicon
|
* used if one is provided by the skin, otherwise the default Etherpad favicon
|
||||||
* is used. If this is a relative path it is interpreted as relative to the
|
* is used. If this is a relative path it is interpreted as relative to the
|
||||||
* Etherpad root directory.
|
* Etherpad root directory.
|
||||||
*/
|
*/
|
||||||
exports.favicon = null;
|
private favicon: string|null = null;
|
||||||
|
// Exported values that settings.json and credentials.json cannot override.
|
||||||
exports.ttl = {
|
private nonSettings = [
|
||||||
AccessToken: 1 * 60 * 60, // 1 hour in seconds
|
'credentialsFilename',
|
||||||
AuthorizationCode: 10 * 60, // 10 minutes in seconds
|
'settingsFilename',
|
||||||
ClientCredentials: 1 * 60 * 60, // 1 hour in seconds
|
]
|
||||||
IdToken: 1 * 60 * 60, // 1 hour in seconds
|
|
||||||
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Skin name.
|
* Skin name.
|
||||||
*
|
*
|
||||||
* Initialized to null, so we can spot an old configuration file and invite the
|
* Initialized to null, so we can spot an old configuration file and invite the
|
||||||
* user to update it before falling back to the default.
|
* user to update it before falling back to the default.
|
||||||
*/
|
*/
|
||||||
exports.skinName = null;
|
skinName: string | null = null;
|
||||||
|
skinVariants = 'super-light-toolbar super-light-editor light-background';
|
||||||
exports.skinVariants = 'super-light-toolbar super-light-editor light-background';
|
private ttl = {
|
||||||
|
AccessToken: 1 * 60 * 60, // 1 hour in seconds
|
||||||
|
AuthorizationCode: 10 * 60, // 10 minutes in seconds
|
||||||
|
ClientCredentials: 1 * 60 * 60, // 1 hour in seconds
|
||||||
|
IdToken: 1 * 60 * 60, // 1 hour in seconds
|
||||||
|
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* The IP ep-lite should listen to
|
* Should we suppress Error messages from being in Pad Contents
|
||||||
*/
|
*/
|
||||||
exports.ip = '0.0.0.0';
|
private suppressErrorsInPadText = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Port ep-lite should listen to
|
* The Port ep-lite should listen to
|
||||||
*/
|
*/
|
||||||
exports.port = process.env.PORT || 9001;
|
port = process.env.PORT as unknown as number || 9001;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should we suppress Error messages from being in Pad Contents
|
* The IP ep-lite should listen to
|
||||||
*/
|
*/
|
||||||
exports.suppressErrorsInPadText = false;
|
ip: string = '0.0.0.0';
|
||||||
|
|
||||||
|
// This is a function to make it easy to create a new instance. It is important to not reuse a
|
||||||
|
// config object after passing it to log4js.configure() because that method mutates the object. :(
|
||||||
|
private defaultLogConfig = (level: string) => ({
|
||||||
|
appenders: {console: {type: 'console'}},
|
||||||
|
categories: {
|
||||||
|
default: {appenders: ['console'], level},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The SSL signed server key and the Certificate Authority's own certificate
|
* The SSL signed server key and the Certificate Authority's own certificate
|
||||||
* default case: ep-lite does *not* use SSL. A signed server key is not required in this case.
|
* default case: ep-lite does *not* use SSL. A signed server key is not required in this case.
|
||||||
*/
|
*/
|
||||||
exports.ssl = false;
|
ssl:{
|
||||||
|
key:string,
|
||||||
|
cert:string
|
||||||
|
ca?: string[]
|
||||||
|
}|false = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* socket.io transport methods
|
* socket.io transport methods
|
||||||
**/
|
**/
|
||||||
exports.socketTransportProtocols = ['websocket', 'polling'];
|
private socketTransportProtocols = ['websocket', 'polling'];
|
||||||
|
private socketIo = {
|
||||||
exports.socketIo = {
|
|
||||||
/**
|
/**
|
||||||
* Maximum permitted client message size (in bytes).
|
* Maximum permitted client message size (in bytes).
|
||||||
*
|
*
|
||||||
|
@ -156,28 +144,25 @@ exports.socketIo = {
|
||||||
maxHttpBufferSize: 50000,
|
maxHttpBufferSize: 50000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The authentication method used by the server.
|
The authentication method used by the server.
|
||||||
The default value is sso
|
The default value is sso
|
||||||
If you want to use the old authentication system, change this to apikey
|
If you want to use the old authentication system, change this to apikey
|
||||||
*/
|
*/
|
||||||
exports.authenticationMethod = 'sso'
|
private authenticationMethod = 'sso'
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The Type of the database
|
* The Type of the database
|
||||||
*/
|
*/
|
||||||
exports.dbType = 'dirty';
|
private dbType = 'dirty';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This setting is passed with dbType to ueberDB to set up the database
|
* This setting is passed with dbType to ueberDB to set up the database
|
||||||
*/
|
*/
|
||||||
exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
|
private dbSettings = {filename: path.join(this.root, 'var/dirty.db')};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default Text of a new pad
|
* The default Text of a new pad
|
||||||
*/
|
*/
|
||||||
exports.defaultPadText = [
|
private defaultPadText = [
|
||||||
'Welcome to Etherpad!',
|
'Welcome to Etherpad!',
|
||||||
'',
|
'',
|
||||||
'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' +
|
'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' +
|
||||||
|
@ -186,10 +171,11 @@ exports.defaultPadText = [
|
||||||
'Etherpad on Github: https://github.com/ether/etherpad-lite',
|
'Etherpad on Github: https://github.com/ether/etherpad-lite',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default Pad Settings for a user (Can be overridden by changing the setting
|
* The default Pad Settings for a user (Can be overridden by changing the setting
|
||||||
*/
|
*/
|
||||||
exports.padOptions = {
|
padOptions = {
|
||||||
noColors: false,
|
noColors: false,
|
||||||
showControls: true,
|
showControls: true,
|
||||||
showChat: true,
|
showChat: true,
|
||||||
|
@ -206,7 +192,7 @@ exports.padOptions = {
|
||||||
/**
|
/**
|
||||||
* Whether certain shortcut keys are enabled for a user in the pad
|
* Whether certain shortcut keys are enabled for a user in the pad
|
||||||
*/
|
*/
|
||||||
exports.padShortcutEnabled = {
|
padShortcutEnabled = {
|
||||||
altF9: true,
|
altF9: true,
|
||||||
altC: true,
|
altC: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
|
@ -234,7 +220,7 @@ exports.padShortcutEnabled = {
|
||||||
/**
|
/**
|
||||||
* The toolbar buttons and order.
|
* The toolbar buttons and order.
|
||||||
*/
|
*/
|
||||||
exports.toolbar = {
|
private toolbar = {
|
||||||
left: [
|
left: [
|
||||||
['bold', 'italic', 'underline', 'strikethrough'],
|
['bold', 'italic', 'underline', 'strikethrough'],
|
||||||
['orderedlist', 'unorderedlist', 'indent', 'outdent'],
|
['orderedlist', 'unorderedlist', 'indent', 'outdent'],
|
||||||
|
@ -254,87 +240,89 @@ exports.toolbar = {
|
||||||
/**
|
/**
|
||||||
* A flag that requires any user to have a valid session (via the api) before accessing a pad
|
* A flag that requires any user to have a valid session (via the api) before accessing a pad
|
||||||
*/
|
*/
|
||||||
exports.requireSession = false;
|
private requireSession = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flag that prevents users from creating new pads
|
* A flag that prevents users from creating new pads
|
||||||
*/
|
*/
|
||||||
exports.editOnly = false;
|
private editOnly = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Max age that responses will have (affects caching layer).
|
* Max age that responses will have (affects caching layer).
|
||||||
*/
|
*/
|
||||||
exports.maxAge = 1000 * 60 * 60 * 6; // 6 hours
|
private maxAge = 1000 * 60 * 60 * 6; // 6 hours
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flag that shows if minification is enabled or not
|
* A flag that shows if minification is enabled or not
|
||||||
*/
|
*/
|
||||||
exports.minify = true;
|
private minify = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The path of the abiword executable
|
* The path of the abiword executable
|
||||||
*/
|
*/
|
||||||
exports.abiword = null;
|
private abiword = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The path of the libreoffice executable
|
* The path of the libreoffice executable
|
||||||
*/
|
*/
|
||||||
exports.soffice = null;
|
private soffice = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should we support none natively supported file types on import?
|
* Should we support none natively supported file types on import?
|
||||||
*/
|
*/
|
||||||
exports.allowUnknownFileEnds = true;
|
private allowUnknownFileEnds = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The log level of log4js
|
* The log level of log4js
|
||||||
*/
|
*/
|
||||||
exports.loglevel = defaultLogLevel;
|
private loglevel: string = this.defaultLogLevel;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable IP logging
|
* Disable IP logging
|
||||||
*/
|
*/
|
||||||
exports.disableIPlogging = false;
|
disableIPlogging = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of seconds to automatically reconnect pad
|
* Number of seconds to automatically reconnect pad
|
||||||
*/
|
*/
|
||||||
exports.automaticReconnectionTimeout = 0;
|
automaticReconnectionTimeout = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable Load Testing
|
* Disable Load Testing
|
||||||
*/
|
*/
|
||||||
exports.loadTest = false;
|
private loadTest = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable dump of objects preventing a clean exit
|
* Disable dump of objects preventing a clean exit
|
||||||
*/
|
*/
|
||||||
exports.dumpOnUncleanExit = false;
|
dumpOnUncleanExit = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable indentation on new lines
|
* Enable indentation on new lines
|
||||||
*/
|
*/
|
||||||
exports.indentationOnNewLine = true;
|
indentationOnNewLine = true;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* log4js appender configuration
|
* log4js appender configuration
|
||||||
*/
|
*/
|
||||||
exports.logconfig = null;
|
private logconfig: { categories: { default: { level: string, appenders: string[] } }, appenders: { console: { type: string } } } | null = null;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Deprecated cookie signing key.
|
* Deprecated cookie signing key.
|
||||||
*/
|
*/
|
||||||
exports.sessionKey = null;
|
sessionKey: string|null = null;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Trust Proxy, whether or not trust the x-forwarded-for header.
|
* Trust Proxy, whether or not trust the x-forwarded-for header.
|
||||||
*/
|
*/
|
||||||
exports.trustProxy = false;
|
trustProxy = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Settings controlling the session cookie issued by Etherpad.
|
* Settings controlling the session cookie issued by Etherpad.
|
||||||
*/
|
*/
|
||||||
exports.cookie = {
|
cookie = {
|
||||||
keyRotationInterval: 1 * 24 * 60 * 60 * 1000,
|
keyRotationInterval: 1 * 24 * 60 * 60 * 1000,
|
||||||
/*
|
/*
|
||||||
* Value of the SameSite cookie property. "Lax" is recommended unless
|
* Value of the SameSite cookie property. "Lax" is recommended unless
|
||||||
|
@ -357,27 +345,27 @@ exports.cookie = {
|
||||||
* authorization. Note: /admin always requires authentication, and
|
* authorization. Note: /admin always requires authentication, and
|
||||||
* either authorization by a module, or a user with is_admin set
|
* either authorization by a module, or a user with is_admin set
|
||||||
*/
|
*/
|
||||||
exports.requireAuthentication = false;
|
private requireAuthentication = false;
|
||||||
exports.requireAuthorization = false;
|
private requireAuthorization = false;
|
||||||
exports.users = {};
|
users = {};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This setting is used for configuring sso
|
* This setting is used for configuring sso
|
||||||
*/
|
*/
|
||||||
exports.sso = {
|
private sso = {
|
||||||
issuer: "http://localhost:9001"
|
issuer: "http://localhost:9001"
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Show settings in admin page, by default it is true
|
* Show settings in admin page, by default it is true
|
||||||
*/
|
*/
|
||||||
exports.showSettingsInAdminPage = true;
|
private showSettingsInAdminPage = true;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* By default, when caret is moved out of viewport, it scrolls the minimum
|
* By default, when caret is moved out of viewport, it scrolls the minimum
|
||||||
* height needed to make this line visible.
|
* height needed to make this line visible.
|
||||||
*/
|
*/
|
||||||
exports.scrollWhenFocusLineIsOutOfViewport = {
|
scrollWhenFocusLineIsOutOfViewport = {
|
||||||
/*
|
/*
|
||||||
* Percentage of viewport height to be additionally scrolled.
|
* Percentage of viewport height to be additionally scrolled.
|
||||||
*/
|
*/
|
||||||
|
@ -410,12 +398,12 @@ exports.scrollWhenFocusLineIsOutOfViewport = {
|
||||||
*
|
*
|
||||||
* Do not enable on production machines.
|
* Do not enable on production machines.
|
||||||
*/
|
*/
|
||||||
exports.exposeVersion = false;
|
exposeVersion = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Override any strings found in locale directories
|
* Override any strings found in locale directories
|
||||||
*/
|
*/
|
||||||
exports.customLocaleStrings = {};
|
private customLocaleStrings = {};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* From Etherpad 1.8.3 onwards, import and export of pads is always rate
|
* From Etherpad 1.8.3 onwards, import and export of pads is always rate
|
||||||
|
@ -426,7 +414,7 @@ exports.customLocaleStrings = {};
|
||||||
*
|
*
|
||||||
* See https://github.com/nfriedly/express-rate-limit for more options
|
* See https://github.com/nfriedly/express-rate-limit for more options
|
||||||
*/
|
*/
|
||||||
exports.importExportRateLimiting = {
|
private importExportRateLimiting = {
|
||||||
// duration of the rate limit window (milliseconds)
|
// duration of the rate limit window (milliseconds)
|
||||||
windowMs: 90000,
|
windowMs: 90000,
|
||||||
|
|
||||||
|
@ -442,7 +430,7 @@ exports.importExportRateLimiting = {
|
||||||
*
|
*
|
||||||
* See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
|
* See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
|
||||||
*/
|
*/
|
||||||
exports.commitRateLimiting = {
|
commitRateLimiting = {
|
||||||
// duration of the rate limit window (seconds)
|
// duration of the rate limit window (seconds)
|
||||||
duration: 1,
|
duration: 1,
|
||||||
|
|
||||||
|
@ -456,39 +444,53 @@ exports.commitRateLimiting = {
|
||||||
*
|
*
|
||||||
* File size is specified in bytes. Default is 50 MB.
|
* File size is specified in bytes. Default is 50 MB.
|
||||||
*/
|
*/
|
||||||
exports.importMaxFileSize = 50 * 1024 * 1024;
|
private importMaxFileSize = 50 * 1024 * 1024;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Disable Admin UI tests
|
* Disable Admin UI tests
|
||||||
*/
|
*/
|
||||||
exports.enableAdminUITests = false;
|
enableAdminUITests = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enable auto conversion of pad Ids to lowercase.
|
* Enable auto conversion of pad Ids to lowercase.
|
||||||
* e.g. /p/EtHeRpAd to /p/etherpad
|
* e.g. /p/EtHeRpAd to /p/etherpad
|
||||||
*/
|
*/
|
||||||
exports.lowerCasePadIds = false;
|
private lowerCasePadIds = false;
|
||||||
|
|
||||||
|
randomVersionString: string|null = null;
|
||||||
|
|
||||||
|
private initLogging = (config: any) => {
|
||||||
|
// log4js.configure() modifies logconfig so check for equality first.
|
||||||
|
log4js.configure(config);
|
||||||
|
log4js.getLogger('console');
|
||||||
|
|
||||||
|
// Overwrites for console output methods
|
||||||
|
console.debug = this.logger.debug.bind(this.logger);
|
||||||
|
console.log = this.logger.info.bind(this.logger);
|
||||||
|
console.warn = this.logger.warn.bind(this.logger);
|
||||||
|
console.error = this.logger.error.bind(this.logger);
|
||||||
|
}
|
||||||
|
|
||||||
// checks if abiword is avaiable
|
// checks if abiword is avaiable
|
||||||
exports.abiwordAvailable = () => {
|
abiwordAvailable = () => {
|
||||||
if (exports.abiword != null) {
|
if (this.abiword != null) {
|
||||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||||
} else {
|
} else {
|
||||||
return 'no';
|
return 'no';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.sofficeAvailable = () => {
|
sofficeAvailable = () => {
|
||||||
if (exports.soffice != null) {
|
if (this.soffice != null) {
|
||||||
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
|
||||||
} else {
|
} else {
|
||||||
return 'no';
|
return 'no';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.exportAvailable = () => {
|
exportAvailable = () => {
|
||||||
const abiword = exports.abiwordAvailable();
|
const abiword = this.abiwordAvailable();
|
||||||
const soffice = exports.sofficeAvailable();
|
const soffice = this.sofficeAvailable();
|
||||||
|
|
||||||
if (abiword === 'no' && soffice === 'no') {
|
if (abiword === 'no' && soffice === 'no') {
|
||||||
return 'no';
|
return 'no';
|
||||||
|
@ -501,13 +503,13 @@ exports.exportAvailable = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Provide git version if available
|
// Provide git version if available
|
||||||
exports.getGitCommit = () => {
|
getGitCommit = () => {
|
||||||
let version = '';
|
let version = '';
|
||||||
try {
|
try {
|
||||||
let rootPath = exports.root;
|
let rootPath = this.root;
|
||||||
if (fs.lstatSync(`${rootPath}/.git`).isFile()) {
|
if (fs.lstatSync(`${rootPath}/.git`).isFile()) {
|
||||||
rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8');
|
rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8');
|
||||||
rootPath = rootPath.split(' ').pop().trim();
|
rootPath = rootPath.split(' ').pop()!.trim();
|
||||||
} else {
|
} else {
|
||||||
rootPath += '/.git';
|
rootPath += '/.git';
|
||||||
}
|
}
|
||||||
|
@ -520,15 +522,13 @@ exports.getGitCommit = () => {
|
||||||
}
|
}
|
||||||
version = version.substring(0, 7);
|
version = version.substring(0, 7);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.warn(`Can't get git version for server header\n${e.message}`);
|
this.logger.warn(`Can't get git version for server header\n${e.message}`);
|
||||||
}
|
}
|
||||||
return version;
|
return version;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Return etherpad version from package.json
|
// Return etherpad version from package.json
|
||||||
exports.getEpVersion = () => require('../../package.json').version;
|
getEpVersion = () => version;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives a settingsObj and, if the property name is a valid configuration
|
* Receives a settingsObj and, if the property name is a valid configuration
|
||||||
|
@ -537,32 +537,37 @@ exports.getEpVersion = () => require('../../package.json').version;
|
||||||
* This code refactors a previous version that copied & pasted the same code for
|
* This code refactors a previous version that copied & pasted the same code for
|
||||||
* both "settings.json" and "credentials.json".
|
* both "settings.json" and "credentials.json".
|
||||||
*/
|
*/
|
||||||
const storeSettings = (settingsObj: any) => {
|
private storeSettings = (settingsObj: any) => {
|
||||||
for (const i of Object.keys(settingsObj || {})) {
|
for (const i of Object.keys(settingsObj || {})) {
|
||||||
if (nonSettings.includes(i)) {
|
if (this.nonSettings.includes(i)) {
|
||||||
logger.warn(`Ignoring setting: '${i}'`);
|
this.logger.warn(`Ignoring setting: '${i}'`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// test if the setting starts with a lowercase character
|
// test if the setting starts with a lowercase character
|
||||||
if (i.charAt(0).search('[a-z]') !== 0) {
|
if (i.charAt(0).search('[a-z]') !== 0) {
|
||||||
logger.warn(`Settings should start with a lowercase character: '${i}'`);
|
this.logger.warn(`Settings should start with a lowercase character: '${i}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we know this setting, so we overwrite it
|
// we know this setting, so we overwrite it
|
||||||
// or it's a settings hash, specific to a plugin
|
// or it's a settings hash, specific to a plugin
|
||||||
if (exports[i] !== undefined || i.indexOf('ep_') === 0) {
|
// @ts-ignore
|
||||||
|
if (this[i] !== undefined || i.indexOf('ep_') === 0) {
|
||||||
if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) {
|
if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) {
|
||||||
exports[i] = _.defaults(settingsObj[i], exports[i]);
|
// @ts-ignore
|
||||||
|
this[i] = _.defaults(settingsObj[i], exports[i]);
|
||||||
} else {
|
} else {
|
||||||
exports[i] = settingsObj[i];
|
// @ts-ignore
|
||||||
|
this[i] = settingsObj[i];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// this setting is unknown, output a warning and throw it away
|
// this setting is unknown, output a warning and throw it away
|
||||||
logger.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`);
|
this.logger.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If stringValue is a numeric string, or its value is "true" or "false", coerce
|
* If stringValue is a numeric string, or its value is "true" or "false", coerce
|
||||||
|
@ -576,7 +581,7 @@ const storeSettings = (settingsObj: any) => {
|
||||||
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
|
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
|
||||||
* in the literal string "null", instead.
|
* in the literal string "null", instead.
|
||||||
*/
|
*/
|
||||||
const coerceValue = (stringValue: string) => {
|
private coerceValue = (stringValue: string) => {
|
||||||
// cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
|
// cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue));
|
const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue));
|
||||||
|
@ -637,7 +642,7 @@ const coerceValue = (stringValue: string) => {
|
||||||
*
|
*
|
||||||
* see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
|
* see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
|
||||||
*/
|
*/
|
||||||
const lookupEnvironmentVariables = (obj: MapArrayType<any>) => {
|
private lookupEnvironmentVariables = (obj: MapArrayType<any>) => {
|
||||||
const replaceEnvs = (obj: MapArrayType<any>) => {
|
const replaceEnvs = (obj: MapArrayType<any>) => {
|
||||||
for (let [key, value] of Object.entries(obj)) {
|
for (let [key, value] of Object.entries(obj)) {
|
||||||
/*
|
/*
|
||||||
|
@ -696,7 +701,7 @@ const lookupEnvironmentVariables = (obj: MapArrayType<any>) => {
|
||||||
const defaultValue = match[3];
|
const defaultValue = match[3];
|
||||||
|
|
||||||
if ((envVarValue === undefined) && (defaultValue === undefined)) {
|
if ((envVarValue === undefined) && (defaultValue === undefined)) {
|
||||||
logger.warn(`Environment variable "${envVarName}" does not contain any value for ` +
|
this.logger.warn(`Environment variable "${envVarName}" does not contain any value for ` +
|
||||||
`configuration key "${key}", and no default was given. Using null. ` +
|
`configuration key "${key}", and no default was given. Using null. ` +
|
||||||
'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' +
|
'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' +
|
||||||
'explicitly use "null" as the default if you want to continue to use null.');
|
'explicitly use "null" as the default if you want to continue to use null.');
|
||||||
|
@ -710,10 +715,10 @@ const lookupEnvironmentVariables = (obj: MapArrayType<any>) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((envVarValue === undefined) && (defaultValue !== undefined)) {
|
if ((envVarValue === undefined) && (defaultValue !== undefined)) {
|
||||||
logger.debug(`Environment variable "${envVarName}" not found for ` +
|
this.logger.debug(`Environment variable "${envVarName}" not found for ` +
|
||||||
`configuration key "${key}". Falling back to default value.`);
|
`configuration key "${key}". Falling back to default value.`);
|
||||||
|
|
||||||
obj[key] = coerceValue(defaultValue);
|
obj[key] = this.coerceValue(defaultValue);
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,10 +728,10 @@ const lookupEnvironmentVariables = (obj: MapArrayType<any>) => {
|
||||||
* For numeric and boolean strings let's convert it to proper types before
|
* For numeric and boolean strings let's convert it to proper types before
|
||||||
* returning it, in order to maintain backward compatibility.
|
* returning it, in order to maintain backward compatibility.
|
||||||
*/
|
*/
|
||||||
logger.debug(
|
this.logger.debug(
|
||||||
`Configuration key "${key}" will be read from environment variable "${envVarName}"`);
|
`Configuration key "${key}" will be read from environment variable "${envVarName}"`);
|
||||||
|
|
||||||
obj[key] = coerceValue(envVarValue!);
|
obj[key] = this.coerceValue(envVarValue!);
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
@ -758,7 +763,8 @@ const lookupEnvironmentVariables = (obj: MapArrayType<any>) => {
|
||||||
const rooting = root.collectFromLeafsUpwards()
|
const rooting = root.collectFromLeafsUpwards()
|
||||||
obj = Object.assign(obj, rooting)
|
obj = Object.assign(obj, rooting)
|
||||||
return obj;
|
return obj;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -769,7 +775,7 @@ const lookupEnvironmentVariables = (obj: MapArrayType<any>) => {
|
||||||
*
|
*
|
||||||
* The isSettings variable only controls the error logging.
|
* The isSettings variable only controls the error logging.
|
||||||
*/
|
*/
|
||||||
const parseSettings = (settingsFilename: string, isSettings: boolean) => {
|
private parseSettings = (settingsFilename: string, isSettings: boolean) => {
|
||||||
let settingsStr = '';
|
let settingsStr = '';
|
||||||
|
|
||||||
let settingsType, notFoundMessage, notFoundFunction;
|
let settingsType, notFoundMessage, notFoundFunction;
|
||||||
|
@ -777,11 +783,11 @@ const parseSettings = (settingsFilename: string, isSettings: boolean) => {
|
||||||
if (isSettings) {
|
if (isSettings) {
|
||||||
settingsType = 'settings';
|
settingsType = 'settings';
|
||||||
notFoundMessage = 'Continuing using defaults!';
|
notFoundMessage = 'Continuing using defaults!';
|
||||||
notFoundFunction = logger.warn.bind(logger);
|
notFoundFunction = this.logger.warn.bind(this.logger);
|
||||||
} else {
|
} else {
|
||||||
settingsType = 'credentials';
|
settingsType = 'credentials';
|
||||||
notFoundMessage = 'Ignoring.';
|
notFoundMessage = 'Ignoring.';
|
||||||
notFoundFunction = logger.info.bind(logger);
|
notFoundFunction = this.logger.info.bind(this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -799,140 +805,137 @@ const parseSettings = (settingsFilename: string, isSettings: boolean) => {
|
||||||
|
|
||||||
const settings = JSON.parse(settingsStr);
|
const settings = JSON.parse(settingsStr);
|
||||||
|
|
||||||
logger.info(`${settingsType} loaded from: ${settingsFilename}`);
|
this.logger.info(`${settingsType} loaded from: ${settingsFilename}`);
|
||||||
|
|
||||||
return lookupEnvironmentVariables(settings);
|
return this.lookupEnvironmentVariables(settings);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`There was an error processing your ${settingsType} ` +
|
this.logger.error(`There was an error processing your ${settingsType} ` +
|
||||||
`file from ${settingsFilename}: ${e.message}`);
|
`file from ${settingsFilename}: ${e.message}`);
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
exports.reloadSettings = () => {
|
|
||||||
const settings = parseSettings(exports.settingsFilename, true);
|
|
||||||
const credentials = parseSettings(exports.credentialsFilename, false);
|
|
||||||
storeSettings(settings);
|
|
||||||
storeSettings(credentials);
|
|
||||||
|
|
||||||
// Init logging config
|
|
||||||
exports.logconfig = defaultLogConfig(exports.loglevel ? exports.loglevel : defaultLogLevel);
|
|
||||||
initLogging(exports.logconfig);
|
|
||||||
|
|
||||||
if (!exports.skinName) {
|
|
||||||
logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' +
|
|
||||||
'update your settings.json. Falling back to the default "colibris".');
|
|
||||||
exports.skinName = 'colibris';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exports.socketTransportProtocols.includes("websocket") || exports.socketTransportProtocols.includes("polling")) {
|
private reloadSettings = () => {
|
||||||
logger.warn("Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling'].");
|
const settings = this.parseSettings(this.settingsFilename, true);
|
||||||
exports.socketTransportProtocols = ['websocket', 'polling'];
|
const credentials = this.parseSettings(this.credentialsFilename, false);
|
||||||
|
this.storeSettings(settings);
|
||||||
|
this.storeSettings(credentials);
|
||||||
|
|
||||||
|
// Init logging config
|
||||||
|
this.logconfig = this.defaultLogConfig(this.loglevel ? this.loglevel : this.defaultLogLevel);
|
||||||
|
this.initLogging(this.logconfig);
|
||||||
|
|
||||||
|
if (!this.skinName) {
|
||||||
|
this.logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' +
|
||||||
|
'update your settings.json. Falling back to the default "colibris".');
|
||||||
|
this.skinName = 'colibris';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.socketTransportProtocols.includes("websocket") || this.socketTransportProtocols.includes("polling")) {
|
||||||
|
this.logger.warn("Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling'].");
|
||||||
|
this.socketTransportProtocols = ['websocket', 'polling'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if skinName has an acceptable value, otherwise falls back to "colibris"
|
// checks if skinName has an acceptable value, otherwise falls back to "colibris"
|
||||||
if (exports.skinName) {
|
if (this.skinName) {
|
||||||
const skinBasePath = path.join(exports.root, 'src', 'static', 'skins');
|
const skinBasePath = path.join(this.root, 'src', 'static', 'skins');
|
||||||
const countPieces = exports.skinName.split(path.sep).length;
|
const countPieces = this.skinName.split(path.sep).length;
|
||||||
|
|
||||||
if (countPieces !== 1) {
|
if (countPieces !== 1) {
|
||||||
logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` +
|
this.logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` +
|
||||||
`not valid: "${exports.skinName}". Falling back to the default "colibris".`);
|
`not valid: "${this.skinName}". Falling back to the default "colibris".`);
|
||||||
|
this.skinName = 'colibris';
|
||||||
exports.skinName = 'colibris';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// informative variable, just for the log messages
|
// informative variable, just for the log messages
|
||||||
let skinPath = path.join(skinBasePath, exports.skinName);
|
let skinPath = path.join(skinBasePath, this.skinName);
|
||||||
|
|
||||||
// what if someone sets skinName == ".." or "."? We catch him!
|
// what if someone sets skinName == ".." or "."? We catch him!
|
||||||
if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) {
|
if (isSubdir(skinBasePath, skinPath) === false) {
|
||||||
logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` +
|
this.logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` +
|
||||||
'Falling back to the default "colibris".');
|
'Falling back to the default "colibris".');
|
||||||
|
|
||||||
exports.skinName = 'colibris';
|
this.skinName = 'colibris';
|
||||||
skinPath = path.join(skinBasePath, exports.skinName);
|
skinPath = path.join(skinBasePath, this.skinName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(skinPath) === false) {
|
if (fs.existsSync(skinPath) === false) {
|
||||||
logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`);
|
this.logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`);
|
||||||
exports.skinName = 'colibris';
|
this.skinName = 'colibris';
|
||||||
skinPath = path.join(skinBasePath, exports.skinName);
|
skinPath = path.join(skinBasePath,this.skinName);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`);
|
this.logger.info(`Using skin "${this.skinName}" in dir: ${skinPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exports.abiword) {
|
if (this.abiword) {
|
||||||
// Check abiword actually exists
|
// Check abiword actually exists
|
||||||
if (exports.abiword != null) {
|
if (this.abiword != null) {
|
||||||
fs.exists(exports.abiword, (exists: boolean) => {
|
let exists = fs.existsSync(this.abiword)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
const abiwordError = 'Abiword does not exist at this path, check your settings file.';
|
const abiwordError = 'Abiword does not exist at this path, check your settings file.';
|
||||||
if (!exports.suppressErrorsInPadText) {
|
if (!this.suppressErrorsInPadText) {
|
||||||
exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
|
this.defaultPadText += `\nError: ${abiwordError}${this.suppressDisableMsg}`;
|
||||||
}
|
}
|
||||||
logger.error(`${abiwordError} File location: ${exports.abiword}`);
|
this.logger.error(`${abiwordError} File location: ${this.abiword}`);
|
||||||
exports.abiword = null;
|
this.abiword = null;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exports.soffice) {
|
if (this.soffice) {
|
||||||
fs.exists(exports.soffice, (exists: boolean) => {
|
let exists = fs.existsSync(this.soffice)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
const sofficeError =
|
const sofficeError =
|
||||||
'soffice (libreoffice) does not exist at this path, check your settings file.';
|
'soffice (libreoffice) does not exist at this path, check your settings file.';
|
||||||
|
|
||||||
if (!exports.suppressErrorsInPadText) {
|
if (!this.suppressErrorsInPadText) {
|
||||||
exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`;
|
this.defaultPadText += `\nError: ${sofficeError}${this.suppressDisableMsg}`;
|
||||||
}
|
}
|
||||||
logger.error(`${sofficeError} File location: ${exports.soffice}`);
|
this.logger.error(`${sofficeError} File location: ${this.soffice}`);
|
||||||
exports.soffice = null;
|
this.soffice = null;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
|
const sessionkeyFilename = makeAbsolute(argvP.sessionkey || './SESSIONKEY.txt');
|
||||||
if (!exports.sessionKey) {
|
if (!this.sessionKey) {
|
||||||
try {
|
try {
|
||||||
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
|
this.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
|
||||||
logger.info(`Session key loaded from: ${sessionkeyFilename}`);
|
this.logger.info(`Session key loaded from: ${sessionkeyFilename}`);
|
||||||
} catch (err) { /* ignored */
|
} catch (err) { /* ignored */
|
||||||
}
|
}
|
||||||
const keyRotationEnabled = exports.cookie.keyRotationInterval && exports.cookie.sessionLifetime;
|
const keyRotationEnabled = this.cookie.keyRotationInterval && this.cookie.sessionLifetime;
|
||||||
if (!exports.sessionKey && !keyRotationEnabled) {
|
if (!this.sessionKey && !keyRotationEnabled) {
|
||||||
logger.info(
|
this.logger.info(
|
||||||
`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`);
|
`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`);
|
||||||
exports.sessionKey = randomString(32);
|
this.sessionKey = randomString(32);
|
||||||
fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8');
|
fs.writeFileSync(sessionkeyFilename, this.sessionKey, 'utf8');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' +
|
this.logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' +
|
||||||
'This value is auto-generated now. Please remove the setting from the file. -- ' +
|
'This value is auto-generated now. Please remove the setting from the file. -- ' +
|
||||||
'If you are seeing this error after restarting using the Admin User ' +
|
'If you are seeing this error after restarting using the Admin User ' +
|
||||||
'Interface then you can ignore this message.');
|
'Interface then you can ignore this message.');
|
||||||
}
|
}
|
||||||
if (exports.sessionKey) {
|
if (this.sessionKey) {
|
||||||
logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` +
|
this.logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` +
|
||||||
'use automatic key rotation instead (see the cookie.keyRotationInterval setting).');
|
'use automatic key rotation instead (see the cookie.keyRotationInterval setting).');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exports.dbType === 'dirty') {
|
if (this.dbType === 'dirty') {
|
||||||
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
|
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
|
||||||
if (!exports.suppressErrorsInPadText) {
|
if (!this.suppressErrorsInPadText) {
|
||||||
exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
|
this.defaultPadText += `\nWarning: ${dirtyWarning}${this.suppressDisableMsg}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
|
this.dbSettings.filename = makeAbsolute(this.dbSettings.filename);
|
||||||
logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`);
|
this.logger.warn(`${dirtyWarning} File location: ${this.dbSettings.filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exports.ip === '') {
|
if (this.ip === '') {
|
||||||
// using Unix socket for connectivity
|
// using Unix socket for connectivity
|
||||||
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
|
this.logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
|
||||||
'"port" parameter will be interpreted as the path to a Unix socket to bind at.');
|
'"port" parameter will be interpreted as the path to a Unix socket to bind at.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -947,13 +950,14 @@ exports.reloadSettings = () => {
|
||||||
* ACHTUNG: this may prevent caching HTTP proxies to work
|
* ACHTUNG: this may prevent caching HTTP proxies to work
|
||||||
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
|
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
|
||||||
*/
|
*/
|
||||||
exports.randomVersionString = randomString(4);
|
this.randomVersionString = randomString(4);
|
||||||
logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`);
|
this.logger.info(`Random string used for versioning assets: ${this.randomVersionString}`);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
const settings = new Settings()
|
||||||
|
|
||||||
|
export default settings
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
exports.exportedForTestingOnly = {
|
|
||||||
parseSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
// initially load settings
|
|
||||||
exports.reloadSettings();
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
* objects lack.
|
* objects lack.
|
||||||
*/
|
*/
|
||||||
class Stream {
|
class Stream {
|
||||||
private _iter
|
private readonly _iter
|
||||||
private _next: any
|
private _next: any
|
||||||
/**
|
/**
|
||||||
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
|
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
|
||||||
*/
|
*/
|
||||||
static range(start: number, end: number) {
|
static range(start: number, end: number): Stream {
|
||||||
return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })());
|
return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ class Stream {
|
||||||
* @param {(v: any) => any} fn - Value transformation function.
|
* @param {(v: any) => any} fn - Value transformation function.
|
||||||
* @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`.
|
* @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`.
|
||||||
*/
|
*/
|
||||||
map(fn:Function) { return new Stream((function* () { // @ts-ignore
|
map(fn:Function): Stream { return new Stream((function* () { // @ts-ignore
|
||||||
for (const v of this) yield fn(v); }).call(this)); }
|
for (const v of this) yield fn(v); }).call(this)); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -136,4 +136,4 @@ class Stream {
|
||||||
[Symbol.iterator]() { return this._iter; }
|
[Symbol.iterator]() { return this._iter; }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Stream;
|
export default Stream;
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
const semver = require('semver');
|
import semver from 'semver';
|
||||||
const settings = require('./Settings');
|
import {getEpVersion} from './Settings';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'User-Agent': 'Etherpad/' + settings.getEpVersion(),
|
'User-Agent': 'Etherpad/' + getEpVersion(),
|
||||||
}
|
}
|
||||||
|
|
||||||
type Infos = {
|
type Infos = {
|
||||||
|
@ -37,15 +39,15 @@ const loadEtherpadInformations = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
exports.getLatestVersion = () => {
|
export const getLatestVersion = () => {
|
||||||
exports.needsUpdate().catch();
|
needsUpdate().catch();
|
||||||
return infos?.latestVersion;
|
return infos?.latestVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.needsUpdate = async (cb?: Function) => {
|
export const needsUpdate = async (cb?: Function) => {
|
||||||
try {
|
try {
|
||||||
const info = await loadEtherpadInformations()
|
const info = await loadEtherpadInformations()
|
||||||
if (semver.gt(info!.latestVersion, settings.getEpVersion())) {
|
if (semver.gt(info!.latestVersion, getEpVersion())) {
|
||||||
if (cb) return cb(true);
|
if (cb) return cb(true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -54,8 +56,8 @@ exports.needsUpdate = async (cb?: Function) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.check = () => {
|
export const check = () => {
|
||||||
exports.needsUpdate((needsUpdate: boolean) => {
|
needsUpdate((needsUpdate: boolean) => {
|
||||||
if (needsUpdate) {
|
if (needsUpdate) {
|
||||||
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const fsp = fs.promises;
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
const settings = require('./Settings');
|
const settings = require('./Settings');
|
||||||
const existsSync = require('./path_exists');
|
import existsSync from './path_exists';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
import CustomError from '../utils/customError';
|
||||||
|
|
||||||
// checks if a rev is a legal number
|
// checks if a rev is a legal number
|
||||||
// pre-condition is that `rev` is not undefined
|
// pre-condition is that `rev` is not undefined
|
||||||
const checkValidRev = (rev: number|string) => {
|
export const checkValidRev = (rev: number|string) => {
|
||||||
if (typeof rev !== 'number') {
|
if (typeof rev !== 'number') {
|
||||||
rev = parseInt(rev, 10);
|
rev = parseInt(rev, 10);
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,4 @@ const checkValidRev = (rev: number|string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// checks if a number is an int
|
// checks if a number is an int
|
||||||
const isInt = (value:number) => (parseFloat(String(value)) === parseInt(String(value), 10)) && !isNaN(value);
|
export const isInt = (value:number) => (parseFloat(String(value)) === parseInt(String(value), 10)) && !isNaN(value);
|
||||||
|
|
||||||
exports.isInt = isInt;
|
|
||||||
exports.checkValidRev = checkValidRev;
|
|
||||||
|
|
|
@ -21,4 +21,4 @@ class CustomError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = CustomError;
|
export default CustomError
|
||||||
|
|
|
@ -4,19 +4,25 @@ import {PadAuthor, PadType} from "../types/PadType";
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {applyToAText, checkRep, compose, deserializeOps, pack, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
|
||||||
const attributes = require('../../static/js/attributes');
|
import {attribsFromString} from '../../static/js/attributes';
|
||||||
const exportHtml = require('./ExportHtml');
|
import {getHTMLFromAtext} from './ExportHtml';
|
||||||
|
import {Builder} from "../../static/js/Builder";
|
||||||
|
import Pad from "../db/Pad";
|
||||||
|
import {OpAssembler} from "../../static/js/OpAssembler";
|
||||||
|
import {numToString} from "../../static/js/ChangesetUtils";
|
||||||
|
import Op from "../../static/js/Op";
|
||||||
|
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||||
|
|
||||||
|
|
||||||
class PadDiff {
|
class PadDiff {
|
||||||
private readonly _pad: PadType;
|
private readonly _pad: Pad;
|
||||||
private readonly _fromRev: string;
|
private readonly _fromRev: string;
|
||||||
private readonly _toRev: string;
|
private readonly _toRev: string;
|
||||||
private _html: any;
|
private _html: any;
|
||||||
public _authors: any[];
|
public _authors: any[];
|
||||||
private self: PadDiff | undefined
|
private self: PadDiff | undefined
|
||||||
constructor(pad: PadType, fromRev:string, toRev:string) {
|
constructor(pad: Pad, fromRev:number, toRev:number) {
|
||||||
// check parameters
|
// check parameters
|
||||||
if (!pad || !pad.id || !pad.atext || !pad.pool) {
|
if (!pad || !pad.id || !pad.atext || !pad.pool) {
|
||||||
throw new Error('Invalid pad');
|
throw new Error('Invalid pad');
|
||||||
|
@ -33,7 +39,7 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
_isClearAuthorship(changeset: any){
|
_isClearAuthorship(changeset: any){
|
||||||
// unpack
|
// unpack
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
|
|
||||||
// check if there is nothing in the charBank
|
// check if there is nothing in the charBank
|
||||||
if (unpacked.charBank !== '') {
|
if (unpacked.charBank !== '') {
|
||||||
|
@ -45,7 +51,7 @@ class PadDiff {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
|
const [clearOperator, anotherOp] = deserializeOps(unpacked.ops);
|
||||||
|
|
||||||
// check if there is only one operator
|
// check if there is only one operator
|
||||||
if (anotherOp != null) return false;
|
if (anotherOp != null) return false;
|
||||||
|
@ -62,7 +68,7 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [appliedAttribute, anotherAttribute] =
|
const [appliedAttribute, anotherAttribute] =
|
||||||
attributes.attribsFromString(clearOperator.attribs, this._pad.pool);
|
attribsFromString(clearOperator.attribs, this._pad.pool);
|
||||||
|
|
||||||
// Check that the operation has exactly one attribute.
|
// Check that the operation has exactly one attribute.
|
||||||
if (appliedAttribute == null || anotherAttribute != null) return false;
|
if (appliedAttribute == null || anotherAttribute != null) return false;
|
||||||
|
@ -78,7 +84,7 @@ class PadDiff {
|
||||||
const atext = await this._pad.getInternalRevisionAText(rev);
|
const atext = await this._pad.getInternalRevisionAText(rev);
|
||||||
|
|
||||||
// build clearAuthorship changeset
|
// build clearAuthorship changeset
|
||||||
const builder = Changeset.builder(atext.text.length);
|
const builder = new Builder(atext.text.length);
|
||||||
builder.keepText(atext.text, [['author', '']], this._pad.pool);
|
builder.keepText(atext.text, [['author', '']], this._pad.pool);
|
||||||
const changeset = builder.toString();
|
const changeset = builder.toString();
|
||||||
|
|
||||||
|
@ -93,7 +99,7 @@ class PadDiff {
|
||||||
const changeset = await this._createClearAuthorship(rev);
|
const changeset = await this._createClearAuthorship(rev);
|
||||||
|
|
||||||
// apply the clearAuthorship changeset
|
// apply the clearAuthorship changeset
|
||||||
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
|
const newAText = applyToAText(changeset, atext, this._pad.pool);
|
||||||
|
|
||||||
return newAText;
|
return newAText;
|
||||||
}
|
}
|
||||||
|
@ -157,7 +163,7 @@ class PadDiff {
|
||||||
if (superChangeset == null) {
|
if (superChangeset == null) {
|
||||||
superChangeset = changeset;
|
superChangeset = changeset;
|
||||||
} else {
|
} else {
|
||||||
superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool);
|
superChangeset = compose(superChangeset, changeset, this._pad.pool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,10 +177,10 @@ class PadDiff {
|
||||||
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
|
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
|
||||||
|
|
||||||
// apply the superChangeset, which includes all addings
|
// apply the superChangeset, which includes all addings
|
||||||
atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool);
|
atext = applyToAText(superChangeset, atext, this._pad.pool);
|
||||||
|
|
||||||
// apply the deletionChangeset, which adds a deletions
|
// apply the deletionChangeset, which adds a deletions
|
||||||
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
|
atext = applyToAText(deletionChangeset, atext, this._pad.pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
return atext;
|
return atext;
|
||||||
|
@ -192,7 +198,8 @@ class PadDiff {
|
||||||
const authorColors = await this._pad.getAllAuthorColors();
|
const authorColors = await this._pad.getAllAuthorColors();
|
||||||
|
|
||||||
// convert the atext to html
|
// convert the atext to html
|
||||||
this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors);
|
// @ts-ignore
|
||||||
|
this._html = await getHTMLFromAtext(this._pad, atext, authorColors);
|
||||||
|
|
||||||
return this._html;
|
return this._html;
|
||||||
}
|
}
|
||||||
|
@ -209,22 +216,22 @@ class PadDiff {
|
||||||
|
|
||||||
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
|
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
|
||||||
// unpack
|
// unpack
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
|
|
||||||
const assem = Changeset.opAssembler();
|
const assem = new OpAssembler();
|
||||||
|
|
||||||
// create deleted attribs
|
// create deleted attribs
|
||||||
const authorAttrib = apool.putAttrib(['author', author || '']);
|
const authorAttrib = apool.putAttrib(['author', author || '']);
|
||||||
const deletedAttrib = apool.putAttrib(['removed', true]);
|
const deletedAttrib = apool.putAttrib(['removed', true]);
|
||||||
const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`;
|
const attribs = `*${numToString(authorAttrib)}*${numToString(deletedAttrib)}`;
|
||||||
|
|
||||||
for (const operator of Changeset.deserializeOps(unpacked.ops)) {
|
for (const operator of deserializeOps(unpacked.ops)) {
|
||||||
if (operator.opcode === '-') {
|
if (operator.opcode === '-') {
|
||||||
// this is a delete operator, extend it with the author
|
// this is a delete operator, extend it with the author
|
||||||
operator.attribs = attribs;
|
operator.attribs = attribs;
|
||||||
} else if (operator.opcode === '=' && operator.attribs) {
|
} else if (operator.opcode === '=' && operator.attribs) {
|
||||||
// this is operator changes only attributes, let's mark which author did that
|
// this is operator changes only attributes, let's mark which author did that
|
||||||
operator.attribs += `*${Changeset.numToString(authorAttrib)}`;
|
operator.attribs += `*${numToString(authorAttrib)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// append the new operator to our assembler
|
// append the new operator to our assembler
|
||||||
|
@ -232,18 +239,19 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the modified changeset
|
// return the modified changeset
|
||||||
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
|
return pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
|
||||||
}
|
}
|
||||||
_createDeletionChangeset(cs: any, startAText: any, apool: any){
|
_createDeletionChangeset(cs: any, startAText: any, apool: any){
|
||||||
const lines = Changeset.splitTextLines(startAText.text);
|
const lines = splitTextLines(startAText.text)!;
|
||||||
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
|
const alines = splitAttributionLines(startAText.attribs, startAText.text);
|
||||||
|
|
||||||
// lines and alines are what the exports is meant to apply to.
|
// lines and alines are what the exports is meant to apply to.
|
||||||
// They may be arrays or objects with .get(i) and .length methods.
|
// They may be arrays or objects with .get(i) and .length methods.
|
||||||
// They include final newlines on lines.
|
// They include final newlines on lines.
|
||||||
|
|
||||||
const linesGet = (idx: number) => {
|
const linesGet = (idx: number) => {
|
||||||
if (lines.get) {
|
if ("get" in lines) {
|
||||||
|
// @ts-ignore
|
||||||
return lines.get(idx);
|
return lines.get(idx);
|
||||||
} else {
|
} else {
|
||||||
return lines[idx];
|
return lines[idx];
|
||||||
|
@ -251,7 +259,8 @@ class PadDiff {
|
||||||
};
|
};
|
||||||
|
|
||||||
const aLinesGet = (idx: number) => {
|
const aLinesGet = (idx: number) => {
|
||||||
if (alines.get) {
|
if ("get" in alines) {
|
||||||
|
// @ts-ignore
|
||||||
return alines.get(idx);
|
return alines.get(idx);
|
||||||
} else {
|
} else {
|
||||||
return alines[idx];
|
return alines[idx];
|
||||||
|
@ -263,14 +272,14 @@ class PadDiff {
|
||||||
let curLineOps: { next: () => any; } | null = null;
|
let curLineOps: { next: () => any; } | null = null;
|
||||||
let curLineOpsNext: { done: any; value: any; } | null = null;
|
let curLineOpsNext: { done: any; value: any; } | null = null;
|
||||||
let curLineOpsLine: number;
|
let curLineOpsLine: number;
|
||||||
let curLineNextOp = new Changeset.Op('+');
|
let curLineNextOp = new Op('+');
|
||||||
|
|
||||||
const unpacked = Changeset.unpack(cs);
|
const unpacked = unpack(cs);
|
||||||
const builder = Changeset.builder(unpacked.newLen);
|
const builder = new Builder(unpacked.newLen);
|
||||||
|
|
||||||
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
|
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
|
||||||
if (!curLineOps || curLineOpsLine !== curLine) {
|
if (!curLineOps || curLineOpsLine !== curLine) {
|
||||||
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
|
curLineOps = deserializeOps(aLinesGet(curLine));
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
curLineOpsLine = curLine;
|
curLineOpsLine = curLine;
|
||||||
let indexIntoLine = 0;
|
let indexIntoLine = 0;
|
||||||
|
@ -291,13 +300,13 @@ class PadDiff {
|
||||||
curChar = 0;
|
curChar = 0;
|
||||||
curLineOpsLine = curLine;
|
curLineOpsLine = curLine;
|
||||||
curLineNextOp.chars = 0;
|
curLineNextOp.chars = 0;
|
||||||
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
|
curLineOps = deserializeOps(aLinesGet(curLine));
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!curLineNextOp.chars) {
|
if (!curLineNextOp.chars) {
|
||||||
if (curLineOpsNext!.done) {
|
if (curLineOpsNext!.done) {
|
||||||
curLineNextOp = new Changeset.Op();
|
curLineNextOp = new Op();
|
||||||
} else {
|
} else {
|
||||||
curLineNextOp = curLineOpsNext!.value;
|
curLineNextOp = curLineOpsNext!.value;
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
|
@ -332,7 +341,7 @@ class PadDiff {
|
||||||
|
|
||||||
const nextText = (numChars: number) => {
|
const nextText = (numChars: number) => {
|
||||||
let len = 0;
|
let len = 0;
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
const firstString = linesGet(curLine).substring(curChar);
|
const firstString = linesGet(curLine).substring(curChar);
|
||||||
len += firstString.length;
|
len += firstString.length;
|
||||||
assem.append(firstString);
|
assem.append(firstString);
|
||||||
|
@ -360,7 +369,7 @@ class PadDiff {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
|
for (const csOp of deserializeOps(unpacked.ops)) {
|
||||||
if (csOp.opcode === '=') {
|
if (csOp.opcode === '=') {
|
||||||
const textBank = nextText(csOp.chars);
|
const textBank = nextText(csOp.chars);
|
||||||
|
|
||||||
|
@ -442,7 +451,7 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Changeset.checkRep(builder.toString());
|
return checkRep(builder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -450,9 +459,9 @@ class PadDiff {
|
||||||
|
|
||||||
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
|
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
|
||||||
// it adds deletions and attribute changes to the atext.
|
// it adds deletions and attribute changes to the atext.
|
||||||
|
// @ts-ignore
|
||||||
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// export the constructor
|
export default PadDiff
|
||||||
module.exports = PadDiff;
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
|
|
||||||
const check = (path:string) => {
|
const check = (path:string) => {
|
||||||
const existsSync = fs.statSync || fs.existsSync;
|
const existsSync = fs.statSync || fs.existsSync;
|
||||||
|
@ -13,4 +13,4 @@ const check = (path:string) => {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = check;
|
export default check
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
|
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
|
||||||
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
|
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
|
||||||
// the predicate.
|
// the predicate.
|
||||||
exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
|
export const firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
|
||||||
if (predicate == null) {
|
if (predicate == null) {
|
||||||
predicate = (x: any) => x;
|
predicate = (x: any) => x;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) =
|
||||||
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
|
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
|
||||||
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
|
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
|
||||||
// function resolves once all `total` Promises have resolved.
|
// function resolves once all `total` Promises have resolved.
|
||||||
exports.timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
|
export const timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
|
||||||
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
|
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
|
||||||
let next = 0;
|
let next = 0;
|
||||||
const addAnother = () => promiseCreator(next++).finally(() => {
|
const addAnother = () => promiseCreator(next++).finally(() => {
|
||||||
|
@ -61,7 +61,9 @@ exports.timesLimit = async (total: number, concurrency: number, promiseCreator:
|
||||||
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
|
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
|
||||||
* properties.
|
* properties.
|
||||||
*/
|
*/
|
||||||
class Gate<T> extends Promise<T> {
|
export class Gate<T> extends Promise<T> {
|
||||||
|
resolve?:Function
|
||||||
|
private reject?:Function
|
||||||
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
|
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
|
||||||
// https://stackoverflow.com/a/65669070 for the rationale.
|
// https://stackoverflow.com/a/65669070 for the rationale.
|
||||||
static get [Symbol.species]() { return Promise; }
|
static get [Symbol.species]() { return Promise; }
|
||||||
|
@ -75,4 +77,3 @@ class Gate<T> extends Promise<T> {
|
||||||
Object.assign(this, props);
|
Object.assign(this, props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.Gate = Gate;
|
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
* Generates a random String with the given length. Is needed to generate the
|
* Generates a random String with the given length. Is needed to generate the
|
||||||
* Author, Group, readonly, session Ids
|
* Author, Group, readonly, session Ids
|
||||||
*/
|
*/
|
||||||
const cryptoMod = require('crypto');
|
import {randomBytes} from 'crypto';
|
||||||
|
|
||||||
const randomString = (len: number) => cryptoMod.randomBytes(len).toString('hex');
|
export const randomString = (len: number) => randomBytes(len).toString('hex');
|
||||||
|
|
||||||
module.exports = randomString;
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {ErrorExtended, RunCMDOptions, RunCMDPromise} from "../types/RunCMDOptions";
|
import {ErrorExtended, RunCMDOptions, RunCMDPromise} from "../types/RunCMDOptions";
|
||||||
import {ChildProcess} from "node:child_process";
|
import {ChildProcess, SpawnOptions} from "node:child_process";
|
||||||
import {PromiseWithStd} from "../types/PromiseWithStd";
|
import {PromiseWithStd} from "../types/PromiseWithStd";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
|
|
||||||
const spawn = require('cross-spawn');
|
import spawn from 'cross-spawn';
|
||||||
const log4js = require('log4js');
|
import log4js from 'log4js';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const settings = require('./Settings');
|
const settings = require('./Settings');
|
||||||
|
|
||||||
const logger = log4js.getLogger('runCmd');
|
const logger = log4js.getLogger('runCmd');
|
||||||
|
@ -74,7 +74,7 @@ const logLines = (readable: undefined | Readable | null, logLineFn: (arg0: (stri
|
||||||
* - `stderr`: Similar to `stdout` but for stderr.
|
* - `stderr`: Similar to `stdout` but for stderr.
|
||||||
* - `child`: The ChildProcess object.
|
* - `child`: The ChildProcess object.
|
||||||
*/
|
*/
|
||||||
module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => {
|
export default (args: string[], opts:RunCMDOptions = {}) => {
|
||||||
logger.debug(`Executing command: ${args.join(' ')}`);
|
logger.debug(`Executing command: ${args.join(' ')}`);
|
||||||
|
|
||||||
opts = {cwd: settings.root, ...opts};
|
opts = {cwd: settings.root, ...opts};
|
||||||
|
@ -123,7 +123,7 @@ module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => {
|
||||||
// process's `exit` handler so that we get a useful stack trace.
|
// process's `exit` handler so that we get a useful stack trace.
|
||||||
const procFailedErr: Error & ErrorExtended = new Error();
|
const procFailedErr: Error & ErrorExtended = new Error();
|
||||||
|
|
||||||
const proc: ChildProcess = spawn(args[0], args.slice(1), opts);
|
const proc: ChildProcess = spawn(args[0], args.slice(1), opts as SpawnOptions);
|
||||||
const streams:[undefined, Readable|null, Readable|null] = [undefined, proc.stdout, proc.stderr];
|
const streams:[undefined, Readable|null, Readable|null] = [undefined, proc.stdout, proc.stderr];
|
||||||
|
|
||||||
let px: { reject: any; resolve: any; };
|
let px: { reject: any; resolve: any; };
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
|
|
||||||
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
||||||
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
|
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
|
||||||
module.exports = (p: string, pathApi = path) => {
|
export default (p: string, pathApi = path) => {
|
||||||
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
|
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
|
||||||
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
|
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
|
||||||
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
|
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
|
||||||
|
|
|
@ -50,7 +50,7 @@ type ButtonGroupType = {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ButtonGroup {
|
class ButtonGroup {
|
||||||
private buttons: Button[]
|
private readonly buttons: Button[]
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.buttons = []
|
this.buttons = []
|
||||||
|
@ -99,7 +99,8 @@ class Button {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static load(btnName: string) {
|
public static load(btnName: string) {
|
||||||
const button = module.exports.availableButtons[btnName];
|
// @ts-ignore
|
||||||
|
const button = toolbar.availableButtons[btnName];
|
||||||
try {
|
try {
|
||||||
if (button.constructor === Button || button.constructor === SelectButton) {
|
if (button.constructor === Button || button.constructor === SelectButton) {
|
||||||
return button;
|
return button;
|
||||||
|
@ -189,7 +190,7 @@ class Separator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
const toolbar = {
|
||||||
availableButtons: {
|
availableButtons: {
|
||||||
bold: defaultButtonAttributes('bold'),
|
bold: defaultButtonAttributes('bold'),
|
||||||
italic: defaultButtonAttributes('italic'),
|
italic: defaultButtonAttributes('italic'),
|
||||||
|
@ -261,6 +262,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
registerButton(buttonName: string, buttonInfo: any) {
|
registerButton(buttonName: string, buttonInfo: any) {
|
||||||
|
// @ts-ignore
|
||||||
this.availableButtons[buttonName] = buttonInfo;
|
this.availableButtons[buttonName] = buttonInfo;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -304,3 +306,5 @@ module.exports = {
|
||||||
return groups.join(this.separator());
|
return groups.join(this.separator());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default toolbar
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@etherpad/express-session": "^1.18.2",
|
"@etherpad/express-session": "^1.18.2",
|
||||||
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
@ -83,19 +84,24 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.45.2",
|
"@playwright/test": "^1.45.2",
|
||||||
"@types/async": "^3.2.24",
|
"@types/async": "^3.2.24",
|
||||||
|
"@types/cross-spawn": "^6.0.6",
|
||||||
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/http-errors": "^2.0.4",
|
"@types/http-errors": "^2.0.4",
|
||||||
"@types/jquery": "^3.5.30",
|
"@types/jquery": "^3.5.30",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/jsonminify": "^0.4.3",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
"@types/oidc-provider": "^8.5.1",
|
"@types/oidc-provider": "^8.5.1",
|
||||||
|
"@types/resolve": "^1.20.6",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@types/tinycon": "^0.6.5",
|
||||||
"@types/underscore": "^1.11.15",
|
"@types/underscore": "^1.11.15",
|
||||||
"@types/unorm": "^1.3.31",
|
"@types/unorm": "^1.3.31",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
|
@ -110,7 +116,8 @@
|
||||||
"sinon": "^18.0.0",
|
"sinon": "^18.0.0",
|
||||||
"split-grid": "^1.0.11",
|
"split-grid": "^1.0.11",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.5.3",
|
||||||
|
"vitest": "^2.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.2",
|
"node": ">=18.18.2",
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class Builder {
|
||||||
* attribute key, value pairs.
|
* attribute key, value pairs.
|
||||||
* @returns {Builder} this
|
* @returns {Builder} this
|
||||||
*/
|
*/
|
||||||
keepText= (text: string, attribs: string|Attribute[], pool?: AttributePool): Builder=> {
|
keepText= (text: string, attribs?: string|Attribute[], pool?: AttributePool): Builder=> {
|
||||||
for (const op of opsFromText('=', text, attribs, pool)) this.assem.append(op);
|
for (const op of opsFromText('=', text, attribs, pool)) this.assem.append(op);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ export class Builder {
|
||||||
* character must be a newline.
|
* character must be a newline.
|
||||||
* @returns {Builder} this
|
* @returns {Builder} this
|
||||||
*/
|
*/
|
||||||
remove= (N: number, L: number): Builder => {
|
remove= (N: number, L?: number): Builder => {
|
||||||
this.o.opcode = '-';
|
this.o.opcode = '-';
|
||||||
this.o.attribs = '';
|
this.o.attribs = '';
|
||||||
this.o.chars = N;
|
this.o.chars = N;
|
||||||
|
|
|
@ -445,10 +445,10 @@ export const applyToText = (cs: string, str: string): string => {
|
||||||
* @param {string} cs - the changeset to apply
|
* @param {string} cs - the changeset to apply
|
||||||
* @param {string[]} lines - The lines to which the changeset needs to be applied
|
* @param {string[]} lines - The lines to which the changeset needs to be applied
|
||||||
*/
|
*/
|
||||||
const mutateTextLines = (cs: string, lines:string[]) => {
|
export const mutateTextLines = (cs: string, lines: RegExpMatchArray | null) => {
|
||||||
const unpacked = unpack(cs);
|
const unpacked = unpack(cs);
|
||||||
const bankIter = new StringIterator(unpacked.charBank);
|
const bankIter = new StringIterator(unpacked.charBank);
|
||||||
const mut = new TextLinesMutator(lines);
|
const mut = new TextLinesMutator(lines!);
|
||||||
for (const op of deserializeOps(unpacked.ops)) {
|
for (const op of deserializeOps(unpacked.ops)) {
|
||||||
switch (op.opcode) {
|
switch (op.opcode) {
|
||||||
case '+':
|
case '+':
|
||||||
|
@ -709,7 +709,7 @@ export const identity = (N: number): string => pack(N, N, '', '');
|
||||||
* @param {AttributePool.ts} [pool] - Attribute pool.
|
* @param {AttributePool.ts} [pool] - Attribute pool.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const makeSplice = (orig: string, start: number, ndel: number, ins: string, attribs: string | Attribute[] | undefined, pool: AttributePool | null | undefined): string => {
|
export const makeSplice = (orig: string, start: number, ndel: number, ins: string|null, attribs?: string | Attribute[] | undefined, pool?: AttributePool | null | undefined): string => {
|
||||||
if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`);
|
if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`);
|
||||||
if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`);
|
if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`);
|
||||||
if (start > orig.length) start = orig.length;
|
if (start > orig.length) start = orig.length;
|
||||||
|
@ -723,7 +723,7 @@ export const makeSplice = (orig: string, start: number, ndel: number, ins: strin
|
||||||
})();
|
})();
|
||||||
for (const op of ops) assem.append(op);
|
for (const op of ops) assem.append(op);
|
||||||
assem.endDocument();
|
assem.endDocument();
|
||||||
return pack(orig.length, orig.length + ins.length - ndel, assem.toString(), ins);
|
return pack(orig.length, orig.length + ins!.length - ndel, assem.toString(), ins!);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -932,7 +932,7 @@ export const mapAttribNumbers = (cs: string, func: Function): string => {
|
||||||
* attributes
|
* attributes
|
||||||
* @returns {AText}
|
* @returns {AText}
|
||||||
*/
|
*/
|
||||||
export const makeAText = (text: string, attribs: string): AText => ({
|
export const makeAText = (text: string, attribs?: string): AText => ({
|
||||||
text,
|
text,
|
||||||
attribs: (attribs || makeAttribution(text)),
|
attribs: (attribs || makeAttribution(text)),
|
||||||
});
|
});
|
||||||
|
@ -1119,7 +1119,7 @@ export const makeAttribsString = (opcode: string, attribs: Attribute[]|string, p
|
||||||
/**
|
/**
|
||||||
* Like "substring" but on a single-line attribution string.
|
* Like "substring" but on a single-line attribution string.
|
||||||
*/
|
*/
|
||||||
export const subattribution = (astr: string, start: number, optEnd: number) => {
|
export const subattribution = (astr: string, start: number, optEnd?: number) => {
|
||||||
const attOps = deserializeOps(astr);
|
const attOps = deserializeOps(astr);
|
||||||
let attOpsNext = attOps.next();
|
let attOpsNext = attOps.next();
|
||||||
const assem = new SmartOpAssembler();
|
const assem = new SmartOpAssembler();
|
||||||
|
@ -1164,9 +1164,7 @@ export const subattribution = (astr: string, start: number, optEnd: number) => {
|
||||||
return assem.toString();
|
return assem.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const inverse = (cs: string, lines: string|{
|
export const inverse = (cs: string, lines: string|RegExpMatchArray | null, alines: string[]|{
|
||||||
get: (idx: number) => string,
|
|
||||||
}, alines: string|{
|
|
||||||
get: (idx: number) => string,
|
get: (idx: number) => string,
|
||||||
}, pool: AttributePool) => {
|
}, pool: AttributePool) => {
|
||||||
// lines and alines are what the exports is meant to apply to.
|
// lines and alines are what the exports is meant to apply to.
|
||||||
|
@ -1176,9 +1174,10 @@ export const inverse = (cs: string, lines: string|{
|
||||||
const linesGet = (idx: number) => {
|
const linesGet = (idx: number) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if ("get" in lines) {
|
if ("get" in lines) {
|
||||||
|
// @ts-ignore
|
||||||
return lines.get(idx);
|
return lines.get(idx);
|
||||||
} else {
|
} else {
|
||||||
return lines[idx];
|
return lines![idx];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import {padUtils} from './pad_utils'
|
||||||
*
|
*
|
||||||
* Supports serialization to JSON.
|
* Supports serialization to JSON.
|
||||||
*/
|
*/
|
||||||
class ChatMessage {
|
export class ChatMessage {
|
||||||
customMetadata: any
|
customMetadata: any
|
||||||
text: string|null
|
text: string|null
|
||||||
public authorId: string|null
|
public authorId: string|null
|
||||||
private displayName: string|null
|
displayName: string|null
|
||||||
time: number|null
|
time: number|null
|
||||||
static fromObject(obj: ChatMessage) {
|
static fromObject(obj: ChatMessage) {
|
||||||
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept
|
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept
|
||||||
|
|
|
@ -19,7 +19,7 @@ export class TextLinesMutator {
|
||||||
/**
|
/**
|
||||||
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
|
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
|
||||||
*/
|
*/
|
||||||
constructor(lines: string[]) {
|
constructor(lines: string[]| RegExpMatchArray ) {
|
||||||
this.lines = lines;
|
this.lines = lines;
|
||||||
/**
|
/**
|
||||||
* this._curSplice holds values that will be passed as arguments to this._lines.splice() to
|
* this._curSplice holds values that will be passed as arguments to this._lines.splice() to
|
||||||
|
|
|
@ -25,13 +25,22 @@
|
||||||
// of the document. These revisions are connected together by various
|
// of the document. These revisions are connected together by various
|
||||||
// changesets, or deltas, between any two revisions.
|
// changesets, or deltas, between any two revisions.
|
||||||
|
|
||||||
const loadBroadcastRevisionsJS = () => {
|
type RevisionDelta = {
|
||||||
function Revision(revNum) {
|
deltaRev: number;
|
||||||
|
deltaTime: number;
|
||||||
|
getValue: () => RevisionDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Revision {
|
||||||
|
rev: number;
|
||||||
|
changesets: RevisionDelta[];
|
||||||
|
|
||||||
|
constructor(revNum: number) {
|
||||||
this.rev = revNum;
|
this.rev = revNum;
|
||||||
this.changesets = [];
|
this.changesets = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Revision.prototype.addChangeset = function (destIndex, changeset, timeDelta) {
|
addChangeset(destIndex: number, changeset: RevisionDelta, timeDelta: number) {
|
||||||
const changesetWrapper = {
|
const changesetWrapper = {
|
||||||
deltaRev: destIndex - this.rev,
|
deltaRev: destIndex - this.rev,
|
||||||
deltaTime: timeDelta,
|
deltaTime: timeDelta,
|
||||||
|
@ -39,34 +48,39 @@ const loadBroadcastRevisionsJS = () => {
|
||||||
};
|
};
|
||||||
this.changesets.push(changesetWrapper);
|
this.changesets.push(changesetWrapper);
|
||||||
this.changesets.sort((a, b) => (b.deltaRev - a.deltaRev));
|
this.changesets.sort((a, b) => (b.deltaRev - a.deltaRev));
|
||||||
};
|
}
|
||||||
|
|
||||||
const revisionInfo = {};
|
|
||||||
revisionInfo.addChangeset = function (fromIndex, toIndex, changeset, backChangeset, timeDelta) {
|
|
||||||
const startRevision = this[fromIndex] || this.createNew(fromIndex);
|
|
||||||
const endRevision = this[toIndex] || this.createNew(toIndex);
|
|
||||||
startRevision.addChangeset(toIndex, changeset, timeDelta);
|
|
||||||
endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta);
|
|
||||||
};
|
|
||||||
|
|
||||||
revisionInfo.latest = clientVars.collab_client_vars.rev || -1;
|
|
||||||
|
|
||||||
revisionInfo.createNew = function (index) {
|
|
||||||
this[index] = new Revision(index);
|
|
||||||
if (index > this.latest) {
|
|
||||||
this.latest = index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this[index];
|
class RevisionInfo {
|
||||||
};
|
private revisionInfo: Record<number|string, number|Revision> = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.revisionInfo.latest = window.clientVars.collab_client_vars.rev || -1;
|
||||||
|
window.revisionInfo = this.revisionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
addChangeset = (fromIndex: number, toIndex: number, changeset: RevisionDelta, backChangeset: RevisionDelta, timeDelta: number)=> {
|
||||||
|
const startRevision = (this.revisionInfo[fromIndex] || this.createNew(fromIndex)) as Revision;
|
||||||
|
const endRevision = (this.revisionInfo[toIndex] || this.createNew(toIndex)) as Revision;
|
||||||
|
startRevision.addChangeset(toIndex, changeset, timeDelta);
|
||||||
|
endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNew = (index: number)=> {
|
||||||
|
this.revisionInfo![index] = new Revision(index);
|
||||||
|
if (index > (this.revisionInfo.latest as number)) {
|
||||||
|
this.revisionInfo.latest = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.revisionInfo[index];
|
||||||
|
}
|
||||||
// assuming that there is a path from fromIndex to toIndex, and that the links
|
// assuming that there is a path from fromIndex to toIndex, and that the links
|
||||||
// are laid out in a skip-list format
|
// are laid out in a skip-list format
|
||||||
revisionInfo.getPath = function (fromIndex, toIndex) {
|
getPath = (fromIndex: number, toIndex: number)=> {
|
||||||
const changesets = [];
|
const changesets = [];
|
||||||
const spans = [];
|
const spans = [];
|
||||||
const times = [];
|
const times = [];
|
||||||
let elem = this[fromIndex] || this.createNew(fromIndex);
|
let elem = (this.revisionInfo[fromIndex] || this.createNew(fromIndex)) as Revision;
|
||||||
if (elem.changesets.length !== 0 && fromIndex !== toIndex) {
|
if (elem.changesets.length !== 0 && fromIndex !== toIndex) {
|
||||||
const reverse = !(fromIndex < toIndex);
|
const reverse = !(fromIndex < toIndex);
|
||||||
while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse)) {
|
while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse)) {
|
||||||
|
@ -88,7 +102,7 @@ const loadBroadcastRevisionsJS = () => {
|
||||||
changesets.push(topush.getValue());
|
changesets.push(topush.getValue());
|
||||||
spans.push(elem.changesets[i].deltaRev);
|
spans.push(elem.changesets[i].deltaRev);
|
||||||
times.push(topush.deltaTime);
|
times.push(topush.deltaTime);
|
||||||
elem = this[elem.rev + elem.changesets[i].deltaRev];
|
elem = this.revisionInfo[elem.rev + elem.changesets[i].deltaRev] as Revision;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,8 +122,7 @@ const loadBroadcastRevisionsJS = () => {
|
||||||
spans,
|
spans,
|
||||||
times,
|
times,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
window.revisionInfo = revisionInfo;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;
|
export default new RevisionInfo();
|
|
@ -1,343 +0,0 @@
|
||||||
'use strict';
|
|
||||||
/**
|
|
||||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
|
||||||
* This helps other people to understand this code better and helps them to improve it.
|
|
||||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copyright 2009 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// These parameters were global, now they are injected. A reference to the
|
|
||||||
// Timeslider controller would probably be more appropriate.
|
|
||||||
const _ = require('underscore');
|
|
||||||
const padmodals = require('./pad_modals').padmodals;
|
|
||||||
const colorutils = require('./colorutils').colorutils;
|
|
||||||
import html10n from './vendors/html10n';
|
|
||||||
|
|
||||||
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
|
|
||||||
let BroadcastSlider;
|
|
||||||
|
|
||||||
// Hack to ensure timeslider i18n values are in
|
|
||||||
$("[data-key='timeslider_returnToPad'] > a > span").html(
|
|
||||||
html10n.get('timeslider.toolbar.returnbutton'));
|
|
||||||
|
|
||||||
(() => { // wrap this code in its own namespace
|
|
||||||
let sliderLength = 1000;
|
|
||||||
let sliderPos = 0;
|
|
||||||
let sliderActive = false;
|
|
||||||
const slidercallbacks = [];
|
|
||||||
const savedRevisions = [];
|
|
||||||
let sliderPlaying = false;
|
|
||||||
|
|
||||||
const _callSliderCallbacks = (newval) => {
|
|
||||||
sliderPos = newval;
|
|
||||||
for (let i = 0; i < slidercallbacks.length; i++) {
|
|
||||||
slidercallbacks[i](newval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSliderElements = () => {
|
|
||||||
for (let i = 0; i < savedRevisions.length; i++) {
|
|
||||||
const position = parseInt(savedRevisions[i].attr('pos'));
|
|
||||||
savedRevisions[i].css(
|
|
||||||
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
|
|
||||||
}
|
|
||||||
$('#ui-slider-handle').css(
|
|
||||||
'left', sliderPos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addSavedRevision = (position, info) => {
|
|
||||||
const newSavedRevision = $('<div></div>');
|
|
||||||
newSavedRevision.addClass('star');
|
|
||||||
|
|
||||||
newSavedRevision.attr('pos', position);
|
|
||||||
newSavedRevision.css(
|
|
||||||
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
|
|
||||||
$('#ui-slider-bar').append(newSavedRevision);
|
|
||||||
newSavedRevision.on('mouseup', (evt) => {
|
|
||||||
BroadcastSlider.setSliderPosition(position);
|
|
||||||
});
|
|
||||||
savedRevisions.push(newSavedRevision);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Begin small 'API' */
|
|
||||||
|
|
||||||
const onSlider = (callback) => {
|
|
||||||
slidercallbacks.push(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSliderPosition = () => sliderPos;
|
|
||||||
|
|
||||||
const setSliderPosition = (newpos) => {
|
|
||||||
newpos = Number(newpos);
|
|
||||||
if (newpos < 0 || newpos > sliderLength) return;
|
|
||||||
if (!newpos) {
|
|
||||||
newpos = 0; // stops it from displaying NaN if newpos isn't set
|
|
||||||
}
|
|
||||||
window.location.hash = `#${newpos}`;
|
|
||||||
$('#ui-slider-handle').css(
|
|
||||||
'left', newpos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
|
|
||||||
$('a.tlink').map(function () {
|
|
||||||
$(this).attr('href', $(this).attr('thref').replace('%revision%', newpos));
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#revision_label').html(html10n.get('timeslider.version', {version: newpos}));
|
|
||||||
|
|
||||||
$('#leftstar, #leftstep').toggleClass('disabled', newpos === 0);
|
|
||||||
$('#rightstar, #rightstep').toggleClass('disabled', newpos === sliderLength);
|
|
||||||
|
|
||||||
sliderPos = newpos;
|
|
||||||
_callSliderCallbacks(newpos);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSliderLength = () => sliderLength;
|
|
||||||
|
|
||||||
const setSliderLength = (newlength) => {
|
|
||||||
sliderLength = newlength;
|
|
||||||
updateSliderElements();
|
|
||||||
};
|
|
||||||
|
|
||||||
// just take over the whole slider screen with a reconnect message
|
|
||||||
|
|
||||||
const showReconnectUI = () => {
|
|
||||||
padmodals.showModal('disconnected');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setAuthors = (authors) => {
|
|
||||||
const authorsList = $('#authorsList');
|
|
||||||
authorsList.empty();
|
|
||||||
let numAnonymous = 0;
|
|
||||||
let numNamed = 0;
|
|
||||||
const colorsAnonymous = [];
|
|
||||||
_.each(authors, (author) => {
|
|
||||||
if (author) {
|
|
||||||
const authorColor = clientVars.colorPalette[author.colorId] || author.colorId;
|
|
||||||
if (author.name) {
|
|
||||||
if (numNamed !== 0) authorsList.append(', ');
|
|
||||||
const textColor =
|
|
||||||
colorutils.textColorFromBackgroundColor(authorColor, clientVars.skinName);
|
|
||||||
$('<span />')
|
|
||||||
.text(author.name || 'unnamed')
|
|
||||||
.css('background-color', authorColor)
|
|
||||||
.css('color', textColor)
|
|
||||||
.addClass('author')
|
|
||||||
.appendTo(authorsList);
|
|
||||||
|
|
||||||
numNamed++;
|
|
||||||
} else {
|
|
||||||
numAnonymous++;
|
|
||||||
if (authorColor) colorsAnonymous.push(authorColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (numAnonymous > 0) {
|
|
||||||
const anonymousAuthorString = html10n.get('timeslider.unnamedauthors', {num: numAnonymous});
|
|
||||||
|
|
||||||
if (numNamed !== 0) {
|
|
||||||
authorsList.append(` + ${anonymousAuthorString}`);
|
|
||||||
} else {
|
|
||||||
authorsList.append(anonymousAuthorString);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colorsAnonymous.length > 0) {
|
|
||||||
authorsList.append(' (');
|
|
||||||
_.each(colorsAnonymous, (color, i) => {
|
|
||||||
if (i > 0) authorsList.append(' ');
|
|
||||||
$('<span> </span>')
|
|
||||||
.css('background-color', color)
|
|
||||||
.addClass('author author-anonymous')
|
|
||||||
.appendTo(authorsList);
|
|
||||||
});
|
|
||||||
authorsList.append(')');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (authors.length === 0) {
|
|
||||||
authorsList.append(html10n.get('timeslider.toolbar.authorsList'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const playButtonUpdater = () => {
|
|
||||||
if (sliderPlaying) {
|
|
||||||
if (getSliderPosition() + 1 > sliderLength) {
|
|
||||||
$('#playpause_button_icon').toggleClass('pause');
|
|
||||||
sliderPlaying = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSliderPosition(getSliderPosition() + 1);
|
|
||||||
|
|
||||||
setTimeout(playButtonUpdater, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const playpause = () => {
|
|
||||||
$('#playpause_button_icon').toggleClass('pause');
|
|
||||||
|
|
||||||
if (!sliderPlaying) {
|
|
||||||
if (getSliderPosition() === sliderLength) setSliderPosition(0);
|
|
||||||
sliderPlaying = true;
|
|
||||||
playButtonUpdater();
|
|
||||||
} else {
|
|
||||||
sliderPlaying = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
BroadcastSlider = {
|
|
||||||
onSlider,
|
|
||||||
getSliderPosition,
|
|
||||||
setSliderPosition,
|
|
||||||
getSliderLength,
|
|
||||||
setSliderLength,
|
|
||||||
isSliderActive: () => sliderActive,
|
|
||||||
playpause,
|
|
||||||
addSavedRevision,
|
|
||||||
showReconnectUI,
|
|
||||||
setAuthors,
|
|
||||||
};
|
|
||||||
|
|
||||||
// assign event handlers to html UI elements after page load
|
|
||||||
fireWhenAllScriptsAreLoaded.push(() => {
|
|
||||||
$(document).on('keyup', (e) => {
|
|
||||||
if (!e) e = window.event;
|
|
||||||
const code = e.keyCode || e.which;
|
|
||||||
|
|
||||||
if (code === 37) { // left
|
|
||||||
if (e.shiftKey) {
|
|
||||||
$('#leftstar').trigger('click');
|
|
||||||
} else {
|
|
||||||
$('#leftstep').trigger('click');
|
|
||||||
}
|
|
||||||
} else if (code === 39) { // right
|
|
||||||
if (e.shiftKey) {
|
|
||||||
$('#rightstar').trigger('click');
|
|
||||||
} else {
|
|
||||||
$('#rightstep').trigger('click');
|
|
||||||
}
|
|
||||||
} else if (code === 32) { // spacebar
|
|
||||||
$('#playpause_button_icon').trigger('click');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Resize
|
|
||||||
$(window).on('resize', () => {
|
|
||||||
updateSliderElements();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Slider click
|
|
||||||
$('#ui-slider-bar').on('mousedown', (evt) => {
|
|
||||||
$('#ui-slider-handle').css('left', (evt.clientX - $('#ui-slider-bar').offset().left));
|
|
||||||
$('#ui-slider-handle').trigger(evt);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Slider dragging
|
|
||||||
$('#ui-slider-handle').on('mousedown', function (evt) {
|
|
||||||
this.startLoc = evt.clientX;
|
|
||||||
this.currentLoc = parseInt($(this).css('left'));
|
|
||||||
sliderActive = true;
|
|
||||||
$(document).on('mousemove', (evt2) => {
|
|
||||||
$(this).css('pointer', 'move');
|
|
||||||
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
|
|
||||||
if (newloc < 0) newloc = 0;
|
|
||||||
const maxPos = $('#ui-slider-bar').width() - 2;
|
|
||||||
if (newloc > maxPos) newloc = maxPos;
|
|
||||||
const version = Math.floor(newloc * sliderLength / maxPos);
|
|
||||||
$('#revision_label').html(html10n.get('timeslider.version', {version}));
|
|
||||||
$(this).css('left', newloc);
|
|
||||||
if (getSliderPosition() !== version) _callSliderCallbacks(version);
|
|
||||||
});
|
|
||||||
$(document).on('mouseup', (evt2) => {
|
|
||||||
$(document).off('mousemove');
|
|
||||||
$(document).off('mouseup');
|
|
||||||
sliderActive = false;
|
|
||||||
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
|
|
||||||
if (newloc < 0) newloc = 0;
|
|
||||||
const maxPos = $('#ui-slider-bar').width() - 2;
|
|
||||||
if (newloc > maxPos) newloc = maxPos;
|
|
||||||
$(this).css('left', newloc);
|
|
||||||
setSliderPosition(Math.floor(newloc * sliderLength / maxPos));
|
|
||||||
if (parseInt($(this).css('left')) < 2) {
|
|
||||||
$(this).css('left', '2px');
|
|
||||||
} else {
|
|
||||||
this.currentLoc = parseInt($(this).css('left'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// play/pause toggling
|
|
||||||
$('#playpause_button_icon').on('click', (evt) => {
|
|
||||||
BroadcastSlider.playpause();
|
|
||||||
});
|
|
||||||
|
|
||||||
// next/prev saved revision and changeset
|
|
||||||
$('.stepper').on('click', function (evt) {
|
|
||||||
switch ($(this).attr('id')) {
|
|
||||||
case 'leftstep':
|
|
||||||
setSliderPosition(getSliderPosition() - 1);
|
|
||||||
break;
|
|
||||||
case 'rightstep':
|
|
||||||
setSliderPosition(getSliderPosition() + 1);
|
|
||||||
break;
|
|
||||||
case 'leftstar': {
|
|
||||||
let nextStar = 0; // default to first revision in document
|
|
||||||
for (let i = 0; i < savedRevisions.length; i++) {
|
|
||||||
const pos = parseInt(savedRevisions[i].attr('pos'));
|
|
||||||
if (pos < getSliderPosition() && nextStar < pos) nextStar = pos;
|
|
||||||
}
|
|
||||||
setSliderPosition(nextStar);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'rightstar': {
|
|
||||||
let nextStar = sliderLength; // default to last revision in document
|
|
||||||
for (let i = 0; i < savedRevisions.length; i++) {
|
|
||||||
const pos = parseInt(savedRevisions[i].attr('pos'));
|
|
||||||
if (pos > getSliderPosition() && nextStar > pos) nextStar = pos;
|
|
||||||
}
|
|
||||||
setSliderPosition(nextStar);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (clientVars) {
|
|
||||||
$('#timeslider-wrapper').show();
|
|
||||||
|
|
||||||
if (window.location.hash.length > 1) {
|
|
||||||
const hashRev = Number(window.location.hash.substr(1));
|
|
||||||
if (!isNaN(hashRev)) {
|
|
||||||
// this is necessary because of the socket.io-event which loads the changesets
|
|
||||||
setTimeout(() => { setSliderPosition(hashRev); }, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSliderLength(clientVars.collab_client_vars.rev);
|
|
||||||
setSliderPosition(clientVars.collab_client_vars.rev);
|
|
||||||
|
|
||||||
_.each(clientVars.savedRevisions, (revision) => {
|
|
||||||
addSavedRevision(revision.revNum, revision);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
BroadcastSlider.onSlider((loc) => {
|
|
||||||
$('#viewlatest').html(
|
|
||||||
`${loc === BroadcastSlider.getSliderLength() ? 'Viewing' : 'View'} latest content`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return BroadcastSlider;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.loadBroadcastSliderJS = loadBroadcastSliderJS;
|
|
326
src/static/js/broadcast_slider.ts
Normal file
326
src/static/js/broadcast_slider.ts
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
'use strict';
|
||||||
|
/**
|
||||||
|
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||||
|
* This helps other people to understand this code better and helps them to improve it.
|
||||||
|
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {UserInfo} from "./types/SocketIOMessage";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright 2009 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// These parameters were global, now they are injected. A reference to the
|
||||||
|
// Timeslider controller would probably be more appropriate.
|
||||||
|
import _ from 'underscore';
|
||||||
|
import {padModals as padmodals} from "./pad_modals";
|
||||||
|
import colorutils from "./colorutils";
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
|
import {PadRevision} from "./types/PadRevision";
|
||||||
|
|
||||||
|
class BroadcastSlider {
|
||||||
|
private sliderLength = 1000;
|
||||||
|
private sliderPos = 0;
|
||||||
|
private sliderActive = false;
|
||||||
|
private slidercallbacks: ((val: number)=>void)[] = [];
|
||||||
|
private savedRevisions: JQuery<HTMLElement>[] = [];
|
||||||
|
private sliderPlaying = false;
|
||||||
|
private fireWhenAllScriptsAreLoaded: Function[] = [];
|
||||||
|
private startLoc?: number;
|
||||||
|
private currentLoc?: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Hack to ensure timeslider i18n values are in
|
||||||
|
$("[data-key='timeslider_returnToPad'] > a > span").html(
|
||||||
|
html10n.get('timeslider.toolbar.returnbutton'))
|
||||||
|
// assign event handlers to html UI elements after page load
|
||||||
|
this.fireWhenAllScriptsAreLoaded.push(() => {
|
||||||
|
$(document).on('keyup', (e) => {
|
||||||
|
if (!e) { // @ts-ignore
|
||||||
|
e = window.event;
|
||||||
|
}
|
||||||
|
const code = e.keyCode || e.which;
|
||||||
|
|
||||||
|
if (code === 37) { // left
|
||||||
|
if (e.shiftKey) {
|
||||||
|
$('#leftstar').trigger('click');
|
||||||
|
} else {
|
||||||
|
$('#leftstep').trigger('click');
|
||||||
|
}
|
||||||
|
} else if (code === 39) { // right
|
||||||
|
if (e.shiftKey) {
|
||||||
|
$('#rightstar').trigger('click');
|
||||||
|
} else {
|
||||||
|
$('#rightstep').trigger('click');
|
||||||
|
}
|
||||||
|
} else if (code === 32) { // spacebar
|
||||||
|
$('#playpause_button_icon').trigger('click');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
$(window).on('resize', () => {
|
||||||
|
this.updateSliderElements();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Slider click
|
||||||
|
$('#ui-slider-bar').on('mousedown', (evt) => {
|
||||||
|
$('#ui-slider-handle').css('left', (evt.clientX - $('#ui-slider-bar').offset()!.left));
|
||||||
|
$('#ui-slider-handle').trigger(evt);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Slider dragging
|
||||||
|
$('#ui-slider-handle').on('mousedown', (evt)=> {
|
||||||
|
this.startLoc = evt.clientX;
|
||||||
|
this.currentLoc = parseInt($(this).css('left'));
|
||||||
|
this.sliderActive = true;
|
||||||
|
$(document).on('mousemove', (evt2) => {
|
||||||
|
$(this).css('pointer', 'move');
|
||||||
|
let newloc = this.currentLoc! + (evt2.clientX - this.startLoc!);
|
||||||
|
if (newloc < 0) newloc = 0;
|
||||||
|
const maxPos = $('#ui-slider-bar').width()! - 2;
|
||||||
|
if (newloc > maxPos) newloc = maxPos;
|
||||||
|
const version = Math.floor(newloc * this.sliderLength / maxPos);
|
||||||
|
$('#revision_label').html(html10n.get('timeslider.version', {version}));
|
||||||
|
$(this).css('left', newloc);
|
||||||
|
if (this.getSliderPosition() !== version) this.callSliderCallbacks(version);
|
||||||
|
});
|
||||||
|
$(document).on('mouseup', (evt2) => {
|
||||||
|
$(document).off('mousemove');
|
||||||
|
$(document).off('mouseup');
|
||||||
|
this.sliderActive = false;
|
||||||
|
let newloc = this.currentLoc! + (evt2.clientX - this.startLoc!);
|
||||||
|
if (newloc < 0) newloc = 0;
|
||||||
|
const maxPos = $('#ui-slider-bar').width()! - 2;
|
||||||
|
if (newloc > maxPos) newloc = maxPos;
|
||||||
|
$(this).css('left', newloc);
|
||||||
|
this.setSliderPosition(Math.floor(newloc * this.sliderLength / maxPos));
|
||||||
|
if (parseInt($(this).css('left')) < 2) {
|
||||||
|
$(this).css('left', '2px');
|
||||||
|
} else {
|
||||||
|
this.currentLoc = parseInt($(this).css('left'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// play/pause toggling
|
||||||
|
$('#playpause_button_icon').on('click', (evt) => {
|
||||||
|
this.playpause();
|
||||||
|
});
|
||||||
|
|
||||||
|
// next/prev saved revision and changeset
|
||||||
|
$('.stepper').on('click', (evt)=> {
|
||||||
|
switch ($(this).attr('id')) {
|
||||||
|
case 'leftstep':
|
||||||
|
this.setSliderPosition(this.getSliderPosition() - 1);
|
||||||
|
break;
|
||||||
|
case 'rightstep':
|
||||||
|
this.setSliderPosition(this.getSliderPosition() + 1);
|
||||||
|
break;
|
||||||
|
case 'leftstar': {
|
||||||
|
let nextStar = 0; // default to first revision in document
|
||||||
|
for (let i = 0; i < this.savedRevisions.length; i++) {
|
||||||
|
const pos = parseInt(this.savedRevisions[i].attr('pos') as string);
|
||||||
|
if (pos < this.getSliderPosition() && nextStar < pos) nextStar = pos;
|
||||||
|
}
|
||||||
|
this.setSliderPosition(nextStar);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'rightstar': {
|
||||||
|
let nextStar = this.sliderLength; // default to last revision in document
|
||||||
|
for (let i = 0; i < this.savedRevisions.length; i++) {
|
||||||
|
const pos = parseInt(this.savedRevisions[i].attr('pos') as string);
|
||||||
|
if (pos > this.getSliderPosition() && nextStar > pos) nextStar = pos;
|
||||||
|
}
|
||||||
|
this.setSliderPosition(nextStar);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.clientVars) {
|
||||||
|
$('#timeslider-wrapper').show();
|
||||||
|
|
||||||
|
if (window.location.hash.length > 1) {
|
||||||
|
const hashRev = Number(window.location.hash.substr(1));
|
||||||
|
if (!isNaN(hashRev)) {
|
||||||
|
// this is necessary because of the socket.io-event which loads the changesets
|
||||||
|
setTimeout(() => { this.setSliderPosition(hashRev); }, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setSliderLength(window.clientVars.collab_client_vars.rev);
|
||||||
|
this.setSliderPosition(window.clientVars.collab_client_vars.rev);
|
||||||
|
|
||||||
|
_.each(window.clientVars.savedRevisions, (revision: PadRevision) => {
|
||||||
|
this.addSavedRevision(revision.revNum, revision);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.onSlider((loc) => {
|
||||||
|
$('#viewlatest').html(
|
||||||
|
`${loc === this.getSliderLength() ? 'Viewing' : 'View'} latest content`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private callSliderCallbacks = (newval: number) => {
|
||||||
|
this.sliderPos = newval;
|
||||||
|
for (let i = 0; i < this.slidercallbacks.length; i++) {
|
||||||
|
this.slidercallbacks[i](newval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSliderElements = () => {
|
||||||
|
for (let i = 0; i < this.savedRevisions.length; i++) {
|
||||||
|
const position = parseInt(this.savedRevisions[i].attr('pos')!);
|
||||||
|
this.savedRevisions[i].css(
|
||||||
|
'left', (position * ($('#ui-slider-bar').width()! - 2) / (this.sliderLength * 1.0)) - 1);
|
||||||
|
}
|
||||||
|
$('#ui-slider-handle').css(
|
||||||
|
'left', this.sliderPos * ($('#ui-slider-bar').width()! - 2) / (this.sliderLength * 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
addSavedRevision = (position: number, info?:any) => {
|
||||||
|
const newSavedRevision = $('<div></div>');
|
||||||
|
newSavedRevision.addClass('star');
|
||||||
|
|
||||||
|
newSavedRevision.attr('pos', position);
|
||||||
|
newSavedRevision.css(
|
||||||
|
'left', (position * ($('#ui-slider-bar').width()! - 2) / (this.sliderLength * 1.0)) - 1);
|
||||||
|
$('#ui-slider-bar').append(newSavedRevision);
|
||||||
|
newSavedRevision.on('mouseup', (evt) => {
|
||||||
|
this.setSliderPosition(position);
|
||||||
|
});
|
||||||
|
this.savedRevisions.push(newSavedRevision);
|
||||||
|
}
|
||||||
|
/* Begin small 'API' */
|
||||||
|
|
||||||
|
onSlider = (callback: (val: number) => void ) => {
|
||||||
|
this.slidercallbacks.push(callback);
|
||||||
|
}
|
||||||
|
getSliderPosition = () => this.sliderPos
|
||||||
|
setSliderPosition = (newpos: number) => {
|
||||||
|
newpos = Number(newpos);
|
||||||
|
if (newpos < 0 || newpos > this.sliderLength) return;
|
||||||
|
if (!newpos) {
|
||||||
|
newpos = 0; // stops it from displaying NaN if newpos isn't set
|
||||||
|
}
|
||||||
|
window.location.hash = `#${newpos}`;
|
||||||
|
$('#ui-slider-handle').css(
|
||||||
|
'left', newpos * ($('#ui-slider-bar').width()! - 2) / (this.sliderLength * 1.0));
|
||||||
|
$('a.tlink').map(function () {
|
||||||
|
$(this).attr('href', $(this).attr('thref')!.replace('%revision%', String(newpos)));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#revision_label').html(html10n.get('timeslider.version', {version: newpos}));
|
||||||
|
|
||||||
|
$('#leftstar, #leftstep').toggleClass('disabled', newpos === 0);
|
||||||
|
$('#rightstar, #rightstep').toggleClass('disabled', newpos === this.sliderLength);
|
||||||
|
|
||||||
|
this.sliderPos = newpos;
|
||||||
|
this.callSliderCallbacks(newpos);
|
||||||
|
}
|
||||||
|
getSliderLength = () => this.sliderLength;
|
||||||
|
setSliderLength = (newlength: number) => {
|
||||||
|
this.sliderLength = newlength;
|
||||||
|
this.updateSliderElements();
|
||||||
|
}
|
||||||
|
// just take over the whole slider screen with a reconnect message
|
||||||
|
showReconnectUI = () => {
|
||||||
|
padmodals.showModal('disconnected');
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthors = (authors: UserInfo[]) => {
|
||||||
|
const authorsList = $('#authorsList');
|
||||||
|
authorsList.empty();
|
||||||
|
let numAnonymous = 0;
|
||||||
|
let numNamed = 0;
|
||||||
|
const colorsAnonymous: number[] = [];
|
||||||
|
_.each(authors, (author: UserInfo) => {
|
||||||
|
if (author) {
|
||||||
|
const authorColor = window.clientVars.colorPalette[author.colorId] || author.colorId;
|
||||||
|
if (author.name) {
|
||||||
|
if (numNamed !== 0) authorsList.append(', ');
|
||||||
|
const textColor =
|
||||||
|
colorutils.textColorFromBackgroundColor(authorColor, window.clientVars.skinName);
|
||||||
|
$('<span />')
|
||||||
|
.text(author.name || 'unnamed')
|
||||||
|
.css('background-color', authorColor)
|
||||||
|
.css('color', textColor)
|
||||||
|
.addClass('author')
|
||||||
|
.appendTo(authorsList);
|
||||||
|
|
||||||
|
numNamed++;
|
||||||
|
} else {
|
||||||
|
numAnonymous++;
|
||||||
|
if (authorColor) colorsAnonymous.push(authorColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (numAnonymous > 0) {
|
||||||
|
const anonymousAuthorString = html10n.get('timeslider.unnamedauthors', {num: numAnonymous});
|
||||||
|
|
||||||
|
if (numNamed !== 0) {
|
||||||
|
authorsList.append(` + ${anonymousAuthorString}`);
|
||||||
|
} else {
|
||||||
|
authorsList.append(anonymousAuthorString);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorsAnonymous.length > 0) {
|
||||||
|
authorsList.append(' (');
|
||||||
|
_.each(colorsAnonymous, (color: number, i: number) => {
|
||||||
|
if (i > 0) authorsList.append(' ');
|
||||||
|
$('<span> </span>')
|
||||||
|
.css('background-color', color)
|
||||||
|
.addClass('author author-anonymous')
|
||||||
|
.appendTo(authorsList);
|
||||||
|
});
|
||||||
|
authorsList.append(')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (authors.length === 0) {
|
||||||
|
authorsList.append(html10n.get('timeslider.toolbar.authorsList'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playButtonUpdater = () => {
|
||||||
|
if (this.sliderPlaying) {
|
||||||
|
if (this.getSliderPosition() + 1 > this.sliderLength) {
|
||||||
|
$('#playpause_button_icon').toggleClass('pause');
|
||||||
|
this.sliderPlaying = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setSliderPosition(this.getSliderPosition() + 1);
|
||||||
|
|
||||||
|
setTimeout(this.playButtonUpdater, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playpause = () => {
|
||||||
|
$('#playpause_button_icon').toggleClass('pause');
|
||||||
|
|
||||||
|
if (!this.sliderPlaying) {
|
||||||
|
if (this.getSliderPosition() === this.sliderLength) this.setSliderPosition(0);
|
||||||
|
this.sliderPlaying = true;
|
||||||
|
this.playButtonUpdater();
|
||||||
|
} else {
|
||||||
|
this.sliderPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isSliderActive= () => this.sliderActive
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default new BroadcastSlider();
|
|
@ -1,280 +0,0 @@
|
||||||
'use strict';
|
|
||||||
/**
|
|
||||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const ChatMessage = require('./ChatMessage');
|
|
||||||
|
|
||||||
import {padUtils as padutils} from "./pad_utils";
|
|
||||||
import padcookie from "./pad_cookie";
|
|
||||||
const Tinycon = require('tinycon/tinycon');
|
|
||||||
const hooks = require('./pluginfw/hooks');
|
|
||||||
const padeditor = require('./pad_editor').padeditor;
|
|
||||||
import html10n from './vendors/html10n';
|
|
||||||
|
|
||||||
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
|
|
||||||
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
||||||
|
|
||||||
|
|
||||||
exports.chat = (() => {
|
|
||||||
let isStuck = false;
|
|
||||||
let userAndChat = false;
|
|
||||||
let chatMentions = 0;
|
|
||||||
return {
|
|
||||||
show() {
|
|
||||||
$('#chaticon').removeClass('visible');
|
|
||||||
$('#chatbox').addClass('visible');
|
|
||||||
this.scrollDown(true);
|
|
||||||
chatMentions = 0;
|
|
||||||
Tinycon.setBubble(0);
|
|
||||||
$('.chat-gritter-msg').each(function () {
|
|
||||||
$.gritter.remove(this.id);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
focus: () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
$('#chatinput').trigger('focus');
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
// Make chat stick to right hand side of screen
|
|
||||||
stickToScreen(fromInitialCall) {
|
|
||||||
if ($('#options-stickychat').prop('checked')) {
|
|
||||||
$('#options-stickychat').prop('checked', false);
|
|
||||||
}
|
|
||||||
if (pad.settings.hideChat) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.show();
|
|
||||||
isStuck = (!isStuck || fromInitialCall);
|
|
||||||
$('#chatbox').hide();
|
|
||||||
// Add timeout to disable the chatbox animations
|
|
||||||
setTimeout(() => {
|
|
||||||
$('#chatbox, .sticky-container').toggleClass('stickyChat', isStuck);
|
|
||||||
$('#chatbox').css('display', 'flex');
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
padcookie.setPref('chatAlwaysVisible', isStuck);
|
|
||||||
$('#options-stickychat').prop('checked', isStuck);
|
|
||||||
},
|
|
||||||
chatAndUsers(fromInitialCall) {
|
|
||||||
const toEnable = $('#options-chatandusers').is(':checked');
|
|
||||||
if (toEnable || !userAndChat || fromInitialCall) {
|
|
||||||
this.stickToScreen(true);
|
|
||||||
$('#options-stickychat').prop('checked', true);
|
|
||||||
$('#options-chatandusers').prop('checked', true);
|
|
||||||
$('#options-stickychat').prop('disabled', true);
|
|
||||||
userAndChat = true;
|
|
||||||
} else {
|
|
||||||
$('#options-stickychat').prop('disabled', false);
|
|
||||||
userAndChat = false;
|
|
||||||
}
|
|
||||||
padcookie.setPref('chatAndUsers', userAndChat);
|
|
||||||
$('#users, .sticky-container')
|
|
||||||
.toggleClass('chatAndUsers popup-show stickyUsers', userAndChat);
|
|
||||||
$('#chatbox').toggleClass('chatAndUsersChat', userAndChat);
|
|
||||||
},
|
|
||||||
hide() {
|
|
||||||
// decide on hide logic based on chat window being maximized or not
|
|
||||||
if ($('#options-stickychat').prop('checked')) {
|
|
||||||
this.stickToScreen();
|
|
||||||
$('#options-stickychat').prop('checked', false);
|
|
||||||
} else {
|
|
||||||
$('#chatcounter').text('0');
|
|
||||||
$('#chaticon').addClass('visible');
|
|
||||||
$('#chatbox').removeClass('visible');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollDown(force) {
|
|
||||||
if ($('#chatbox').hasClass('visible')) {
|
|
||||||
if (force || !this.lastMessage || !this.lastMessage.position() ||
|
|
||||||
this.lastMessage.position().top < ($('#chattext').outerHeight() + 20)) {
|
|
||||||
// if we use a slow animate here we can have a race condition
|
|
||||||
// when a users focus can not be moved away from the last message recieved.
|
|
||||||
$('#chattext').animate(
|
|
||||||
{scrollTop: $('#chattext')[0].scrollHeight},
|
|
||||||
{duration: 400, queue: false});
|
|
||||||
this.lastMessage = $('#chattext > p').eq(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async send() {
|
|
||||||
const text = $('#chatinput').val();
|
|
||||||
if (text.replace(/\s+/, '').length === 0) return;
|
|
||||||
const message = new ChatMessage(text);
|
|
||||||
await hooks.aCallAll('chatSendMessage', Object.freeze({message}));
|
|
||||||
this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', message});
|
|
||||||
$('#chatinput').val('');
|
|
||||||
},
|
|
||||||
async addMessage(msg, increment, isHistoryAdd) {
|
|
||||||
msg = ChatMessage.fromObject(msg);
|
|
||||||
// correct the time
|
|
||||||
msg.time += this._pad.clientTimeOffset;
|
|
||||||
|
|
||||||
if (!msg.authorId) {
|
|
||||||
/*
|
|
||||||
* If, for a bug or a database corruption, the message coming from the
|
|
||||||
* server does not contain the authorId field (see for example #3731),
|
|
||||||
* let's be defensive and replace it with "unknown".
|
|
||||||
*/
|
|
||||||
msg.authorId = 'unknown';
|
|
||||||
console.warn(
|
|
||||||
'The "authorId" field of a chat message coming from the server was not present. ' +
|
|
||||||
'Replacing with "unknown". This may be a bug or a database corruption.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const authorClass = (authorId) => `author-${authorId.replace(/[^a-y0-9]/g, (c) => {
|
|
||||||
if (c === '.') return '-';
|
|
||||||
return `z${c.charCodeAt(0)}z`;
|
|
||||||
})}`;
|
|
||||||
|
|
||||||
// the hook args
|
|
||||||
const ctx = {
|
|
||||||
authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'),
|
|
||||||
author: msg.authorId,
|
|
||||||
text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'),
|
|
||||||
message: msg,
|
|
||||||
rendered: null,
|
|
||||||
sticky: false,
|
|
||||||
timestamp: msg.time,
|
|
||||||
timeStr: (() => {
|
|
||||||
let minutes = `${new Date(msg.time).getMinutes()}`;
|
|
||||||
let hours = `${new Date(msg.time).getHours()}`;
|
|
||||||
if (minutes.length === 1) minutes = `0${minutes}`;
|
|
||||||
if (hours.length === 1) hours = `0${hours}`;
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
})(),
|
|
||||||
duration: 4000,
|
|
||||||
};
|
|
||||||
|
|
||||||
// is the users focus already in the chatbox?
|
|
||||||
const alreadyFocused = $('#chatinput').is(':focus');
|
|
||||||
|
|
||||||
// does the user already have the chatbox open?
|
|
||||||
const chatOpen = $('#chatbox').hasClass('visible');
|
|
||||||
|
|
||||||
// does this message contain this user's name? (is the current user mentioned?)
|
|
||||||
const wasMentioned =
|
|
||||||
msg.authorId !== window.clientVars.userId &&
|
|
||||||
ctx.authorName !== html10n.get('pad.userlist.unnamed') &&
|
|
||||||
normalize(ctx.text).includes(normalize(ctx.authorName));
|
|
||||||
|
|
||||||
// If the user was mentioned, make the message sticky
|
|
||||||
if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) {
|
|
||||||
chatMentions++;
|
|
||||||
Tinycon.setBubble(chatMentions);
|
|
||||||
ctx.sticky = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await hooks.aCallAll('chatNewMessage', ctx);
|
|
||||||
const cls = authorClass(ctx.author);
|
|
||||||
const chatMsg = ctx.rendered != null ? $(ctx.rendered) : $('<p>')
|
|
||||||
.attr('data-authorId', ctx.author)
|
|
||||||
.addClass(cls)
|
|
||||||
.append($('<b>').text(`${ctx.authorName}:`))
|
|
||||||
.append($('<span>')
|
|
||||||
.addClass('time')
|
|
||||||
.addClass(cls)
|
|
||||||
// Hook functions are trusted to not introduce an XSS vulnerability by adding
|
|
||||||
// unescaped user input to ctx.timeStr.
|
|
||||||
.html(ctx.timeStr))
|
|
||||||
.append(' ')
|
|
||||||
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not
|
|
||||||
// introduce an XSS vulnerability by adding unescaped user input.
|
|
||||||
.append($('<div>').html(ctx.text).contents());
|
|
||||||
if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton');
|
|
||||||
else $('#chattext').append(chatMsg);
|
|
||||||
chatMsg.each((i, e) => html10n.translateElement(html10n.translations, e));
|
|
||||||
|
|
||||||
// should we increment the counter??
|
|
||||||
if (increment && !isHistoryAdd) {
|
|
||||||
// Update the counter of unread messages
|
|
||||||
let count = Number($('#chatcounter').text());
|
|
||||||
count++;
|
|
||||||
$('#chatcounter').text(count);
|
|
||||||
|
|
||||||
if (!chatOpen && ctx.duration > 0) {
|
|
||||||
const text = $('<p>')
|
|
||||||
.append($('<span>').addClass('author-name').text(ctx.authorName))
|
|
||||||
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted
|
|
||||||
// to not introduce an XSS vulnerability by adding unescaped user input.
|
|
||||||
.append($('<div>').html(ctx.text).contents());
|
|
||||||
text.each((i, e) => html10n.translateElement(html10n.translations, e));
|
|
||||||
$.gritter.add({
|
|
||||||
text,
|
|
||||||
sticky: ctx.sticky,
|
|
||||||
time: ctx.duration,
|
|
||||||
position: 'bottom',
|
|
||||||
class_name: 'chat-gritter-msg',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isHistoryAdd) this.scrollDown();
|
|
||||||
},
|
|
||||||
init(pad) {
|
|
||||||
this._pad = pad;
|
|
||||||
$('#chatinput').on('keydown', (evt) => {
|
|
||||||
// If the event is Alt C or Escape & we're already in the chat menu
|
|
||||||
// Send the users focus back to the pad
|
|
||||||
if ((evt.altKey === true && evt.which === 67) || evt.which === 27) {
|
|
||||||
// If we're in chat already..
|
|
||||||
$(':focus').trigger('blur'); // required to do not try to remove!
|
|
||||||
padeditor.ace.focus(); // Sends focus back to pad
|
|
||||||
evt.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Clear the chat mentions when the user clicks on the chat input box
|
|
||||||
$('#chatinput').on('click', () => {
|
|
||||||
chatMentions = 0;
|
|
||||||
Tinycon.setBubble(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
$('body:not(#chatinput)').on('keypress', function (evt) {
|
|
||||||
if (evt.altKey && evt.which === 67) {
|
|
||||||
// Alt c focuses on the Chat window
|
|
||||||
$(this).trigger('blur');
|
|
||||||
self.show();
|
|
||||||
$('#chatinput').trigger('focus');
|
|
||||||
evt.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#chatinput').on('keypress', (evt) => {
|
|
||||||
// if the user typed enter, fire the send
|
|
||||||
if (evt.key === 'Enter' && !evt.shiftKey) {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.send();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// initial messages are loaded in pad.js' _afterHandshake
|
|
||||||
|
|
||||||
$('#chatcounter').text(0);
|
|
||||||
$('#chatloadmessagesbutton').on('click', () => {
|
|
||||||
const start = Math.max(this.historyPointer - 20, 0);
|
|
||||||
const end = this.historyPointer;
|
|
||||||
|
|
||||||
if (start === end) return; // nothing to load
|
|
||||||
|
|
||||||
$('#chatloadmessagesbutton').css('display', 'none');
|
|
||||||
$('#chatloadmessagesball').css('display', 'block');
|
|
||||||
|
|
||||||
pad.collabClient.sendMessage({type: 'GET_CHAT_MESSAGES', start, end});
|
|
||||||
this.historyPointer = start;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})();
|
|
293
src/static/js/chat.ts
Normal file
293
src/static/js/chat.ts
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
'use strict';
|
||||||
|
import {Pad} from "./pad";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ChatMessage from './ChatMessage';
|
||||||
|
|
||||||
|
import {padUtils as padutils} from "./pad_utils";
|
||||||
|
import padcookie from "./pad_cookie";
|
||||||
|
import Tinycon from 'tinycon';
|
||||||
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
const padeditor = require('./pad_editor').padeditor;
|
||||||
|
import html10n from './vendors/html10n';
|
||||||
|
|
||||||
|
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
|
||||||
|
const normalize = (s: string) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||||
|
|
||||||
|
|
||||||
|
export class Chat {
|
||||||
|
private isStuck = false;
|
||||||
|
private userAndChat = false;
|
||||||
|
private chatMentions = 0;
|
||||||
|
private pad?: Pad
|
||||||
|
private historyPointer?: number
|
||||||
|
private lastMessage?: JQuery<HTMLElement>
|
||||||
|
|
||||||
|
init(pad: Pad) {
|
||||||
|
this.pad = pad;
|
||||||
|
$('#chatinput').on('keydown', (evt) => {
|
||||||
|
// If the event is Alt C or Escape & we're already in the chat menu
|
||||||
|
// Send the users focus back to the pad
|
||||||
|
if ((evt.altKey === true && evt.which === 67) || evt.which === 27) {
|
||||||
|
// If we're in chat already..
|
||||||
|
$(':focus').trigger('blur'); // required to do not try to remove!
|
||||||
|
padeditor.ace.focus(); // Sends focus back to pad
|
||||||
|
evt.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Clear the chat mentions when the user clicks on the chat input box
|
||||||
|
$('#chatinput').on('click', () => {
|
||||||
|
this.chatMentions = 0;
|
||||||
|
Tinycon.setBubble(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
$('body:not(#chatinput)').on('keypress', function (evt) {
|
||||||
|
if (evt.altKey && evt.which === 67) {
|
||||||
|
// Alt c focuses on the Chat window
|
||||||
|
$(this).trigger('blur');
|
||||||
|
self.show();
|
||||||
|
$('#chatinput').trigger('focus');
|
||||||
|
evt.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#chatinput').on('keypress', (evt) => {
|
||||||
|
// if the user typed enter, fire the send
|
||||||
|
if (evt.key === 'Enter' && !evt.shiftKey) {
|
||||||
|
evt.preventDefault();
|
||||||
|
this.send();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// initial messages are loaded in pad.js' _afterHandshake
|
||||||
|
|
||||||
|
$('#chatcounter').text(0);
|
||||||
|
$('#chatloadmessagesbutton').on('click', () => {
|
||||||
|
const start = Math.max(this.historyPointer! - 20, 0);
|
||||||
|
const end = this.historyPointer!;
|
||||||
|
|
||||||
|
if (start === end) return; // nothing to load
|
||||||
|
|
||||||
|
$('#chatloadmessagesbutton').css('display', 'none');
|
||||||
|
$('#chatloadmessagesball').css('display', 'block');
|
||||||
|
|
||||||
|
pad.collabClient!.sendMessage({type: 'GET_CHAT_MESSAGES', start, end});
|
||||||
|
this.historyPointer = start;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
show() {
|
||||||
|
$('#chaticon').removeClass('visible');
|
||||||
|
$('#chatbox').addClass('visible');
|
||||||
|
this.scrollDown(true);
|
||||||
|
this.chatMentions = 0;
|
||||||
|
Tinycon.setBubble(0);
|
||||||
|
$('.chat-gritter-msg').each(function () {
|
||||||
|
// @ts-ignore
|
||||||
|
$.gritter.remove(this.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
focus = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#chatinput').trigger('focus');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make chat stick to right hand side of screen
|
||||||
|
stickToScreen(fromInitialCall?: boolean) {
|
||||||
|
if ($('#options-stickychat').prop('checked')) {
|
||||||
|
$('#options-stickychat').prop('checked', false);
|
||||||
|
}
|
||||||
|
if (pad.settings.hideChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.show();
|
||||||
|
this.isStuck = (!this.isStuck || fromInitialCall)!;
|
||||||
|
$('#chatbox').hide();
|
||||||
|
// Add timeout to disable the chatbox animations
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#chatbox, .sticky-container').toggleClass('stickyChat', this.isStuck);
|
||||||
|
$('#chatbox').css('display', 'flex');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
padcookie.setPref('chatAlwaysVisible', this.isStuck);
|
||||||
|
$('#options-stickychat').prop('checked', this.isStuck);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatAndUsers(fromInitialCall: boolean) {
|
||||||
|
const toEnable = $('#options-chatandusers').is(':checked');
|
||||||
|
if (toEnable || !this.userAndChat || fromInitialCall) {
|
||||||
|
this.stickToScreen(true);
|
||||||
|
$('#options-stickychat').prop('checked', true);
|
||||||
|
$('#options-chatandusers').prop('checked', true);
|
||||||
|
$('#options-stickychat').prop('disabled', true);
|
||||||
|
this.userAndChat = true;
|
||||||
|
} else {
|
||||||
|
$('#options-stickychat').prop('disabled', false);
|
||||||
|
this.userAndChat = false;
|
||||||
|
}
|
||||||
|
padcookie.setPref('chatAndUsers', this.userAndChat);
|
||||||
|
$('#users, .sticky-container')
|
||||||
|
.toggleClass('chatAndUsers popup-show stickyUsers', this.userAndChat);
|
||||||
|
$('#chatbox').toggleClass('chatAndUsersChat', this.userAndChat);
|
||||||
|
|
||||||
|
}
|
||||||
|
hide() {
|
||||||
|
// decide on hide logic based on chat window being maximized or not
|
||||||
|
if ($('#options-stickychat').prop('checked')) {
|
||||||
|
this.stickToScreen();
|
||||||
|
$('#options-stickychat').prop('checked', false);
|
||||||
|
} else {
|
||||||
|
$('#chatcounter').text('0');
|
||||||
|
$('#chaticon').addClass('visible');
|
||||||
|
$('#chatbox').removeClass('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollDown = (force?:boolean)=> {
|
||||||
|
if ($('#chatbox').hasClass('visible')) {
|
||||||
|
if (force || !this.lastMessage || !this.lastMessage.position() ||
|
||||||
|
this.lastMessage.position().top < ($('#chattext').outerHeight()! + 20)) {
|
||||||
|
// if we use a slow animate here we can have a race condition
|
||||||
|
// when a users focus can not be moved away from the last message recieved.
|
||||||
|
$('#chattext').animate(
|
||||||
|
{scrollTop: $('#chattext')[0].scrollHeight},
|
||||||
|
{duration: 400, queue: false});
|
||||||
|
this.lastMessage = $('#chattext > p').eq(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send = async () => {
|
||||||
|
const text = $('#chatinput').val() as string;
|
||||||
|
if (text.replace(/\s+/, '').length === 0) return;
|
||||||
|
const message = new ChatMessage(text);
|
||||||
|
await hooks.aCallAll('chatSendMessage', Object.freeze({message}));
|
||||||
|
this.pad!.collabClient!.sendMessage({type: 'CHAT_MESSAGE', message});
|
||||||
|
$('#chatinput').val('');
|
||||||
|
}
|
||||||
|
addMessage = async (msg: ChatMessage, increment: boolean, isHistoryAdd: boolean) => {
|
||||||
|
msg = ChatMessage.fromObject(msg);
|
||||||
|
// correct the time
|
||||||
|
msg.time! += this.pad!.clientTimeOffset!;
|
||||||
|
|
||||||
|
if (!msg.authorId) {
|
||||||
|
/*
|
||||||
|
* If, for a bug or a database corruption, the message coming from the
|
||||||
|
* server does not contain the authorId field (see for example #3731),
|
||||||
|
* let's be defensive and replace it with "unknown".
|
||||||
|
*/
|
||||||
|
msg.authorId = 'unknown';
|
||||||
|
console.warn(
|
||||||
|
'The "authorId" field of a chat message coming from the server was not present. ' +
|
||||||
|
'Replacing with "unknown". This may be a bug or a database corruption.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorClass = (authorId: string) => `author-${authorId.replace(/[^a-y0-9]/g, (c) => {
|
||||||
|
if (c === '.') return '-';
|
||||||
|
return `z${c.charCodeAt(0)}z`;
|
||||||
|
})}`;
|
||||||
|
|
||||||
|
// the hook args
|
||||||
|
const ctx = {
|
||||||
|
authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'),
|
||||||
|
author: msg.authorId,
|
||||||
|
text: padutils.escapeHtmlWithClickableLinks(msg.text!, '_blank'),
|
||||||
|
message: msg,
|
||||||
|
rendered: null,
|
||||||
|
sticky: false,
|
||||||
|
timestamp: msg.time,
|
||||||
|
timeStr: (() => {
|
||||||
|
let minutes = `${new Date(msg.time!).getMinutes()}`;
|
||||||
|
let hours = `${new Date(msg.time!).getHours()}`;
|
||||||
|
if (minutes.length === 1) minutes = `0${minutes}`;
|
||||||
|
if (hours.length === 1) hours = `0${hours}`;
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
})(),
|
||||||
|
duration: 4000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// is the users focus already in the chatbox?
|
||||||
|
const alreadyFocused = $('#chatinput').is(':focus');
|
||||||
|
|
||||||
|
// does the user already have the chatbox open?
|
||||||
|
const chatOpen = $('#chatbox').hasClass('visible');
|
||||||
|
|
||||||
|
// does this message contain this user's name? (is the current user mentioned?)
|
||||||
|
const wasMentioned =
|
||||||
|
msg.authorId !== window.clientVars.userId &&
|
||||||
|
ctx.authorName !== html10n.get('pad.userlist.unnamed') &&
|
||||||
|
normalize(ctx.text).includes(normalize(ctx.authorName));
|
||||||
|
|
||||||
|
// If the user was mentioned, make the message sticky
|
||||||
|
if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) {
|
||||||
|
this.chatMentions++;
|
||||||
|
Tinycon.setBubble(this.chatMentions);
|
||||||
|
ctx.sticky = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hooks.aCallAll('chatNewMessage', ctx);
|
||||||
|
const cls = authorClass(ctx.author);
|
||||||
|
const chatMsg = ctx.rendered != null ? $(ctx.rendered) : $('<p>')
|
||||||
|
.attr('data-authorId', ctx.author)
|
||||||
|
.addClass(cls)
|
||||||
|
.append($('<b>').text(`${ctx.authorName}:`))
|
||||||
|
.append($('<span>')
|
||||||
|
.addClass('time')
|
||||||
|
.addClass(cls)
|
||||||
|
// Hook functions are trusted to not introduce an XSS vulnerability by adding
|
||||||
|
// unescaped user input to ctx.timeStr.
|
||||||
|
.html(ctx.timeStr))
|
||||||
|
.append(' ')
|
||||||
|
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not
|
||||||
|
// introduce an XSS vulnerability by adding unescaped user input.
|
||||||
|
.append($('<div>').html(ctx.text).contents());
|
||||||
|
if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton');
|
||||||
|
else $('#chattext').append(chatMsg);
|
||||||
|
chatMsg.each((i, e) => html10n.translateElement(html10n.translations, e));
|
||||||
|
|
||||||
|
// should we increment the counter??
|
||||||
|
if (increment && !isHistoryAdd) {
|
||||||
|
// Update the counter of unread messages
|
||||||
|
let count = Number($('#chatcounter').text());
|
||||||
|
count++;
|
||||||
|
$('#chatcounter').text(count);
|
||||||
|
|
||||||
|
if (!chatOpen && ctx.duration > 0) {
|
||||||
|
const text = $('<p>')
|
||||||
|
.append($('<span>').addClass('author-name').text(ctx.authorName))
|
||||||
|
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted
|
||||||
|
// to not introduce an XSS vulnerability by adding unescaped user input.
|
||||||
|
.append($('<div>').html(ctx.text).contents());
|
||||||
|
text.each((i, e) => html10n.translateElement(html10n.translations, e));
|
||||||
|
// @ts-ignore
|
||||||
|
$.gritter.add({
|
||||||
|
text,
|
||||||
|
sticky: ctx.sticky,
|
||||||
|
time: ctx.duration,
|
||||||
|
position: 'bottom',
|
||||||
|
class_name: 'chat-gritter-msg',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isHistoryAdd) this.scrollDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const chat = new Chat()
|
|
@ -367,18 +367,17 @@ export class CollabClient {
|
||||||
|
|
||||||
tellAceAuthorInfo = (userId: string, colorId: number|object, inactive?: boolean) => {
|
tellAceAuthorInfo = (userId: string, colorId: number|object, inactive?: boolean) => {
|
||||||
if (typeof colorId === 'number') {
|
if (typeof colorId === 'number') {
|
||||||
|
// @ts-ignore
|
||||||
colorId = window.clientVars.colorPalette[colorId];
|
colorId = window.clientVars.colorPalette[colorId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssColor = colorId;
|
const cssColor = colorId;
|
||||||
if (inactive) {
|
if (inactive) {
|
||||||
// @ts-ignore
|
|
||||||
this.editor.setAuthorInfo(userId, {
|
this.editor.setAuthorInfo(userId, {
|
||||||
bgcolor: cssColor,
|
bgcolor: cssColor,
|
||||||
fade: 0.5,
|
fade: 0.5,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
|
||||||
this.editor.setAuthorInfo(userId, {
|
this.editor.setAuthorInfo(userId, {
|
||||||
bgcolor: cssColor,
|
bgcolor: cssColor,
|
||||||
});
|
});
|
||||||
|
|
|
@ -142,11 +142,11 @@ class ContentCollector {
|
||||||
private selEnd = [-1, -1];
|
private selEnd = [-1, -1];
|
||||||
private collectStyles: boolean;
|
private collectStyles: boolean;
|
||||||
private apool: AttributePool;
|
private apool: AttributePool;
|
||||||
private className2Author: (c: string) => string;
|
private className2Author?: (c: string) => string;
|
||||||
private breakLine?: boolean
|
private breakLine?: boolean
|
||||||
private abrowser?: null|BrowserDetector;
|
private abrowser?: null|BrowserDetector;
|
||||||
|
|
||||||
constructor(collectStyles: boolean, abrowser: null, apool: AttributePool, className2Author: (c: string)=>string) {
|
constructor(collectStyles: boolean, abrowser: null, apool: AttributePool, className2Author?: (c: string)=>string) {
|
||||||
this.blockElems = {
|
this.blockElems = {
|
||||||
div: 1,
|
div: 1,
|
||||||
p: 1,
|
p: 1,
|
||||||
|
@ -352,7 +352,7 @@ class ContentCollector {
|
||||||
this.incrementAttrib(state, na);
|
this.incrementAttrib(state, na);
|
||||||
}
|
}
|
||||||
|
|
||||||
collectContent = (node: ContentElem, state: ContentCollectorState)=> {
|
collectContent = (node: ContentElem, state?: ContentCollectorState)=> {
|
||||||
let unsupportedElements = null;
|
let unsupportedElements = null;
|
||||||
if (!state) {
|
if (!state) {
|
||||||
state = {
|
state = {
|
||||||
|
@ -764,3 +764,5 @@ class ContentCollector {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ContentCollector
|
||||||
|
|
|
@ -410,7 +410,7 @@ export class Pad {
|
||||||
collabDiagnosticInfo?: any
|
collabDiagnosticInfo?: any
|
||||||
};
|
};
|
||||||
private initTime: number;
|
private initTime: number;
|
||||||
private clientTimeOffset: null | number;
|
public clientTimeOffset: null | number;
|
||||||
_messageQ: MessageQueue;
|
_messageQ: MessageQueue;
|
||||||
private padOptions: PadOption;
|
private padOptions: PadOption;
|
||||||
settings: PadSettings = {
|
settings: PadSettings = {
|
||||||
|
|
|
@ -62,7 +62,7 @@ class PadCookie {
|
||||||
return this.readPrefs_()[prefName];
|
return this.readPrefs_()[prefName];
|
||||||
}
|
}
|
||||||
|
|
||||||
setPref(prefName: string, value: string) {
|
setPref(prefName: string, value: any) {
|
||||||
const prefs = this.readPrefs_();
|
const prefs = this.readPrefs_();
|
||||||
prefs[prefName] = value;
|
prefs[prefName] = value;
|
||||||
this.writePrefs_(prefs);
|
this.writePrefs_(prefs);
|
||||||
|
|
|
@ -629,7 +629,7 @@ class PadUserList {
|
||||||
|
|
||||||
if (!colorPickerSetup) {
|
if (!colorPickerSetup) {
|
||||||
const colorsList = $('#colorpickerswatches');
|
const colorsList = $('#colorpickerswatches');
|
||||||
for (let i = 0; i < palette.length; i++) {
|
for (let i = 0; i < palette.length!; i++) {
|
||||||
const li = $('<li>', {
|
const li = $('<li>', {
|
||||||
style: `background: ${palette[i]};`,
|
style: `background: ${palette[i]};`,
|
||||||
});
|
});
|
||||||
|
|
6
src/static/js/pluginfw/IPluginInfoExtended.ts
Normal file
6
src/static/js/pluginfw/IPluginInfoExtended.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import {IPluginInfo} from "live-plugin-manager";
|
||||||
|
|
||||||
|
export type IPluginInfoExtended = IPluginInfo & {
|
||||||
|
path?: string
|
||||||
|
realPath? : string
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import {dependencies, name} from '../../../package.json'
|
||||||
import {pathToFileURL} from 'node:url';
|
import {pathToFileURL} from 'node:url';
|
||||||
const settings = require('../../../node/utils/Settings');
|
const settings = require('../../../node/utils/Settings');
|
||||||
import {readFileSync} from "fs";
|
import {readFileSync} from "fs";
|
||||||
|
import {IPluginInfoExtended} from "./IPluginInfoExtended";
|
||||||
|
|
||||||
export class LinkInstaller {
|
export class LinkInstaller {
|
||||||
private livePluginManager: PluginManager;
|
private livePluginManager: PluginManager;
|
||||||
|
@ -46,7 +47,7 @@ export class LinkInstaller {
|
||||||
|
|
||||||
public async installPlugin(pluginName: string, version?: string) {
|
public async installPlugin(pluginName: string, version?: string) {
|
||||||
if (version) {
|
if (version) {
|
||||||
const installedPlugin = await this.livePluginManager.install(pluginName, version);
|
const installedPlugin = await this.livePluginManager.install(pluginName, version) as IPluginInfoExtended;
|
||||||
this.linkDependency(pluginName)
|
this.linkDependency(pluginName)
|
||||||
await this.checkLinkedDependencies(installedPlugin)
|
await this.checkLinkedDependencies(installedPlugin)
|
||||||
} else {
|
} else {
|
||||||
|
@ -57,7 +58,7 @@ export class LinkInstaller {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listPlugins() {
|
public async listPlugins() {
|
||||||
const plugins = this.livePluginManager.list()
|
const plugins = this.livePluginManager.list() as IPluginInfoExtended[]
|
||||||
if (plugins && plugins.length > 0 && this.loadedPlugins.length == 0) {
|
if (plugins && plugins.length > 0 && this.loadedPlugins.length == 0) {
|
||||||
this.loadedPlugins = plugins
|
this.loadedPlugins = plugins
|
||||||
// Check already installed plugins
|
// Check already installed plugins
|
||||||
|
@ -224,7 +225,6 @@ export class LinkInstaller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async checkLinkedDependencies(plugin: IPluginInfo) {
|
private async checkLinkedDependencies(plugin: IPluginInfo) {
|
||||||
// Check if the plugin really exists at source
|
// Check if the plugin really exists at source
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const pluginUtils = require('./shared');
|
|
||||||
const defs = require('./plugin_defs');
|
|
||||||
|
|
||||||
exports.baseURL = '';
|
|
||||||
|
|
||||||
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
|
|
||||||
|
|
||||||
exports.update = async (modules) => {
|
|
||||||
const data = await jQuery.getJSON(
|
|
||||||
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`);
|
|
||||||
defs.plugins = data.plugins;
|
|
||||||
defs.parts = data.parts;
|
|
||||||
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks', null, modules);
|
|
||||||
defs.loaded = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const adoptPluginsFromAncestorsOf = (frame) => {
|
|
||||||
// Bind plugins with parent;
|
|
||||||
let parentRequire = null;
|
|
||||||
try {
|
|
||||||
while ((frame = frame.parent)) {
|
|
||||||
if (typeof (frame.require) !== 'undefined') {
|
|
||||||
parentRequire = frame.require;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Silence (this can only be a XDomain issue).
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parentRequire) throw new Error('Parent plugins could not be found.');
|
|
||||||
|
|
||||||
const ancestorPluginDefs = parentRequire('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
|
|
||||||
defs.hooks = ancestorPluginDefs.hooks;
|
|
||||||
defs.loaded = ancestorPluginDefs.loaded;
|
|
||||||
defs.parts = ancestorPluginDefs.parts;
|
|
||||||
defs.plugins = ancestorPluginDefs.plugins;
|
|
||||||
const ancestorPlugins = parentRequire('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
|
||||||
exports.baseURL = ancestorPlugins.baseURL;
|
|
||||||
exports.ensure = ancestorPlugins.ensure;
|
|
||||||
exports.update = ancestorPlugins.update;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.adoptPluginsFromAncestorsOf = adoptPluginsFromAncestorsOf;
|
|
18
src/static/js/pluginfw/client_plugins.ts
Normal file
18
src/static/js/pluginfw/client_plugins.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {pluginDefs, PluginResp} from './plugin_defs'
|
||||||
|
import {extractHooks} from "./shared";
|
||||||
|
|
||||||
|
export const baseURL = '';
|
||||||
|
|
||||||
|
export const ensure = (cb: Function) => !pluginDefs.isLoaded() ? update(cb) : cb();
|
||||||
|
|
||||||
|
export const update = async (modules: Function) => {
|
||||||
|
const data = await jQuery.getJSON(
|
||||||
|
`${baseURL}pluginfw/plugin-definitions.json?v=${window.clientVars.randomVersionString}`) as PluginResp;
|
||||||
|
pluginDefs.setParts(data.parts)
|
||||||
|
pluginDefs.setPlugins(data.plugins)
|
||||||
|
const hooks = extractHooks(pluginDefs.getParts(), 'client_hooks', null, modules)!
|
||||||
|
pluginDefs.setHooks(hooks)
|
||||||
|
pluginDefs.setLoaded(true)
|
||||||
|
};
|
|
@ -1,6 +1,9 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const pluginDefs = require('./plugin_defs');
|
import {PluginHook} from "./plugin_defs";
|
||||||
|
import {MapArrayType} from "../../../node/types/MapType";
|
||||||
|
|
||||||
|
import {pluginDefs} from './plugin_defs';
|
||||||
|
|
||||||
// Maps the name of a server-side hook to a string explaining the deprecation
|
// Maps the name of a server-side hook to a string explaining the deprecation
|
||||||
// (e.g., 'use the foo hook instead').
|
// (e.g., 'use the foo hook instead').
|
||||||
|
@ -10,12 +13,12 @@ const pluginDefs = require('./plugin_defs');
|
||||||
// const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
// const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
|
||||||
// hooks.deprecationNotices.fooBar = 'use the newSpiffy hook instead';
|
// hooks.deprecationNotices.fooBar = 'use the newSpiffy hook instead';
|
||||||
//
|
//
|
||||||
exports.deprecationNotices = {};
|
export const deprecationNotices: MapArrayType<string> = {};
|
||||||
|
|
||||||
const deprecationWarned = {};
|
const deprecationWarned: MapArrayType<boolean> = {};
|
||||||
|
|
||||||
const checkDeprecation = (hook) => {
|
const checkDeprecation = (hook: PluginHook) => {
|
||||||
const notice = exports.deprecationNotices[hook.hook_name];
|
const notice = deprecationNotices[hook.hook_name];
|
||||||
if (notice == null) return;
|
if (notice == null) return;
|
||||||
if (deprecationWarned[hook.hook_fn_name]) return;
|
if (deprecationWarned[hook.hook_fn_name]) return;
|
||||||
console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` +
|
console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` +
|
||||||
|
@ -26,7 +29,7 @@ const checkDeprecation = (hook) => {
|
||||||
// Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a
|
// Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a
|
||||||
// Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a
|
// Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a
|
||||||
// function that returns undefined).
|
// function that returns undefined).
|
||||||
const attachCallback = (p, cb) => p.then(
|
const attachCallback = (p: Promise<any>, cb: Function) => p.then(
|
||||||
(val) => cb(null, val),
|
(val) => cb(null, val),
|
||||||
// Callbacks often only check the truthiness, not the nullness, of the first parameter. To avoid
|
// Callbacks often only check the truthiness, not the nullness, of the first parameter. To avoid
|
||||||
// problems, always pass a truthy value as the first argument if the Promise is rejected.
|
// problems, always pass a truthy value as the first argument if the Promise is rejected.
|
||||||
|
@ -35,7 +38,7 @@ const attachCallback = (p, cb) => p.then(
|
||||||
// Normalizes the value provided by hook functions so that it is always an array. `undefined` (but
|
// Normalizes the value provided by hook functions so that it is always an array. `undefined` (but
|
||||||
// not `null`!) becomes an empty array, array values are returned unmodified, and non-array values
|
// not `null`!) becomes an empty array, array values are returned unmodified, and non-array values
|
||||||
// are wrapped in an array (so `null` becomes `[null]`).
|
// are wrapped in an array (so `null` becomes `[null]`).
|
||||||
const normalizeValue = (val) => {
|
const normalizeValue = (val: string|number|string[]|number[]) => {
|
||||||
// `undefined` is treated the same as `[]`. IMPORTANT: `null` is *not* treated the same as `[]`
|
// `undefined` is treated the same as `[]`. IMPORTANT: `null` is *not* treated the same as `[]`
|
||||||
// because some hooks use `null` as a special value.
|
// because some hooks use `null` as a special value.
|
||||||
if (val === undefined) return [];
|
if (val === undefined) return [];
|
||||||
|
@ -44,7 +47,7 @@ const normalizeValue = (val) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Flattens the array one level.
|
// Flattens the array one level.
|
||||||
const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []);
|
const flatten1 = (array: any[]) => array.reduce<any[]>((a, b) => a.concat(b), []);
|
||||||
|
|
||||||
// Calls the hook function synchronously and returns the value provided by the hook function (via
|
// Calls the hook function synchronously and returns the value provided by the hook function (via
|
||||||
// callback or return value).
|
// callback or return value).
|
||||||
|
@ -75,16 +78,25 @@ const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []);
|
||||||
// See the tests in src/tests/backend/specs/hooks.js for examples of supported and prohibited
|
// See the tests in src/tests/backend/specs/hooks.js for examples of supported and prohibited
|
||||||
// behaviors.
|
// behaviors.
|
||||||
//
|
//
|
||||||
const callHookFnSync = (hook, context) => {
|
type OutCome = {
|
||||||
|
state: string,
|
||||||
|
err: Error|null,
|
||||||
|
val: any,
|
||||||
|
how: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const callHookFnSync = (hook: PluginHook, context: {
|
||||||
|
|
||||||
|
}) => {
|
||||||
checkDeprecation(hook);
|
checkDeprecation(hook);
|
||||||
|
|
||||||
// This var is used to keep track of whether the hook function already settled.
|
// This var is used to keep track of whether the hook function already settled.
|
||||||
let outcome;
|
let outcome: OutCome;
|
||||||
|
|
||||||
// This is used to prevent recursion.
|
// This is used to prevent recursion.
|
||||||
let doubleSettleErr;
|
let doubleSettleErr;
|
||||||
|
|
||||||
const settle = (err, val, how) => {
|
const settle = (err: Error|null, val: any|null, how: string) => {
|
||||||
doubleSettleErr = null;
|
doubleSettleErr = null;
|
||||||
const state = err == null ? 'resolved' : 'rejected';
|
const state = err == null ? 'resolved' : 'rejected';
|
||||||
if (outcome != null) {
|
if (outcome != null) {
|
||||||
|
@ -113,14 +125,14 @@ const callHookFnSync = (hook, context) => {
|
||||||
|
|
||||||
// IMPORTANT: This callback must return `undefined` so that a hook function can safely do
|
// IMPORTANT: This callback must return `undefined` so that a hook function can safely do
|
||||||
// `return callback(value);` for backwards compatibility.
|
// `return callback(value);` for backwards compatibility.
|
||||||
const callback = (ret) => {
|
const callback = (ret: string) => {
|
||||||
settle(null, ret, 'callback');
|
settle(null, ret, 'callback');
|
||||||
};
|
};
|
||||||
|
|
||||||
let val;
|
let val;
|
||||||
try {
|
try {
|
||||||
val = hook.hook_fn(hook.hook_name, context, callback);
|
val = hook.hook_fn(hook.hook_name, context, callback);
|
||||||
} catch (err) {
|
} catch (err:any) {
|
||||||
if (err === doubleSettleErr) throw err; // Avoid recursion.
|
if (err === doubleSettleErr) throw err; // Avoid recursion.
|
||||||
try {
|
try {
|
||||||
settle(err, null, 'thrown exception');
|
settle(err, null, 'thrown exception');
|
||||||
|
@ -137,6 +149,7 @@ const callHookFnSync = (hook, context) => {
|
||||||
// IMPORTANT: This MUST check for undefined -- not nullish -- because some hooks intentionally use
|
// IMPORTANT: This MUST check for undefined -- not nullish -- because some hooks intentionally use
|
||||||
// null as a special value.
|
// null as a special value.
|
||||||
if (val === undefined) {
|
if (val === undefined) {
|
||||||
|
// @ts-ignore
|
||||||
if (outcome != null) return outcome.val; // Already settled via callback.
|
if (outcome != null) return outcome.val; // Already settled via callback.
|
||||||
if (hook.hook_fn.length >= 3) {
|
if (hook.hook_fn.length >= 3) {
|
||||||
console.error(`UNSETTLED FUNCTION BUG IN HOOK FUNCTION (plugin: ${hook.part.plugin}, ` +
|
console.error(`UNSETTLED FUNCTION BUG IN HOOK FUNCTION (plugin: ${hook.part.plugin}, ` +
|
||||||
|
@ -173,7 +186,7 @@ const callHookFnSync = (hook, context) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
settle(null, val, 'returned value');
|
settle(null, val, 'returned value');
|
||||||
return outcome.val;
|
return outcome!.val;
|
||||||
};
|
};
|
||||||
|
|
||||||
// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead.
|
// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead.
|
||||||
|
@ -189,9 +202,11 @@ const callHookFnSync = (hook, context) => {
|
||||||
// 1. Collect all values returned by the hook functions into an array.
|
// 1. Collect all values returned by the hook functions into an array.
|
||||||
// 2. Convert each `undefined` entry into `[]`.
|
// 2. Convert each `undefined` entry into `[]`.
|
||||||
// 3. Flatten one level.
|
// 3. Flatten one level.
|
||||||
exports.callAll = (hookName, context) => {
|
export const callAll = (hookName: string, context?: {
|
||||||
|
|
||||||
|
}) => {
|
||||||
if (context == null) context = {};
|
if (context == null) context = {};
|
||||||
const hooks = pluginDefs.hooks[hookName] || [];
|
const hooks = pluginDefs.getHooks()[hookName] || [];
|
||||||
return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context))));
|
return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context))));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -230,13 +245,13 @@ exports.callAll = (hookName, context) => {
|
||||||
// See the tests in src/tests/backend/specs/hooks.js for examples of supported and prohibited
|
// See the tests in src/tests/backend/specs/hooks.js for examples of supported and prohibited
|
||||||
// behaviors.
|
// behaviors.
|
||||||
//
|
//
|
||||||
const callHookFnAsync = async (hook, context) => {
|
const callHookFnAsync = async (hook: PluginHook, context: {}) => {
|
||||||
checkDeprecation(hook);
|
checkDeprecation(hook);
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
// This var is used to keep track of whether the hook function already settled.
|
// This var is used to keep track of whether the hook function already settled.
|
||||||
let outcome;
|
let outcome: OutCome;
|
||||||
|
|
||||||
const settle = (err, val, how) => {
|
const settle = (err: Error|null, val: string|null, how: string) => {
|
||||||
const state = err == null ? 'resolved' : 'rejected';
|
const state = err == null ? 'resolved' : 'rejected';
|
||||||
if (outcome != null) {
|
if (outcome != null) {
|
||||||
// It was already settled, which indicates a bug.
|
// It was already settled, which indicates a bug.
|
||||||
|
@ -258,7 +273,7 @@ const callHookFnAsync = async (hook, context) => {
|
||||||
|
|
||||||
// IMPORTANT: This callback must return `undefined` so that a hook function can safely do
|
// IMPORTANT: This callback must return `undefined` so that a hook function can safely do
|
||||||
// `return callback(value);` for backwards compatibility.
|
// `return callback(value);` for backwards compatibility.
|
||||||
const callback = (ret) => {
|
const callback = (ret:any) => {
|
||||||
// Wrap ret in a Promise so that a hook function can do `callback(asyncFunction());`. Note: If
|
// Wrap ret in a Promise so that a hook function can do `callback(asyncFunction());`. Note: If
|
||||||
// ret is a Promise (or other thenable), Promise.resolve() will flatten it into this new
|
// ret is a Promise (or other thenable), Promise.resolve() will flatten it into this new
|
||||||
// Promise.
|
// Promise.
|
||||||
|
@ -270,7 +285,7 @@ const callHookFnAsync = async (hook, context) => {
|
||||||
let ret;
|
let ret;
|
||||||
try {
|
try {
|
||||||
ret = hook.hook_fn(hook.hook_name, context, callback);
|
ret = hook.hook_fn(hook.hook_name, context, callback);
|
||||||
} catch (err) {
|
} catch (err:any) {
|
||||||
try {
|
try {
|
||||||
settle(err, null, 'thrown exception');
|
settle(err, null, 'thrown exception');
|
||||||
} catch (doubleSettleErr) {
|
} catch (doubleSettleErr) {
|
||||||
|
@ -342,24 +357,26 @@ const callHookFnAsync = async (hook, context) => {
|
||||||
// 2. Convert each `undefined` entry into `[]`.
|
// 2. Convert each `undefined` entry into `[]`.
|
||||||
// 3. Flatten one level.
|
// 3. Flatten one level.
|
||||||
// If cb is non-null, this function resolves to the value returned by cb.
|
// If cb is non-null, this function resolves to the value returned by cb.
|
||||||
exports.aCallAll = async (hookName, context, cb = null) => {
|
export const aCallAll = async (hookName: string, context?: {}, cb = null): Promise<any[]> => {
|
||||||
if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb);
|
if (cb != null) return await attachCallback(aCallAll(hookName, context), cb);
|
||||||
if (context == null) context = {};
|
if (context == null) context = {};
|
||||||
const hooks = pluginDefs.hooks[hookName] || [];
|
const hooks = pluginDefs.getHooks()[hookName] || [];
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context))));
|
hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context) as any)));
|
||||||
return flatten1(results);
|
return flatten1(results);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
|
// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
|
||||||
// Only use this function if the hook functions must be called one at a time, otherwise use
|
// Only use this function if the hook functions must be called one at a time, otherwise use
|
||||||
// `aCallAll()`.
|
// `aCallAll()`.
|
||||||
exports.callAllSerial = async (hookName, context) => {
|
export const callAllSerial = async (hookName: string, context: {
|
||||||
|
|
||||||
|
}) => {
|
||||||
if (context == null) context = {};
|
if (context == null) context = {};
|
||||||
const hooks = pluginDefs.hooks[hookName] || [];
|
const hooks = pluginDefs.getHooks()[hookName] || [];
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const hook of hooks) {
|
for (const hook of hooks) {
|
||||||
results.push(normalizeValue(await callHookFnAsync(hook, context)));
|
results.push(normalizeValue(await callHookFnAsync(hook, context) as any));
|
||||||
}
|
}
|
||||||
return flatten1(results);
|
return flatten1(results);
|
||||||
};
|
};
|
||||||
|
@ -367,10 +384,10 @@ exports.callAllSerial = async (hookName, context) => {
|
||||||
// DEPRECATED: Use `aCallFirst()` instead.
|
// DEPRECATED: Use `aCallFirst()` instead.
|
||||||
//
|
//
|
||||||
// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.
|
// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.
|
||||||
exports.callFirst = (hookName, context) => {
|
export const callFirst = (hookName: string, context: {}) => {
|
||||||
if (context == null) context = {};
|
if (context == null) context = {};
|
||||||
const predicate = (val) => val.length;
|
const predicate = (val: (string|number)[]) => val.length;
|
||||||
const hooks = pluginDefs.hooks[hookName] || [];
|
const hooks = pluginDefs.getHooks()[hookName] || [];
|
||||||
for (const hook of hooks) {
|
for (const hook of hooks) {
|
||||||
const val = normalizeValue(callHookFnSync(hook, context));
|
const val = normalizeValue(callHookFnSync(hook, context));
|
||||||
if (predicate(val)) return val;
|
if (predicate(val)) return val;
|
||||||
|
@ -399,22 +416,24 @@ exports.callFirst = (hookName, context) => {
|
||||||
// If cb is nullish, resolves to an array that is either the normalized value that satisfied the
|
// If cb is nullish, resolves to an array that is either the normalized value that satisfied the
|
||||||
// predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the
|
// predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the
|
||||||
// value returned from cb().
|
// value returned from cb().
|
||||||
exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => {
|
export const aCallFirst = async (hookName: string, context:{}, cb: Function|null = null, predicate: ((val: any[])=>number)|null = null):Promise<any> => {
|
||||||
if (cb != null) {
|
if (cb != null) {
|
||||||
return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb);
|
return await attachCallback(aCallFirst(hookName, context, null, predicate), cb);
|
||||||
}
|
}
|
||||||
if (context == null) context = {};
|
if (context == null) context = {};
|
||||||
if (predicate == null) predicate = (val) => val.length;
|
if (predicate == null) {
|
||||||
const hooks = pluginDefs.hooks[hookName] || [];
|
predicate = (val) => val.length;
|
||||||
|
}
|
||||||
|
const hooks = pluginDefs.getHooks()[hookName] || [];
|
||||||
for (const hook of hooks) {
|
for (const hook of hooks) {
|
||||||
const val = normalizeValue(await callHookFnAsync(hook, context));
|
const val = normalizeValue(await callHookFnAsync(hook, context) as any);
|
||||||
if (predicate(val)) return val;
|
if (predicate(val)) return val;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.exportedForTestingOnly = {
|
export const exportedForTestingOnly = {
|
||||||
callHookFnAsync,
|
callHookFnAsync,
|
||||||
callHookFnSync,
|
callHookFnSync,
|
||||||
deprecationWarned,
|
deprecationWarned,
|
||||||
};
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// This module contains processed plugin definitions. The data structures in this file are set by
|
|
||||||
// plugins.js (server) or client_plugins.js (client).
|
|
||||||
|
|
||||||
// Maps a hook name to a list of hook objects. Each hook object has the following properties:
|
|
||||||
// * hook_name: Name of the hook.
|
|
||||||
// * hook_fn: Plugin-supplied hook function.
|
|
||||||
// * hook_fn_name: Name of the hook function, with the form <filename>:<functionName>.
|
|
||||||
// * part: The ep.json part object that declared the hook. See exports.plugins.
|
|
||||||
exports.hooks = {};
|
|
||||||
|
|
||||||
// Whether the plugins have been loaded.
|
|
||||||
exports.loaded = false;
|
|
||||||
|
|
||||||
// Topologically sorted list of parts from exports.plugins.
|
|
||||||
exports.parts = [];
|
|
||||||
|
|
||||||
// Maps the name of a plugin to the plugin's definition provided in ep.json. The ep.json object is
|
|
||||||
// augmented with additional metadata:
|
|
||||||
// * parts: Each part from the ep.json object is augmented with the following properties:
|
|
||||||
// - plugin: The name of the plugin.
|
|
||||||
// - full_name: Equal to <plugin>/<name>.
|
|
||||||
// * package (server-side only): Object containing details about the plugin package:
|
|
||||||
// - version
|
|
||||||
// - path
|
|
||||||
// - realPath
|
|
||||||
exports.plugins = {};
|
|
98
src/static/js/pluginfw/plugin_defs.ts
Normal file
98
src/static/js/pluginfw/plugin_defs.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// This module contains processed plugin definitions. The data structures in this file are set by
|
||||||
|
// plugins.js (server) or client_plugins.js (client).
|
||||||
|
|
||||||
|
// Maps a hook name to a list of hook objects. Each hook object has the following properties:
|
||||||
|
// * hook_name: Name of the hook.
|
||||||
|
// * hook_fn: Plugin-supplied hook function.
|
||||||
|
// * hook_fn_name: Name of the hook function, with the form <filename>:<functionName>.
|
||||||
|
// * part: The ep.json part object that declared the hook. See exports.plugins.
|
||||||
|
|
||||||
|
import {MapArrayType} from "../../../node/types/MapType";
|
||||||
|
|
||||||
|
class PluginDef {
|
||||||
|
private hooks: MapArrayType<PluginHook[]>
|
||||||
|
private loaded: boolean
|
||||||
|
private parts: MappedPlugin[]
|
||||||
|
private plugins: MapArrayType<any>
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.hooks = {}
|
||||||
|
this.loaded = false
|
||||||
|
// Topologically sorted list of parts from exports.plugins.
|
||||||
|
this.parts = []
|
||||||
|
// Maps the name of a plugin to the plugin's definition provided in ep.json. The ep.json object is
|
||||||
|
// augmented with additional metadata:
|
||||||
|
// * parts: Each part from the ep.json object is augmented with the following properties:
|
||||||
|
// - plugin: The name of the plugin.
|
||||||
|
// - full_name: Equal to <plugin>/<name>.
|
||||||
|
// * package (server-side only): Object containing details about the plugin package:
|
||||||
|
// - version
|
||||||
|
// - path
|
||||||
|
// - realPath
|
||||||
|
this.plugins = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
getHooks() {
|
||||||
|
return this.hooks
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoaded() {
|
||||||
|
return this.loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
getParts() {
|
||||||
|
return this.parts
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugins() {
|
||||||
|
return this.plugins
|
||||||
|
}
|
||||||
|
setHooks(hooks: MapArrayType<PluginHook[]>) {
|
||||||
|
this.hooks = hooks
|
||||||
|
}
|
||||||
|
setLoaded(loaded: boolean) {
|
||||||
|
this.loaded = loaded
|
||||||
|
}
|
||||||
|
setParts(parts: any[]) {
|
||||||
|
this.parts = parts
|
||||||
|
}
|
||||||
|
setPlugins(plugins: MapArrayType<any>) {
|
||||||
|
this.plugins = plugins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginResp = {
|
||||||
|
plugins: MapArrayType<Plugin>
|
||||||
|
parts: MappedPlugin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type MappedPlugin = Part& {
|
||||||
|
plugin: string
|
||||||
|
full_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Plugin = {
|
||||||
|
parts: Part[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Part = {
|
||||||
|
name: string,
|
||||||
|
client_hooks: MapArrayType<string>,
|
||||||
|
hooks: MapArrayType<string>
|
||||||
|
pre?: string[]
|
||||||
|
post?: string[]
|
||||||
|
plugin?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginHook = {
|
||||||
|
hook_name: string
|
||||||
|
hook_fn: Function
|
||||||
|
hook_fn_name: string
|
||||||
|
part: Part
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pluginDefs = new PluginDef()
|
|
@ -1,13 +1,16 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import {Part} from "./plugin_defs";
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const hooks = require('./hooks');
|
const hooks = require('./hooks');
|
||||||
const log4js = require('log4js');
|
import log4js from 'log4js';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const runCmd = require('../../../node/utils/run_cmd');
|
const runCmd = require('../../../node/utils/run_cmd');
|
||||||
const tsort = require('./tsort');
|
import {TSort} from './tsort';
|
||||||
const pluginUtils = require('./shared');
|
const pluginUtils = require('./shared');
|
||||||
const defs = require('./plugin_defs');
|
import {pluginDefs} from './plugin_defs';
|
||||||
|
import {IPluginInfo} from "live-plugin-manager";
|
||||||
const settings = require('../../../node/utils/Settings');
|
const settings = require('../../../node/utils/Settings');
|
||||||
|
|
||||||
const logger = log4js.getLogger('plugins');
|
const logger = log4js.getLogger('plugins');
|
||||||
|
@ -18,53 +21,54 @@ const logger = log4js.getLogger('plugins');
|
||||||
const version = await runCmd(['pnpm', '--version'], {stdio: [null, 'string']});
|
const version = await runCmd(['pnpm', '--version'], {stdio: [null, 'string']});
|
||||||
logger.info(`pnpm --version: ${version}`);
|
logger.info(`pnpm --version: ${version}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// @ts-ignore
|
||||||
logger.error(`Failed to get pnpm version: ${err.stack || err}`);
|
logger.error(`Failed to get pnpm version: ${err.stack || err}`);
|
||||||
// This isn't a fatal error so don't re-throw.
|
// This isn't a fatal error so don't re-throw.
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.prefix = 'ep_';
|
export const prefix = 'ep_';
|
||||||
|
|
||||||
exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
|
export const formatPlugins = () => Object.keys(pluginDefs.getPlugins()).join(', ');
|
||||||
|
|
||||||
exports.getPlugins = () => Object.keys(defs.plugins);
|
export const getPlugins = () => Object.keys(pluginDefs.getPlugins());
|
||||||
|
|
||||||
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
|
export const formatParts = () => pluginDefs.getParts().map((part) => part.full_name).join('\n');
|
||||||
|
|
||||||
exports.getParts = () => defs.parts.map((part) => part.full_name);
|
export const getParts = () => pluginDefs.getParts().map((part) => part.full_name);
|
||||||
|
|
||||||
const sortHooks = (hookSetName, hooks) => {
|
const sortHooks = (hookSetName: string, hooks: Map<string, Map<string,Map<string, string>>>) => {
|
||||||
for (const [pluginName, def] of Object.entries(defs.plugins)) {
|
for (const [pluginName, def] of Object.entries(pluginDefs.getPlugins())) {
|
||||||
for (const part of def.parts) {
|
for (const part of def.parts) {
|
||||||
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
|
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||||
let hookEntry = hooks.get(hookName);
|
let hookEntry = hooks.get(hookName);
|
||||||
if (!hookEntry) {
|
if (!hookEntry) {
|
||||||
hookEntry = new Map();
|
hookEntry = new Map<string,Map<string, string>>();
|
||||||
hooks.set(hookName, hookEntry);
|
hooks.set(hookName, hookEntry);
|
||||||
}
|
}
|
||||||
let pluginEntry = hookEntry.get(pluginName);
|
let pluginEntry = hookEntry.get(pluginName);
|
||||||
if (!pluginEntry) {
|
if (!pluginEntry) {
|
||||||
pluginEntry = new Map();
|
pluginEntry = new Map<string, string>();
|
||||||
hookEntry.set(pluginName, pluginEntry);
|
hookEntry.set(pluginName, pluginEntry);
|
||||||
}
|
}
|
||||||
pluginEntry.set(part.name, hookFnName);
|
pluginEntry.set(part.name, hookFnName as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.getHooks = (hookSetName) => {
|
export const getHooks = (hookSetName: string) => {
|
||||||
const hooks = new Map();
|
const hooks = new Map();
|
||||||
sortHooks(hookSetName, hooks);
|
sortHooks(hookSetName, hooks);
|
||||||
return hooks;
|
return hooks;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.formatHooks = (hookSetName, html) => {
|
export const formatHooks = (hookSetName: string, html: string|false) => {
|
||||||
let hooks = new Map();
|
let hooks:Map<string, Map<string,Map<string, string>>> = new Map();
|
||||||
sortHooks(hookSetName, hooks);
|
sortHooks(hookSetName, hooks);
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const sortStringKeys = (a, b) => String(a[0]).localeCompare(b[0]);
|
const sortStringKeys = (a: [string, Map<string, Map<string, string>>]| [string, Map<string, string>]|[string,string], b: [string, Map<string, Map<string, string>>]|[string,string]| [string, Map<string, string>]) => String(a[0]).localeCompare(b[0]);
|
||||||
if (html) lines.push('<dl>');
|
if (html) lines.push('<dl>');
|
||||||
hooks = new Map([...hooks].sort(sortStringKeys));
|
hooks = new Map([...hooks].sort(sortStringKeys));
|
||||||
for (const [hookName, hookEntry] of hooks) {
|
for (const [hookName, hookEntry] of hooks) {
|
||||||
|
@ -88,20 +92,20 @@ exports.formatHooks = (hookSetName, html) => {
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.pathNormalization = (part, hookFnName, hookName) => {
|
export const pathNormalization = (part: Part, hookFnName: string, hookName: string) => {
|
||||||
const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'.
|
const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'.
|
||||||
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
|
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
|
||||||
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName;
|
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName;
|
||||||
const moduleName = tmp.join(':') || part.plugin;
|
const moduleName = tmp.join(':') || part.plugin;
|
||||||
const packageDir = path.dirname(defs.plugins[part.plugin].package.path);
|
const packageDir = path.dirname(pluginDefs.getPlugins()[part.plugin!].package.path);
|
||||||
const fileName = path.join(packageDir, moduleName);
|
const fileName = path.join(packageDir, moduleName!);
|
||||||
return `${fileName}:${functionName}`;
|
return `${fileName}:${functionName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.update = async () => {
|
export const update = async () => {
|
||||||
const packages = await exports.getPackages();
|
const packages = await getPackages();
|
||||||
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array.
|
const parts: MapArrayType<Part> = {}; // Key is full name. sortParts converts this into a topologically sorted array.
|
||||||
const plugins = {};
|
const plugins: MapArrayType<any> = {};
|
||||||
|
|
||||||
// Load plugin metadata ep.json
|
// Load plugin metadata ep.json
|
||||||
await Promise.all(Object.keys(packages).map(async (pluginName) => {
|
await Promise.all(Object.keys(packages).map(async (pluginName) => {
|
||||||
|
@ -110,23 +114,27 @@ exports.update = async () => {
|
||||||
}));
|
}));
|
||||||
logger.info(`Loaded ${Object.keys(packages).length} plugins`);
|
logger.info(`Loaded ${Object.keys(packages).length} plugins`);
|
||||||
|
|
||||||
defs.plugins = plugins;
|
pluginDefs.setPlugins(plugins);
|
||||||
defs.parts = sortParts(parts);
|
pluginDefs.setParts(sortParts(parts));
|
||||||
defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization);
|
pluginDefs.setHooks(pluginUtils.extractHooks(pluginDefs.getParts(), 'hooks', pathNormalization))
|
||||||
defs.loaded = true;
|
pluginDefs.setLoaded(true);
|
||||||
await Promise.all(Object.keys(defs.plugins).map(async (p) => {
|
|
||||||
|
await Promise.all(Object.keys(pluginDefs.getPlugins()).map(async (p) => {
|
||||||
const logger = log4js.getLogger(`plugin:${p}`);
|
const logger = log4js.getLogger(`plugin:${p}`);
|
||||||
await hooks.aCallAll(`init_${p}`, {logger});
|
await hooks.aCallAll(`init_${p}`, {logger});
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getPackages = async () => {
|
import {linkInstaller} from "./installer";
|
||||||
const {linkInstaller} = require("./installer");
|
import {MapArrayType} from "../../../node/types/MapType";
|
||||||
|
import {IPluginInfoExtended} from "./IPluginInfoExtended";
|
||||||
|
|
||||||
|
export const getPackages = async () => {
|
||||||
const plugins = await linkInstaller.listPlugins();
|
const plugins = await linkInstaller.listPlugins();
|
||||||
const newDependencies = {};
|
const newDependencies:MapArrayType<IPluginInfoExtended> = {};
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
if (!plugin.name.startsWith(exports.prefix)) {
|
if (!plugin.name.startsWith(prefix)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
plugin.path = plugin.realPath = plugin.location;
|
plugin.path = plugin.realPath = plugin.location;
|
||||||
|
@ -134,17 +142,20 @@ exports.getPackages = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
newDependencies['ep_etherpad-lite'] = {
|
newDependencies['ep_etherpad-lite'] = {
|
||||||
|
dependencies: {},
|
||||||
|
location: "",
|
||||||
|
mainFile: "",
|
||||||
name: 'ep_etherpad-lite',
|
name: 'ep_etherpad-lite',
|
||||||
version: settings.getEpVersion(),
|
version: settings.getEpVersion(),
|
||||||
path: path.join(settings.root, 'node_modules/ep_etherpad-lite'),
|
path: path.join(settings.root, 'node_modules/ep_etherpad-lite'),
|
||||||
realPath: path.join(settings.root, 'src'),
|
realPath: path.join(settings.root, 'src')
|
||||||
};
|
};
|
||||||
|
|
||||||
return newDependencies;
|
return newDependencies;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadPlugin = async (packages, pluginName, plugins, parts) => {
|
const loadPlugin = async (packages: MapArrayType<IPluginInfoExtended>, pluginName: string, plugins: MapArrayType<any>, parts: MapArrayType<Part>) => {
|
||||||
const pluginPath = path.resolve(packages[pluginName].path, 'ep.json');
|
const pluginPath = path.resolve(packages[pluginName].path!, 'ep.json');
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(pluginPath);
|
const data = await fs.readFile(pluginPath);
|
||||||
try {
|
try {
|
||||||
|
@ -156,16 +167,16 @@ const loadPlugin = async (packages, pluginName, plugins, parts) => {
|
||||||
part.full_name = `${pluginName}/${part.name}`;
|
part.full_name = `${pluginName}/${part.name}`;
|
||||||
parts[part.full_name] = part;
|
parts[part.full_name] = part;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
logger.error(`Unable to parse plugin definition file ${pluginPath}: ${err.stack || err}`);
|
logger.error(`Unable to parse plugin definition file ${pluginPath}: ${err.stack || err}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
logger.error(`Unable to load plugin definition file ${pluginPath}: ${err.stack || err}`);
|
logger.error(`Unable to load plugin definition file ${pluginPath}: ${err.stack || err}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const partsToParentChildList = (parts) => {
|
const partsToParentChildList = (parts: MapArrayType<Part>) => {
|
||||||
const res = [];
|
const res:[string,string][] = [];
|
||||||
for (const name of Object.keys(parts)) {
|
for (const name of Object.keys(parts)) {
|
||||||
for (const childName of parts[name].post || []) {
|
for (const childName of parts[name].post || []) {
|
||||||
res.push([name, childName]);
|
res.push([name, childName]);
|
||||||
|
@ -181,6 +192,6 @@ const partsToParentChildList = (parts) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Used only in Node, so no need for _
|
// Used only in Node, so no need for _
|
||||||
const sortParts = (parts) => tsort(partsToParentChildList(parts))
|
const sortParts = (parts: MapArrayType<Part>) => new TSort(partsToParentChildList(parts)).getSorted()
|
||||||
.filter((name) => parts[name] !== undefined)
|
.filter((name) => parts[name] !== undefined)
|
||||||
.map((name) => parts[name]);
|
.map((name) => parts[name]);
|
|
@ -1,15 +1,16 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const defs = require('./plugin_defs');
|
import {Part, pluginDefs, PluginHook} from './plugin_defs';
|
||||||
|
import {MapArrayType} from "../../../node/types/MapType";
|
||||||
|
|
||||||
const disabledHookReasons = {
|
const disabledHookReasons: MapArrayType<any> = {
|
||||||
hooks: {
|
hooks: {
|
||||||
indexCustomInlineScripts: 'The hook makes it impossible to use a Content Security Policy ' +
|
indexCustomInlineScripts: 'The hook makes it impossible to use a Content Security Policy ' +
|
||||||
'that prohibits inline code. Permitting inline code makes XSS vulnerabilities more likely',
|
'that prohibits inline code. Permitting inline code makes XSS vulnerabilities more likely',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFn = (path, hookName, modules) => {
|
const loadFn = (path: string, hookName: string, modules: Function) => {
|
||||||
let functionName;
|
let functionName;
|
||||||
const parts = path.split(':');
|
const parts = path.split(':');
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ const loadFn = (path, hookName, modules) => {
|
||||||
if (modules === undefined || !("get" in modules)) {
|
if (modules === undefined || !("get" in modules)) {
|
||||||
fn = require(/* webpackIgnore: true */ path);
|
fn = require(/* webpackIgnore: true */ path);
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
fn = modules.get(path);
|
fn = modules.get(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,9 +41,10 @@ const loadFn = (path, hookName, modules) => {
|
||||||
return fn;
|
return fn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractHooks = (parts, hookSetName, normalizer, modules) => {
|
export const extractHooks = (parts: Part[], hookSetName: string, normalizer: Function|null, modules: Function) => {
|
||||||
const hooks = {};
|
const hooks: MapArrayType<PluginHook[]> = {};
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
|
// @ts-ignore
|
||||||
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
|
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||||
/* On the server side, you can't just
|
/* On the server side, you can't just
|
||||||
* require("pluginname/whatever") if the plugin is installed as
|
* require("pluginname/whatever") if the plugin is installed as
|
||||||
|
@ -64,6 +67,7 @@ const extractHooks = (parts, hookSetName, normalizer, modules) => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
||||||
`part "${part.name}" hook set "${hookSetName}" hook "${hookName}": ` +
|
`part "${part.name}" hook set "${hookSetName}" hook "${hookName}": ` +
|
||||||
|
// @ts-ignore
|
||||||
`${err.stack || err}`);
|
`${err.stack || err}`);
|
||||||
}
|
}
|
||||||
if (hookFn) {
|
if (hookFn) {
|
||||||
|
@ -80,7 +84,6 @@ const extractHooks = (parts, hookSetName, normalizer, modules) => {
|
||||||
return hooks;
|
return hooks;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.extractHooks = extractHooks;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns an array containing the names of the installed client-side plugins
|
* Returns an array containing the names of the installed client-side plugins
|
||||||
|
@ -94,8 +97,8 @@ exports.extractHooks = extractHooks;
|
||||||
* No plugins: []
|
* No plugins: []
|
||||||
* Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ]
|
* Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ]
|
||||||
*/
|
*/
|
||||||
exports.clientPluginNames = () => {
|
export const clientPluginNames = () => {
|
||||||
const clientPluginNames = defs.parts
|
const clientPluginNames = pluginDefs.getParts()
|
||||||
.filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks'))
|
.filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks'))
|
||||||
.map((part) => `plugin-${part.plugin}`);
|
.map((part) => `plugin-${part.plugin}`);
|
||||||
return [...new Set(clientPluginNames)];
|
return [...new Set(clientPluginNames)];
|
|
@ -1,112 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* general topological sort
|
|
||||||
* from https://gist.github.com/1232505
|
|
||||||
* @author SHIN Suzuki (shinout310@gmail.com)
|
|
||||||
* @param Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
|
|
||||||
*
|
|
||||||
* @returns Array : topological sorted list of IDs
|
|
||||||
**/
|
|
||||||
|
|
||||||
const tsort = (edges) => {
|
|
||||||
const nodes = {}; // hash: stringified id of the node => { id: id, afters: lisf of ids }
|
|
||||||
const sorted = []; // sorted list of IDs ( returned value )
|
|
||||||
const visited = {}; // hash: id of already visited node => true
|
|
||||||
|
|
||||||
const Node = function (id) {
|
|
||||||
this.id = id;
|
|
||||||
this.afters = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. build data structures
|
|
||||||
edges.forEach((v) => {
|
|
||||||
const from = v[0]; const
|
|
||||||
to = v[1];
|
|
||||||
if (!nodes[from]) nodes[from] = new Node(from);
|
|
||||||
if (!nodes[to]) nodes[to] = new Node(to);
|
|
||||||
nodes[from].afters.push(to);
|
|
||||||
});
|
|
||||||
|
|
||||||
const visit = (idstr, ancestors) => {
|
|
||||||
const node = nodes[idstr];
|
|
||||||
const id = node.id;
|
|
||||||
|
|
||||||
// if already exists, do nothing
|
|
||||||
if (visited[idstr]) return;
|
|
||||||
|
|
||||||
if (!Array.isArray(ancestors)) ancestors = [];
|
|
||||||
|
|
||||||
ancestors.push(id);
|
|
||||||
|
|
||||||
visited[idstr] = true;
|
|
||||||
|
|
||||||
node.afters.forEach((afterID) => {
|
|
||||||
// if already in ancestors, a closed chain exists.
|
|
||||||
if (ancestors.indexOf(afterID) >= 0) throw new Error(`closed chain : ${afterID} is in ${id}`);
|
|
||||||
|
|
||||||
visit(afterID.toString(), ancestors.map((v) => v)); // recursive call
|
|
||||||
});
|
|
||||||
|
|
||||||
sorted.unshift(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. topological sort
|
|
||||||
Object.keys(nodes).forEach(visit);
|
|
||||||
|
|
||||||
return sorted;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST
|
|
||||||
**/
|
|
||||||
const tsortTest = () => {
|
|
||||||
// example 1: success
|
|
||||||
let edges = [
|
|
||||||
[1, 2],
|
|
||||||
[1, 3],
|
|
||||||
[2, 4],
|
|
||||||
[3, 4],
|
|
||||||
];
|
|
||||||
|
|
||||||
let sorted = tsort(edges);
|
|
||||||
|
|
||||||
// example 2: failure ( A > B > C > A )
|
|
||||||
edges = [
|
|
||||||
['A', 'B'],
|
|
||||||
['B', 'C'],
|
|
||||||
['C', 'A'],
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
sorted = tsort(edges);
|
|
||||||
console.log('succeeded', sorted);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// example 3: generate random edges
|
|
||||||
const max = 100;
|
|
||||||
const iteration = 30;
|
|
||||||
const randomInt = (max) => Math.floor(Math.random() * max) + 1;
|
|
||||||
|
|
||||||
edges = (() => {
|
|
||||||
const ret = [];
|
|
||||||
let i = 0;
|
|
||||||
while (i++ < iteration) ret.push([randomInt(max), randomInt(max)]);
|
|
||||||
return ret;
|
|
||||||
})();
|
|
||||||
|
|
||||||
try {
|
|
||||||
sorted = tsort(edges);
|
|
||||||
console.log('succeeded', sorted);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('failed', e.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// for node.js
|
|
||||||
if (typeof exports === 'object' && exports === this) {
|
|
||||||
module.exports = tsort;
|
|
||||||
if (process.argv[1] === __filename) tsortTest();
|
|
||||||
}
|
|
74
src/static/js/pluginfw/tsort.ts
Normal file
74
src/static/js/pluginfw/tsort.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {MapArrayType} from "../../../node/types/MapType";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* general topological sort
|
||||||
|
* from https://gist.github.com/1232505
|
||||||
|
* @author SHIN Suzuki (shinout310@gmail.com)
|
||||||
|
* @param Array<Array> edges : list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
|
||||||
|
*
|
||||||
|
* @returns Array : topological sorted list of IDs
|
||||||
|
**/
|
||||||
|
|
||||||
|
export class TSort {
|
||||||
|
private nodes: MapArrayType<Node> = {}; // hash: stringified id of the node => { id: id, afters: lisf of ids }
|
||||||
|
private sorted: (number|string)[] = []; // sorted list of IDs ( returned value )
|
||||||
|
private visited: MapArrayType<boolean> = {}; // hash: id of already visited node => true
|
||||||
|
|
||||||
|
constructor(edges: (number|string)[][]) {
|
||||||
|
// 1. build data structures
|
||||||
|
edges.forEach((v) => {
|
||||||
|
const from = v[0]; const
|
||||||
|
to = v[1];
|
||||||
|
if (!this.nodes[from]) {
|
||||||
|
this.nodes[from] = new Node(from);
|
||||||
|
}
|
||||||
|
if (!this.nodes[to]) {
|
||||||
|
this.nodes[to] = new Node(to);
|
||||||
|
}
|
||||||
|
this.nodes[from].afters.push(to);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. topological sort
|
||||||
|
for (const key in this.nodes) {
|
||||||
|
this.visit(key, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visit = (idstr: string, ancestors:(number|string)[]) => {
|
||||||
|
const node = this.nodes[idstr];
|
||||||
|
const id = node.id;
|
||||||
|
|
||||||
|
// if already exists, do nothing
|
||||||
|
if (this.visited[idstr]) return;
|
||||||
|
|
||||||
|
if (!Array.isArray(ancestors)) ancestors = [];
|
||||||
|
|
||||||
|
ancestors.push(id);
|
||||||
|
|
||||||
|
this.visited[idstr] = true;
|
||||||
|
|
||||||
|
node.afters.forEach((afterID) => {
|
||||||
|
// if already in ancestors, a closed chain exists.
|
||||||
|
if (ancestors.indexOf(afterID) >= 0) throw new Error(`closed chain : ${afterID} is in ${id}`);
|
||||||
|
|
||||||
|
this.visit(afterID.toString(), ancestors.map((v) => v)); // recursive call
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sorted.unshift(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSorted = ()=>{
|
||||||
|
return this.sorted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
id: number|string
|
||||||
|
afters: (number|string)[]
|
||||||
|
constructor(id: number|string) {
|
||||||
|
this.id = id
|
||||||
|
this.afters = []
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,8 +32,7 @@ import connect from './socketio'
|
||||||
import html10n from '../js/vendors/html10n'
|
import html10n from '../js/vendors/html10n'
|
||||||
import {Socket} from "socket.io";
|
import {Socket} from "socket.io";
|
||||||
import {ClientVarData, ClientVarMessage, ClientVarPayload, SocketIOMessage} from "./types/SocketIOMessage";
|
import {ClientVarData, ClientVarMessage, ClientVarPayload, SocketIOMessage} from "./types/SocketIOMessage";
|
||||||
import {Func} from "mocha";
|
import {Revision} from "./broadcast_revisions";
|
||||||
|
|
||||||
export type ChangeSetLoader = {
|
export type ChangeSetLoader = {
|
||||||
handleMessageFromServer(msg: ClientVarMessage): void
|
handleMessageFromServer(msg: ClientVarMessage): void
|
||||||
}
|
}
|
||||||
|
@ -111,13 +110,14 @@ const sendSocketMsg = (type: string, data: Object) => {
|
||||||
|
|
||||||
const fireWhenAllScriptsAreLoaded: Function[] = [];
|
const fireWhenAllScriptsAreLoaded: Function[] = [];
|
||||||
|
|
||||||
const handleClientVars = (message: ClientVarData) => {
|
const handleClientVars = async (message: ClientVarData) => {
|
||||||
// save the client Vars
|
// save the client Vars
|
||||||
window.clientVars = message.data;
|
window.clientVars = message.data;
|
||||||
|
|
||||||
if (window.clientVars.sessionRefreshInterval) {
|
if (window.clientVars.sessionRefreshInterval) {
|
||||||
const ping =
|
const ping =
|
||||||
() => $.ajax('../../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
|
() => $.ajax('../../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {
|
||||||
|
});
|
||||||
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ const handleClientVars = (message: ClientVarData) => {
|
||||||
BroadcastSlider = require('./broadcast_slider')
|
BroadcastSlider = require('./broadcast_slider')
|
||||||
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
|
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
|
||||||
|
|
||||||
require('./broadcast_revisions').loadBroadcastRevisionsJS();
|
await import('./broadcast_revisions')
|
||||||
changesetLoader = require('./broadcast')
|
changesetLoader = require('./broadcast')
|
||||||
.loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
|
.loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
|
||||||
|
|
||||||
|
|
7
src/static/js/types/PadRevision.ts
Normal file
7
src/static/js/types/PadRevision.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export type PadRevision = {
|
||||||
|
revNum: number;
|
||||||
|
savedById: string;
|
||||||
|
label: string;
|
||||||
|
timestamp: number;
|
||||||
|
id: string;
|
||||||
|
}
|
|
@ -2,6 +2,9 @@ import {MapArrayType} from "../../../node/types/MapType";
|
||||||
import {AText} from "./AText";
|
import {AText} from "./AText";
|
||||||
import AttributePool from "../AttributePool";
|
import AttributePool from "../AttributePool";
|
||||||
import attributePool from "../AttributePool";
|
import attributePool from "../AttributePool";
|
||||||
|
import ChatMessage from "../ChatMessage";
|
||||||
|
import {PadRevision} from "./PadRevision";
|
||||||
|
import {MappedPlugin} from "../pluginfw/plugin_defs";
|
||||||
|
|
||||||
export type SocketIOMessage = {
|
export type SocketIOMessage = {
|
||||||
type: string
|
type: string
|
||||||
|
@ -11,16 +14,25 @@ export type SocketIOMessage = {
|
||||||
export type HistoricalAuthorData = MapArrayType<{
|
export type HistoricalAuthorData = MapArrayType<{
|
||||||
name: string;
|
name: string;
|
||||||
colorId: number;
|
colorId: number;
|
||||||
userId: string
|
userId?: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type ServerVar = {
|
export type ServerVar = {
|
||||||
rev: number
|
rev: number
|
||||||
historicalAuthorData: HistoricalAuthorData,
|
clientIp: string
|
||||||
initialAttributedText: string,
|
padId: string
|
||||||
apool: AttributePool
|
historicalAuthorData?: HistoricalAuthorData,
|
||||||
|
initialAttributedText: {
|
||||||
|
attribs: string
|
||||||
|
text: string
|
||||||
|
},
|
||||||
|
apool: AttributePoolWire
|
||||||
|
time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AttributePoolWire = {numToAttrib: {[p: number]: [string, string]}, nextNum: number}
|
||||||
|
|
||||||
|
|
||||||
export type UserInfo = {
|
export type UserInfo = {
|
||||||
userId: string
|
userId: string
|
||||||
colorId: number,
|
colorId: number,
|
||||||
|
@ -31,18 +43,18 @@ export type ClientVarPayload = {
|
||||||
readOnlyId: string
|
readOnlyId: string
|
||||||
automaticReconnectionTimeout: number
|
automaticReconnectionTimeout: number
|
||||||
sessionRefreshInterval: number,
|
sessionRefreshInterval: number,
|
||||||
historicalAuthorData: HistoricalAuthorData,
|
atext?: AText,
|
||||||
atext: AText,
|
apool?: AttributePool,
|
||||||
apool: AttributePool,
|
userName?: string,
|
||||||
noColors: boolean,
|
|
||||||
userName: string,
|
|
||||||
userColor: number,
|
userColor: number,
|
||||||
hideChat: boolean,
|
hideChat?: boolean,
|
||||||
padOptions: PadOption,
|
padOptions: PadOption,
|
||||||
padId: string,
|
padId: string,
|
||||||
clientIp: string,
|
clientIp: string,
|
||||||
colorPalette: MapArrayType<number>,
|
colorPalette: string[],
|
||||||
accountPrivs: MapArrayType<string>,
|
accountPrivs: {
|
||||||
|
maxRevisions: number,
|
||||||
|
},
|
||||||
collab_client_vars: ServerVar,
|
collab_client_vars: ServerVar,
|
||||||
chatHead: number,
|
chatHead: number,
|
||||||
readonly: boolean,
|
readonly: boolean,
|
||||||
|
@ -54,6 +66,29 @@ export type ClientVarPayload = {
|
||||||
skinName: string
|
skinName: string
|
||||||
skinVariants: string,
|
skinVariants: string,
|
||||||
exportAvailable: string
|
exportAvailable: string
|
||||||
|
savedRevisions: PadRevision[],
|
||||||
|
initialRevisionList: number[],
|
||||||
|
padShortcutEnabled: MapArrayType<boolean>,
|
||||||
|
initialTitle: string,
|
||||||
|
opts: {}
|
||||||
|
numConnectedUsers: number
|
||||||
|
abiwordAvailable: string
|
||||||
|
sofficeAvailable: string
|
||||||
|
plugins: {
|
||||||
|
plugins: MapArrayType<any>
|
||||||
|
parts: MappedPlugin[]
|
||||||
|
}
|
||||||
|
indentationOnNewLine: boolean
|
||||||
|
scrollWhenFocusLineIsOutOfViewport : {
|
||||||
|
percentage: {
|
||||||
|
editionAboveViewport: number,
|
||||||
|
editionBelowViewport: number
|
||||||
|
}
|
||||||
|
duration: number
|
||||||
|
scrollWhenCaretIsInTheLastLineOfViewport: boolean
|
||||||
|
percentageToScrollWhenUserPressesArrowUp: number
|
||||||
|
}
|
||||||
|
initialChangesets: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClientVarData = {
|
export type ClientVarData = {
|
||||||
|
@ -105,7 +140,9 @@ export type ClientMessageMessage = {
|
||||||
|
|
||||||
export type ChatMessageMessage = {
|
export type ChatMessageMessage = {
|
||||||
type: 'CHAT_MESSAGE'
|
type: 'CHAT_MESSAGE'
|
||||||
message: string
|
data: {
|
||||||
|
message: ChatMessage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChatMessageMessages = {
|
export type ChatMessageMessages = {
|
||||||
|
@ -122,7 +159,18 @@ export type ClientUserChangesMessage = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type ClientSendMessages = ClientUserChangesMessage | ClientSendUserInfoUpdate| ClientMessageMessage | GetChatMessageMessage |ClientSuggestUserName | NewRevisionListMessage | RevisionLabel | PadOptionsMessage| ClientSaveRevisionMessage
|
export type ClientSendMessages = ClientUserChangesMessage |ClientReadyMessage| ClientSendUserInfoUpdate|ChatMessageMessage| ClientMessageMessage | GetChatMessageMessage |ClientSuggestUserName | NewRevisionListMessage | RevisionLabel | PadOptionsMessage| ClientSaveRevisionMessage
|
||||||
|
|
||||||
|
export type ClientReadyMessage = {
|
||||||
|
type: 'CLIENT_READY',
|
||||||
|
component: string,
|
||||||
|
padId: string,
|
||||||
|
sessionID: string,
|
||||||
|
token: string,
|
||||||
|
userInfo: UserInfo,
|
||||||
|
reconnect?: boolean
|
||||||
|
client_rev?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientSaveRevisionMessage = {
|
export type ClientSaveRevisionMessage = {
|
||||||
type: 'SAVE_REVISION'
|
type: 'SAVE_REVISION'
|
||||||
|
@ -191,18 +239,44 @@ export type ClientDisconnectedMessage = {
|
||||||
type: "disconnected"
|
type: "disconnected"
|
||||||
disconnected: boolean
|
disconnected: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserChanges = {
|
||||||
|
data: ClientUserChangesMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserSuggestUserName = {
|
||||||
|
data: {
|
||||||
|
payload: ClientSuggestUserName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChangesetRequestMessage = {
|
||||||
|
type: 'CHANGESET_REQ'
|
||||||
|
data: {
|
||||||
|
granularity: number
|
||||||
|
start: number
|
||||||
|
requestID: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type CollabroomMessage = {
|
||||||
|
type: 'COLLABROOM'
|
||||||
|
data: ClientSendUserInfoUpdate | ClientUserChangesMessage | ChatMessageMessage | GetChatMessageMessage | ClientSaveRevisionMessage | ClientMessageMessage
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientVarMessage = {
|
export type ClientVarMessage = {
|
||||||
type: 'CHANGESET_REQ'| 'COLLABROOM'| 'CUSTOM'
|
type: 'CUSTOM'
|
||||||
data:
|
data: ClientVarData
|
||||||
| ClientNewChanges
|
} | ClientVarData | ClientDisconnectedMessage | ClientReadyMessage| ChangesetRequestMessage | CollabroomMessage
|
||||||
| ClientAcceptCommitMessage
|
|
||||||
|UserNewInfoMessage
|
export type ClientCustomMessage = {
|
||||||
| UserLeaveMessage
|
type: 'CUSTOM',
|
||||||
|ClientMessageMessage
|
action: string,
|
||||||
|ChatMessageMessage
|
payload: any
|
||||||
|ChatMessageMessages
|
|
||||||
|ClientConnectMessage,
|
}
|
||||||
} | ClientVarData | ClientDisconnectedMessage
|
|
||||||
|
|
||||||
export type SocketClientReadyMessage = {
|
export type SocketClientReadyMessage = {
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {ClientVarData, ClientVarPayload} from "./SocketIOMessage";
|
import {ClientVarData, ClientVarPayload} from "./SocketIOMessage";
|
||||||
import {Pad} from "../pad";
|
import {Pad} from "../pad";
|
||||||
|
import {Revision} from "../broadcast_revisions";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -7,6 +8,7 @@ declare global {
|
||||||
$: any,
|
$: any,
|
||||||
customStart?:any,
|
customStart?:any,
|
||||||
ajlog: string
|
ajlog: string
|
||||||
|
revisionInfo: Record<number|string, number|Revision>
|
||||||
}
|
}
|
||||||
let pad: Pad
|
let pad: Pad
|
||||||
}
|
}
|
||||||
|
|
74
src/tests/backend/specs/tsort.spec.ts
Normal file
74
src/tests/backend/specs/tsort.spec.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import {describe, it} from 'vitest'
|
||||||
|
import {TSort} from "../../../static/js/pluginfw/tsort";
|
||||||
|
import {expect} from "@playwright/test";
|
||||||
|
|
||||||
|
|
||||||
|
describe('tsort', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
let edges: (number|string)[][] = [
|
||||||
|
[1, 2],
|
||||||
|
[1, 3],
|
||||||
|
[2, 4],
|
||||||
|
[3, 4],
|
||||||
|
];
|
||||||
|
|
||||||
|
let sorted = new TSort(edges).getSorted();
|
||||||
|
expect(sorted).toEqual([1, 3, 2, 4]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// example 3: generate random edges
|
||||||
|
const max = 100;
|
||||||
|
const iteration = 30;
|
||||||
|
const randomInt = (max: number) => Math.floor(Math.random() * max) + 1;
|
||||||
|
edges = (() => {
|
||||||
|
const ret = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i++ < iteration) ret.push([randomInt(max), randomInt(max)]);
|
||||||
|
return ret;
|
||||||
|
})();
|
||||||
|
try {
|
||||||
|
sorted = new TSort(edges).getSorted();
|
||||||
|
console.log('succeeded', sorted);
|
||||||
|
} catch (e) {
|
||||||
|
// @ts-ignore
|
||||||
|
console.log('failed', e.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails with closed chain', ()=> {
|
||||||
|
// example 2: failure ( A > B > C > A )
|
||||||
|
let edges = [
|
||||||
|
['A', 'B'],
|
||||||
|
['B', 'C'],
|
||||||
|
['C', 'A'],
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => new TSort(edges)).toThrowError('closed chain : A is in C');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails with closed chain 2', ()=> {
|
||||||
|
// example 1: failure ( 1 > 2 > 3 > 1 )
|
||||||
|
let edges = [
|
||||||
|
[1, 2],
|
||||||
|
[2, 3],
|
||||||
|
[3, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => new TSort(edges)).toThrowError('closed chain : 1 is in 3');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with 6 nodes', ()=> {
|
||||||
|
let edges = [
|
||||||
|
[1, 2],
|
||||||
|
[1, 3],
|
||||||
|
[2, 4],
|
||||||
|
[3, 4],
|
||||||
|
[4, 5],
|
||||||
|
[4, 6],
|
||||||
|
];
|
||||||
|
|
||||||
|
let sorted = new TSort(edges).getSorted();
|
||||||
|
expect(sorted).toEqual([1, 3, 2, 4, 6, 5]);
|
||||||
|
})
|
||||||
|
})
|
Loading…
Add table
Add a link
Reference in a new issue