diff --git a/doc/api/changeset_library.md b/doc/api/changeset_library.md index 7929aa48b92..89846a55b7b 100644 --- a/doc/api/changeset_library.md +++ b/doc/api/changeset_library.md @@ -7,7 +7,7 @@ provides tools to create, read, and apply changesets. ## Changeset ```javascript -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const Changeset = require('src/static/js/Changeset'); ``` A changeset describes the difference between two revisions of a document. When a @@ -24,7 +24,7 @@ A transmitted changeset looks like this: ## Attribute Pool ```javascript -const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); +const AttributePool = require('src/static/js/AttributePool'); ``` Changesets do not include any attribute key–value pairs. Instead, they use diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 05a66209f10..663715373b7 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -825,16 +825,16 @@ Context properties: Example: ```javascript -const AttributeMap = require('ep_etherpad-lite/static/js/AttributeMap'); -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const AttributeMap = require('src/static/js/AttributeMap'); +const Changeset = require('src/static/js/Changeset'); exports.getLineHTMLForExport = async (hookName, context) => { - if (!context.attribLine) return; - const [op] = Changeset.deserializeOps(context.attribLine); - if (op == null) return; - const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading'); - if (!heading) return; - context.lineContent = `<${heading}>${context.lineContent}`; + if (!context.attribLine) return; + const [op] = Changeset.deserializeOps(context.attribLine); + if (op == null) return; + const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading'); + if (!heading) return; + context.lineContent = `<${heading}>${context.lineContent}`; }; ``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eca4dceb393..111b5e8134c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,6 +143,9 @@ importers: '@etherpad/express-session': specifier: ^1.18.2 version: 1.18.2 + '@types/cookie-parser': + specifier: ^1.4.7 + version: 1.4.7 async: specifier: ^3.2.5 version: 3.2.5 @@ -185,6 +188,9 @@ importers: jose: specifier: ^5.6.3 version: 5.6.3 + jquery: + specifier: ^3.7.1 + version: 3.7.1 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -282,6 +288,12 @@ importers: '@types/async': specifier: ^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': specifier: ^4.17.21 version: 4.17.21 @@ -294,12 +306,24 @@ importers: '@types/jquery': specifier: ^3.5.30 version: 3.5.30 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 + '@types/jsonminify': + specifier: ^0.4.3 + version: 0.4.3 '@types/jsonwebtoken': specifier: ^9.0.6 version: 9.0.6 + '@types/lodash.clonedeep': + specifier: ^4.5.9 + version: 4.5.9 + '@types/mime-types': + specifier: ^2.1.4 + version: 2.1.4 '@types/mocha': specifier: ^10.0.7 version: 10.0.7 @@ -309,6 +333,12 @@ importers: '@types/oidc-provider': specifier: ^8.5.1 version: 8.5.1 + '@types/proxy-addr': + specifier: ^2.0.3 + version: 2.0.3 + '@types/resolve': + specifier: ^1.20.6 + version: 1.20.6 '@types/semver': specifier: ^7.5.8 version: 7.5.8 @@ -318,9 +348,15 @@ importers: '@types/supertest': specifier: ^6.0.2 version: 6.0.2 + '@types/tinycon': + specifier: ^0.6.5 + version: 0.6.5 '@types/underscore': specifier: ^1.11.15 version: 1.11.15 + '@types/unorm': + specifier: ^1.3.31 + version: 1.3.31 chokidar: specifier: ^3.6.0 version: 3.6.0 @@ -360,6 +396,9 @@ importers: typescript: specifier: ^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: devDependencies: @@ -1453,6 +1492,9 @@ packages: '@types/content-disposition@0.5.8': resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==} + '@types/cookie-parser@1.4.7': + resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==} + '@types/cookie@0.4.1': resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} @@ -1465,9 +1507,15 @@ packages: '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/debug@4.1.12': 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': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -1498,6 +1546,9 @@ packages: '@types/jquery@3.5.30': resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/jsdom@21.1.7': resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} @@ -1507,6 +1558,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonminify@0.4.3': + resolution: {integrity: sha512-+oz7EbPz1Nwmn/sr3UztgXpRhdFpvFrjGi5ictEYxUri5ZvQMTcdTi36MTfD/gCb1A5xhJKdH8Hwz2uz5k6s9A==} + '@types/jsonwebtoken@9.0.6': resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} @@ -1525,6 +1579,12 @@ packages: '@types/lockfile@1.0.4': resolution: {integrity: sha512-Q8oFIHJHr+htLrTXN2FuZfg+WXVHQRwU/hC2GpUu+Q8e3FUM9EDkS2pE3R2AO1ZGu56f479ybdMCNF1DAu8cAQ==} + '@types/lodash.clonedeep@4.5.9': + resolution: {integrity: sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==} + + '@types/lodash@4.17.7': + resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + '@types/markdown-it@14.1.1': resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==} @@ -1537,6 +1597,9 @@ packages: '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/mime-types@2.1.4': + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -1558,6 +1621,9 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/proxy-addr@2.0.3': + resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==} + '@types/qs@6.9.15': resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} @@ -1570,6 +1636,9 @@ packages: '@types/react@18.3.3': 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': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -1597,6 +1666,9 @@ packages: '@types/tar@6.1.13': 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': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -1606,6 +1678,9 @@ packages: '@types/unist@3.0.2': resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + '@types/unorm@1.3.31': + resolution: {integrity: sha512-qCPX/Lo14ECb9Wkb/1sxdcTQqIiHTVNlaHczGrh2WqMVSlWjfn8Hu7DxraCtBYz1+Ud6Id/d+4OH/hkd+dlnpw==} + '@types/url-join@4.0.3': resolution: {integrity: sha512-3l1qMm3wqO0iyC5gkADzT95UVW7C/XXcdvUcShOideKF0ddgVRErEQQJXBd2kvQm+aSgqhBGHGB38TgMeT57Ww==} @@ -1685,6 +1760,24 @@ packages: vite: ^5.0.0 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': resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==} @@ -1869,6 +1962,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -1949,6 +2046,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 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: resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} engines: {node: '>= 6.0.0'} @@ -1979,6 +2080,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1993,6 +2098,10 @@ packages: character-entities-legacy@3.0.0: 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: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2168,6 +2277,10 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-equal@1.0.1: resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} @@ -2501,6 +2614,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2518,6 +2634,10 @@ packages: engines: {node: '>=18.0.0'} 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: resolution: {integrity: sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==} engines: {node: '>= 16'} @@ -2679,6 +2799,9 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2691,6 +2814,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} @@ -2856,6 +2983,10 @@ packages: resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} 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: resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==} @@ -2999,6 +3130,10 @@ packages: resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} 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: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -3036,6 +3171,9 @@ packages: jose@5.6.3: resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} + jquery@3.7.1: + resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} @@ -3208,6 +3346,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3251,6 +3392,9 @@ packages: merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3296,6 +3440,10 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -3414,6 +3562,10 @@ packages: resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} 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: resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==} @@ -3469,6 +3621,10 @@ packages: once@1.4.0: 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: resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} @@ -3535,6 +3691,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -3548,6 +3708,13 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 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: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -3870,9 +4037,16 @@ packages: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: 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: resolution: {integrity: sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==} @@ -3931,6 +4105,9 @@ packages: sprintf-js@1.1.3: 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: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -3939,6 +4116,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamroller@3.1.5: resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} engines: {node: '>=8.0'} @@ -3969,6 +4149,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4035,9 +4219,24 @@ packages: tiny-worker@2.3.0: resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} + tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} + tinycon@0.6.8: 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: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -4234,6 +4433,11 @@ packages: vfile@6.0.1: 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: resolution: {integrity: sha512-3uSvsMwDVFZRitqoWHj0t4137Kz7UynnJeq1EZlRW7e25h2068fyIZX4ORCCOAkfp1FklGxJNVJBkBOD+PZIew==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4285,6 +4489,31 @@ packages: postcss: 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: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -4347,6 +4576,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5412,6 +5646,10 @@ snapshots: '@types/content-disposition@0.5.8': {} + '@types/cookie-parser@1.4.7': + dependencies: + '@types/express': 4.17.21 + '@types/cookie@0.4.1': {} '@types/cookiejar@2.1.5': {} @@ -5427,10 +5665,16 @@ snapshots: dependencies: '@types/node': 20.14.11 + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 20.14.11 + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 + '@types/ejs@3.1.5': {} + '@types/estree@1.0.5': {} '@types/express-serve-static-core@4.19.3': @@ -5469,6 +5713,8 @@ snapshots: dependencies: '@types/sizzle': 2.3.8 + '@types/js-cookie@3.0.6': {} + '@types/jsdom@21.1.7': dependencies: '@types/node': 20.14.11 @@ -5479,6 +5725,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonminify@0.4.3': {} + '@types/jsonwebtoken@9.0.6': dependencies: '@types/node': 20.14.11 @@ -5504,6 +5752,12 @@ snapshots: '@types/lockfile@1.0.4': {} + '@types/lodash.clonedeep@4.5.9': + dependencies: + '@types/lodash': 4.17.7 + + '@types/lodash@4.17.7': {} + '@types/markdown-it@14.1.1': dependencies: '@types/linkify-it': 5.0.0 @@ -5517,6 +5771,8 @@ snapshots: '@types/methods@1.1.4': {} + '@types/mime-types@2.1.4': {} + '@types/mime@1.3.5': {} '@types/mocha@10.0.7': {} @@ -5539,6 +5795,10 @@ snapshots: '@types/prop-types@15.7.12': {} + '@types/proxy-addr@2.0.3': + dependencies: + '@types/node': 20.14.11 + '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} @@ -5552,6 +5812,8 @@ snapshots: '@types/prop-types': 15.7.12 csstype: 3.1.3 + '@types/resolve@1.20.6': {} + '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -5589,12 +5851,16 @@ snapshots: '@types/node': 20.14.11 minipass: 4.2.8 + '@types/tinycon@0.6.5': {} + '@types/tough-cookie@4.0.5': {} '@types/underscore@1.11.15': {} '@types/unist@3.0.2': {} + '@types/unorm@1.3.31': {} + '@types/url-join@4.0.3': {} '@types/web-bluetooth@0.0.20': {} @@ -5694,6 +5960,39 @@ snapshots: vite: 5.3.4(@types/node@20.14.11) 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': dependencies: '@babel/parser': 7.24.7 @@ -5927,6 +6226,8 @@ snapshots: asap@2.0.6: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.6.3 @@ -6012,6 +6313,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + cache-content-type@1.0.1: dependencies: mime-types: 2.1.35 @@ -6045,6 +6348,14 @@ snapshots: 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: dependencies: ansi-styles: 3.2.1 @@ -6060,6 +6371,8 @@ snapshots: character-entities-legacy@3.0.0: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -6217,6 +6530,8 @@ snapshots: dependencies: mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-equal@1.0.1: {} deep-is@0.1.4: {} @@ -6710,6 +7025,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + esutils@2.0.3: {} eta@3.4.0: {} @@ -6726,6 +7045,18 @@ snapshots: - supports-color - 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): dependencies: express: 4.19.2 @@ -6921,6 +7252,8 @@ snapshots: get-caller-file@2.0.5: {} + get-func-name@2.0.2: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -6933,6 +7266,8 @@ snapshots: get-stream@6.0.1: {} + get-stream@8.0.1: {} + get-symbol-description@1.0.2: dependencies: call-bind: 1.0.7 @@ -7178,6 +7513,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@5.0.0: {} + i18next-browser-languagedetector@8.0.0: dependencies: '@babel/runtime': 7.24.7 @@ -7306,6 +7643,8 @@ snapshots: dependencies: call-bind: 1.0.7 + is-stream@3.0.0: {} + is-string@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -7339,6 +7678,8 @@ snapshots: jose@5.6.3: {} + jquery@3.7.1: {} + js-cookie@3.0.5: {} js-tokens@4.0.0: {} @@ -7564,6 +7905,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + lower-case@2.0.2: dependencies: tslib: 2.6.3 @@ -7609,6 +7954,8 @@ snapshots: merge-descriptors@1.0.1: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -7645,6 +7992,8 @@ snapshots: mime@2.6.0: {} + mimic-fn@4.0.0: {} + mimic-response@3.1.0: {} mimic-response@4.0.0: {} @@ -7758,6 +8107,10 @@ snapshots: normalize-url@8.0.1: {} + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + nwsapi@2.2.10: {} object-assign@4.1.1: {} @@ -7826,6 +8179,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + only@0.0.2: {} openapi-backend@5.10.6: @@ -7916,6 +8273,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-to-regexp@0.1.7: {} @@ -7924,6 +8283,10 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + + pathval@2.0.0: {} + perfect-debounce@1.0.0: {} picocolors@1.0.1: {} @@ -8276,8 +8639,12 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.1 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + sinon@18.0.0: dependencies: '@sinonjs/commons': 3.0.1 @@ -8363,10 +8730,14 @@ snapshots: sprintf-js@1.1.3: {} + stackback@0.0.2: {} + statuses@1.5.0: {} statuses@2.0.1: {} + std-env@3.7.0: {} + streamroller@3.1.5: dependencies: date-format: 4.0.14 @@ -8411,6 +8782,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@3.0.0: {} + strip-json-comments@3.1.1: {} superagent@8.1.2: @@ -8504,8 +8877,16 @@ snapshots: esm: 3.2.25 optional: true + tinybench@2.8.0: {} + tinycon@0.6.8: {} + tinypool@1.0.0: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.0: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -8714,6 +9095,23 @@ snapshots: unist-util-stringify-position: 4.0.0 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)): dependencies: chokidar: 3.6.0 @@ -8789,6 +9187,39 @@ snapshots: - typescript - 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: {} vue-demi@0.14.8(vue@3.4.31(typescript@5.5.3)): @@ -8846,6 +9277,11 @@ snapshots: dependencies: 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: {} workerpool@6.5.1: {} diff --git a/src/node/db/API.ts b/src/node/db/API.ts index 9ce84fc5138..460e90c6c10 100644 --- a/src/node/db/API.ts +++ b/src/node/db/API.ts @@ -19,59 +19,61 @@ * limitations under the License. */ -const Changeset = require('../../static/js/Changeset'); -const ChatMessage = require('../../static/js/ChatMessage'); -const CustomError = require('../utils/customError'); -const padManager = require('./PadManager'); -const padMessageHandler = require('../handler/PadMessageHandler'); -const readOnlyManager = require('./ReadOnlyManager'); -const groupManager = require('./GroupManager'); -const authorManager = require('./AuthorManager'); -const sessionManager = require('./SessionManager'); -const exportHtml = require('../utils/ExportHtml'); -const exportTxt = require('../utils/ExportTxt'); -const importHtml = require('../utils/ImportHtml'); -const cleanText = require('./Pad').cleanText; -const PadDiff = require('../utils/padDiff'); -const {checkValidRev, isInt} = require('../utils/checkValidRev'); +import {deserializeOps} from '../../static/js/Changeset'; +import ChatMessage from '../../static/js/ChatMessage'; +import CustomError from '../utils/customError'; +import * as padManager from './PadManager'; +import * as padMessageHandler from '../handler/PadMessageHandler'; +import {getPadId, getReadOnlyId} from './ReadOnlyManager'; +import * as groupManager from './GroupManager'; +import * as authorManager from './AuthorManager'; +import * as sessionManager from './SessionManager'; +import {getPadHTML} from '../utils/ExportHtml'; +import {getTXTFromAtext} from '../utils/ExportTxt'; +import {setPadHTML} from '../utils/ImportHtml'; +import {cleanText} from './Pad' +import PadDiff from '../utils/padDiff'; +import {checkValidRev, isInt} from '../utils/checkValidRev'; +import {Builder} from "../../static/js/Builder"; +import {Attribute} from "../../static/js/types/Attribute"; /* ******************** * GROUP FUNCTIONS **** ******************** */ -exports.listAllGroups = groupManager.listAllGroups; -exports.createGroup = groupManager.createGroup; -exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; -exports.deleteGroup = groupManager.deleteGroup; -exports.listPads = groupManager.listPads; -exports.createGroupPad = groupManager.createGroupPad; +export const listAllGroups = groupManager.listAllGroups; +export const createGroup = groupManager.createGroup; +export const createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; +export const deleteGroup = groupManager.deleteGroup; +export const listPads = groupManager.listPads; +export const createGroupPad = groupManager.createGroupPad; /* ******************** * PADLIST FUNCTION *** ******************** */ -exports.listAllPads = padManager.listAllPads; +export const listAllPads = padManager.listAllPads; /* ******************** * AUTHOR FUNCTIONS *** ******************** */ -exports.createAuthor = authorManager.createAuthor; -exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; -exports.getAuthorName = authorManager.getAuthorName; -exports.listPadsOfAuthor = authorManager.listPadsOfAuthor; -exports.padUsers = padMessageHandler.padUsers; -exports.padUsersCount = padMessageHandler.padUsersCount; +export const createAuthor = authorManager.createAuthor; +export const createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; +export const getAuthorName = authorManager.getAuthorName; +export const listPadsOfAuthor = authorManager.listPadsOfAuthor; +export const padUsers = padMessageHandler.padUsers; +export const padUsersCount = padMessageHandler.padUsersCount; /* ******************** * SESSION FUNCTIONS ** ******************** */ -exports.createSession = sessionManager.createSession; -exports.deleteSession = sessionManager.deleteSession; -exports.getSessionInfo = sessionManager.getSessionInfo; -exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup; -exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; +export const createSession = sessionManager.createSession; +export const deleteSession = sessionManager.deleteSession; +export const getSessionInfo = sessionManager.getSessionInfo; +export const listSessionsOfGroup = sessionManager.listSessionsOfGroup; +export const listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; /* *********************** * 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); 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 if (rev !== undefined) { rev = checkValidRev(rev); @@ -155,7 +157,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {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 if (rev !== undefined) { 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 - const text = exportTxt.getTXTFromAtext(pad, pad.atext); + const text = getTXTFromAtext(pad, pad.atext); return {text}; }; @@ -200,7 +202,7 @@ Example returns: * @param {String} authorId the id of the author, defaulting to empty string * @returns {Promise} */ -exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise => { +export const setText = async (padID: string, text?: string, authorId: string = ''): Promise => { // text is required if (typeof text !== 'string') { 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} 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 if (typeof text !== 'string') { 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 @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) { rev = checkValidRev(rev); } @@ -264,7 +266,7 @@ exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }> } // get the html of this revision - let html = await exportHtml.getPadHTML(pad, rev); + let html = await getPadHTML(pad, rev); // wrap the HTML html = `${html}`; @@ -283,7 +285,7 @@ Example returns: @param {String} html the html of the pad @param {String} authorId the id of the author, defaulting to empty string */ -exports.setHTML = async (padID: string, html:string|object, authorId = '') => { +export const setHTML = async (padID: string, html:string|object, authorId = '') => { // html string is required if (typeof html !== 'string') { throw new CustomError('html is not a string', 'apierror'); @@ -294,13 +296,13 @@ exports.setHTML = async (padID: string, html:string|object, authorId = '') => { // add a new changeset with the new html to the pad try { - await importHtml.setPadHTML(pad, cleanText(html), authorId); + await setPadHTML(pad, cleanText(html), authorId); } catch (e) { throw new CustomError('HTML is malformed', 'apierror'); } // 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} 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 < 0) { throw new CustomError('start is below zero', 'apierror'); @@ -374,7 +376,7 @@ Example returns: @param {String} authorID the id of the author @param {Number} time the timestamp of the chat-message */ -exports.appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => { +export const appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -404,7 +406,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.getRevisionsCount = async (padID: string) => { +export const getRevisionsCount = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); return {revisions: pad.getHeadRevisionNumber()}; @@ -419,7 +421,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.getSavedRevisionsCount = async (padID: string) => { +export const getSavedRevisionsCount = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsNumber()}; @@ -434,7 +436,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.listSavedRevisions = async (padID: string) => { +export const listSavedRevisions = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsList()}; @@ -450,7 +452,7 @@ Example returns: @param {String} padID the id of the pad @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 if (rev !== undefined) { rev = checkValidRev(rev); @@ -483,7 +485,7 @@ Example returns: @param {String} padID the id of the pad @return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad */ -exports.getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => { +export const getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => { // get the pad const pad = await getPadSafe(padID, true); const lastEdited = await pad.getLastEdit(); @@ -501,7 +503,7 @@ Example returns: @param {String} text the initial text of the pad @param {String} authorId the id of the author, defaulting to empty string */ -exports.createPad = async (padID: string, text: string, authorId = '') => { +export const createPad = async (padID: string, text: string, authorId = '') => { if (padID) { // ensure there is no $ in the padID if (padID.indexOf('$') !== -1) { @@ -527,7 +529,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @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); await pad.remove(); }; @@ -543,7 +545,7 @@ exports.deletePad = async (padID: string) => { @param {Number} rev the revision number, defaulting to the latest revision @param {String} authorId the id of the author, defaulting to empty string */ -exports.restoreRevision = async (padID: string, rev: number, authorId = '') => { +export const restoreRevision = async (padID: string, rev: number, authorId = '') => { // check if rev is a number if (rev === undefined) { throw new CustomError('rev is not defined', 'apierror'); @@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => { const oldText = pad.text(); atext.text += '\n'; - const eachAttribRun = (attribs: string[], func:Function) => { + const eachAttribRun = (attribs: string, func:Function) => { let textIndex = 0; const newTextStart = 0; const newTextEnd = atext.text.length; - for (const op of Changeset.deserializeOps(attribs)) { + for (const op of deserializeOps(attribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { 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 - const builder = Changeset.builder(oldText.length); + const builder = new Builder(oldText.length); // assemble each line into the builder - eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => { + eachAttribRun(atext.attribs, (start: number, end: number, attribs: Attribute[]) => { builder.insert(atext.text.substring(start, end), attribs); }); @@ -588,7 +590,7 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => { if (lastNewlinePos < 0) { builder.remove(oldText.length - 1, 0); } 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); } @@ -610,7 +612,7 @@ Example returns: @param {String} destinationID the id of the destination pad @param {Boolean} force whether to overwrite the destination pad if it exists */ -exports.copyPad = async (sourceID: string, destinationID: string, force: boolean) => { +export const copyPad = async (sourceID: string, destinationID: string, force: boolean) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); }; @@ -628,7 +630,7 @@ Example returns: @param {Boolean} force whether to overwrite the destination pad if it exists @param {String} authorId the id of the author, defaulting to empty string */ -exports.copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId = '') => { +export const copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId: string = '') => { const pad = await getPadSafe(sourceID, true); await pad.copyPadWithoutHistory(destinationID, force, authorId); }; @@ -645,7 +647,7 @@ Example returns: @param {String} destinationID the id of the destination pad @param {Boolean} force whether to overwrite the destination pad if it exists */ -exports.movePad = async (sourceID: string, destinationID: string, force:boolean) => { +export const movePad = async (sourceID: string, destinationID: string, force:boolean) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); await pad.remove(); @@ -660,12 +662,12 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @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 await getPadSafe(padID, true); // get the readonlyId - const readOnlyID = await readOnlyManager.getReadOnlyId(padID); + const readOnlyID = await getReadOnlyId(padID); return {readOnlyID}; }; @@ -679,9 +681,9 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} roID the readonly id of the pad */ -exports.getPadID = async (roID: string) => { +export const getPadID = async (roID: string) => { // get the PadId - const padID = await readOnlyManager.getPadId(roID); + const padID = await getPadId(roID); if (padID == null) { throw new CustomError('padID does not exist', 'apierror'); } @@ -699,7 +701,7 @@ Example returns: @param {String} padID the id of the pad @param {Boolean} publicStatus the public status of the pad */ -exports.setPublicStatus = async (padID: string, publicStatus: boolean|string) => { +export const setPublicStatus = async (padID: string, publicStatus: boolean|string) => { // ensure this is a group pad checkGroupPad(padID, 'publicStatus'); @@ -723,7 +725,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @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 checkGroupPad(padID, 'publicStatus'); @@ -741,7 +743,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.listAuthorsOfPad = async (padID: string) => { +export const listAuthorsOfPad = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); const authorIDs = pad.getAllAuthors(); @@ -773,7 +775,7 @@ Example returns: @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. padMessageHandler.handleCustomMessage(padID, msg); }; @@ -786,7 +788,7 @@ Example returns: {"code":0,"message":"ok","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 @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 const pad = await getPadSafe(padID, true); return {chatHead: pad.chatHead}; @@ -825,7 +827,7 @@ Example returns: @param {Number} startRev the start revision number @param {Number} endRev the end revision number */ -exports.createDiffHTML = async (padID: string, startRev: number, endRev: number) => { +export const createDiffHTML = async (padID: string, startRev: number, endRev: number) => { // check if startRev is a number if (startRev !== undefined) { startRev = checkValidRev(startRev); @@ -901,7 +903,7 @@ const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:stri } // check if the pad exists - const exists = await padManager.doesPadExists(padID); + const exists = await padManager.doesPadExist(padID); if (!exists && shouldExist) { // does not exist, but should diff --git a/src/node/db/AuthorManager.ts b/src/node/db/AuthorManager.ts index 2f4e7d751af..f5a3b9212a4 100644 --- a/src/node/db/AuthorManager.ts +++ b/src/node/db/AuthorManager.ts @@ -19,12 +19,12 @@ * limitations under the License. */ -const db = require('./DB'); -const CustomError = require('../utils/customError'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); +import {get, getSub, set, setSub} from './DB'; +import CustomError from '../utils/customError'; +import {aCallFirst} from '../../static/js/pluginfw/hooks'; +import {padUtils, randomString} from '../../static/js/pad_utils' -exports.getColorPalette = () => [ +export const getColorPalette = () => [ '#ffc7c7', '#fff1c7', '#e3ffc7', @@ -95,8 +95,8 @@ exports.getColorPalette = () => [ * Checks if the author exists * @param {String} authorID The id of the author */ -exports.doesAuthorExist = async (authorID: string) => { - const author = await db.get(`globalAuthor:${authorID}`); +export const doesAuthorExist = async (authorID: string) => { + const author = await get(`globalAuthor:${authorID}`); return author != null; }; @@ -105,7 +105,6 @@ exports.doesAuthorExist = async (authorID: string) => { exported for backwards compatibility @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) => { // try to map to an author - const author = await db.get(`${mapperkey}:${mapper}`); + const author = await get(`${mapperkey}:${mapper}`); if (author == null) { // there is no author with this mapper, so create one const author = await exports.createAuthor(null); // create the token2author relation - await db.set(`${mapperkey}:${mapper}`, author.authorID); + await set(`${mapperkey}:${mapper}`, author.authorID); // return the author return author; @@ -131,7 +130,8 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => { // there is an author with this mapper // update the timestamp of this author - await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now()); + // @ts-ignore + await db!.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now()); // return the author return {authorID: author}; @@ -142,7 +142,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => { * @param {String} token The token of the author * @return {Promise} */ -const getAuthor4Token = async (token: string) => { +const _getAuthor4Token = async (token: string) => { const author = await mapAuthorWithDBKey('token2author', token); // return only the sub value authorID @@ -155,9 +155,9 @@ const getAuthor4Token = async (token: string) => { * @param {Object} user * @return {Promise<*>} */ -exports.getAuthorId = async (token: string, user: object) => { +export const getAuthorId = async (token: string, user: object) => { 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); return authorId; }; @@ -168,10 +168,10 @@ exports.getAuthorId = async (token: string, user: object) => { * @deprecated Use `getAuthorId` instead. * @param {String} token The token */ -exports.getAuthor4Token = async (token: string) => { - warnDeprecated( +export const getAuthor4Token = async (token: string) => { + padUtils.warnDeprecated( '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} 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); if (name) { // set the name of this author - await exports.setAuthorName(author.authorID, name); + await setAuthorName(author.authorID, name); } return author; @@ -195,19 +195,20 @@ exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string) * Internal function that creates the database entry for an 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 const author = `a.${randomString(16)}`; // create the globalAuthors db entry const authorObj = { - colorId: Math.floor(Math.random() * (exports.getColorPalette().length)), + colorId: Math.floor(Math.random() * (getColorPalette().length)), name, timestamp: Date.now(), }; // set the global author db entry - await db.set(`globalAuthor:${author}`, authorObj); + // @ts-ignore + await db!.set(`globalAuthor:${author}`, authorObj); return {authorID: author}; }; @@ -216,48 +217,49 @@ exports.createAuthor = async (name: string) => { * Returns the Author Obj of the author * @param {String} author The id of the author */ -exports.getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`); +export const getAuthor = async (author: string) => await get(`globalAuthor:${author}`); /** * Returns the color Id of the author * @param {String} author The id of the author */ -exports.getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']); +export const getAuthorColorId = async (author: string) => await getSub(`globalAuthor:${author}`, ['colorId']) as number; /** * Sets the color Id of the author * @param {String} author The id of the author * @param {String} colorId The color id of the author */ -exports.setAuthorColorId = async (author: string, colorId: string) => await db.setSub( - `globalAuthor:${author}`, ['colorId'], colorId); +export const setAuthorColorId = async (author: string, colorId: string) => await setSub( + // @ts-ignore + `globalAuthor:${author}`, ['colorId'], colorId); /** * Returns the name of the author * @param {String} author The id of the author */ -exports.getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']); +export const getAuthorName = async (author: string) => await getSub(`globalAuthor:${author}`, ['name']); /** * Sets the name of the author * @param {String} author The id of the author * @param {String} name The name of the author */ -exports.setAuthorName = async (author: string, name: string) => await db.setSub( +export const setAuthorName = async (author: string, name: string) => await setSub( `globalAuthor:${author}`, ['name'], name); /** * Returns an array of all pads this author contributed to * @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: * (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 */ // get the globalAuthor - const author = await db.get(`globalAuthor:${authorID}`); + const author = await get(`globalAuthor:${authorID}`); if (author == null) { // author does not exist @@ -275,9 +277,9 @@ exports.listPadsOfAuthor = async (authorID: string) => { * @param {String} authorID The id of the author * @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 - const author = await db.get(`globalAuthor:${authorID}`); + const author = await get(`globalAuthor:${authorID}`); 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 // 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} padID The id of the pad the author contributes to */ -exports.removePad = async (authorID: string, padID: string) => { - const author = await db.get(`globalAuthor:${authorID}`); +export const removePad = async (authorID: string, padID?: string) => { + const author = await get(`globalAuthor:${authorID}`); if (author == null) return; if (author.padIDs != null) { // remove pad from author - delete author.padIDs[padID]; - await db.set(`globalAuthor:${authorID}`, author); + delete author.padIDs[padID!]; + await set(`globalAuthor:${authorID}`, author); } }; diff --git a/src/node/db/DB.ts b/src/node/db/DB.ts index 542da673544..610227246dc 100644 --- a/src/node/db/DB.ts +++ b/src/node/db/DB.ts @@ -22,39 +22,68 @@ */ import ueberDB from 'ueberdb2'; -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; import log4js from 'log4js'; -const stats = require('../stats') +import {measuredCollection} from '../stats'; const logger = log4js.getLogger('ueberDB'); /** * 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 */ -exports.init = async () => { - exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger); - await exports.db.init(); - if (exports.db.metrics != null) { - for (const [metric, value] of Object.entries(exports.db.metrics)) { +export const init = async () => { + db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger); + await db.init(); + if (db.metrics != null) { + for (const [metric, value] of Object.entries(db.metrics)) { 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)); - } -}; +} + +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); +} -exports.shutdown = async (hookName: string, context: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(); exports.db = null; logger.log('Database closed'); }; + diff --git a/src/node/db/GroupManager.ts b/src/node/db/GroupManager.ts index 0524c4edaa0..49904aade8a 100644 --- a/src/node/db/GroupManager.ts +++ b/src/node/db/GroupManager.ts @@ -19,18 +19,18 @@ * limitations under the License. */ -const CustomError = require('../utils/customError'); +import CustomError from '../utils/customError'; const randomString = require('../../static/js/pad_utils').randomString; -const db = require('./DB'); -const padManager = require('./PadManager'); -const sessionManager = require('./SessionManager'); +import {get, getSub, remove, set, setSub} from './DB'; +import {doesPadExist, getPad} from './PadManager'; +import {deleteSession} from './SessionManager'; /** * Lists all groups * @return {Promise<{groupIDs: string[]}>} The ids of all groups */ -exports.listAllGroups = async () => { - let groups = await db.get('groups'); +export const listAllGroups = async (): Promise<{ groupIDs: string[]; }> => { + let groups = await get('groups'); groups = groups || {}; const groupIDs = Object.keys(groups); @@ -42,8 +42,8 @@ exports.listAllGroups = async () => { * @param {String} groupID The id of the group * @return {Promise} Resolves when the group is deleted */ -exports.deleteGroup = async (groupID: string): Promise => { - const group = await db.get(`group:${groupID}`); +export const deleteGroup = async (groupID: string): Promise => { + const group = await get(`group:${groupID}`); // ensure group exists if (group == null) { @@ -53,28 +53,28 @@ exports.deleteGroup = async (groupID: string): Promise => { // iterate through all pads of this group and delete them (in parallel) await Promise.all(Object.keys(group.pads).map(async (padId) => { - const pad = await padManager.getPad(padId); + const pad = await getPad(padId); await pad.remove(); })); // Delete associated sessions in parallel. This should be done before deleting the group2sessions // record because deleting a session updates the group2sessions record. - const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {}; + const {sessionIDs = {}} = await get(`group2sessions:${groupID}`) || {}; await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => { - await sessionManager.deleteSession(sessionId); + await deleteSession(sessionId); })); await Promise.all([ - db.remove(`group2sessions:${groupID}`), + remove(`group2sessions:${groupID}`), // UeberDB's setSub() method atomically reads the record, updates the appropriate property, and // writes the result. Setting a property to `undefined` deletes that property (JSON.stringify() // ignores such properties). - db.setSub('groups', [groupID], undefined), - ...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)), + setSub('groups', [groupID], undefined), + ...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. - await db.remove(`group:${groupID}`); + await remove(`group:${groupID}`); }; /** @@ -82,9 +82,9 @@ exports.deleteGroup = async (groupID: string): Promise => { * @param {String} groupID the id of the group to delete * @return {Promise} Resolves to true if the group exists */ -exports.doesGroupExist = async (groupID: string) => { +export const doesGroupExist = async (groupID: string) => { // try to get the group entry - const group = await db.get(`group:${groupID}`); + const group = await get(`group:${groupID}`); return (group != null); }; @@ -93,13 +93,13 @@ exports.doesGroupExist = async (groupID: string) => { * Creates a 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)}`; - 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 // the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates // the appropriate property, and writes the result. - await db.setSub('groups', [groupID], 1); + await setSub('groups', [groupID], 1); return {groupID}; }; @@ -108,20 +108,20 @@ exports.createGroup = async () => { * @param groupMapper the mapper of the group * @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') { throw new CustomError('groupMapper is not a string', 'apierror'); } - const groupID = await db.get(`mapper2group:${groupMapper}`); - if (groupID && await exports.doesGroupExist(groupID)) return {groupID}; - const result = await exports.createGroup(); + const groupID = await get(`mapper2group:${groupMapper}`) as string; + if (groupID && await doesGroupExist(groupID)) return {groupID}; + const result = await createGroup(); 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 // deleted. Although the core Etherpad API does not support multiple mappings for the same // group, the database record does support multiple mappings in case a plugin decides to extend // the core Etherpad functionality. (It's also easy to implement it this way.) - db.setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1), + setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1), ]); return result; }; @@ -134,19 +134,19 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => { * @param {String} authorId The id of the author * @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad */ -exports.createGroupPad = async (groupID: string, padName: string, text: string, authorId: string = ''): Promise<{ padID: string; }> => { +export const createGroupPad = async (groupID: string, padName: string, text: string, authorId: string = ''): Promise<{ padID: string; }> => { // create the padID const padID = `${groupID}$${padName}`; // ensure group exists - const groupExists = await exports.doesGroupExist(groupID); + const groupExists = await doesGroupExist(groupID); if (!groupExists) { throw new CustomError('groupID does not exist', 'apierror'); } // ensure pad doesn't exist already - const padExists = await padManager.doesPadExists(padID); + const padExists = await doesPadExist(padID); if (padExists) { // pad exists already @@ -154,10 +154,10 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string, } // create the pad - await padManager.getPad(padID, text, authorId); + await getPad(padID, text, authorId); // 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}; }; @@ -167,8 +167,8 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string, * @param {String} groupID The id of the group * @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group */ -exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => { - const exists = await exports.doesGroupExist(groupID); +export const listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => { + const exists = await doesGroupExist(groupID); // ensure the group exists if (!exists) { @@ -176,7 +176,7 @@ exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => { } // 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); return {padIDs}; diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index fa4af994d57..2f5237695f5 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -7,24 +7,27 @@ import {MapArrayType} from "../types/MapType"; * The pad object, defined with joose */ -const AttributeMap = require('../../static/js/AttributeMap'); -const Changeset = require('../../static/js/Changeset'); -const ChatMessage = require('../../static/js/ChatMessage'); -const AttributePool = require('../../static/js/AttributePool'); -const Stream = require('../utils/Stream'); +import AttributeMap from '../../static/js/AttributeMap'; +import {applyToAText, checkRep, copyAText, deserializeOps, makeAText, makeSplice, opsFromAText, pack, unpack} from '../../static/js/Changeset'; +import ChatMessage from '../../static/js/ChatMessage'; +import AttributePool from '../../static/js/AttributePool'; +import Stream from '../utils/Stream'; const assert = require('assert').strict; -const db = require('./DB'); -const settings = require('../utils/Settings'); -const authorManager = require('./AuthorManager'); -const padManager = require('./PadManager'); -const padMessageHandler = require('../handler/PadMessageHandler'); -const groupManager = require('./GroupManager'); -const CustomError = require('../utils/customError'); -const readOnlyManager = require('./ReadOnlyManager'); -const randomString = require('../utils/randomstring'); -const hooks = require('../../static/js/pluginfw/hooks'); -const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); -const promises = require('../utils/promises'); +import {get, set, setSub, remove} from './DB'; +import settings from '../utils/Settings'; +import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager'; +import {doesPadExist, getPad} from './PadManager'; +import {kickSessionsFromPad} from '../handler/PadMessageHandler'; +import {doesGroupExist} from './GroupManager'; +import CustomError from '../utils/customError'; +import {getReadOnlyId} from './ReadOnlyManager'; +import {randomString} from '../utils/randomstring'; +import {aCallAll} from '../../static/js/pluginfw/hooks'; +import {padUtils} from "../../static/js/pad_utils"; +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 @@ -32,19 +35,18 @@ const promises = require('../utils/promises'); * @param {String} txt The text to clean * @returns {String} The cleaned text */ -exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n') +export const cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .replace(/\t/g, ' ') .replace(/\xa0/g, ' '); class Pad { - private db: Database; - private atext: AText; - private pool: APool; - private head: number; - private chatHead: number; + atext: AText; + pool: AttributePool; + head: number; + chatHead: number; private publicStatus: boolean; - private id: string; + id: string; private savedRevisions: any[]; /** * @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 * own database table, or to validate imported pad data before it is written to the database. */ - constructor(id:string, database = db) { - this.db = database; - this.atext = Changeset.makeAText('\n'); + constructor(id:string) { + this.atext = makeAText('\n'); this.pool = new AttributePool(); this.head = -1; this.chatHead = -1; @@ -93,13 +94,13 @@ class Pad { * @param {String} authorId The id of the author * @return {Promise} */ - async appendRevision(aChangeset:AChangeSet, authorId = '') { - const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); + async appendRevision(aChangeset:string, authorId: string = ''): Promise { + const newAText = applyToAText(aChangeset, this.atext, this.pool); if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs && this.head !== -1) { return this.head; } - Changeset.copyAText(newAText, this.atext); + copyAText(newAText, this.atext); const newRev = ++this.head; @@ -121,16 +122,17 @@ class Pad { }, }), this.saveToDatabase(), - authorId && authorManager.addPad(authorId, this.id), - hooks.aCallAll(hook, { + authorId && addPad(authorId, this.id), + aCallAll(hook, { pad: this, authorId, get author() { - warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); - return this.authorId; + padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); + return authorId; }, set author(authorId) { - 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.head === 0 ? {} : { @@ -192,8 +194,8 @@ class Pad { * Returns all authors that worked on this pad * @return {[String]} The id of authors who contributed to this pad */ - getAllAuthors() { - const authorIds = []; + getAllAuthors(): string[] { + const authorIds: string[] = []; for (const key in this.pool.numToAttrib) { if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') { @@ -215,22 +217,23 @@ class Pad { ]); const apool = this.apool(); 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; } async getRevision(revNum: number) { - return await this.db.get(`pad:${this.id}:revs:${revNum}`); + return await get(`pad:${this.id}:revs:${revNum}`); } async getAllAuthorColors() { const authorIds = this.getAllAuthors(); - const returnTable:MapArrayType = {}; - const colorPalette = authorManager.getColorPalette(); + const returnTable:MapArrayType = {}; + const colorPalette = getColorPalette(); 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 + // @ts-ignore returnTable[authorId] = colorPalette[colorId] || colorId; }))); @@ -293,7 +296,7 @@ class Pad { (!ins && start > 0 && orig[start - 1] === '\n'); if (!willEndWithNewline) ins += '\n'; if (ndel === 0 && ins.length === 0) return; - const changeset = Changeset.makeSplice(orig, start, ndel, ins); + const changeset = makeSplice(orig, start, ndel, ins); 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 * applicable. */ - async appendText(newText:string, authorId = '') { + async appendText(newText:string, authorId: string = '') { 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 * `msgOrText.time` instead. */ - async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) { + async appendChatMessage(msgOrText: string| ChatMessage, authorId = null, time = null) { const msg = msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); 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 // `displayName` property will be populated with the current value when the message is read // from the database. + // @ts-ignore this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}), this.saveToDatabase(), ]); @@ -347,11 +351,11 @@ class Pad { * @param {number} entryNum - ID of the desired chat message. * @returns {?ChatMessage} */ - async getChatMessage(entryNum: number) { - const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`); + async getChatMessage(entryNum: number): Promise { + const entry = await get(`pad:${this.id}:chat:${entryNum}`); if (entry == null) return null; const message = ChatMessage.fromObject(entry); - message.displayName = await authorManager.getAuthorName(message.authorId); + message.displayName = await getAuthorName(message.authorId!); return message; } @@ -362,7 +366,7 @@ class Pad { * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open * interval as is typical in code. */ - async getChatMessages(start: string, end: number) { + async getChatMessages(start: number, end: number) { const entries = 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 - const value = await this.db.get(`pad:${this.id}`); + const value = await get(`pad:${this.id}`); // if this pad exists, load it if (value != null) { @@ -389,14 +393,14 @@ class Pad { } else { if (text == null) { 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}`); 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 hooks.aCallAll('padLoad', {pad: this}); + await aCallAll('padLoad', {pad: this}); } async copy(destinationID: string, force: boolean) { @@ -415,8 +419,8 @@ class Pad { await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); const copyRecord = async (keySuffix: string) => { - const val = await this.db.get(`pad:${this.id}${keySuffix}`); - await db.set(`pad:${destinationID}${keySuffix}`, val); + const val = await get(`pad:${this.id}${keySuffix}`); + await set(`pad:${destinationID}${keySuffix}`, val); }; const promises = (function* () { @@ -427,22 +431,24 @@ class Pad { yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`)); // @ts-ignore yield this.copyAuthorInfoToDestinationPad(destinationID); - if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); + if (destGroupID) yield setSub(`group:${destGroupID}`, ['pads', destinationID], 1); }).call(this); for (const p of new Stream(promises).batch(100).buffer(99)) await p; // Initialize the new pad (will update the listAllPads cache) - const dstPad = await padManager.getPad(destinationID, null); + const dstPad = await getPad(destinationID, null); // let the plugins know the pad was copied - await hooks.aCallAll('padCopy', { + await aCallAll('padCopy', { get originalPad() { - 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; }, get destinationID() { - warnDeprecated( + padUtils.warnDeprecated( 'padCopy destinationID context property is deprecated; use dstPad.id instead'); + // @ts-ignore return this.dstPad.id; }, srcPad: this, @@ -457,7 +463,7 @@ class Pad { if (destinationID.indexOf('$') >= 0) { destGroupID = destinationID.split('$')[0]; - const groupExists = await groupManager.doesGroupExist(destGroupID); + const groupExists = await doesGroupExist(destGroupID); // group does not exist if (!groupExists) { @@ -469,7 +475,7 @@ class Pad { async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) { // 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 if (typeof force === 'string') { @@ -485,7 +491,7 @@ class Pad { } // exists and forcing - const pad = await padManager.getPad(destinationID); + const pad = await getPad(destinationID); await pad.remove(); } } @@ -493,7 +499,7 @@ class Pad { async copyAuthorInfoToDestinationPad(destinationID: string) { // add the new sourcePad to all authors who contributed to the old one await Promise.all(this.getAllAuthors().map( - (authorID) => authorManager.addPad(authorID, destinationID))); + (authorID) => addPad(authorID, destinationID))); } async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') { @@ -510,18 +516,18 @@ class Pad { // Group pad? Add it to the group's list 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 - const dstPad = await padManager.getPad(destinationID, '\n', authorId); + const dstPad = await getPad(destinationID, '\n', authorId); dstPad.pool = this.pool.clone(); const oldAText = this.atext; // based on Changeset.makeSplice - const assem = Changeset.smartOpAssembler(); - for (const op of Changeset.opsFromAText(oldAText)) assem.append(op); + const assem = new SmartOpAssembler(); + for (const op of opsFromAText(oldAText)) assem.append(op); assem.endDocument(); // 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 // all atributes present on the source pad - const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); - dstPad.appendRevision(changeset, authorId); + const changeset = pack(oldLength, newLength, assem.toString(), newText); + await dstPad.appendRevision(changeset, authorId); - await hooks.aCallAll('padCopy', { + await aCallAll('padCopy', { get originalPad() { - 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; }, get destinationID() { - warnDeprecated( + padUtils.warnDeprecated( 'padCopy destinationID context property is deprecated; use dstPad.id instead'); + // @ts-ignore return this.dstPad.id; }, srcPad: this, @@ -558,7 +566,7 @@ class Pad { const p = []; // kick everyone from this pad - padMessageHandler.kickSessionsFromPad(padID); + kickSessionsFromPad(padID); // delete all relations - the original code used async.parallel but // none of the operations except getting the group depended on callbacks @@ -569,41 +577,44 @@ class Pad { if (padID.indexOf('$') >= 0) { // it is a group pad const groupID = padID.substring(0, padID.indexOf('$')); - const group = await db.get(`group:${groupID}`); + const group = await get(`group:${groupID}`); // remove the pad entry delete group.pads[padID]; // set the new value - p.push(db.set(`group:${groupID}`, group)); + p.push(set(`group:${groupID}`, group)); } // remove the readonly entries - p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => { - await db.remove(`readonly2pad:${readonlyID}`); + p.push(getReadOnlyId(padID).then(async (readonlyID: string) => { + await remove(`readonly2pad:${readonlyID}`); })); - p.push(db.remove(`pad2readonly:${padID}`)); + p.push(remove(`pad2readonly:${padID}`)); // delete all chat messages - p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => { - await this.db.remove(`pad:${this.id}:chat:${i}`, null); + // @ts-ignore + p.push(timesLimit(this.chatHead + 1, 500, async (i: string) => { + await remove(`pad:${this.id}:chat:${i}`, null); })); // delete all revisions - p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => { - await this.db.remove(`pad:${this.id}:revs:${i}`, null); + // @ts-ignore + 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 this.getAllAuthors().forEach((authorId) => { - p.push(authorManager.removePad(authorId, padID)); + p.push(removePad(authorId, padID)); }); // delete the pad entry and delete pad from padManager - p.push(padManager.removePad(padID)); - p.push(hooks.aCallAll('padRemove', { + p.push(removePad(padID)); + p.push(aCallAll('padRemove', { get padID() { - 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; }, pad: this, @@ -617,7 +628,7 @@ class Pad { 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 for (const i in this.savedRevisions) { if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { @@ -626,12 +637,13 @@ class Pad { } // build the saved revision object - const savedRevision:MapArrayType = {}; - savedRevision.revNum = revNum; - savedRevision.savedById = savedById; - savedRevision.label = label || `Revision ${revNum}`; - savedRevision.timestamp = Date.now(); - savedRevision.id = randomString(10); + const savedRevision:PadRevision = { + revNum, + savedById, + label: label || `Revision ${revNum}`, + timestamp: Date.now(), + id: randomString(10), + }; // save this new saved revision this.savedRevisions.push(savedRevision); @@ -706,7 +718,7 @@ class Pad { } }) .batch(100).buffer(99); - let atext = Changeset.makeAText('\n'); + let atext = makeAText('\n'); for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) { try { assert(authorId != null); @@ -717,10 +729,10 @@ class Pad { assert(timestamp > 0); assert(changeset != null); assert.equal(typeof changeset, 'string'); - Changeset.checkRep(changeset); - const unpacked = Changeset.unpack(changeset); + checkRep(changeset); + const unpacked = unpack(changeset); let text = atext.text; - for (const op of Changeset.deserializeOps(unpacked.ops)) { + for (const op of deserializeOps(unpacked.ops)) { if (['=', '-'].includes(op.opcode)) { assert(text.length >= 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()); } - atext = Changeset.applyToAText(changeset, atext, pool); + atext = applyToAText(changeset, atext, pool); if (isKeyRev) assert.deepEqual(keyAText, atext); } catch (err:any) { err.message = `(pad ${this.id} revision ${r}) ${err.message}`; @@ -759,7 +771,7 @@ class Pad { .batch(100).buffer(99); for (const p of chats) await p; - await hooks.aCallAll('padCheck', {pad: this}); + await aCallAll('padCheck', {pad: this}); } } -exports.Pad = Pad; +export default Pad diff --git a/src/node/db/PadManager.ts b/src/node/db/PadManager.ts index 54dbbf0892e..33e987ae452 100644 --- a/src/node/db/PadManager.ts +++ b/src/node/db/PadManager.ts @@ -20,11 +20,9 @@ */ import {MapArrayType} from "../types/MapType"; -import {PadType} from "../types/PadType"; - -const CustomError = require('../utils/customError'); -const Pad = require('../db/Pad'); -const db = require('./DB'); +import CustomError from '../utils/customError'; +import Pad from '../db/Pad'; +import {findKeys, get, remove} from './DB'; const settings = require('../utils/Settings'); /** @@ -74,7 +72,7 @@ const padList = new class { async getPads() { if (!this._loaded) { this._loaded = (async () => { - const dbData = await db.findKeys('pad:*', '*:*:*'); + const dbData = await findKeys('pad:*', '*:*:*'); if (dbData == null) return; 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 * applicable). */ -exports.getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise => { +export const getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise => { // check if this is a valid padId - if (!exports.isValidPadId(id)) { + if (!isValidPadId(id)) { 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 - pad = new Pad.Pad(id); + pad = new Pad(id); // initialize the pad await pad.init(text, authorId); @@ -143,7 +141,7 @@ exports.getPad = async (id: string, text?: string|null, authorId:string|null = ' return pad; }; -exports.listAllPads = async () => { +export const listAllPads = async () => { const padIDs = await padList.getPads(); return {padIDs}; @@ -153,14 +151,13 @@ exports.listAllPads = async () => { // checks if a pad exists -exports.doesPadExist = async (padId: string) => { - const value = await db.get(`pad:${padId}`); +// alias for backwards compatibility +export const doesPadExist = async (padId: string) => { + const value = await get(`pad:${padId}`); 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 @@ -172,9 +169,9 @@ const padIdTransforms = [ ]; // 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) { - const exists = await exports.doesPadExist(padId); + const exists = await doesPadExist(padId); if (exists) { return padId; @@ -192,19 +189,19 @@ exports.sanitizePadId = async (padId: string) => { 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. */ -exports.removePad = async (padId: string) => { - const p = db.remove(`pad:${padId}`); - exports.unloadPad(padId); +export const removePad = async (padId: string) => { + const p = await remove(`pad:${padId}`); + unloadPad(padId); padList.removePad(padId); await p; }; // removes a pad from the cache -exports.unloadPad = (padId: string) => { +export const unloadPad = (padId: string) => { globalPads.remove(padId); }; diff --git a/src/node/db/ReadOnlyManager.ts b/src/node/db/ReadOnlyManager.ts index 23639d6656b..093f6eed92e 100644 --- a/src/node/db/ReadOnlyManager.ts +++ b/src/node/db/ReadOnlyManager.ts @@ -20,8 +20,8 @@ */ -const db = require('./DB'); -const randomString = require('../utils/randomstring'); +import {get, set} from './DB'; +import {randomString} from '../utils/randomstring'; /** @@ -29,23 +29,23 @@ const randomString = require('../utils/randomstring'); * @param {String} id the pad's id * @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 * @param {String} padId the id of the pad * @return {String} the read only id */ -exports.getReadOnlyId = async (padId:string) => { +export const getReadOnlyId = async (padId:string): Promise => { // 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 if (readOnlyId == null) { readOnlyId = `r.${randomString(16)}`; await Promise.all([ - db.set(`pad2readonly:${padId}`, readOnlyId), - db.set(`readonly2pad:${readOnlyId}`, padId), + set(`pad2readonly:${padId}`, readOnlyId), + set(`readonly2pad:${readOnlyId}`, padId), ]); } @@ -57,19 +57,23 @@ exports.getReadOnlyId = async (padId:string) => { * @param {String} readOnlyId read only id * @return {String} the padId */ -exports.getPadId = async (readOnlyId:string) => await db.get(`readonly2pad:${readOnlyId}`); +export const getPadId = async (readOnlyId:string): Promise => await get(`readonly2pad:${readOnlyId}`) as string; /** * returns the padId and readonlyPadId in an object for any id * @param {String} id read only id or real pad id * @return {Object} an object with the padId and readonlyPadId */ -exports.getIds = async (id:string) => { - const readonly = exports.isReadOnlyId(id); +export const getIds = async (id:string): Promise<{ + readOnlyPadId: string, + padId: string, + readonly: boolean +}> => { + const readonly = isReadOnlyId(id); // Might be null, if this is an unknown read-only id - const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); - const padId = readonly ? await exports.getPadId(id) : id; + const readOnlyPadId = readonly ? id : await getReadOnlyId(id); + const padId = readonly ? await getPadId(id) : id; return {readOnlyPadId, padId, readonly}; }; diff --git a/src/node/db/SecurityManager.ts b/src/node/db/SecurityManager.ts index 326bf36595d..4ce8536f398 100644 --- a/src/node/db/SecurityManager.ts +++ b/src/node/db/SecurityManager.ts @@ -21,16 +21,16 @@ import {UserSettingsObject} from "../types/UserSettingsObject"; -const authorManager = require('./AuthorManager'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const padManager = require('./PadManager'); -const readOnlyManager = require('./ReadOnlyManager'); -const sessionManager = require('./SessionManager'); -const settings = require('../utils/Settings'); -const webaccess = require('../hooks/express/webaccess'); -const log4js = require('log4js'); +import {getAuthorId} from './AuthorManager'; +import {callAll} from '../../static/js/pluginfw/hooks.js'; +import {doesPadExist, getPad} from './PadManager'; +import {getPadId, isReadOnlyId} from './ReadOnlyManager'; +import {findAuthorID} from './SessionManager'; +import settings from '../utils/Settings'; +import {normalizeAuthzLevel} from '../hooks/express/webaccess'; +import log4js from 'log4js'; const authLogger = log4js.getLogger('auth'); -const {padutils} = require('../../static/js/pad_utils'); +import {padUtils as padutils} from '../../static/js/pad_utils'; const DENY = Object.freeze({accessStatus: 'deny'}); @@ -57,7 +57,14 @@ const DENY = Object.freeze({accessStatus: 'deny'}); * @param {Object} userSettings * @return {DENY|{accessStatus: String, authorID: String}} */ -exports.checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject) => { + + +type CheckAccessStat = { + accessStatus: string; + authorID?: string; +} + +export const checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject): Promise => { if (!padID) { authLogger.debug('access denied: missing padID'); return DENY; @@ -65,9 +72,9 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u let canCreate = !settings.editOnly; - if (readOnlyManager.isReadOnlyId(padID)) { + if (isReadOnlyId(padID)) { canCreate = false; - padID = await readOnlyManager.getPadId(padID); + padID = await getPadId(padID); if (padID == null) { authLogger.debug('access denied: read-only pad ID for a pad that does not exist'); 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 // settings.requireAuthorization is false. const padAuthzs = userSettings.padAuthorizations || {}; - const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]); + const level = normalizeAuthzLevel(padAuthzs[padID]); if (!level) { authLogger.debug('access denied: unauthorized'); return DENY; @@ -98,18 +105,18 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u // allow plugins to deny access 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'); return DENY; } - const padExists = await padManager.doesPadExist(padID); + const padExists = await doesPadExist(padID); if (!padExists && !canCreate) { authLogger.debug('access denied: user attempted to create a pad, which is prohibited'); return DENY; } - const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie); + const sessionAuthorID = await findAuthorID(padID.split('$')[0], sessionCookie); if (settings.requireSession && !sessionAuthorID) { authLogger.debug('access denied: HTTP API session is required'); return DENY; @@ -122,7 +129,7 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u const grant = { accessStatus: 'grant', - authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings), + authorID: sessionAuthorID || await getAuthorId(token, userSettings), }; if (!padID.includes('$')) { @@ -139,7 +146,7 @@ exports.checkAccess = async (padID:string, sessionCookie:string, token:string, u return grant; } - const pad = await padManager.getPad(padID); + const pad = await getPad(padID); if (!pad.getPublicStatus() && sessionAuthorID == null) { authLogger.debug('access denied: must have an HTTP API session to access private group pads'); diff --git a/src/node/db/SessionManager.ts b/src/node/db/SessionManager.ts index c0e43a6592c..922eb615b97 100644 --- a/src/node/db/SessionManager.ts +++ b/src/node/db/SessionManager.ts @@ -20,12 +20,25 @@ * limitations under the License. */ -const CustomError = require('../utils/customError'); -const promises = require('../utils/promises'); -const randomString = require('../utils/randomstring'); -const db = require('./DB'); -const groupManager = require('./GroupManager'); -const authorManager = require('./AuthorManager'); +import CustomError from '../utils/customError'; +import {firstSatisfies} from '../utils/promises'; +import {randomString} from '../utils/randomstring'; +import {get, remove, set, setSub} from './DB'; +import {doesGroupExist} from './GroupManager'; +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. @@ -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 * 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; /* * 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 sessionInfoPromises = sessionIDs.map(async (id) => { try { - return await exports.getSessionInfo(id); + return await getSessionInfo(id); } catch (err:any) { if (err.message === 'sessionID does not exist') { console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`); @@ -79,7 +92,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => { groupID: string; validUntil: number; }|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; return sessionInfo.authorID; }; @@ -89,9 +102,9 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => { * @param {String} sessionID The id of the session * @return {Promise} 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 - const session = await db.get(`session:${sessionID}`); + const session = await get(`session:${sessionID}`) as Session; return (session != null); }; @@ -102,15 +115,15 @@ exports.doesSessionExist = async (sessionID: string) => { * @param {Number} validUntil The unix timestamp when the session should expire * @return {Promise<{sessionID: string}>} the id of the new session */ -exports.createSession = async (groupID: string, authorID: string, validUntil: number) => { +export const createSession = async (groupID: string, authorID: string, validUntil: number) => { // check if the group exists - const groupExists = await groupManager.doesGroupExist(groupID); + const groupExists = await doesGroupExist(groupID); if (!groupExists) { throw new CustomError('groupID does not exist', 'apierror'); } // check if the author exists - const authorExists = await authorManager.doesAuthorExist(authorID); + const authorExists = await doesAuthorExist(authorID); if (!authorExists) { 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)}`; // 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 // so that the state is consistent. await Promise.all([ // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object // property, and writes the result. - db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1), - db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1), + setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1), + setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1), ]); return {sessionID}; @@ -163,9 +176,9 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu * @param {String} sessionID The id of the session * @return {Promise} the sessioninfos */ -exports.getSessionInfo = async (sessionID:string) => { +export const getSessionInfo = async (sessionID:string): Promise => { // 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) { // session does not exist @@ -181,9 +194,9 @@ exports.getSessionInfo = async (sessionID:string) => { * @param {String} sessionID The id of the session * @return {Promise} Resolves when the session is deleted */ -exports.deleteSession = async (sessionID:string) => { +export const deleteSession = async (sessionID:string): Promise => { // ensure that the session exists - const session = await db.get(`session:${sessionID}`); + const session = await get(`session:${sessionID}`) as Session; if (session == null) { 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 // property, and writes the result. Setting a property to `undefined` deletes that property // (JSON.stringify() ignores such properties). - db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined), - db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined), + setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined), + setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined), ]); // Delete the session record after updating group2sessions and author2sessions so that the state // 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 * @return {Promise} The sessioninfos of all sessions of this group */ -exports.listSessionsOfGroup = async (groupID: string) => { +export const listSessionsOfGroup = async (groupID: string) => { // check that the group exists - const exists = await groupManager.doesGroupExist(groupID); + const exists = await doesGroupExist(groupID); if (!exists) { throw new CustomError('groupID does not exist', 'apierror'); } - const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`); - return sessions; + return await listSessionsWithDBKey(`group2sessions:${groupID}`); }; /** @@ -226,9 +238,9 @@ exports.listSessionsOfGroup = async (groupID: string) => { * @param {String} authorID The id of the author * @return {Promise} The sessioninfos of all sessions of this author */ -exports.listSessionsOfAuthor = async (authorID: string) => { +export const listSessionsOfAuthor = async (authorID: string): Promise => { // check that the author exists - const exists = await authorManager.doesAuthorExist(authorID); + const exists = await doesAuthorExist(authorID); if (!exists) { 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 * @return {Promise<*>} */ -const listSessionsWithDBKey = async (dbkey: string) => { +const listSessionsWithDBKey = async (dbkey: string): Promise => { // get the group2sessions entry - const sessionObject = await db.get(dbkey); + const sessionObject = await get(dbkey); const sessions = sessionObject ? sessionObject.sessionIDs : null; // iterate through the sessions and get the sessioninfos for (const sessionID of Object.keys(sessions || {})) { try { - sessions[sessionID] = await exports.getSessionInfo(sessionID); + sessions[sessionID] = await getSessionInfo(sessionID); } catch (err:any) { if (err.name === 'apierror') { console.warn(`Found bad session ${sessionID} in ${dbkey}`); diff --git a/src/node/db/SessionStore.ts b/src/node/db/SessionStore.ts index 0b398efad6d..28b49494ebc 100644 --- a/src/node/db/SessionStore.ts +++ b/src/node/db/SessionStore.ts @@ -1,9 +1,9 @@ 'use strict'; -const DB = require('./DB'); +import {get, remove, set} from './DB'; const Store = require('@etherpad/express-session').Store; -const log4js = require('log4js'); -const util = require('util'); +import log4js from 'log4js'; +import util from 'util'; 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. * Ignored if the cookie does not expire. */ - constructor(refresh = null) { + constructor(refresh: number | null = null) { super(); this._refresh = refresh; // Maps session ID to an object with the following properties: @@ -65,12 +65,12 @@ class SessionStore extends Store { } async _write(sid: string, sess: any) { - await DB.set(`sessionstorage:${sid}`, sess); + await set(`sessionstorage:${sid}`, sess); } async _get(sid: string) { logger.debug(`GET ${sid}`); - const s = await DB.get(`sessionstorage:${sid}`); + const s = await get(`sessionstorage:${sid}`); return await this._updateExpirations(sid, s); } @@ -84,7 +84,7 @@ class SessionStore extends Store { logger.debug(`DESTROY ${sid}`); clearTimeout((this._expirations.get(sid) || {}).timeout); 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 @@ -111,4 +111,4 @@ for (const m of ['get', 'set', 'destroy', 'touch']) { SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]); } -module.exports = SessionStore; +export default SessionStore diff --git a/src/node/eejs/index.ts b/src/node/eejs/index.ts index b7f2cf99836..1adc64b9819 100644 --- a/src/node/eejs/index.ts +++ b/src/node/eejs/index.ts @@ -20,54 +20,61 @@ * require("./index").require("./path/to/template.ejs") */ -const ejs = require('ejs'); -const fs = require('fs'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const path = require('path'); -const resolve = require('resolve'); -const settings = require('../utils/Settings'); +import ejs from 'ejs'; +import fs from 'fs'; +import {callAll} from '../../static/js/pluginfw/hooks.js'; +import path from 'path'; +import resolve from 'resolve'; +import settings from '../utils/Settings'; import {pluginInstallPath} from '../../static/js/pluginfw/installer' 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: [], block_stack: [], file_stack: [], 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) => { - exports.info.__output_stack.push(exports.info.__output); - exports.info.__output = b; +export const _init = (b: any, recursive: boolean) => { + info.__output_stack.push(info.__output); + info.__output = b; }; -exports._exit = (b:any, recursive:boolean) => { - exports.info.__output = exports.info.__output_stack.pop(); +export const _exit = (b:any, recursive:boolean) => { + info.__output = info.__output_stack.pop(); }; -exports.begin_block = (name:string) => { - exports.info.block_stack.push(name); - exports.info.__output_stack.push(exports.info.__output.get()); - exports.info.__output.set(''); +export const begin_block = (name:string) => { + info.block_stack.push(name); + info.__output_stack.push(info.__output.get()); + info.__output.set(''); }; -exports.end_block = () => { - const name = exports.info.block_stack.pop(); - const renderContext = exports.info.args[exports.info.args.length - 1]; - const content = exports.info.__output.get(); - exports.info.__output.set(exports.info.__output_stack.pop()); +export const end_block = () => { + const name = info.block_stack.pop(); + const renderContext = info.args[info.args.length - 1]; + const content = info.__output.get(); + info.__output.set(info.__output_stack.pop()); const args = {content, renderContext}; - hooks.callAll(`eejsBlock_${name}`, args); - exports.info.__output.set(exports.info.__output.get().concat(args.content)); + callAll(`eejsBlock_${name}`, args); + info.__output.set(info.__output.get().concat(args.content)); }; -exports.require = (name:string, args:{ +export const requireP = (name:string, args:{ e?: Function, - require?: Function, -}, mod:{ + require?: string, +}, mod?:{ filename:string, paths:string[], }) => { @@ -76,7 +83,7 @@ exports.require = (name:string, args:{ let basedir = __dirname; let paths:string[] = []; - if (exports.info.file_stack.length) { + if (info.file_stack.length) { basedir = path.dirname(getCurrentFile().path); } if (mod) { @@ -93,21 +100,29 @@ exports.require = (name:string, args:{ const ejspath = resolve.sync(name, {paths, basedir, extensions: ['.html', '.ejs']}); - args.e = exports; - args.require = require; - - const cache = settings.maxAge !== 0; + args.e = { + // @ts-ignore + _init, + _exit, + begin_block, + end_block, + // Include other methods as necessary + }; + // @ts-ignore + args.require = requireP; + + const cache: boolean = settings.maxAge !== 0; const template = cache && templateCache.get(ejspath) || ejs.compile( '<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' + `${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`, {filename: ejspath}); if (cache) templateCache.set(ejspath, template); - exports.info.args.push(args); - exports.info.file_stack.push({path: ejspath}); + info.args.push(args); + info.file_stack.push({path: ejspath}); const res = template(args); - exports.info.file_stack.pop(); - exports.info.args.pop(); + info.file_stack.pop(); + info.args.pop(); return res; }; diff --git a/src/node/handler/APIHandler.ts b/src/node/handler/APIHandler.ts index 5feb74eb965..b0de202d026 100644 --- a/src/node/handler/APIHandler.ts +++ b/src/node/handler/APIHandler.ts @@ -21,15 +21,15 @@ import {MapArrayType} from "../types/MapType"; -const api = require('../db/API'); -const padManager = require('../db/PadManager'); +import * as api from '../db/API'; +import {sanitizePadId} from '../db/PadManager'; import createHTTPError from 'http-errors'; -import {Http2ServerRequest, Http2ServerResponse} from "node:http2"; +import {Http2ServerRequest} from "node:http2"; import {publicKeyExported} from "../security/OAuth2Provider"; import {jwtVerify} from "jose"; import {apikey} from './APIKeyHandler' // a list of all functions -const version:MapArrayType = {}; +export const version:MapArrayType = {}; version['1'] = { createGroup: [], @@ -142,10 +142,9 @@ version['1.3.0'] = { }; // set the latest available API version here -exports.latestApiVersion = '1.3.0'; +export const latestApiVersion = '1.3.0'; // exports the versions so it can be used by the new Swagger endpoint -exports.version = version; type APIFields = { @@ -163,7 +162,7 @@ type APIFields = { * @param fields the params of the called function * @param req express request object */ -exports.handle = async function (apiVersion: string, functionName: string, fields: APIFields, +export const handle = async function (apiVersion: string, functionName: string, fields: APIFields, req: Http2ServerRequest) { // say goodbye if this is an unknown API version if (!(apiVersion in version)) { @@ -197,19 +196,20 @@ exports.handle = async function (apiVersion: string, functionName: string, field // sanitize any padIDs before continuing if (fields.padID) { - fields.padID = await padManager.sanitizePadId(fields.padID); + fields.padID = await sanitizePadId(fields.padID); } // there was an 'else' here before - removed it to ensure // that this sanitize step can't be circumvented by forcing // the first branch to be taken if (fields.padName) { - fields.padName = await padManager.sanitizePadId(fields.padName); + fields.padName = await sanitizePadId(fields.padName); } // put the function parameters in an array // @ts-ignore - const functionParams = version[apiVersion][functionName].map((field) => fields[field]); + const functionParams = version[apiVersion][functionName].map((field: string) => fields[field]); // call the api function + // @ts-ignore return api[functionName].apply(this, functionParams); }; diff --git a/src/node/handler/APIKeyHandler.ts b/src/node/handler/APIKeyHandler.ts index b4e70f6e4b0..7ddbc0dd664 100644 --- a/src/node/handler/APIKeyHandler.ts +++ b/src/node/handler/APIKeyHandler.ts @@ -1,15 +1,15 @@ -const absolutePaths = require('../utils/AbsolutePaths'); +import {makeAbsolute} from '../utils/AbsolutePaths'; import fs from 'fs'; import log4js from 'log4js'; -const randomString = require('../utils/randomstring'); -const argv = require('../utils/Cli').argv; -const settings = require('../utils/Settings'); +import {randomString} from '../utils/randomstring'; +import {argvP} from "../utils/Cli"; +import settings from '../utils/Settings'; const apiHandlerLogger = log4js.getLogger('APIHandler'); // ensure we have an apikey export let apikey:string|null = null; -const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); +const apikeyFilename = makeAbsolute(argvP.apikey || './APIKEY.txt'); if(settings.authenticationMethod === 'apikey') { diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 3909496075d..826fa969e8b 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -21,35 +21,39 @@ import {MapArrayType} from "../types/MapType"; -const AttributeMap = require('../../static/js/AttributeMap'); -const padManager = require('../db/PadManager'); -const Changeset = require('../../static/js/Changeset'); -const ChatMessage = require('../../static/js/ChatMessage'); -const AttributePool = require('../../static/js/AttributePool'); -const AttributeManager = require('../../static/js/AttributeManager'); -const authorManager = require('../db/AuthorManager'); -const {padutils} = require('../../static/js/pad_utils'); -const readOnlyManager = require('../db/ReadOnlyManager'); -const settings = require('../utils/Settings'); -const securityManager = require('../db/SecurityManager'); -const plugins = require('../../static/js/pluginfw/plugin_defs.js'); +import AttributeMap from '../../static/js/AttributeMap'; +import {doesPadExist, getPad, sanitizePadId} from '../db/PadManager'; +import {checkRep, cloneAText, compose, deserializeOps, follow, identity, inverse, makeAText, makeSplice, moveOpsToNewPool, mutateTextLines, oldLen, prepareForWire, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset'; +import ChatMessage from '../../static/js/ChatMessage'; +import AttributePool from '../../static/js/AttributePool'; +import AttributeManager from '../../static/js/AttributeManager'; +import {getAuthor, getAuthorColorId, getAuthorName, getColorPalette, setAuthorColorId, setAuthorName} from '../db/AuthorManager'; +import {padUtils} from '../../static/js/pad_utils'; +import {getIds} from '../db/ReadOnlyManager'; +import settings from '../utils/Settings'; +import {checkAccess} from '../db/SecurityManager'; +import {pluginDefs} from '../../static/js/pluginfw/plugin_defs.js'; import log4js from 'log4js'; const messageLogger = log4js.getLogger('message'); const accessLogger = log4js.getLogger('access'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const stats = require('../stats') +import {aCallAll, deprecationNotices} from '../../static/js/pluginfw/hooks.js'; +import {measuredCollection} from '../stats'; const assert = require('assert').strict; import {RateLimiterMemory} from 'rate-limiter-flexible'; 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"; -const webaccess = require('../hooks/express/webaccess'); -const { checkValidRev } = require('../utils/checkValidRev'); +import Pad from "../db/Pad"; +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 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 newErr = new Error(`${pfx}${err.message}`, {cause: err}); @@ -60,7 +64,7 @@ const addContextToError = (err:any, pfx:string) => { return err; }; -exports.socketio = () => { +export const socketio = () => { // 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 // 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). * - rev: The last revision that was sent to the client. */ -const sessioninfos:MapArrayType = {}; -exports.sessioninfos = sessioninfos; +export const sessioninfos:MapArrayType = {}; -stats.gauge('totalUsers', () => socketio ? socketio.engine.clientsCount : 0); -stats.gauge('activePads', () => { +measuredCollection.gauge('totalUsers', () => _socketio ? _socketio.engine.clientsCount : 0); +measuredCollection.gauge('activePads', () => { const padIds = new Set(); for (const {padId} of Object.values(sessioninfos)) { if (!padId) continue; @@ -108,7 +111,7 @@ class Channels { * @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. */ - 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._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 * @param socket_io The Socket */ -exports.setSocketIO = (socket_io:any) => { - socketio = socket_io; +export const setSocketIO = (socket_io:any) => { + _socketio = socket_io; }; /** * Handles the connection of a new user * @param socket the socket.io Socket object for the new connection from the client */ -exports.handleConnect = (socket:any) => { - stats.meter('connects').mark(); +export const handleConnect = (socket:any) => { + measuredCollection.meter('connects').mark(); // Initialize sessioninfos for this new session sessioninfos[socket.id] = {}; @@ -161,23 +164,23 @@ exports.handleConnect = (socket:any) => { /** * 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 if (_getRoomSockets(padID).length === 0) return; // 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 * @param socket the socket.io Socket object for the client */ -exports.handleDisconnect = async (socket:any) => { - stats.meter('disconnects').mark(); +export const handleDisconnect = async (socket:any) => { + measuredCollection.meter('disconnects').mark(); const session = sessioninfos[socket.id]; delete sessioninfos[socket.id]; // session.padId can be nullish if the user disconnects before sending CLIENT_READY. @@ -196,12 +199,12 @@ exports.handleDisconnect = async (socket:any) => { data: { type: 'USER_LEAVE', userInfo: { - colorId: await authorManager.getAuthorColorId(session.author), + colorId: await getAuthorColorId(session.author), userId: session.author, }, }, }); - await hooks.aCallAll('userLeave', { + await aCallAll('userLeave', { ...session, // For backwards compatibility. authorId: session.author, readOnly: session.readonly, @@ -214,7 +217,7 @@ exports.handleDisconnect = async (socket:any) => { * @param socket the socket.io Socket object for 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'; if (env === 'production') { @@ -223,7 +226,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => { } catch (err) { messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` + 'limiting that happens edit the rateLimit values in settings.json'); - stats.meter('rateLimited').mark(); + measuredCollection.meter('rateLimited').mark(); socket.emit('message', {disconnect: 'rateLimited'}); 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 - if (!(await padManager.doesPadExist(thisSession.auth.padID))) { - thisSession.auth.padID = await padManager.sanitizePadId(thisSession.auth.padID); + if (!(await doesPadExist(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.readOnlyPadId = padIds.readOnlyPadId; 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 // 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 {accessStatus, authorID} = - await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user); + await checkAccess(auth.padID, auth.sessionID, auth.token, user); if (accessStatus !== 'grant') { socket.emit('message', {accessStatus}); throw new Error('access denied'); @@ -303,16 +306,16 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => { }, socket, get client() { - padutils.warnDeprecated( + padUtils.warnDeprecated( 'the `client` context property for the handleMessageSecurity and handleMessage hooks ' + 'is deprecated; use the `socket` property instead'); return this.socket; }, }; - for (const res of await hooks.aCallAll('handleMessageSecurity', context)) { + for (const res of await aCallAll('handleMessageSecurity', context)) { switch (res) { case true: - padutils.warnDeprecated( + padUtils.warnDeprecated( 'returning `true` from a `handleMessageSecurity` hook function is deprecated; ' + 'return "permitOnce" instead'); 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. - if ((await hooks.aCallAll('handleMessage', context)).some((m: null|string) => m == null)) { + if ((await aCallAll('handleMessage', context)).some((m: null|string) => m == null)) { return; } @@ -345,7 +348,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => { try { switch (type) { case 'USER_CHANGES': - stats.counter('pendingEdits').inc(); + measuredCollection.counter('pendingEdits').inc(); await padChannels.enqueue(thisSession.padId, {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 {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); }; @@ -397,14 +400,14 @@ const handleSaveRevisionMessage = async (socket:any, message: string) => { * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = (msg: typeof ChatMessage, sessionID: string) => { - if (msg.data.type === 'CUSTOM') { +export const handleCustomObjectMessage = (msg: ClientVarMessage, sessionID: string) => { + if ("data" in msg && msg.type != 'CLIENT_VARS' && msg.data.type === 'CUSTOM') { if (sessionID) { // a sessionID is targeted: directly to this sessionID - socketio.sockets.socket(sessionID).emit('message', msg); + _socketio.sockets.socket(sessionID).emit('message', msg); } else { // 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 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 msg = { type: 'COLLABROOM', @@ -424,7 +427,7 @@ exports.handleCustomMessage = (padID: string, msgString:string) => { 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 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 {padId, author: authorId} = sessioninfos[socket.id]; // Don't trust the user-supplied values. chatMessage.time = Date.now(); 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 * 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); padId = mt instanceof ChatMessage ? puId : padId; - const pad = await padManager.getPad(padId, null, message.authorId); - await hooks.aCallAll('chatNewMessage', {message, pad, padId}); + const pad = await getPad(padId!, null, message.authorId); + await aCallAll('chatNewMessage', {message, pad, padId}); // 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. const promise = pad.appendChatMessage(message); - message.displayName = await authorManager.getAuthorName(message.authorId); - socketio.sockets.in(padId).emit('message', { + message.displayName = await getAuthorName(message.authorId!); + _socketio.sockets.in(padId).emit('message', { type: 'COLLABROOM', data: {type: 'CHAT_MESSAGE', message}, }); @@ -479,7 +482,7 @@ const handleGetChatMessages = async (socket:any, {data: {start, end}}:any) => { const count = end - start; if (count < 0 || count > 100) throw new Error(`invalid number of messages: ${count}`); 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 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 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; if (newName == null) throw new Error('missing newName'); 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 const p = Promise.all([ - authorManager.setAuthorColorId(author, colorId), - authorManager.setAuthorName(author, name), + setAuthorColorId(author, colorId), + setAuthorName(author, name!), ]); 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 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 - stats.counter('pendingEdits').dec(); + measuredCollection.counter('pendingEdits').dec(); // The client might disconnect between our callbacks. We should still // 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'); // Measure time to process edit - const stopWatch = stats.timer('edits').start(); + const stopWatch = measuredCollection.timer('edits').start(); try { const {data: {baseRev, apool, changeset}} = message; if (baseRev == null) throw new Error('missing baseRev'); if (apool == null) throw new Error('missing apool'); if (changeset == null) throw new Error('missing changeset'); 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 - Changeset.checkRep(changeset); + checkRep(changeset); // 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 change or add 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 // 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 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); if (changeset === c && thisSession.author === authorId) { // 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 // client) are relative to revision r - 1. The follow function // rebases "changeset" so that it is relative to revision r // 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(); - if (Changeset.oldLen(rebasedChangeset) !== prevText.length) { + if (oldLen(rebasedChangeset) !== prevText.length) { throw new Error( `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 // changeset has a net effect. 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. 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); } @@ -665,10 +668,10 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => { socket.emit('message', {type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}}); thisSession.rev = newRev; if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev); - await exports.updatePadClients(pad); + await updatePadClients(pad); } catch (err:any) { socket.emit('message', {disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); + measuredCollection.meter('failedChangesets').mark(); messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` + `(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`); } 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 const roomSockets = _getRoomSockets(pad.id); if (roomSockets.length === 0) return; @@ -710,7 +713,7 @@ exports.updatePadClients = async (pad: PadType) => { const revChangeset = revision.changeset; const currentTime = revision.meta.timestamp; - const forWire = Changeset.prepareForWire(revChangeset, pad.pool); + const forWire = prepareForWire(revChangeset, pad.pool); const msg = { type: 'COLLABROOM', data: { @@ -738,14 +741,14 @@ exports.updatePadClients = async (pad: PadType) => { /** * Copied from the Etherpad Source Code. Don't know what this method does excatly... */ -const _correctMarkersInPad = (atext: AText, apool: APool) => { +const _correctMarkersInPad = (atext: AText, apool: AttributePool) => { const text = atext.text; // collect char positions of line markers (e.g. bullets) in new atext // that aren't at the start of a line const badMarkers = []; 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 hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a)); if (hasMarker) { @@ -767,7 +770,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => { // create changeset that removes these bad markers offset = 0; - const builder = Changeset.builder(text.length); + const builder = new Builder(text.length); badMarkers.forEach((pos) => { builder.keepText(text.substring(offset, pos)); @@ -785,26 +788,29 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => { * @param socket the socket.io Socket object for 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]; if (sessionInfo == null) throw new Error('client disconnected'); 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 || {}; + // @ts-ignore if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) { messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`); + // @ts-ignore authorColorId = null; } await Promise.all([ - authorName && authorManager.setAuthorName(sessionInfo.author, authorName), - authorColorId && authorManager.setAuthorColorId(sessionInfo.author, authorColorId), + authorName && setAuthorName(sessionInfo.author, authorName), + // @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 - 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) const authors = pad.getAllAuthors(); @@ -813,12 +819,9 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); // get all author data out of the database (in parallel) - const historicalAuthorData:MapArrayType<{ - name: string; - colorId: string; - }> = {}; + const historicalAuthorData: HistoricalAuthorData = {}; await Promise.all(authors.map(async (authorId: string) => { - const author = await authorManager.getAuthor(authorId); + const author = await getAuthor(authorId); if (!author) { messageLogger.error(`There is no author for authorId: ${authorId}. ` + '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 changesets:MapArrayType = {}; - let startNum = message.client_rev + 1; + let startNum = message.client_rev! + 1; let endNum = pad.getHeadRevisionNumber() + 1; const headNum = pad.getHeadRevisionNumber(); @@ -901,7 +904,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { // return pending changesets 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', data: {type: 'CLIENT_RECONNECT', headRev: pad.getHeadRevisionNumber(), @@ -923,11 +926,11 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { } else { // This is a normal first connect 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 try { - atext = Changeset.cloneAText(pad.atext); - const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + atext = cloneAText(pad.atext); + const attribsForWire = prepareForWire(atext.attribs, pad.pool); apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; } 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 // would open a security hole 1 swedish mile wide... - const clientVars:MapArrayType = { - skinName: settings.skinName, - skinVariants: settings.skinVariants, - randomVersionString: settings.randomVersionString, + const clientVars:ClientVarPayload = { + skinName: settings.skinName!, + skinVariants: settings.skinVariants!, + randomVersionString: settings.randomVersionString!, accountPrivs: { maxRevisions: 100, }, @@ -958,7 +961,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { rev: pad.getHeadRevisionNumber(), time: currentTime, }, - colorPalette: authorManager.getColorPalette(), + colorPalette: getColorPalette(), clientIp: '127.0.0.1', userColor: authorColorId, padId: sessionInfo.auth.padID, @@ -979,8 +982,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { sofficeAvailable: settings.sofficeAvailable(), exportAvailable: settings.exportAvailable(), plugins: { - plugins: plugins.plugins, - parts: plugins.parts, + plugins: pluginDefs.getPlugins(), + parts: pluginDefs.getParts(), }, indentationOnNewLine: settings.indentationOnNewLine, scrollWhenFocusLineIsOutOfViewport: { @@ -997,7 +1000,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, }, initialChangesets: [], // FIXME: REMOVE THIS SHIT, - mode: process.env.NODE_ENV + mode: process.env.NODE_ENV! }; // 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 - 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 for (const msg of messages) { @@ -1052,7 +1055,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => { if (authorId == null) return; // 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) { messageLogger.error( `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); })); - await hooks.aCallAll('userJoin', { + await aCallAll('userJoin', { authorId: sessionInfo.author, displayName: authorName, 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 */ -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 (!Number.isInteger(granularity)) throw new Error('granularity is not an integer'); 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'); const end = start + (100 * granularity); 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(); if (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 * 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(); // 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; 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()); - Changeset.mutateTextLines(forwards, lines.textlines); + new AttributionLinesMutator(forwards, lines.alines, pad.apool()); + mutateTextLines(forwards, lines.textlines); - const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); - const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool); + const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool); const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 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 * 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 let atext; if (revNum >= 0) { atext = await pad.getInternalRevisionAText(revNum); } else { - atext = Changeset.makeAText('\n'); + atext = makeAText('\n'); } return { - textlines: Changeset.splitTextLines(atext.text), - alines: Changeset.splitAttributionLines(atext.attribs, atext.text), + textlines: splitTextLines(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 * 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 const headNum = pad.getHeadRevisionNumber(); 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++) { const cs = changesets[r]; - changeset = Changeset.compose(changeset, cs, pool); + changeset = compose(changeset, cs, pool); } return changeset; } catch (e) { @@ -1247,7 +1250,7 @@ const composePadChangesets = async (pad: PadType, startNum: number, endNum: numb }; 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 // it does here, but synchronously to avoid a race condition. This code will have to change when // we update to socket.io v3. @@ -1263,21 +1266,21 @@ const _getRoomSockets = (padID: string) => { /** * Get the number of users in a pad */ -exports.padUsersCount = (padID:string) => ({ +export const padUsersCount = (padID:string) => ({ padUsersCount: _getRoomSockets(padID).length, }); /** * Get the list of users in a pad */ -exports.padUsers = async (padID: string) => { +export const padUsers = async (padID: string) => { const padUsers:PadAuthor[] = []; // iterate over all clients (in parallel) await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { const s = sessioninfos[roomSocket.id]; if (s) { - const author = await authorManager.getAuthor(s.author); + const author = await getAuthor(s.author); // Fixes: https://github.com/ether/etherpad-lite/issues/4120 // On restart author might not be populated? if (author) { @@ -1289,5 +1292,3 @@ exports.padUsers = async (padID: string) => { return {padUsers}; }; - -exports.sessioninfos = sessioninfos; diff --git a/src/node/handler/SocketIORouter.ts b/src/node/handler/SocketIORouter.ts index 482276834df..237f0098ad5 100644 --- a/src/node/handler/SocketIORouter.ts +++ b/src/node/handler/SocketIORouter.ts @@ -22,9 +22,9 @@ import {MapArrayType} from "../types/MapType"; import {SocketModule} from "../types/SocketModule"; -const log4js = require('log4js'); -const settings = require('../utils/Settings'); -const stats = require('../../node/stats') +import log4js from 'log4js'; +import settings from '../utils/Settings'; +import {measuredCollection} from '../stats'; const logger = log4js.getLogger('socket.io'); @@ -41,8 +41,8 @@ let io:any; * @param {string} moduleName * @param {Module} module */ -exports.addComponent = (moduleName: string, module: SocketModule) => { - if (module == null) return exports.deleteComponent(moduleName); +export const addComponent = (moduleName: string, module: SocketModule) => { + if (module == null) return deleteComponent(moduleName); components[moduleName] = module; module.setSocketIO(io); }; @@ -51,13 +51,13 @@ exports.addComponent = (moduleName: string, module: SocketModule) => { * removes a component * @param {Module} moduleName */ -exports.deleteComponent = (moduleName: string) => { delete components[moduleName]; }; +export const deleteComponent = (moduleName: string) => { delete components[moduleName]; }; /** * sets the socket.io and adds event functions for routing * @param {Object} _io the socket.io instance */ -exports.setSocketIO = (_io:any) => { +export const setSocketIO = (_io:any) => { io = _io; io.sockets.on('connection', (socket:any) => { @@ -96,7 +96,7 @@ exports.setSocketIO = (_io:any) => { // when the last user disconnected. If your activePads is 0 and totalUsers is 0 // you can say, if there has been no active pads or active users for 10 minutes // this instance can be brought out of a scaling cluster. - stats.gauge('lastDisconnect', () => Date.now()); + measuredCollection.gauge('lastDisconnect', () => Date.now()); // tell all components about this disconnect for (const i of Object.keys(components)) { components[i].handleDisconnect(socket); diff --git a/src/node/hooks/express.ts b/src/node/hooks/express.ts index d4e92dd35e3..0e3bd873578 100644 --- a/src/node/hooks/express.ts +++ b/src/node/hooks/express.ts @@ -1,49 +1,50 @@ 'use strict'; -import {Socket} from "node:net"; import type {MapArrayType} from "../types/MapType"; import _ from 'underscore'; -// @ts-ignore import cookieParser from 'cookie-parser'; import events from 'events'; import express from 'express'; // @ts-ignore import expressSession from '@etherpad/express-session'; import fs from 'fs'; -const hooks = require('../../static/js/pluginfw/hooks'); +import {aCallAll} from '../../static/js/pluginfw/hooks'; import log4js from 'log4js'; -const SessionStore = require('../db/SessionStore'); -const settings = require('../utils/Settings'); -const stats = require('../stats') +import SessionStore from '../db/SessionStore'; +import settings from '../utils/Settings'; +import {measuredCollection} from '../stats'; import util from 'util'; -const webaccess = require('./express/webaccess'); +import {checkAccess} from './express/webaccess'; import SecretRotator from '../security/SecretRotator'; +import {Server, Socket} from "socket.io"; +import {DefaultEventsMap} from "socket.io/dist/typed-events"; let secretRotator: SecretRotator|null = null; const logger = log4js.getLogger('http'); let serverName:string; let sessionStore: { shutdown: () => void; } | null; -const sockets:Set = new Set(); +const sockets:Set< Socket> = new Set(); const socketsEvents = new events.EventEmitter(); -const startTime = stats.settableGauge('httpStartTime'); +const startTime = measuredCollection.settableGauge('httpStartTime'); -exports.server = null; const closeServer = async () => { - if (exports.server != null) { + if (server != null) { 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. - const p = util.promisify(exports.server.close.bind(exports.server))(); - await hooks.aCallAll('expressCloseServer'); + const p = util.promisify(server.close.bind(server))(); + await aCallAll('expressCloseServer'); // Give existing connections some time to close on their own before forcibly terminating. The // time should be long enough to avoid interrupting most preexisting transmissions but short // enough to avoid a noticeable outage. const timeout = setTimeout(async () => { logger.info(`Forcibly terminating remaining ${sockets.size} HTTP connections...`); - for (const socket of sockets) socket.destroy(new Error('HTTP server is closing')); + for (const socket of sockets) { // @ts-ignore + socket.destroy(new Error('HTTP server is closing')); + } }, 5000); let lastLogged = 0; while (sockets.size > 0 && !settings.enableAdminUITests) { @@ -55,7 +56,7 @@ const closeServer = async () => { } await p; clearTimeout(timeout); - exports.server = null; + server = null; startTime.setValue(0); logger.info('HTTP server closed'); } @@ -65,14 +66,14 @@ const closeServer = async () => { secretRotator = null; }; -exports.createServer = async () => { +export const createServer = async () => { console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); - await exports.restartServer(); + await restartServer(); if (settings.ip === '') { // 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(); const app = express(); // New syntax for express v3 @@ -121,10 +125,11 @@ exports.restartServer = async () => { } const https = require('https'); - exports.server = https.createServer(options, app); + + server = https.createServer(options, app); } else { const http = require('http'); - exports.server = http.createServer(app); + server = http.createServer(app); } app.use((req, res, next) => { @@ -167,7 +172,7 @@ exports.restartServer = async () => { // Measure response time app.use((req, res, next) => { - const stopWatch = stats.timer('httpRequests').start(); + const stopWatch = measuredCollection.timer('httpRequests').start(); const sendFn = res.send.bind(res); res.send = (...args) => { stopWatch.end(); return sendFn(...args); }; next(); @@ -177,7 +182,7 @@ exports.restartServer = async () => { // starts listening to requests as reported in issue #158. Not installing the log4js connect // logger when the log level has a higher severity than INFO since it would not log at that level // anyway. - if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { + if (!(settings.loglevel === 'WARN' || settings.loglevel === 'ERROR')) { app.use(log4js.connectLogger(logger, { level: log4js.levels.DEBUG.levelStr, format: ':status, :method :url', @@ -185,7 +190,7 @@ exports.restartServer = async () => { } const {keyRotationInterval, sessionLifetime} = settings.cookie; - let secret = settings.sessionKey; + let secret: string|string[] = settings.sessionKey!; if (keyRotationInterval && sessionLifetime) { secretRotator = new SecretRotator( 'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey); @@ -197,7 +202,7 @@ exports.restartServer = async () => { app.use(cookieParser(secret, {})); sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval); - exports.sessionMiddleware = expressSession({ + sessionMiddleware = expressSession({ propagateTouch: true, rolling: true, secret, @@ -234,16 +239,16 @@ exports.restartServer = async () => { // Give plugins an opportunity to install handlers/middleware before the express-session // middleware. This allows plugins to avoid creating an express-session record in the database // when it is not needed (e.g., public static content). - await hooks.aCallAll('expressPreSession', {app}); - app.use(exports.sessionMiddleware); + await aCallAll('expressPreSession', {app}); + app.use(sessionMiddleware); - app.use(webaccess.checkAccess); + app.use(checkAccess); await Promise.all([ - hooks.aCallAll('expressConfigure', {app}), - hooks.aCallAll('expressCreateServer', {app, server: exports.server}), + aCallAll('expressConfigure', {app}), + aCallAll('expressCreateServer', {app, server: server}), ]); - exports.server.on('connection', (socket:Socket) => { + server!.on('connection', (socket) => { sockets.add(socket); socketsEvents.emit('updated'); socket.on('close', () => { @@ -251,11 +256,12 @@ exports.restartServer = async () => { 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)(settings.port, settings.ip); startTime.setValue(Date.now()); logger.info('HTTP server listening for connections'); }; -exports.shutdown = async (hookName:string, context: any) => { +export const shutdown = async () => { await closeServer(); }; diff --git a/src/node/hooks/express/admin.ts b/src/node/hooks/express/admin.ts index e802750f25c..4847d47137b 100644 --- a/src/node/hooks/express/admin.ts +++ b/src/node/hooks/express/admin.ts @@ -5,7 +5,7 @@ import fs from "fs"; import * as url from "node:url"; import {MapArrayType} from "../../types/MapType"; -const settings = require('ep_etherpad-lite/node/utils/Settings'); +import settings from 'ep_etherpad-lite/node/utils/Settings'; const ADMIN_PATH = path.join(settings.root, 'src', 'templates'); const PROXY_HEADER = "x-proxy-path" diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index 8b04adf9363..ecbe03d2172 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -20,13 +20,13 @@ import {ErrorCaused} from "../../types/ErrorCaused"; const OpenAPIBackend = require('openapi-backend').default; const IncomingForm = require('formidable').IncomingForm; -const cloneDeep = require('lodash.clonedeep'); -const createHTTPError = require('http-errors'); +import cloneDeep from 'lodash.clonedeep'; +import createHTTPError from 'http-errors'; -const apiHandler = require('../../handler/APIHandler'); -const settings = require('../../utils/Settings'); +import {handle, latestApiVersion, version as apiVersion} from '../../handler/APIHandler'; +import settings from '../../utils/Settings'; -const log4js = require('log4js'); +import log4js from 'log4js'; const logger = log4js.getLogger('API'); // https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0 @@ -48,7 +48,7 @@ const info = { name: 'Apache 2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0.html', }, - version: apiHandler.latestApiVersion, + version: latestApiVersion, }; const APIPathStyle = { @@ -401,6 +401,7 @@ for (const [resource, actions] of Object.entries(resources)) { // add response objects const responses:OpenAPISuccessResponse = {...defaultResponseRefs}; if (responseSchema) { + // @ts-ignore responses[200] = cloneDeep(defaultResponses.Success); responses[200].content!['application/json'].schema.properties.data = { type: 'object', @@ -504,7 +505,7 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) }; // build operations - for (const funcName of Object.keys(apiHandler.version[version])) { + for (const funcName of Object.keys(apiVersion[version])) { let operation:OpenAPIOperations = {}; if (operations[funcName]) { operation = {...operations[funcName]}; @@ -518,7 +519,7 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) // set parameters operation.parameters = operation.parameters || []; - for (const paramName of apiHandler.version[version][funcName]) { + for (const paramName of apiVersion[version][funcName]) { operation.parameters.push({$ref: `#/components/parameters/${paramName}`}); // @ts-ignore if (!definition.components.parameters[paramName]) { @@ -559,7 +560,7 @@ const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) exports.expressPreSession = async (hookName:string, {app}:any) => { // create openapi-backend handlers for each api version under /api/{version}/* - for (const version of Object.keys(apiHandler.version)) { + for (const version of Object.keys(apiVersion)) { // we support two different styles of api: flat + rest // TODO: do we really want to support both? @@ -577,7 +578,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { }); // serve latest openapi definition file under /api/openapi.json - const isLatestAPIVersion = version === apiHandler.latestApiVersion; + const isLatestAPIVersion = version === latestApiVersion; if (isLatestAPIVersion) { app.get(`/${style}/openapi.json`, (req:any, res:any) => { res.header('Access-Control-Allow-Origin', '*'); @@ -605,7 +606,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { }); // register operation handlers - for (const funcName of Object.keys(apiHandler.version[version])) { + for (const funcName of Object.keys(apiVersion[version])) { const handler = async (c: any, req:any, res:any) => { // parse fields from request const {headers, params, query} = c.request; @@ -630,7 +631,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { // pass to api handler let data; try { - data = await apiHandler.handle(version, funcName, fields, req, res); + data = await handle(version, funcName, fields, req); } catch (err) { const errCaused = err as ErrorCaused // convert all errors to http errors @@ -645,7 +646,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { // an unknown error happened // log it and throw internal error logger.error(errCaused.stack || errCaused.toString()); - throw new createHTTPError.InternalError('internal error'); + throw new createHTTPError.InternalServerError('internal error'); } } diff --git a/src/node/hooks/express/socketio.ts b/src/node/hooks/express/socketio.ts index bbdec1c1cf4..a7de4d8b3d1 100644 --- a/src/node/hooks/express/socketio.ts +++ b/src/node/hooks/express/socketio.ts @@ -3,14 +3,14 @@ import {ArgsExpressType} from "../../types/ArgsExpressType"; import events from 'events'; -const express = require('../express'); +import {sessionMiddleware} from '../express'; import log4js from 'log4js'; -const proxyaddr = require('proxy-addr'); -const settings = require('../../utils/Settings'); +import proxyaddr from 'proxy-addr'; +import settings from '../../utils/Settings'; import {Server, Socket} from 'socket.io' -const socketIORouter = require('../../handler/SocketIORouter'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const padMessageHandler = require('../../handler/PadMessageHandler'); +import {addComponent, setSocketIO} from '../../handler/SocketIORouter'; +import {callAll} from '../../../static/js/pluginfw/hooks'; +import * as padMessageHandler from '../../handler/PadMessageHandler'; let io:any; const logger = log4js.getLogger('socket.io'); @@ -62,7 +62,7 @@ const socketSessionMiddleware = (args: any) => (socket: any, next: Function) => // socketio.js-client on node.js doesn't support cookies, so pass them via a query parameter. req.headers.cookie = socket.handshake.query.cookie; } - express.sessionMiddleware(req, {}, next); + sessionMiddleware(req, {}, next); }; export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => { @@ -71,6 +71,7 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu // transports in this list at once // e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling io = new Server(args.server,{ + // @ts-ignore transports: settings.socketTransportProtocols, cookie: false, maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, @@ -133,10 +134,10 @@ export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Fu // if(settings.minify) io.enable('browser client minification'); // Initialize the Socket.IO Router - socketIORouter.setSocketIO(io); - socketIORouter.addComponent('pad', padMessageHandler); + setSocketIO(io); + addComponent('pad', padMessageHandler); - hooks.callAll('socketio', {app: args.app, io, server: args.server}); + callAll('socketio', {app: args.app, io, server: args.server}); return cb(); }; diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 90bb4e2fe9b..73b9e33e128 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -1,25 +1,27 @@ 'use strict'; import path from 'node:path'; -const eejs = require('../../eejs') +import {requireP} from '../../eejs'; import fs from 'node:fs'; const fsp = fs.promises; -const toolbar = require('../../utils/toolbar'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const settings = require('../../utils/Settings'); +import toolbar from '../../utils/toolbar'; +import {callAll} from '../../../static/js/pluginfw/hooks'; +import settings from '../../utils/Settings'; import util from 'node:util'; -const webaccess = require('./webaccess'); -const plugins = require('../../../static/js/pluginfw/plugin_defs'); +import {userCanModify} from './webaccess'; +import {pluginDefs} from '../../../static/js/pluginfw/plugin_defs'; import {build, buildSync} from 'esbuild' +import {availableLangs} from "../i18n"; +import {clientPluginNames} from "../../../static/js/pluginfw/shared"; let ioI: { sockets: { sockets: any[]; }; } | null = null -exports.socketio = (hookName: string, {io}: any) => { +export const socketio = (hookName: string, {io}: any) => { ioI = io } -exports.expressPreSession = async (hookName:string, {app}:any) => { +export const expressPreSession = async (hookName:string, {app}:any) => { // This endpoint is intended to conform to: // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html app.get('/health', (req:any, res:any) => { @@ -35,12 +37,12 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { }); app.get('/javascript', (req:any, res:any) => { - res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); + res.send(requireP('ep_etherpad-lite/templates/javascript.html', {req})); }); app.get('/robots.txt', (req:any, res:any) => { let filePath = - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); + path.join(settings.root, 'src', 'static', 'skins', settings.skinName!, 'robots.txt'); res.sendFile(filePath, (err:any) => { // there is no custom robots.txt, send the default robots.txt which dissallows all if (err) { @@ -64,7 +66,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { const fns = [ ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), + path.join(settings.root, 'src', 'static', 'skins', settings.skinName!, 'favicon.ico'), path.join(settings.root, 'src', 'static', 'favicon.ico'), ]; for (const fn of fns) { @@ -147,16 +149,19 @@ const handleLiveReload = async (args: any, padString: string, timeSliderString: setRouteHandler("/p/:pad", (req: any, res: any, next: Function) => { // The below might break for pads being rewritten - const isReadOnly = !webaccess.userCanModify(req.params.pad, req); + const isReadOnly = !userCanModify(req.params.pad, req); - hooks.callAll('padInitToolbar', { + callAll('padInitToolbar', { toolbar, isReadOnly }); - const content = eejs.require('ep_etherpad-lite/templates/pad.html', { + const content = requireP('ep_etherpad-lite/templates/pad.html', { req, toolbar, + settings: settings, + clientPluginNames: clientPluginNames, + langs: availableLangs, isReadOnly, entrypoint: '/watch/pad?hash=' + hash }) @@ -176,16 +181,18 @@ const handleLiveReload = async (args: any, padString: string, timeSliderString: setRouteHandler("/p/:pad/timeslider", (req: any, res: any, next: Function) => { console.log("Reloading pad") // The below might break for pads being rewritten - const isReadOnly = !webaccess.userCanModify(req.params.pad, req); + const isReadOnly = !userCanModify(req.params.pad, req); - hooks.callAll('padInitToolbar', { + callAll('padInitToolbar', { toolbar, isReadOnly }); - const content = eejs.require('ep_etherpad-lite/templates/timeslider.html', { + const content = requireP('ep_etherpad-lite/templates/timeslider.html', { req, toolbar, + langs: availableLangs, + settings: settings, isReadOnly, entrypoint: '/watch/timeslider?hash=' + hash }) @@ -210,7 +217,8 @@ const convertTypescriptWatched = (content: string, cb: (output:string, hash: str }, alias:{ "ep_etherpad-lite/static/js/browser": 'ep_etherpad-lite/static/js/vendors/browser', - "ep_etherpad-lite/static/js/nice-select": 'ep_etherpad-lite/static/js/vendors/nice-select' + "ep_etherpad-lite/static/js/nice-select": 'ep_etherpad-lite/static/js/vendors/nice-select', + "jquery": "ep_etherpad-lite/node_modules/jquery" }, bundle: true, // Bundle the files together minify: process.env.NODE_ENV === "production", // Minify the output @@ -227,17 +235,18 @@ const convertTypescriptWatched = (content: string, cb: (output:string, hash: str }) } -exports.expressCreateServer = async (hookName: string, args: any, cb: Function) => { +export const expressCreateServer = async (hookName: string, args: any, cb: Function) => { // serve index.html under / args.app.get('/', (req: any, res: any) => { - res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); + res.send(requireP('ep_etherpad-lite/templates/index.html', {req})); }); - const padString = eejs.require('ep_etherpad-lite/templates/padBootstrap.js', { - pluginModules: (() => { + const padString = requireP('ep_etherpad-lite/templates/padBootstrap.ts', { + // @ts-ignore + pluginModules: (() => { const pluginModules = new Set(); - for (const part of plugins.parts) { + for (const part of pluginDefs.getParts()) { for (const [, hookFnName] of Object.entries(part.client_hooks || {})) { // @ts-ignore pluginModules.add(hookFnName.split(':')[0]); @@ -248,10 +257,11 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) settings, }) - const timeSliderString = eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', { + const timeSliderString = requireP('ep_etherpad-lite/templates/timeSliderBootstrap.ts', { + // @ts-ignore pluginModules: (() => { const pluginModules = new Set(); - for (const part of plugins.parts) { + for (const part of pluginDefs.getParts()) { for (const [, hookFnName] of Object.entries(part.client_hooks || {})) { // @ts-ignore pluginModules.add(hookFnName.split(':')[0]); @@ -297,14 +307,14 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) // serve pad.html under /p args.app.get('/p/:pad', (req: any, res: any, next: Function) => { // The below might break for pads being rewritten - const isReadOnly = !webaccess.userCanModify(req.params.pad, req); + const isReadOnly = !userCanModify(req.params.pad, req); - hooks.callAll('padInitToolbar', { + callAll('padInitToolbar', { toolbar, isReadOnly }); - const content = eejs.require('ep_etherpad-lite/templates/pad.html', { + const content = requireP('ep_etherpad-lite/templates/pad.html', { req, toolbar, isReadOnly, @@ -315,11 +325,11 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function) // serve timeslider.html under /p/$padname/timeslider args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => { - hooks.callAll('padInitToolbar', { + callAll('padInitToolbar', { toolbar, }); - res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { + res.send(requireP('ep_etherpad-lite/templates/timeslider.html', { req, toolbar, entrypoint: "/"+fileNameTimeSlider diff --git a/src/node/hooks/express/static.ts b/src/node/hooks/express/static.ts index 18ff8c76a16..a4829eef4a9 100644 --- a/src/node/hooks/express/static.ts +++ b/src/node/hooks/express/static.ts @@ -4,10 +4,10 @@ import {MapArrayType} from "../../types/MapType"; import {PartType} from "../../types/PartType"; const fs = require('fs').promises; -const minify = require('../../utils/Minify'); -const path = require('path'); -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const settings = require('../../utils/Settings'); +import {minify} from '../../utils/Minify'; +import path from 'path'; +import {pluginDefs} from '../../../static/js/pluginfw/plugin_defs'; +import settings from '../../utils/Settings'; import CachingMiddleware from '../../utils/caching_middleware'; // Rewrite tar to include modules with no extensions and proper rooted paths. @@ -40,13 +40,15 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { // Minify will serve static files compressed (minify enabled). It also has // file-specific hacks for ace/require-kernel/etc. - app.all('/static/:filename(*)', minify.minify); + app.all('/static/:filename(*)', (req: Request, res: Response, next: Function)=>{ + minify(req,res, next) + }); // serve plugin definitions // not very static, but served here so that client can do // require("pluginfw/static/js/plugin-definitions.js"); app.get('/pluginfw/plugin-definitions.json', (req: any, res:any, next:Function) => { - const clientParts = plugins.parts.filter((part: PartType) => part.client_hooks != null); + const clientParts = pluginDefs.getParts().filter((part: PartType) => part.client_hooks != null); const clientPlugins:MapArrayType = {}; for (const name of new Set(clientParts.map((part: PartType) => part.plugin))) { // @ts-ignore diff --git a/src/node/hooks/express/tests.ts b/src/node/hooks/express/tests.ts index f8a1417ef71..08407e30b1a 100644 --- a/src/node/hooks/express/tests.ts +++ b/src/node/hooks/express/tests.ts @@ -3,11 +3,11 @@ import {Dirent} from "node:fs"; import {PluginDef} from "../../types/PartType"; -const path = require('path'); -const fsp = require('fs').promises; -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const sanitizePathname = require('../../utils/sanitizePathname'); -const settings = require('../../utils/Settings'); +import path from 'path'; +import {promises as fsp} from 'fs'; +import {pluginDefs} from '../../../static/js/pluginfw/plugin_defs'; +import sanitizePathname from '../../utils/sanitizePathname'; +import settings from '../../utils/Settings'; // Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/' // instead of path.sep to separate pathname components. @@ -32,11 +32,11 @@ const findSpecs = async (specDir: string) => { 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) => { (async () => { 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; if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep; const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; diff --git a/src/node/hooks/express/webaccess.ts b/src/node/hooks/express/webaccess.ts index cb6884dc358..f4fbcb74b49 100644 --- a/src/node/hooks/express/webaccess.ts +++ b/src/node/hooks/express/webaccess.ts @@ -7,21 +7,19 @@ import {WebAccessTypes} from "../../types/WebAccessTypes"; import {SettingsUser} from "../../types/SettingsUser"; const httpLogger = log4js.getLogger('http'); const settings = require('../../utils/Settings'); -const hooks = require('../../../static/js/pluginfw/hooks'); +import {deprecationNotices, aCallFirst as HookAcall} from '../../../static/js/pluginfw/hooks'; 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. 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 = - // @ts-ignore - async (hookName: string, context:any, pred = null) => (await aCallFirst(hookName, context, pred))[0]; +const aCallFirst0 = async (hookName: string, context:any, pred = null): Promise => (await aCallFirst(hookName, context, pred)) as any[0]; -exports.normalizeAuthzLevel = (level: string|boolean) => { +export const normalizeAuthzLevel = (level: string|boolean) => { if (!level) return false; switch (level) { case true: @@ -36,20 +34,20 @@ exports.normalizeAuthzLevel = (level: string|boolean) => { return false; }; -exports.userCanModify = (padId: string, req: SocketClientRequest) => { +export const userCanModify = (padId: string, req: SocketClientRequest) => { if (readOnlyManager.isReadOnlyId(padId)) return false; if (!settings.requireAuthentication) return true; const {session: {user} = {}} = req; if (!user || user.readOnly) return false; assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. - const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); + const level = normalizeAuthzLevel(user.padAuthorizations[padId]); return level && level !== 'readOnly'; }; // 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'); // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -93,7 +91,7 @@ const checkAccess = async (req:any, res:any, next: Function) => { // authentication is checked and once after (if settings.requireAuthorization is true). const authorize = async () => { const grant = async (level: string|false) => { - level = exports.normalizeAuthzLevel(level); + level = normalizeAuthzLevel(level); if (!level) return false; const user = req.session.user; 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"'); } // 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'); 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-session middleware. */ -exports.checkAccess = (req:any, res:any, next:Function) => { - checkAccess(req, res, next).catch((err) => next(err || new Error(err))); +export const checkAccess = (req:any, res:any, next:Function) => { + _checkAccess(req, res, next).catch((err) => next(err || new Error(err))); }; diff --git a/src/node/hooks/i18n.ts b/src/node/hooks/i18n.ts index 500f1f88784..e5112defe31 100644 --- a/src/node/hooks/i18n.ts +++ b/src/node/hooks/i18n.ts @@ -4,12 +4,12 @@ import type {MapArrayType} from "../types/MapType"; import {I18nPluginDefs} from "../types/I18nPluginDefs"; const languages = require('languages4translatewiki'); -const fs = require('fs'); -const path = require('path'); -const _ = require('underscore'); -const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js'); -const existsSync = require('../utils/path_exists'); -const settings = require('../utils/Settings'); +import fs from 'fs'; +import path from 'path'; +import _ from 'underscore'; +import {pluginDefs} from '../../static/js/pluginfw/plugin_defs.js'; +import existsSync from '../utils/path_exists'; +import settings from '../utils/Settings'; // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} @@ -43,7 +43,7 @@ const getAllLocales = () => { extractLangs(path.join(settings.root, 'src/locales')); // add plugins languages (if any) - for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) { + for (const {package: {path: pluginPath}} of Object.values(pluginDefs.getPlugins())) { // plugin locales should overwrite etherpad's core locales if (pluginPath.endsWith('/ep_etherpad-lite')) continue; extractLangs(path.join(pluginPath, 'locales')); @@ -122,17 +122,18 @@ const generateLocaleIndex = (locales:MapArrayType) => { return JSON.stringify(result); }; +export let availableLangs: any -exports.expressPreSession = async (hookName:string, {app}:any) => { +export const expressPreSession = async (hookName:string, {app}:any) => { // regenerate locales on server restart const locales = getAllLocales(); const localeIndex = generateLocaleIndex(locales); - exports.availableLangs = getAvailableLangs(locales); + availableLangs = getAvailableLangs(locales); app.get('/locales/:locale', (req:any, res:any) => { // works with /locale/en and /locale/en.json requests const locale = req.params.locale.split('.')[0]; - if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) { + if (Object.prototype.hasOwnProperty.call(availableLangs, locale)) { res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); diff --git a/src/node/padaccess.ts b/src/node/padaccess.ts index ce3cf9ddd71..843d4574c3e 100644 --- a/src/node/padaccess.ts +++ b/src/node/padaccess.ts @@ -1,10 +1,10 @@ 'use strict'; -const securityManager = require('./db/SecurityManager'); +import {checkAccess} from './db/SecurityManager'; // 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 accessObj = await securityManager.checkAccess( + const accessObj = await checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); if (accessObj.accessStatus === 'grant') { diff --git a/src/node/security/OAuth2Provider.ts b/src/node/security/OAuth2Provider.ts index e212113504b..cab966f828e 100644 --- a/src/node/security/OAuth2Provider.ts +++ b/src/node/security/OAuth2Provider.ts @@ -3,7 +3,7 @@ import Provider, {Account, Configuration} from 'oidc-provider'; import {generateKeyPair, exportJWK, KeyLike} from 'jose' import MemoryAdapter from "./OIDCAdapter"; import path from "path"; -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; import {IncomingForm} from 'formidable' import express, {Request, Response} from 'express'; import {format} from 'url' diff --git a/src/node/server.ts b/src/node/server.ts index f96db3ab194..717517dc17c 100755 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -28,8 +28,7 @@ import log4js from 'log4js'; import pkg from '../package.json'; import {checkForMigration} from "../static/js/pluginfw/installer"; import axios from "axios"; - -const settings = require('./utils/Settings'); +import settings from "./utils/Settings"; let wtfnode: any; if (settings.dumpOnUncleanExit) { @@ -64,18 +63,18 @@ if (process.env['https_proxy']) { * early check for version compatibility before calling * any modules that require newer versions of NodeJS */ -const NodeVersion = require('./utils/NodeVersion'); -NodeVersion.enforceMinNodeVersion(pkg.engines.node.replace(">=", "")); -NodeVersion.checkDeprecationStatus(pkg.engines.node.replace(">=", ""), '2.1.0'); - -const UpdateCheck = require('./utils/UpdateCheck'); -const db = require('./db/DB'); -const express = require('./hooks/express'); -const hooks = require('../static/js/pluginfw/hooks'); -const pluginDefs = require('../static/js/pluginfw/plugin_defs'); -const plugins = require('../static/js/pluginfw/plugins'); -const {Gate} = require('./utils/promises'); -const stats = require('./stats') +import {checkDeprecationStatus, enforceMinNodeVersion} from './utils/NodeVersion'; +enforceMinNodeVersion(pkg.engines.node.replace(">=", "")); +checkDeprecationStatus(pkg.engines.node.replace(">=", ""), '2.1.0'); + +import {check} from './utils/UpdateCheck'; +import {init} from './db/DB'; +import {server} from './hooks/express'; +import {aCallAll} from '../static/js/pluginfw/hooks'; +import {pluginDefs} from '../static/js/pluginfw/plugin_defs'; +import {formatHooks, formatParts, update} from '../static/js/pluginfw/plugins'; +import {Gate} from './utils/promises'; +import {measuredCollection} from './stats'; const logger = log4js.getLogger('server'); @@ -100,17 +99,17 @@ const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsLi }; -let startDoneGate: { resolve: () => void; } -exports.start = async () => { +let startDoneGate: Gate +export const start = async (): Promise => { switch (state) { case State.INITIAL: break; case State.STARTING: await startDoneGate; // Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED. - return await exports.start(); + return await start(); case State.RUNNING: - return express.server; + return server; case State.STOPPING: case State.STOPPED: case State.EXITING: @@ -121,22 +120,20 @@ exports.start = async () => { throw new Error(`unknown State: ${state.toString()}`); } logger.info('Starting Etherpad...'); - startDoneGate = new Gate(); + startDoneGate = new Gate(); state = State.STARTING; try { // Check if Etherpad version is up-to-date - UpdateCheck.check(); + check(); - // @ts-ignore - stats.gauge('memoryUsage', () => process.memoryUsage().rss); - // @ts-ignore - stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); + measuredCollection.gauge('memoryUsage', () => process.memoryUsage().rss); + measuredCollection.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); process.on('uncaughtException', (err: ErrorCaused) => { logger.debug(`uncaught exception: ${err.stack || err}`); // eslint-disable-next-line promise/no-promise-in-callback - exports.exit(err) + exit(err) .catch((err: ErrorCaused) => { logger.error('Error in process exit', err); // eslint-disable-next-line n/no-process-exit @@ -153,12 +150,12 @@ exports.start = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as NodeJS.Signals[]) { // 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 - // 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. for (const listener of process.listeners(signal)) { removeSignalListener(signal, listener); } - process.on(signal, exports.exit); + process.on(signal, exit); // Prevent signal listeners from being added in the future. process.on('newListener', (event, listener) => { if (event !== signal) return; @@ -166,40 +163,40 @@ exports.start = async () => { }); } - await db.init(); + await init(); await checkForMigration(); - await plugins.update(); - const installedPlugins = (Object.values(pluginDefs.plugins) as PluginType[]) + await update(); + const installedPlugins = (Object.values(pluginDefs.getPlugins()) as PluginType[]) .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') .map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .join(', '); logger.info(`Installed plugins: ${installedPlugins}`); - logger.debug(`Installed parts:\n${plugins.formatParts()}`); - logger.debug(`Installed server-side hooks:\n${plugins.formatHooks('hooks', false)}`); - await hooks.aCallAll('loadSettings', {settings}); - await hooks.aCallAll('createServer'); + logger.debug(`Installed parts:\n${formatParts()}`); + logger.debug(`Installed server-side hooks:\n${formatHooks('hooks', false)}`); + await aCallAll('loadSettings', {settings}); + await aCallAll('createServer'); } catch (err) { logger.error('Error occurred while starting Etherpad'); state = State.STATE_TRANSITION_FAILED; - startDoneGate.resolve(); - return await exports.exit(err); + startDoneGate.resolve!(); + return await exit(err as any); } logger.info('Etherpad is running'); state = State.RUNNING; - startDoneGate.resolve(); + startDoneGate.resolve!(); // Return the HTTP server to make it easier to write tests. - return express.server; + return server; }; const stopDoneGate = new Gate(); -exports.stop = async () => { +export const stop = async (): Promise => { switch (state) { case State.STARTING: - await exports.start(); + await start(); // 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: break; case State.STOPPING: @@ -219,7 +216,7 @@ exports.stop = async () => { try { let timeout: NodeJS.Timeout = null as unknown as NodeJS.Timeout; await Promise.race([ - hooks.aCallAll('shutdown'), + aCallAll('shutdown'), new Promise((resolve, reject) => { timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); }), @@ -228,24 +225,25 @@ exports.stop = async () => { } catch (err) { logger.error('Error occurred while stopping Etherpad'); state = State.STATE_TRANSITION_FAILED; - stopDoneGate.resolve(); - return await exports.exit(err); + stopDoneGate.resolve!(); + // @ts-ignore + return await exit(err); } logger.info('Etherpad stopped'); state = State.STOPPED; - stopDoneGate.resolve(); + stopDoneGate.resolve!(); }; let exitGate: any; let exitCalled = false; -exports.exit = async (err: ErrorCaused|string|null = null) => { +export const exit = async (err: ErrorCaused|string|null = null): Promise => { /* eslint-disable no-process-exit */ if (err === 'SIGTERM') { // Termination from SIGTERM is not treated as an abnormal termination. logger.info('Received SIGTERM signal'); 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()); process.exitCode = 1; if (exitCalled) { @@ -259,11 +257,11 @@ exports.exit = async (err: ErrorCaused|string|null = null) => { case State.STARTING: case State.RUNNING: 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 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.) - return await exports.exit(); + return await exit(); case State.INITIAL: case State.STOPPED: case State.STATE_TRANSITION_FAILED: @@ -303,7 +301,7 @@ exports.exit = async (err: ErrorCaused|string|null = null) => { /* eslint-enable no-process-exit */ }; -if (require.main === module) exports.start(); +if (require.main === module) start(); // @ts-ignore -if (typeof(PhusionPassenger) !== 'undefined') exports.start(); +if (typeof(PhusionPassenger) !== 'undefined') start(); diff --git a/src/node/stats.ts b/src/node/stats.ts index f1fc0cccfdd..e3a2c69329b 100644 --- a/src/node/stats.ts +++ b/src/node/stats.ts @@ -1,10 +1,10 @@ 'use strict'; -const measured = require('measured-core'); +// @ts-ignore +import measured from 'measured-core'; -module.exports = measured.createCollection(); +export const measuredCollection = measured.createCollection(); -// @ts-ignore -module.exports.shutdown = async (hookName, context) => { - module.exports.end(); -}; \ No newline at end of file +export const shutdown = async (hookName: string, context:any) => { + measuredCollection.end(); +}; diff --git a/src/node/types/PadType.ts b/src/node/types/PadType.ts index b344ed8c555..5b5cc7923e4 100644 --- a/src/node/types/PadType.ts +++ b/src/node/types/PadType.ts @@ -1,4 +1,5 @@ import {MapArrayType} from "./MapType"; +import {PadOption} from "../../static/js/types/SocketIOMessage"; export type PadType = { id: string, @@ -19,6 +20,7 @@ export type PadType = { getRevisionDate: (rev: number)=>Promise, getRevisionChangeset: (rev: number)=>Promise, appendRevision: (changeset: AChangeSet, author: string)=>Promise, + settings: PadOption } diff --git a/src/node/types/RunCMDOptions.ts b/src/node/types/RunCMDOptions.ts index 74298f22185..b8aa8739002 100644 --- a/src/node/types/RunCMDOptions.ts +++ b/src/node/types/RunCMDOptions.ts @@ -1,6 +1,6 @@ export type RunCMDOptions = { cwd?: string, - stdio?: string[], + stdio?: (string|null)[] env?: NodeJS.ProcessEnv } @@ -12,4 +12,4 @@ export type RunCMDPromise = { export type ErrorExtended = { code?: number|null, signal?: NodeJS.Signals|null -} \ No newline at end of file +} diff --git a/src/node/types/SocketClientRequest.ts b/src/node/types/SocketClientRequest.ts index 07c015fc52f..54d56141f40 100644 --- a/src/node/types/SocketClientRequest.ts +++ b/src/node/types/SocketClientRequest.ts @@ -1,3 +1,5 @@ +import {UserSettingsObject} from "./UserSettingsObject"; + export type SocketClientRequest = { session: { user: { @@ -5,6 +7,7 @@ export type SocketClientRequest = { readOnly: boolean; padAuthorizations: { [key: string]: string; + user: UserSettingsObject } } } diff --git a/src/node/utils/Abiword.ts b/src/node/utils/Abiword.ts index c0937fcd9fc..d7bc760e5a7 100644 --- a/src/node/utils/Abiword.ts +++ b/src/node/utils/Abiword.ts @@ -22,15 +22,17 @@ import {ChildProcess} from "node:child_process"; import {AsyncQueueTask} from "../types/AsyncQueueTask"; -const spawn = require('child_process').spawn; -const async = require('async'); +import {spawn} from 'child_process' +import async from 'async'; const settings = require('./Settings'); -const os = require('os'); +import os from 'os'; + +export let convertFile: (srcFile: string, destFile: string, type: string)=> Promise // on windows we have to spawn a process for each convertion, // cause the plugin abicommand doesn't exist on this platform 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]); let stdoutBuffer = ''; abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); }); @@ -87,7 +89,7 @@ if (os.type().indexOf('Windows') > -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}); }; } diff --git a/src/node/utils/AbsolutePaths.ts b/src/node/utils/AbsolutePaths.ts index c257440a1f7..6423ae4d70e 100644 --- a/src/node/utils/AbsolutePaths.ts +++ b/src/node/utils/AbsolutePaths.ts @@ -18,9 +18,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const log4js = require('log4js'); -const path = require('path'); -const _ = require('underscore'); +import log4js from 'log4js'; +import path from 'path'; +import _ from 'underscore'; 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 * identified, prints a log and exits the application. */ -exports.findEtherpadRoot = () => { +export const findEtherpadRoot = () => { if (etherpadRoot != null) { return etherpadRoot; } @@ -130,12 +130,12 @@ exports.findEtherpadRoot = () => { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = (somePath: string) => { +export const makeAbsolute = (somePath: string) => { if (path.isAbsolute(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}"`); return rewrittenPath; @@ -149,7 +149,7 @@ exports.makeAbsolute = (somePath: string) => { * a subdirectory of the base one * @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 const relative = path.relative(parent, arbitraryDir); return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); diff --git a/src/node/utils/Cli.ts b/src/node/utils/Cli.ts index 1579dd3ce91..4ef436ce2af 100644 --- a/src/node/utils/Cli.ts +++ b/src/node/utils/Cli.ts @@ -21,7 +21,9 @@ */ // An object containing the parsed command-line options -exports.argv = {}; +import {MapArrayType} from "../types/MapType"; + +export const argvP: MapArrayType = {}; const argv = process.argv.slice(2); let arg, prevArg; @@ -32,22 +34,22 @@ for (let i = 0; i < argv.length; i++) { // Override location of settings.json file if (prevArg === '--settings' || prevArg === '-s') { - exports.argv.settings = arg; + argvP.settings = arg; } // Override location of credentials.json file if (prevArg === '--credentials') { - exports.argv.credentials = arg; + argvP.credentials = arg; } // Override location of settings.json file if (prevArg === '--sessionkey') { - exports.argv.sessionkey = arg; + argvP.sessionkey = arg; } // Override location of APIKEY.txt file if (prevArg === '--apikey') { - exports.argv.apikey = arg; + argvP.apikey = arg; } prevArg = arg; diff --git a/src/node/utils/ExportEtherpad.ts b/src/node/utils/ExportEtherpad.ts index 292fbcec49e..176740caae7 100644 --- a/src/node/utils/ExportEtherpad.ts +++ b/src/node/utils/ExportEtherpad.ts @@ -15,35 +15,36 @@ * limitations under the License. */ -const Stream = require('./Stream'); -const assert = require('assert').strict; -const authorManager = require('../db/AuthorManager'); -const hooks = require('../../static/js/pluginfw/hooks'); -const padManager = require('../db/PadManager'); +import Stream from './Stream'; +import {strict as assert} from 'assert' +import {getAuthor} from '../db/AuthorManager'; +import {aCallAll} from '../../static/js/pluginfw/hooks'; +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 [pad, customPrefixes] = await Promise.all([ - padManager.getPad(padId), - hooks.aCallAll('exportEtherpadAdditionalContent'), + getPad(padId), + aCallAll('exportEtherpadAdditionalContent'), ]); const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix:string) => { const srcPfx = `${customPrefix}:${padId}`; const dstPfx = `${customPrefix}:${readOnlyId || padId}`; assert(!srcPfx.includes('*')); - const srcKeys = await pad.db.findKeys(`${srcPfx}:*`, null); + const srcKeys = await findKeys(`${srcPfx}:*`, null); return (function* () { - yield [dstPfx, pad.db.get(srcPfx)]; + yield [dstPfx, get(srcPfx)]; for (const k of srcKeys) { 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* () { for (const authorId of pad.getAllAuthors()) { 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.padIDs) authorEntry.padIDs = readOnlyId || padId; return authorEntry; @@ -55,7 +56,7 @@ exports.getPadRaw = async (padId:string, readOnlyId:string) => { })(); const data = {[dstPfx]: pad}; for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p; - await hooks.aCallAll('exportEtherpad', { + await aCallAll('exportEtherpad', { pad, data, dstPadId: readOnlyId || padId, diff --git a/src/node/utils/ExportHelper.ts b/src/node/utils/ExportHelper.ts index f3a438e86e6..81c8f06611a 100644 --- a/src/node/utils/ExportHelper.ts +++ b/src/node/utils/ExportHelper.ts @@ -19,18 +19,19 @@ * limitations under the License. */ -const AttributeMap = require('../../static/js/AttributeMap'); -const Changeset = require('../../static/js/Changeset'); -const { checkValidRev } = require('./checkValidRev'); +import AttributeMap from '../../static/js/AttributeMap'; +import AttributePool from "../../static/js/AttributePool"; +import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset'; +import {checkValidRev} from './checkValidRev'; /* * 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 atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext); 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 pieces = []; @@ -51,14 +52,14 @@ type LineModel = { [id:string]:string|number|LineModel } -exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => { +export const _analyzeLine = (text:string, aline: string, apool: AttributePool) => { const line: LineModel = {}; // identify list let lineMarker = 0; line.listLevel = 0; if (aline) { - const [op] = Changeset.deserializeOps(aline); + const [op] = deserializeOps(aline); if (op != null) { const attribs = AttributeMap.fromString(op.attribs, apool); let listType = attribs.get('list'); @@ -78,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => { } if (lineMarker) { line.text = text.substring(1); - line.aline = Changeset.subattribution(aline, 1); + line.aline = subattribution(aline, 1); } else { line.text = text; line.aline = aline; @@ -87,5 +88,5 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => { }; -exports._encodeWhitespace = +export const _encodeWhitespace = (s:string) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); diff --git a/src/node/utils/ExportHtml.ts b/src/node/utils/ExportHtml.ts index 3b84c4380ec..760475c88f2 100644 --- a/src/node/utils/ExportHtml.ts +++ b/src/node/utils/ExportHtml.ts @@ -1,6 +1,7 @@ 'use strict'; import {AText, PadType} from "../types/PadType"; import {MapArrayType} from "../types/MapType"; +import Pad from "../db/Pad"; /** * Copyright 2009 Google Inc. @@ -18,18 +19,19 @@ import {MapArrayType} from "../types/MapType"; * limitations under the License. */ -const Changeset = require('../../static/js/Changeset'); -const attributes = require('../../static/js/attributes'); -const padManager = require('../db/PadManager'); -const _ = require('underscore'); -const Security = require('../../static/js/security'); -const hooks = require('../../static/js/pluginfw/hooks'); -const eejs = require('../eejs'); -const _analyzeLine = require('./ExportHelper')._analyzeLine; -const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; +import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset'; +import {decodeAttribString} from '../../static/js/attributes'; +import {getPad} from '../db/PadManager'; +import _ from 'underscore'; +const Security = require('security'); +import {aCallAll} from '../../static/js/pluginfw/hooks'; +import {requireP} from '../eejs'; +import {_analyzeLine, _encodeWhitespace} from './ExportHelper' +import {StringIterator} from "../../static/js/StringIterator"; +import {StringAssembler} from "../../static/js/StringAssembler"; 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; // fetch revision atext @@ -41,17 +43,17 @@ const getPadHTML = async (pad: PadType, revNum: string) => { 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 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 props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; await Promise.all([ // 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) => { tags.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 // like - hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps: string[]) => { + aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps: string[]) => { newProps.forEach((prop) => { tags.push(`span data-${prop[0]}="${prop[1]}"`); props.push(prop); @@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string css += '