From 5f39a1ee7f77b40aa93673c7ee9f02a03f904cfc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 9 Jul 2021 18:41:57 -0400 Subject: [PATCH 001/322] CSS: Underline links in error dialogs Underlining was removed for unknown reasons by commit d872b42e31ec4698d9a9e0c3bc04e4f215ae7473. --- src/static/css/pad/popup.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css index 0eb000996..6de108e40 100644 --- a/src/static/css/pad/popup.css +++ b/src/static/css/pad/popup.css @@ -45,9 +45,6 @@ .popup input[type=text], #users input[type=text] { outline: none; } -.popup a { - text-decoration: none -} .popup h1 { font-size: 1.8rem; margin-bottom: 10px; From 09f8ffbdb6426be829cfa167f1f26851544e641d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 10 Jul 2021 18:26:31 -0400 Subject: [PATCH 002/322] deps: Bump ueberdb2 to 1.4.11 --- src/package-lock.json | 273 +++++++++++++++++++++++++++++++----------- src/package.json | 2 +- 2 files changed, 201 insertions(+), 74 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 04358c82c..304b09f88 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -29,23 +29,23 @@ "integrity": "sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==" }, "@azure/core-auth": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.0.tgz", - "integrity": "sha512-kSDSZBL6c0CYdhb+7KuutnKGf2geeT+bCJAgccB0DD7wmNJSsQPcF7TcuoZX83B7VK4tLz/u+8sOO/CnCsYp8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.2.tgz", + "integrity": "sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==", "requires": { "@azure/abort-controller": "^1.0.0", - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@azure/core-http": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", - "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.0.0.tgz", + "integrity": "sha512-VBOfUh0z9ZF1WVqrLCtiGWMjkKic171p6mLXRkJKu+p5wuQTb4cU3bPq7nB6UuGAK17LI7hnU0SzydlCQrBuOw==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/core-tracing": "1.0.0-preview.12", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.1", @@ -96,6 +96,70 @@ "@azure/core-tracing": "1.0.0-preview.11", "events": "^3.0.0", "tslib": "^2.0.0" + }, + "dependencies": { + "@azure/core-http": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", + "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-asynciterator-polyfill": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.11", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.1", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "process": "^0.11.10", + "tough-cookie": "^4.0.0", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.4.19" + } + }, + "@azure/core-tracing": { + "version": "1.0.0-preview.11", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", + "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "1.0.0-rc.0", + "tslib": "^2.0.0" + } + }, + "@opentelemetry/api": { + "version": "1.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", + "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, "@azure/core-paging": { @@ -107,22 +171,21 @@ } }, "@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "version": "1.0.0-preview.12", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.12.tgz", + "integrity": "sha512-nvo2Wc4EKZGN6eFu9n3U7OXmASmL8VxoPIH7xaD6OlQqi44bouF0YIi9ID5rEsKLiAU59IYx6M297nqWVMWPDg==", "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.0", + "tslib": "^2.2.0" } }, "@azure/identity": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.3.0.tgz", - "integrity": "sha512-qYTaWA+5ir4+/iEry7n3l1TyeNhTHP8IRpjsbNv8ur8W/QjqZmCz1H2naebRp5tQmehXfo1pUrp2ew+qGhTh0g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.4.0.tgz", + "integrity": "sha512-nusX+L1qrCuQbRWQqPcgUCj07EvDVOgPVMnNS/cVtH8lfaGjWU6vdDJ49gROruh1jNjjZC0qpJBaM7OsK84zkw==", "requires": { - "@azure/core-http": "^1.2.4", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/core-http": "^2.0.0", + "@azure/core-tracing": "1.0.0-preview.12", "@azure/logger": "^1.0.0", "@azure/msal-node": "1.0.0-beta.6", "@types/stoppable": "^1.1.0", @@ -146,9 +209,9 @@ } }, "@azure/keyvault-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.2.1.tgz", - "integrity": "sha512-bO3Dl4cJgOkYSLudmzkSFg4os4gsDvaUozcJ9ZKdqZjIp/RHIZRFytbRcNe40rpKH2iLXcavNGVpMvEzAfERyQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.2.2.tgz", + "integrity": "sha512-SWRx0Z8ShLYnuqCitIOi3DqLSLBTI6G1F+Wv5/hy1w6ZXRnOwc74deQ8kfO0Tbm3n09SpaN4ytp6dmg6C2cRmQ==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-http": "^1.2.0", @@ -157,6 +220,70 @@ "@azure/core-tracing": "1.0.0-preview.11", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" + }, + "dependencies": { + "@azure/core-http": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", + "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-asynciterator-polyfill": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.11", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.1", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "process": "^0.11.10", + "tough-cookie": "^4.0.0", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.4.19" + } + }, + "@azure/core-tracing": { + "version": "1.0.0-preview.11", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", + "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "requires": { + "@opencensus/web-types": "0.0.7", + "@opentelemetry/api": "1.0.0-rc.0", + "tslib": "^2.0.0" + } + }, + "@opentelemetry/api": { + "version": "1.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", + "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, "@azure/logger": { @@ -226,17 +353,17 @@ } }, "@azure/msal-common": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.3.0.tgz", - "integrity": "sha512-jFqUWe83wVb6O8cNGGBFg2QlKvqM1ezUgJTEV7kIsAPX0RXhGFE4B1DLNt6hCnkTXDbw+KGW0zgxOEr4MJQwLw==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.4.0.tgz", + "integrity": "sha512-Qrs33Ctt2KM7NxArFPIUKc8UbIcm7zYxJFdJeQ9k7HKBhVk3e88CUz1Mw33cS/Jr+YA1H02OAzHg++bJ+4SFyQ==", "requires": { "debug": "^4.1.1" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -376,9 +503,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -401,9 +528,9 @@ "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==" }, "@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.1.tgz", + "integrity": "sha512-H5Djcc2txGAINgf3TNaq4yFofYSIK3722PM89S/3R8FuI/eqi1UscajlXk7EBkG9s2pxss/q6SHlpturaavXaw==" }, "@sinonjs/commons": { "version": "1.8.3", @@ -461,14 +588,14 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==" + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.1.tgz", + "integrity": "sha512-N87VuQi7HEeRJkhzovao/JviiqKjDKMVKxKMfUvSKw+MbkbW8R0nA3fi/MQhhlxV2fQ+2ReM+/Nt4efdrJx3zA==" }, "@types/node-fetch": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.10.tgz", - "integrity": "sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ==", + "version": "2.5.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.11.tgz", + "integrity": "sha512-2upCKaqVZETDRb8A2VTaRymqFBEgH8u6yr96b/u3+1uQEPDRo3mJLEiPk7vdXBHRtjwkjqzFYMJXrt0Z9QsYjQ==", "requires": { "@types/node": "*", "form-data": "^3.0.0" @@ -487,9 +614,9 @@ } }, "@types/request": { - "version": "2.48.5", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", - "integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==", + "version": "2.48.6", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.6.tgz", + "integrity": "sha512-vrZaV3Ij7j/l/3hz6OttZFtpRCu7zlq7XgkYHJP6FwVEAZkGQ095WqyJV08/GlW9eyXKVcp/xmtruHm8eHpw1g==", "requires": { "@types/caseless": "*", "@types/node": "*", @@ -518,9 +645,9 @@ } }, "@types/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", + "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" }, "@types/tunnel": { "version": "0.0.1", @@ -1209,9 +1336,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -1462,9 +1589,9 @@ "dev": true }, "dirty": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.1.tgz", - "integrity": "sha512-l/SMZcT+MjqOPpjarzJ8nQdxtxurURJM7js1l0Q2TQWtNbPzDYzkK++HlbT+XmM+adPFNdb3SOlVz9Jr7Df7xQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.2.tgz", + "integrity": "sha512-bf2Iw8z2spf6eitAPVLERYrNNB85itJXOa/gctg1mSjDBt0qt8E8212+XrXcYGlERXmqq6EfvQJKsrplVY8/5A==" }, "doctrine": { "version": "3.0.0", @@ -3058,9 +3185,9 @@ } }, "jsbi": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.4.tgz", - "integrity": "sha512-52QRRFSsi9impURE8ZUbzAMCLjPm4THO7H2fcuIvaaeFTbSysvkodbQQXIVsNgq/ypDbq6dJiuGKL0vZ/i9hUg==" + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.5.tgz", + "integrity": "sha512-w2BY0VOYC1ahe+w6Qhl4SFoPvPsZ9NPHY4bwass+LCgU7RK3PBoVQlQ3G1s7vI8W3CYyJiEXcbKF7FIM/L8q3Q==" }, "jsbn": { "version": "0.1.1", @@ -3693,9 +3820,9 @@ } }, "mongodb": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.9.tgz", - "integrity": "sha512-1nSCKgSunzn/CXwgOWgbPHUWOO5OfERcuOWISmqd610jn0s8BU9K4879iJVabqgpPPbA6hO7rG48eq+fGED3Mg==", + "version": "3.6.10", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.10.tgz", + "integrity": "sha512-fvIBQBF7KwCJnDZUnFFy4WqEFP8ibdXeFANnylW19+vOwdjOAvqIzPdsNCEMT6VKTHnYu4K64AWRih0mkFms6Q==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", @@ -3738,9 +3865,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -3810,9 +3937,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -3842,9 +3969,9 @@ "dev": true }, "needle": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", - "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.8.0.tgz", + "integrity": "sha512-ZTq6WYkN/3782H1393me3utVYdq2XyqNUFBsprEE3VMAT0+hP/cItpnITpqsY6ep2yeFE4Tqtqwc74VqUlUYtw==", "optional": true, "requires": { "debug": "^3.2.6", @@ -8073,9 +8200,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -8853,9 +8980,9 @@ } }, "ueberdb2": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.10.tgz", - "integrity": "sha512-Odg0Mdj17oK6biHuHMKN2DLny+WDG07qb2JoRx6SoluRIXb6eqceK7TilkydyDSiY6MrEeqWcjh9NKBLS2KtSQ==", + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.11.tgz", + "integrity": "sha512-yh9oh2hRhjfj7AcSSOGQhiYmHd7CYD4RT8NXtwYuYfMUP69o5RKjdR4kKaRMNefi8angxtfziuWxhXWd4u1EmQ==", "requires": { "async": "^3.2.0", "cassandra-driver": "^4.5.1", diff --git a/src/package.json b/src/package.json index 61c4cac15..9c40f9e31 100644 --- a/src/package.json +++ b/src/package.json @@ -69,7 +69,7 @@ "threads": "^1.4.0", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", - "ueberdb2": "^1.4.7", + "ueberdb2": "^1.4.11", "underscore": "1.13.1", "unorm": "1.6.0", "wtfnode": "^0.9.0" From 336d48add73a7d22191d810cc68855ecd72c11a5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 3 Jul 2021 18:19:26 -0400 Subject: [PATCH 003/322] Add support for square brackets in URLs This reverts commit 9022877cc60efb694578506771c294c25793415e. --- src/static/js/pad_utils.js | 4 ++-- .../frontend/specs/urls_become_clickable.js | 21 ++----------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 48f0624e9..fbb9f1637 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -60,10 +60,10 @@ const wordCharRegex = new RegExp(`[${[ const urlRegex = (() => { // TODO: wordCharRegex matches many characters that are not permitted in URIs. Are they included // here as an attempt to support IRIs? (See https://tools.ietf.org/html/rfc3987.) - const urlChar = `[-:@_.,~%+/?=&#!;()$'*${wordCharRegex.source.slice(1, -1)}]`; + const urlChar = `[-:@_.,~%+/?=&#!;()\\[\\]$'*${wordCharRegex.source.slice(1, -1)}]`; // Matches a single character that should not be considered part of the URL if it is the last // character that matches urlChar. - const postUrlPunct = '[:.,;?!)\'*]'; + const postUrlPunct = '[:.,;?!)\\]\'*]'; // Schemes that must be followed by :// const withAuth = `(?:${[ '(?:x-)?man', diff --git a/src/tests/frontend/specs/urls_become_clickable.js b/src/tests/frontend/specs/urls_become_clickable.js index f170a2a41..bb3f00b34 100644 --- a/src/tests/frontend/specs/urls_become_clickable.js +++ b/src/tests/frontend/specs/urls_become_clickable.js @@ -28,7 +28,7 @@ describe('urls', function () { }); describe('special characters inside URL', function () { - for (const char of '-:@_.,~%+/?=&#!;()$\'*') { + for (const char of '-:@_.,~%+/?=&#!;()[]$\'*') { const url = `https://etherpad.org/${char}foo`; it(url, async function () { await helper.edit(url); @@ -41,7 +41,7 @@ describe('urls', function () { }); describe('punctuation after URL is ignored', function () { - for (const char of ':.,;?!)\'*]') { + for (const char of ':.,;?!)]\'*') { const want = 'https://etherpad.org'; const input = want + char; it(input, async function () { @@ -53,21 +53,4 @@ describe('urls', function () { }); } }); - - // Square brackets are in the RFC3986 reserved set so they can legally appear in URIs, but they - // are explicitly excluded from linkification because including them is usually not desired (e.g., - // it can interfere with wiki/markdown link syntax). - describe('square brackets are excluded from linkified URLs', function () { - for (const char of '[]') { - const want = 'https://etherpad.org/'; - const input = `${want}${char}foo`; - it(input, async function () { - await helper.edit(input); - await helper.waitForPromise(() => txt().find('a').length === 1); - const link = txt().find('a'); - expect(link.attr('href')).to.be(want); - expect(link.text()).to.be(want); - }); - } - }); }); From ca4cc2d7c00874c88b384ea5dcd55462513ab394 Mon Sep 17 00:00:00 2001 From: Xavier Mehrenberger Date: Tue, 13 Jul 2021 11:46:08 +0200 Subject: [PATCH 004/322] Fix settings.useMonospaceFontGlobal When settings.useMonospaceFontGlobal is set to `true`, it sets the default font to 'monospace'. This font seems to have been removed in a5164dad430d99f1fc0a7fd045c37ce56e70efa6. This commit sets the default font to "RobotoMono" which is a valid option. Tested in a Docker environment, setting `PAD_OPTIONS_USE_MONOSPACE_FONT` to `true` Signed-off-by: Xavier Mehrenberger --- src/static/js/pad.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index ec156eb46..a7e80ede1 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -338,7 +338,7 @@ const handshake = () => { // If the Monospacefont value is set to true then change it to monospace. if (settings.useMonospaceFontGlobal === true) { - pad.changeViewOption('padFontFamily', 'monospace'); + pad.changeViewOption('padFontFamily', 'RobotoMono'); } // if the globalUserName value is set we need to tell the server and // the client about the new authorname From c6f643dd75187ac9bfe33730b65808174eca70ce Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 19 Jul 2021 13:02:46 +0200 Subject: [PATCH 005/322] Localisation updates from https://translatewiki.net. --- src/locales/tr.json | 74 ++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/locales/tr.json b/src/locales/tr.json index f9a9602f2..f3d7ae33f 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -44,12 +44,12 @@ "admin_settings.current": "Geçerli yapılandırma", "admin_settings.current_example-devel": "Örnek geliştirme ayarları şablonu", "admin_settings.current_example-prod": "Örnek üretim ayarları şablonu", - "admin_settings.current_restart.value": "Etherpad'ı sıfırla", + "admin_settings.current_restart.value": "Etherpad'i yeniden başlatın", "admin_settings.current_save.value": "Ayarları Kaydet", "admin_settings.page-title": "Ayarlar - Etherpad", "index.newPad": "Yeni Bloknot", "index.createOpenPad": "veya şu adla bir Bloknot oluşturun/açın:", - "index.openPad": "şu adla varolan bir Pad'i açın:", + "index.openPad": "şu adla varolan bir Bloknot'u açın:", "pad.toolbar.bold.title": "Kalın (Ctrl+B)", "pad.toolbar.italic.title": "Eğik (Ctrl+I)", "pad.toolbar.underline.title": "Altı Çizili (Ctrl+U)", @@ -65,12 +65,12 @@ "pad.toolbar.timeslider.title": "Zaman Çizelgesi", "pad.toolbar.savedRevision.title": "Düzeltmeyi Kaydet", "pad.toolbar.settings.title": "Ayarlar", - "pad.toolbar.embed.title": "Bu bloknotu Paylaş ve Göm", + "pad.toolbar.embed.title": "Bu Bloknot'u Paylaş ve Göm", "pad.toolbar.showusers.title": "Kullanıcıları bu bloknotta göster", "pad.colorpicker.save": "Kaydet", "pad.colorpicker.cancel": "İptal", "pad.loading": "Yükleniyor...", - "pad.noCookie": "Çerez bulunamadı. Lütfen tarayıcınızda çerezlere izin veriniz! Lütfen tarayıcınızda çerezlere izin verin! Oturumunuz ve ayarlarınız ziyaretler arasında kaydedilmez. Bunun nedeni, Etherpad'in bazı Tarayıcılarda bir iFrame'e dahil edilmiş olması olabilir. Lütfen Etherpad'in üst iFrame ile aynı alt alanda/alanda olduğundan emin olun", + "pad.noCookie": "Çerez bulunamadı. Lütfen tarayıcınızda çerezlere izin verin! Oturumunuz ve ayarlarınız ziyaretler arasında kaydedilmez. Bunun nedeni, bazı Tarayıcılarda Etherpad'in bir iFrame'e dahil edilmesi olabilir. Lütfen Etherpad'in üst iFrame ile aynı alt etki alanında/etki alanında olduğundan emin olun.", "pad.permissionDenied": "Bu bloknota erişmeye izniniz yok", "pad.settings.padSettings": "Bloknot Ayarları", "pad.settings.myView": "Görünümüm", @@ -83,7 +83,7 @@ "pad.settings.fontType.normal": "Olağan", "pad.settings.language": "Dil:", "pad.settings.about": "Hakkında", - "pad.settings.poweredBy": "Destekleyen", + "pad.settings.poweredBy": "Destekleyen:", "pad.importExport.import_export": "İçe/Dışa aktar", "pad.importExport.import": "Herhangi bir metin dosyası ya da belgesi yükle", "pad.importExport.importSuccessful": "Başarılı!", @@ -94,37 +94,37 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Açık Doküman Biçimi)", - "pad.importExport.abiword.innerHTML": "Yalnızca düz metin ya da HTML biçimlerini içe aktarabilirsiniz. Daha fazla gelişmiş içe aktarım özellikleri için lütfen AbiWord veya LibreOffice yükleyin.", + "pad.importExport.abiword.innerHTML": "Yalnızca düz metin veya HTML biçimlerinden içe aktarabilirsiniz. Daha gelişmiş içe aktarma özellikleri için lütfen AbiWord veya LibreOffice yükleyin .", "pad.modals.connected": "Bağlandı.", - "pad.modals.reconnecting": "Pedinize tekrar bağlanılıyor…", + "pad.modals.reconnecting": "Bloknotuza tekrar bağlanılıyor…", "pad.modals.forcereconnect": "Yeniden bağlanmaya zorla", "pad.modals.reconnecttimer": "Yeniden bağlanmaya çalışılıyor", "pad.modals.cancel": "İptal", "pad.modals.userdup": "Başka pencerede açıldı", "pad.modals.userdup.explanation": "Bu bloknot bu bilgisayarda birden fazla tarayıcı penceresinde açılmış gibi görünüyor.", "pad.modals.userdup.advice": "Bu pencereden kullanmak için yeniden bağlanın.", - "pad.modals.unauth": "Yetkili değil", + "pad.modals.unauth": "Yetkilendirilmemiş", "pad.modals.unauth.explanation": "Bu sayfayı görüntülerken izinleriniz değiştirildi. Tekrar bağlanmayı deneyin.", - "pad.modals.looping.explanation": "Eşitleme sunucusu ile iletişim sorunları yaşanıyor.", + "pad.modals.looping.explanation": "Senkronizasyon sunucusuyla iletişim sorunları yaşanıyor.", "pad.modals.looping.cause": "Belki de uygun olmayan güvenlik duvarı ya da vekil sunucu (proxy) ile bağlanmaya çalışıyorsunuz.", - "pad.modals.initsocketfail": "Sunucuya erişilemiyor.", - "pad.modals.initsocketfail.explanation": "Eşitleme sunucusuna bağlantı kurulamıyor.", + "pad.modals.initsocketfail": "Sunucuya ulaşılamıyor.", + "pad.modals.initsocketfail.explanation": "Senkronizasyon sunucusuna bağlanılamadı.", "pad.modals.initsocketfail.cause": "Bu sorun muhtemelen, tarayıcınızdan ya da internet bağlantınızdan kaynaklanıyor.", "pad.modals.slowcommit.explanation": "Sunucu yanıt vermiyor.", - "pad.modals.slowcommit.cause": "Bu hata ağ bağlantısı sebebiyle olabilir.", - "pad.modals.badChangeset.explanation": "Yaptığınız bir düzenleme eşitleme sunucusu tarafından kullanışsız/kural dışı olarak sınıflandırıldı.", - "pad.modals.badChangeset.cause": "Bunun nedeni, yanlış bir sunucu yapılandırması veya beklenmeyen başka bir davranış olabilir. Bunun bir hata olduğunu düşünüyorsanız lütfen servis yöneticisine başvurun. Düzenlemeye devam etmek için yeniden bağlanmayı deneyin.", + "pad.modals.slowcommit.cause": "Bu durum, ağ bağlantısıyla ilgili sorunlardan kaynaklanıyor olabilir.", + "pad.modals.badChangeset.explanation": "Yaptığınız bir düzenleme, senkronizasyon sunucusu tarafından yasa dışı olarak sınıflandırıldı.", + "pad.modals.badChangeset.cause": "Bunun nedeni yanlış bir sunucu yapılandırması veya başka bir beklenmeyen davranış olabilir. Bunun bir hata olduğunu düşünüyorsanız lütfen servis yöneticisi ile iletişime geçin. Düzenlemeye devam etmek için yeniden bağlanmayı deneyin.", "pad.modals.corruptPad.explanation": "Erişmeye çalıştığınız bloknot bozuk.", - "pad.modals.corruptPad.cause": "Bunun nedeni yanlış bir sunucu yapılandırması veya beklenmeyen başka bir davranış olabilir. Lütfen servis yöneticisine başvurun.", + "pad.modals.corruptPad.cause": "Bunun nedeni yanlış bir sunucu yapılandırması veya başka bir beklenmeyen davranış olabilir. Lütfen servis yöneticisine başvurun.", "pad.modals.deleted": "Silindi.", "pad.modals.deleted.explanation": "Bu bloknot kaldırılmış.", "pad.modals.rateLimited": "Oran Sınırlı.", - "pad.modals.rateLimited.explanation": "Bu pad'e çok fazla mesaj gönderdiniz, böylece bağlantı kesildi.", + "pad.modals.rateLimited.explanation": "Bu bloknota çok fazla mesaj gönderdiğiniz için bağlantı kesildi.", "pad.modals.rejected.explanation": "Sunucu, tarayıcınız tarafından gönderilen bir mesajı reddetti.", - "pad.modals.rejected.cause": "Siz pedi görüntülerken sunucu güncellenmiş olabilir veya Etherpad'de bir hata olabilir. Sayfayı yeniden yüklemeyi deneyin.", - "pad.modals.disconnected": "Bağlantınız koptu.", - "pad.modals.disconnected.explanation": "Sunucu bağlantısı kaybedildi", - "pad.modals.disconnected.cause": "Sunucu kullanılamıyor olabilir. Bunun devam etmesi durumunda servis yöneticisine bildirin.", + "pad.modals.rejected.cause": "Bloknotu görüntülerken sunucu güncellenmiş olabilir veya Etherpad'de bir hata olabilir. Sayfayı yeniden yüklemeyi deneyin.", + "pad.modals.disconnected": "Bağlantınız kesildi.", + "pad.modals.disconnected.explanation": "Sunucuyla bağlantı kesildi", + "pad.modals.disconnected.cause": "Sunucu kullanılamıyor olabilir. Böyle devam ederse lütfen hizmet yöneticisine bildirin.", "pad.share": "Bu bloknotu paylaş", "pad.share.readonly": "Yalnızca oku", "pad.share.link": "Bağlantı", @@ -133,20 +133,20 @@ "pad.chat.title": "Bu bloknot için sohbeti açın.", "pad.chat.loadmessages": "Daha fazla mesaj yükle", "pad.chat.stick.title": "Sohbeti ekrana yapıştır", - "pad.chat.writeMessage.placeholder": "Mesajını buraya yaz", - "timeslider.followContents": "Pad içerik güncellemelerini takip edin", + "pad.chat.writeMessage.placeholder": "Mesajınızı buraya yazın", + "timeslider.followContents": "Bloknot içerik güncellemelerini takip edin", "timeslider.pageTitle": "{{appTitle}} Zaman Çizelgesi", "timeslider.toolbar.returnbutton": "Bloknota geri dön", "timeslider.toolbar.authors": "Yazarlar:", "timeslider.toolbar.authorsList": "Yazar Yok", "timeslider.toolbar.exportlink.title": "Dışa aktar", - "timeslider.exportCurrent": "Mevcut sürümü şu olarak dışa aktar:", - "timeslider.version": "{{version}} sürümü", + "timeslider.exportCurrent": "Geçerli sürümü şu şekilde dışa aktar:", + "timeslider.version": "Sürüm {{version}}", "timeslider.saved": "{{day}} {{month}} {{year}} tarihinde kaydedildi", "timeslider.playPause": "Bloknot İçeriğini Oynat / Durdur", - "timeslider.backRevision": "Bu bloknottaki bir revizyona geri git", - "timeslider.forwardRevision": "Bu bloknatta sonraki revizyona git", - "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", + "timeslider.backRevision": "Bu bloknottaki bir sürüme geri dön", + "timeslider.forwardRevision": "Bu bloknatta sonraki sürüme git", + "timeslider.dateformat": "{{day}}.{{month}}.{{year}} {{hours}}.{{minutes}}.{{seconds}}", "timeslider.month.january": "Ocak", "timeslider.month.february": "Şubat", "timeslider.month.march": "Mart", @@ -160,19 +160,19 @@ "timeslider.month.november": "Kasım", "timeslider.month.december": "Aralık", "timeslider.unnamedauthors": "{{num}} isimsiz {[plural(num) one: yazar, other: yazar ]}", - "pad.savedrevs.marked": "Bu düzenleme artık kayıtlı bir düzeltme olarak işaretlendi", - "pad.savedrevs.timeslider": "Zaman kaydırıcısını ziyaret ederek kaydedilen revizyonları görebilirsiniz", + "pad.savedrevs.marked": "Bu sürüm, artık kaydedilmiş bir sürüm olarak işaretlendi.", + "pad.savedrevs.timeslider": "Kaydedilmiş sürümleri, zaman kaydırıcısını ziyaret ederek görebilirsiniz.", "pad.userlist.entername": "Adınızı girin", "pad.userlist.unnamed": "isimsiz", - "pad.editbar.clearcolors": "Bütün belgedeki yazarlık renkleri silinsin mi? Bu işlem geri alınamaz", + "pad.editbar.clearcolors": "Bütün belgedeki yazarlık renkleri silinsin mi? Bu işlem geri alınamaz.", "pad.impexp.importbutton": "Şimdi İçe Aktar", - "pad.impexp.importing": "İçe aktarıyor...", - "pad.impexp.confirmimport": "Bir dosya içe aktarılırken bloknotun mevcut metninin üzerine yazdırılır. Devam etmek istediğinizden emin misiniz?", - "pad.impexp.convertFailed": "Bu dosyayı içe aktarmak mümkün değil. Lütfen farklı bir belge biçimi kullanın ya da elle kopyala yapıştır yapın", - "pad.impexp.padHasData": "Bu Pad'in zaten değişiklikleri olduğu için bu dosyayı içe aktaramadık, lütfen yeni bir bloknota aktarın", - "pad.impexp.uploadFailed": "Yükleme başarısız, lütfen tekrar deneyin", - "pad.impexp.importfailed": "İçe aktarım başarısız oldu", + "pad.impexp.importing": "İçe aktarılıyor...", + "pad.impexp.confirmimport": "Bir dosyanın içe aktarılması, bloknotun mevcut metninin üzerine yazacaktır. Devam etmek istediğinizden emin misiniz?", + "pad.impexp.convertFailed": "Bu dosyayı içe aktaramadık. Lütfen farklı bir belge biçimi kullanın veya elle kopyalayıp yapıştırın", + "pad.impexp.padHasData": "Bu bloknotta zaten değişiklikler olduğu için bu dosyayı içe aktaramadık, lütfen yeni bir bloknota aktarın.", + "pad.impexp.uploadFailed": "Yükleme başarısız oldu, lütfen tekrar deneyin.", + "pad.impexp.importfailed": "İçe aktarılamadı", "pad.impexp.copypaste": "Lütfen kopyala yapıştır yapın", - "pad.impexp.exportdisabled": "{{type}} biçimiyle dışa aktarma devre dışı bırakıldı. Ayrıntılar için sistem yöneticinizle iletişime geçiniz.", + "pad.impexp.exportdisabled": "{{type}} biçiminde dışa aktarma devre dışı bırakıldı. Ayrıntılar için lütfen sistem yöneticinize başvurun.", "pad.impexp.maxFileSize": "Dosya çok büyük. İçe aktarma için izin verilen dosya boyutunu artırmak için site yöneticinize başvurun" } From ce5ef9350f166ef591ea3a5ad4f3099fe2b88212 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 13 Jul 2021 21:16:26 -0400 Subject: [PATCH 006/322] tests: sendkeys: Fix `{enter}` keypress event --- src/tests/frontend/lib/sendkeys.js | 4 ++-- src/tests/frontend/specs/change_user_color.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tests/frontend/lib/sendkeys.js b/src/tests/frontend/lib/sendkeys.js index 7550df38f..91433f964 100644 --- a/src/tests/frontend/lib/sendkeys.js +++ b/src/tests/frontend/lib/sendkeys.js @@ -426,8 +426,8 @@ $.fn.sendkeys.defaults = { '{enter}': function (rng){ rng.insertEOL(); rng.select(); - var x = '\n'.charCodeAt(0); - $(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x}); + $(rng._el).trigger( + {type: 'keypress', keyCode: 13, which: 13, charCode: 13, code: 'Enter', key: 'Enter'}); }, '{backspace}': function (rng){ var b = rng.bounds(); diff --git a/src/tests/frontend/specs/change_user_color.js b/src/tests/frontend/specs/change_user_color.js index d8fd97643..e39dc52e6 100644 --- a/src/tests/frontend/specs/change_user_color.js +++ b/src/tests/frontend/specs/change_user_color.js @@ -89,7 +89,6 @@ describe('change user color', function () { $chatButton.click(); const $chatInput = chrome$('#chatinput'); $chatInput.sendkeys('O hi'); // simulate a keypress of typing user - // simulate a keypress of enter actually does evt.which = 10 not 13 $chatInput.sendkeys('{enter}'); // wait until the chat message shows up From 834e05fc9c0b8ab72f40df887debc42713f5faa0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 13 Jul 2021 19:00:31 -0400 Subject: [PATCH 007/322] Chat: Use a ` diff --git a/src/tests/frontend/specs/chat.js b/src/tests/frontend/specs/chat.js index 3b1eaebf3..82527f372 100644 --- a/src/tests/frontend/specs/chat.js +++ b/src/tests/frontend/specs/chat.js @@ -25,7 +25,9 @@ describe('Chat messages and UI', function () { const username = helper.chatTextParagraphs().children('b').text(); const time = helper.chatTextParagraphs().children('.time').text(); - expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}`); + // TODO: The '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of it + // without breaking the other tests that use $.sendkeys(). + expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}\n`); await helper.hideChat(); }); @@ -46,7 +48,9 @@ describe('Chat messages and UI', function () { const username = chat.children('b').text(); const time = chat.children('.time').text(); - expect(chat.text()).to.be(`${username}${time} ${chatValue}`); + // TODO: Each '\n' is an artifact of $.sendkeys('{enter}'). Figure out how to get rid of them + // without breaking the other tests that use $.sendkeys(). + expect(chat.text()).to.be(`${username}${time} \n${chatValue}\n`); }); it('makes chat stick to right side of the screen via settings, ' + From faf84f014314562bba5339bcf9b370fc577fe023 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 13 Jul 2021 20:08:20 -0400 Subject: [PATCH 008/322] Chat: Display whitespace in chat messages --- src/static/css/pad/chat.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/static/css/pad/chat.css b/src/static/css/pad/chat.css index ac7334bea..9e0a17dac 100644 --- a/src/static/css/pad/chat.css +++ b/src/static/css/pad/chat.css @@ -92,6 +92,7 @@ #chattext p { padding: 3px; overflow-x: hidden; + white-space: pre-wrap; word-wrap: break-word; } #chattext .time { From cf86ae8b63ac43f65579420bb799961eb783e4ee Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 13 Jul 2021 18:54:41 -0400 Subject: [PATCH 009/322] Chat: Use `KeyboardEvent.key` instead of deprecated `.which` --- src/static/js/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 1d16e75bf..3f91445d2 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -238,7 +238,7 @@ exports.chat = (() => { $('#chatinput').keypress((evt) => { // if the user typed enter, fire the send - if (evt.which === 13 || evt.which === 10) { + if (evt.key === 'Enter') { evt.preventDefault(); this.send(); } From 4ceb3ca4c89a8b317528870b08f656ca6ee5b11f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 13 Jul 2021 19:00:08 -0400 Subject: [PATCH 010/322] Chat: Allow Shift-Enter to insert a newline --- src/static/js/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 3f91445d2..b62dc13dc 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -238,7 +238,7 @@ exports.chat = (() => { $('#chatinput').keypress((evt) => { // if the user typed enter, fire the send - if (evt.key === 'Enter') { + if (evt.key === 'Enter' && !evt.shiftKey) { evt.preventDefault(); this.send(); } From 62093adce53ce1532ebc40ce53bef37289d181c5 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 4 Jul 2021 05:36:35 +0200 Subject: [PATCH 011/322] tests: refactor inclusion of jquery and sendkeys via script tags Readability is increased by explicitly checking if jquery/sendkeys was already loaded before evaluating it in the context of ace_inner and the enclosing container (pad.html). Note that sendkeys is no longer evaluated in the context of ace_outer, as this isn't needed Also removes some IE 8/9 legacy code --- src/tests/frontend/helper.js | 54 +++++++++++----------- src/tests/frontend/helper/multipleUsers.js | 39 ++++++++++------ src/tests/frontend/index.html | 1 + src/tests/frontend/runner.js | 1 - 4 files changed, 51 insertions(+), 44 deletions(-) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 9e63cc8f5..b3c8d90db 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -4,19 +4,6 @@ const helper = {}; (() => { let $iframe; - const jsLibraries = {}; - - helper.init = async () => { - [ - jsLibraries.jquery, - jsLibraries.sendkeys, - ] = await Promise.all([ - $.get('../../static/js/vendors/jquery.js'), - $.get('lib/sendkeys.js'), - ]); - // make sure we don't override existing jquery - jsLibraries.jquery = `if (typeof $ === 'undefined') {\n${jsLibraries.jquery}\n}`; - }; helper.randomString = (len) => { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; @@ -28,20 +15,29 @@ const helper = {}; return randomstring; }; - const getFrameJQuery = ($iframe) => { - /* - I tried over 9001 ways to inject javascript into iframes. - This is the only way I found that worked in IE 7+8+9, FF and Chrome - */ + const getFrameJQuery = async ($iframe, includeJquery = false, includeSendkeys = false) => { const win = $iframe[0].contentWindow; const doc = win.document; - // IE 8+9 Hack to make eval appear - // https://stackoverflow.com/q/2720444 - win.execScript && win.execScript('null'); + const load = async (url) => { + const elem = doc.createElement('script'); + elem.setAttribute('src', url); + const p = new Promise((resolve, reject) => { + const handler = (evt) => { + elem.removeEventListener('load', handler); + elem.removeEventListener('error', handler); + if (evt.type === 'error') return reject(new Error(`failed to load ${url}`)); + resolve(); + }; + elem.addEventListener('load', handler); + elem.addEventListener('error', handler); + }); + doc.head.appendChild(elem); + await p; + }; - win.eval(jsLibraries.jquery); - win.eval(jsLibraries.sendkeys); + if (!win.$ && includeJquery) await load('../../static/js/vendors/jquery.js'); + if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js'); win.$.window = win; win.$.document = doc; @@ -125,7 +121,7 @@ const helper = {}; // set new iframe $('#iframe-container').append($iframe); await new Promise((resolve) => $iframe.one('load', resolve)); - helper.padChrome$ = getFrameJQuery($('#iframe-container iframe')); + helper.padChrome$ = await getFrameJQuery($('#iframe-container iframe'), true, true); helper.padChrome$.padeditor = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor; if (opts.clearCookies) { @@ -141,8 +137,10 @@ const helper = {}; if (opts._retry++ >= 4) throw new Error('Pad never loaded'); return await helper.aNewPad(opts); } - helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); - helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]')); + helper.padOuter$ = await getFrameJQuery( + helper.padChrome$('iframe[name="ace_outer"]'), true, false); + helper.padInner$ = await getFrameJQuery( + helper.padOuter$('iframe[name="ace_inner"]'), true, true); // disable all animations, this makes tests faster and easier helper.padChrome$.fx.off = true; @@ -184,8 +182,8 @@ const helper = {}; $('#iframe-container iframe').remove(); // set new iframe $('#iframe-container').append($iframe); - $iframe.one('load', () => { - helper.admin$ = getFrameJQuery($('#iframe-container iframe')); + $iframe.one('load', async () => { + helper.admin$ = await getFrameJQuery($('#iframe-container iframe'), true, false); }); }; diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js index d34676a66..9266bbe28 100644 --- a/src/tests/frontend/helper/multipleUsers.js +++ b/src/tests/frontend/helper/multipleUsers.js @@ -34,18 +34,11 @@ helper.multipleUsers = { }, async _loadJQueryForUser1Frame() { - const code = await $.get('/static/js/jquery.js'); - - // make sure we don't override existing jquery - const jQueryCode = `if(typeof $ === "undefined") {\n${code}\n}`; - const sendkeysCode = await $.get('/tests/frontend/lib/sendkeys.js'); - const codesToLoad = [jQueryCode, sendkeysCode]; - - this._user1.padChrome$ = getFrameJQuery(codesToLoad, this._user1.$frame); + this._user1.padChrome$ = await getFrameJQuery(this._user1.$frame, true, true); this._user1.padOuter$ = - getFrameJQuery(codesToLoad, this._user1.padChrome$('iframe[name="ace_outer"]')); + await getFrameJQuery(this._user1.padChrome$('iframe[name="ace_outer"]'), true, false); this._user1.padInner$ = - getFrameJQuery(codesToLoad, this._user1.padOuter$('iframe[name="ace_inner"]')); + await getFrameJQuery(this._user1.padOuter$('iframe[name="ace_inner"]'), true, true); // update helper vars now that they are available helper.padChrome$ = this._user1.padChrome$; @@ -86,14 +79,30 @@ helper.multipleUsers = { }, }; -// adapted form helper.js on Etherpad code -const getFrameJQuery = (codesToLoad, $iframe) => { +// copied from helper.js +const getFrameJQuery = async ($iframe, includeJquery = false, includeSendkeys = false) => { const win = $iframe[0].contentWindow; const doc = win.document; - for (let i = 0; i < codesToLoad.length; i++) { - win.eval(codesToLoad[i]); - } + const load = async (url) => { + const elem = doc.createElement('script'); + elem.setAttribute('src', url); + const p = new Promise((resolve, reject) => { + const handler = (evt) => { + elem.removeEventListener('load', handler); + elem.removeEventListener('error', handler); + if (evt.type === 'error') return reject(new Error(`failed to load ${url}`)); + resolve(); + }; + elem.addEventListener('load', handler); + elem.addEventListener('error', handler); + }); + doc.head.appendChild(elem); + await p; + }; + + if (!win.$ && includeJquery) await load('../../static/js/vendors/jquery.js'); + if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js'); win.$.window = win; win.$.document = doc; diff --git a/src/tests/frontend/index.html b/src/tests/frontend/index.html index 2b70ca667..9f542ee13 100644 --- a/src/tests/frontend/index.html +++ b/src/tests/frontend/index.html @@ -16,6 +16,7 @@ + diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 449c126c5..48a473aa7 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -314,7 +314,6 @@ $(() => (async () => { } $progressArea.remove(); - await helper.init(); const grep = getURLParameter('grep'); if (grep != null) { mocha.grep(grep); From 3d804092362f6e1f0605bd68b5587d70f5cb6fbc Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 22 Jul 2021 13:03:00 +0200 Subject: [PATCH 012/322] Localisation updates from https://translatewiki.net. --- src/locales/mk.json | 2 +- src/locales/uk.json | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/locales/mk.json b/src/locales/mk.json index a5a106060..68ba2f1cd 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -77,7 +77,7 @@ "pad.settings.about": "За додатоков", "pad.settings.poweredBy": "Овозможено од", "pad.importExport.import_export": "Увоз/Извоз", - "pad.importExport.import": "Подигање на било каква текстуална податотека или документ", + "pad.importExport.import": "Подигање на било каква било текстуална податотека или документ", "pad.importExport.importSuccessful": "Успешно!", "pad.importExport.export": "Извези ја тековната тетратка како", "pad.importExport.exportetherpad": "Etherpad", diff --git a/src/locales/uk.json b/src/locales/uk.json index 3604e38b9..e01af194e 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -4,6 +4,7 @@ "Andriykopanytsia", "Base", "Bunyk", + "DDPAT", "Lxlalexlxl", "Movses", "Olvin", @@ -31,6 +32,10 @@ "admin_plugins.page-title": "Менеджер плагінів — Etherpad", "admin_plugins.version": "Версія", "admin_plugins_info": "Інформація щодо виправлення неполадок", + "admin_plugins_info.hooks": "Встановлені гачки", + "admin_plugins_info.hooks_client": "Гачки на стороні клієнта", + "admin_plugins_info.hooks_server": "Серверні гачки", + "admin_plugins_info.parts": "Встановлені деталі", "admin_plugins_info.plugins": "Встановлені плагіни", "admin_plugins_info.page-title": "Інформація про плагіни — Etherpad", "admin_plugins_info.version": "Версія Etherpad", @@ -38,6 +43,8 @@ "admin_plugins_info.version_number": "Номер версії", "admin_settings": "Налаштування", "admin_settings.current": "Поточна конфігурація", + "admin_settings.current_example-devel": "Приклад шаблону налаштувань розробки", + "admin_settings.current_example-prod": "Приклад шаблону налаштувань виробництва", "admin_settings.current_restart.value": "Перезапустити Etherpad", "admin_settings.current_save.value": "Зберегти налаштування", "admin_settings.page-title": "Налаштування — Etherpad", @@ -64,10 +71,10 @@ "pad.colorpicker.save": "Зберегти", "pad.colorpicker.cancel": "Скасувати", "pad.loading": "Завантаження…", - "pad.noCookie": "Реп'яшки не знайдено. Будь ласка, увімкніть реп'яшки у вашому браузері! Ваша сесія та налаштування не зберігатимуться між візитами. Це може спричинятися тим, що Etherpad у деяких браузерах включений через iFrame. Будь ласка, переконайтеся, що iFrame міститься на тому ж піддомені/домені, що й батьківський iFrame", + "pad.noCookie": "Cookie не знайдено. Будь ласка, увімкніть cookie у вашому браузері! Ваша сесія та налаштування не зберігатимуться між візитами. Це може спричинятися тим, що Etherpad у деяких браузерах включений через iFrame. Будь ласка, переконайтеся, що iFrame міститься на тому ж піддомені/домені, що й батьківський iFrame", "pad.permissionDenied": "У Вас немає дозволу для доступу до цього документа", "pad.settings.padSettings": "Налаштування документа", - "pad.settings.myView": "Мій Вигляд", + "pad.settings.myView": "Мій погляд", "pad.settings.stickychat": "Завжди відображувати чат", "pad.settings.chatandusers": "Показати чат і користувачів", "pad.settings.colorcheck": "Кольори авторства", @@ -98,7 +105,7 @@ "pad.modals.userdup.explanation": "Документ, можливо, відкрито більш ніж в одному вікні браузера на цьому комп'ютері.", "pad.modals.userdup.advice": "Перепідключитись використовуючи це вікно.", "pad.modals.unauth": "Не авторизовано", - "pad.modals.unauth.explanation": "Ваші права було змінено під час перегляду цієї сторінк. Спробуйте перепідключитись.", + "pad.modals.unauth.explanation": "Ваші права було змінено під час перегляду цієї сторінки. Спробуйте відновити зв’язок.", "pad.modals.looping.explanation": "Проблеми зв'єзку з сервером синхронізації.", "pad.modals.looping.cause": "Можливо, підключились через несумісний брандмауер або проксі-сервер.", "pad.modals.initsocketfail": "Сервер недоступний.", @@ -137,7 +144,7 @@ "timeslider.exportCurrent": "Експортувати поточну версію як:", "timeslider.version": "Версія {{version}}", "timeslider.saved": "Збережено {{month}} {{day}}, {{year}}", - "timeslider.playPause": "Відтворення / Пауза Панель Зміст", + "timeslider.playPause": "Вміст панелі відтворення/паузи", "timeslider.backRevision": "Переглянути попередню ревізію цієї панелі", "timeslider.forwardRevision": "Переглянути наступну ревізію цієї панелі", "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", @@ -156,7 +163,7 @@ "timeslider.unnamedauthors": "{{num}} {[plural(num) one: безіменний автор, few: безіменні автори, many: безіменних авторів, other: безіменних авторів]}", "pad.savedrevs.marked": "Цю версію помічено збереженою версією", "pad.savedrevs.timeslider": "Ви можете побачити збережені ревізії, відвідавши \"Слайдер Змін Ревізій\"", - "pad.userlist.entername": "Введіть Ваше ім'я", + "pad.userlist.entername": "Введіть ваше ім'я", "pad.userlist.unnamed": "безіменний", "pad.editbar.clearcolors": "Очистити кольори у всьому документі? Це не можна буде відкотити", "pad.impexp.importbutton": "Імпортувати зараз", From c83bb058d1208d05ca54b2ed24d5908dd1fde223 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 19 Jul 2021 13:54:39 -0400 Subject: [PATCH 013/322] PadMessageHandler: Fix stats null dereference It is possible for the stats to be read before the `expressCreateServer` hook is called (in particular: when there is an error during startup), which is when the `socketio` variable is set. Check for non-null `socketio` before attempting to count the number of socket.io connections. --- src/node/handler/PadMessageHandler.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index a1194ea91..eb2bbc1e7 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -41,6 +41,7 @@ const {RateLimiterMemory} = require('rate-limiter-flexible'); const webaccess = require('../hooks/express/webaccess'); let rateLimiter; +let socketio = null; exports.socketio = () => { // The rate limiter is created in this hook so that restarting the server resets the limiter. The @@ -70,7 +71,7 @@ exports.socketio = () => { const sessioninfos = {}; exports.sessioninfos = sessioninfos; -stats.gauge('totalUsers', () => Object.keys(socketio.sockets.sockets).length); +stats.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0); stats.gauge('activePads', () => { const padIds = new Set(); for (const {padId} of Object.values(sessioninfos)) { @@ -87,11 +88,6 @@ const padChannels = new channels.channels( ({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback) ); -/** - * Saves the Socket class we need to send and receive data from the client - */ -let socketio; - /** * This Method is called by server.js to tell the message handler on which socket it should send * @param socket_io The Socket From 4a670e96ab44a15b7827d3a0f749381aca345757 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 23 Jul 2021 04:22:23 +0000 Subject: [PATCH 014/322] fix: upgrade express-rate-limit from 5.2.6 to 5.3.0 Snyk has created this PR to upgrade express-rate-limit from 5.2.6 to 5.3.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 304b09f88..1b8d1889d 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2358,9 +2358,9 @@ } }, "express-rate-limit": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.6.tgz", - "integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA==" + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz", + "integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew==" }, "express-session": { "version": "1.17.2", diff --git a/src/package.json b/src/package.json index 9c40f9e31..75f6bd89a 100644 --- a/src/package.json +++ b/src/package.json @@ -41,7 +41,7 @@ "etherpad-require-kernel": "1.0.11", "etherpad-yajsml": "0.0.4", "express": "4.17.1", - "express-rate-limit": "5.2.6", + "express-rate-limit": "5.3.0", "express-session": "1.17.2", "find-root": "1.1.0", "formidable": "1.2.2", From ed6fa9400a1283c6d33d4fd7983caa383d0e084a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 19 Jul 2021 23:00:15 +0200 Subject: [PATCH 015/322] Add healthcheck to Dockerfile --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 85b673fd6..c6339ae72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,6 +63,7 @@ RUN export DEBIAN_FRONTEND=noninteractive; \ apt-get -qq --no-install-recommends install \ ca-certificates \ git \ + curl \ ${INSTALL_ABIWORD:+abiword} \ ${INSTALL_SOFFICE:+libreoffice} \ && \ @@ -94,5 +95,7 @@ COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json # Fix group permissions RUN chmod -R g=u . +HEALTHCHECK --interval=20s --timeout=3s CMD curl -f http://localhost:9001 || exit 1 + EXPOSE 9001 CMD ["node", "src/node/server.js"] From 01184cd1aa856323d3b7b5b83250cc2c19d21eb9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 22 Jul 2021 13:28:17 -0400 Subject: [PATCH 016/322] tests: Remove unnecessary `includeJquery` parameter --- src/tests/frontend/helper.js | 14 ++++++-------- src/tests/frontend/helper/multipleUsers.js | 10 +++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index b3c8d90db..1f1cc3e71 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -15,7 +15,7 @@ const helper = {}; return randomstring; }; - const getFrameJQuery = async ($iframe, includeJquery = false, includeSendkeys = false) => { + const getFrameJQuery = async ($iframe, includeSendkeys = false) => { const win = $iframe[0].contentWindow; const doc = win.document; @@ -36,7 +36,7 @@ const helper = {}; await p; }; - if (!win.$ && includeJquery) await load('../../static/js/vendors/jquery.js'); + if (!win.$) await load('../../static/js/vendors/jquery.js'); if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js'); win.$.window = win; @@ -121,7 +121,7 @@ const helper = {}; // set new iframe $('#iframe-container').append($iframe); await new Promise((resolve) => $iframe.one('load', resolve)); - helper.padChrome$ = await getFrameJQuery($('#iframe-container iframe'), true, true); + helper.padChrome$ = await getFrameJQuery($('#iframe-container iframe'), true); helper.padChrome$.padeditor = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor; if (opts.clearCookies) { @@ -137,10 +137,8 @@ const helper = {}; if (opts._retry++ >= 4) throw new Error('Pad never loaded'); return await helper.aNewPad(opts); } - helper.padOuter$ = await getFrameJQuery( - helper.padChrome$('iframe[name="ace_outer"]'), true, false); - helper.padInner$ = await getFrameJQuery( - helper.padOuter$('iframe[name="ace_inner"]'), true, true); + helper.padOuter$ = await getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'), false); + helper.padInner$ = await getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'), true); // disable all animations, this makes tests faster and easier helper.padChrome$.fx.off = true; @@ -183,7 +181,7 @@ const helper = {}; // set new iframe $('#iframe-container').append($iframe); $iframe.one('load', async () => { - helper.admin$ = await getFrameJQuery($('#iframe-container iframe'), true, false); + helper.admin$ = await getFrameJQuery($('#iframe-container iframe'), false); }); }; diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js index 9266bbe28..206913265 100644 --- a/src/tests/frontend/helper/multipleUsers.js +++ b/src/tests/frontend/helper/multipleUsers.js @@ -34,11 +34,11 @@ helper.multipleUsers = { }, async _loadJQueryForUser1Frame() { - this._user1.padChrome$ = await getFrameJQuery(this._user1.$frame, true, true); + this._user1.padChrome$ = await getFrameJQuery(this._user1.$frame, true); this._user1.padOuter$ = - await getFrameJQuery(this._user1.padChrome$('iframe[name="ace_outer"]'), true, false); + await getFrameJQuery(this._user1.padChrome$('iframe[name="ace_outer"]'), false); this._user1.padInner$ = - await getFrameJQuery(this._user1.padOuter$('iframe[name="ace_inner"]'), true, true); + await getFrameJQuery(this._user1.padOuter$('iframe[name="ace_inner"]'), true); // update helper vars now that they are available helper.padChrome$ = this._user1.padChrome$; @@ -80,7 +80,7 @@ helper.multipleUsers = { }; // copied from helper.js -const getFrameJQuery = async ($iframe, includeJquery = false, includeSendkeys = false) => { +const getFrameJQuery = async ($iframe, includeSendkeys = false) => { const win = $iframe[0].contentWindow; const doc = win.document; @@ -101,7 +101,7 @@ const getFrameJQuery = async ($iframe, includeJquery = false, includeSendkeys = await p; }; - if (!win.$ && includeJquery) await load('../../static/js/vendors/jquery.js'); + if (!win.$) await load('../../static/js/vendors/jquery.js'); if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js'); win.$.window = win; From 42026ff7717c15aecaa8ccb46fd31f45ddb49fb3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 22 Jul 2021 13:30:28 -0400 Subject: [PATCH 017/322] tests: Add a comment explaining why `Promise.all()` is not used --- src/tests/frontend/helper.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 1f1cc3e71..cf8cdcee6 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -37,6 +37,8 @@ const helper = {}; }; if (!win.$) await load('../../static/js/vendors/jquery.js'); + // sendkeys.js depends on jQuery, so it cannot be loaded until jQuery has finished loading. (In + // other words, do not load both jQuery and sendkeys inside a Promise.all() call.) if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js'); win.$.window = win; From ff39eeafca14094c9bd26918818812953832feba Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 22 Jul 2021 13:33:48 -0400 Subject: [PATCH 018/322] tests: Factor out duplicate `getFrameJQuery()` --- src/tests/frontend/helper.js | 12 ++++--- src/tests/frontend/helper/multipleUsers.js | 37 ++-------------------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index cf8cdcee6..37d5d548c 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -15,7 +15,7 @@ const helper = {}; return randomstring; }; - const getFrameJQuery = async ($iframe, includeSendkeys = false) => { + helper.getFrameJQuery = async ($iframe, includeSendkeys = false) => { const win = $iframe[0].contentWindow; const doc = win.document; @@ -123,7 +123,7 @@ const helper = {}; // set new iframe $('#iframe-container').append($iframe); await new Promise((resolve) => $iframe.one('load', resolve)); - helper.padChrome$ = await getFrameJQuery($('#iframe-container iframe'), true); + helper.padChrome$ = await helper.getFrameJQuery($('#iframe-container iframe'), true); helper.padChrome$.padeditor = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor; if (opts.clearCookies) { @@ -139,8 +139,10 @@ const helper = {}; if (opts._retry++ >= 4) throw new Error('Pad never loaded'); return await helper.aNewPad(opts); } - helper.padOuter$ = await getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'), false); - helper.padInner$ = await getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'), true); + helper.padOuter$ = + await helper.getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'), false); + helper.padInner$ = + await helper.getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'), true); // disable all animations, this makes tests faster and easier helper.padChrome$.fx.off = true; @@ -183,7 +185,7 @@ const helper = {}; // set new iframe $('#iframe-container').append($iframe); $iframe.one('load', async () => { - helper.admin$ = await getFrameJQuery($('#iframe-container iframe'), false); + helper.admin$ = await helper.getFrameJQuery($('#iframe-container iframe'), false); }); }; diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js index 206913265..b4dfc9188 100644 --- a/src/tests/frontend/helper/multipleUsers.js +++ b/src/tests/frontend/helper/multipleUsers.js @@ -34,11 +34,11 @@ helper.multipleUsers = { }, async _loadJQueryForUser1Frame() { - this._user1.padChrome$ = await getFrameJQuery(this._user1.$frame, true); + this._user1.padChrome$ = await helper.getFrameJQuery(this._user1.$frame, true); this._user1.padOuter$ = - await getFrameJQuery(this._user1.padChrome$('iframe[name="ace_outer"]'), false); + await helper.getFrameJQuery(this._user1.padChrome$('iframe[name="ace_outer"]'), false); this._user1.padInner$ = - await getFrameJQuery(this._user1.padOuter$('iframe[name="ace_inner"]'), true); + await helper.getFrameJQuery(this._user1.padOuter$('iframe[name="ace_inner"]'), true); // update helper vars now that they are available helper.padChrome$ = this._user1.padChrome$; @@ -79,37 +79,6 @@ helper.multipleUsers = { }, }; -// copied from helper.js -const getFrameJQuery = async ($iframe, includeSendkeys = false) => { - const win = $iframe[0].contentWindow; - const doc = win.document; - - const load = async (url) => { - const elem = doc.createElement('script'); - elem.setAttribute('src', url); - const p = new Promise((resolve, reject) => { - const handler = (evt) => { - elem.removeEventListener('load', handler); - elem.removeEventListener('error', handler); - if (evt.type === 'error') return reject(new Error(`failed to load ${url}`)); - resolve(); - }; - elem.addEventListener('load', handler); - elem.addEventListener('error', handler); - }); - doc.head.appendChild(elem); - await p; - }; - - if (!win.$) await load('../../static/js/vendors/jquery.js'); - if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js'); - - win.$.window = win; - win.$.document = doc; - - return win.$; -}; - const getCookies = () => helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_utils').Cookies; From d723270388a06bfbcfcb589088447d5cf8aec4e6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 22 Jul 2021 13:42:31 -0400 Subject: [PATCH 019/322] tests: Improve readability of `multipleUsers.js` * Define utility functions above their use to silence lint warnings. * Use `.css()` instead of `.attr('style')` to manipulate style. * Pass an object to `.attr()` rather than call once per attribute. * Take advantage of chaining. * Inline unnecessary `padUrl` variable. * Delete some unnecessary comments. --- src/tests/frontend/helper/multipleUsers.js | 48 ++++++++++------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js index b4dfc9188..831bf403e 100644 --- a/src/tests/frontend/helper/multipleUsers.js +++ b/src/tests/frontend/helper/multipleUsers.js @@ -1,5 +1,21 @@ 'use strict'; +const getCookies = + () => helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_utils').Cookies; + +const setToken = (token) => getCookies().set('token', token); + +const getToken = () => getCookies().get('token'); + +const startActingLike = (user) => { + helper.padChrome$ = user.padChrome$; + helper.padOuter$ = user.padOuter$; + helper.padInner$ = user.padInner$; + if (helper.padChrome$) setToken(user.token); +}; + +const clearToken = () => getCookies().remove('token'); + helper.multipleUsers = { _user0: null, _user1: null, @@ -47,14 +63,11 @@ helper.multipleUsers = { }, async _createUser1Frame() { - // create the iframe - const padUrl = this._user0.$frame.attr('src'); - this._user1.$frame = $('`); - $('#linkinput').val(readonlyLink); - } else { - $('#embedinput') - .val(``); - $('#linkinput').val(padUrl); - } - }, - checkAllIconsAreDisplayedInToolbar: () => { - // reset style - $('.toolbar').removeClass('cropped'); - $('body').removeClass('mobile-layout'); - const menu_left = $('.toolbar .menu_left')[0]; - - // this is approximate, we cannot measure it because on mobile - // Layout it takes the full width on the bottom of the page - const menuRightWidth = 280; - if (menu_left && menu_left.scrollWidth > $('.toolbar').width() - menuRightWidth || - $('.toolbar').width() < 1000) { - $('body').addClass('mobile-layout'); - } - if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) { - $('.toolbar').addClass('cropped'); - } - }, - - _bodyKeyEvent(evt) { - // If the event is Alt F9 or Escape & we're already in the editbar menu - // Send the users focus back to the pad - if ((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27) { - if ($(':focus').parents('.toolbar').length === 1) { - // If we're in the editbar already.. - // Close any dropdowns we have open.. - this.toggleDropDown('none'); - // Check we're on a pad and not on the timeslider - // Or some other window I haven't thought about! - if (typeof pad === 'undefined') { - // Timeslider probably.. - // Shift focus away from any drop downs - $(':focus').blur(); // required to do not try to remove! - $('#editorcontainerbox').focus(); // Focus back onto the pad - } else { - // Shift focus away from any drop downs - $(':focus').blur(); // required to do not try to remove! - padeditor.ace.focus(); // Sends focus back to pad - // The above focus doesn't always work in FF, you have to hit enter afterwards - evt.preventDefault(); - } - } else { - // Focus on the editbar :) - const firstEditbarElement = parent.parent.$('#editbar button').first(); - - $(evt.currentTarget).blur(); - firstEditbarElement.focus(); - evt.preventDefault(); - } - } - // Are we in the toolbar?? - if ($(':focus').parents('.toolbar').length === 1) { - // On arrow keys go to next/previous button item in editbar - if (evt.keyCode !== 39 && evt.keyCode !== 37) return; - - // Get all the focusable items in the editbar - const focusItems = $('#editbar').find('button, select'); - - // On left arrow move to next button in editbar - if (evt.keyCode === 37) { - // If a dropdown is visible or we're in an input don't move to the next button - if ($('.popup').is(':visible') || evt.target.localName === 'input') return; - - this._editbarPosition--; - // Allow focus to shift back to end of row and start of row - if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1; - $(focusItems[this._editbarPosition]).focus(); - } - - // On right arrow move to next button in editbar - if (evt.keyCode === 39) { - // If a dropdown is visible or we're in an input don't move to the next button - if ($('.popup').is(':visible') || evt.target.localName === 'input') return; - - this._editbarPosition++; - // Allow focus to shift back to end of row and start of row - if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0; - $(focusItems[this._editbarPosition]).focus(); - } - } - }, - - _registerDefaultCommands() { - this.registerDropdownCommand('showusers', 'users'); - this.registerDropdownCommand('settings'); - this.registerDropdownCommand('connectivity'); - this.registerDropdownCommand('import_export'); - this.registerDropdownCommand('embed'); - - this.registerCommand('settings', () => { - this.toggleDropDown('settings', () => { - $('#options-stickychat').focus(); - }); - }); - - this.registerCommand('import_export', () => { - this.toggleDropDown('import_export', () => { - // If Import file input exists then focus on it.. - if ($('#importfileinput').length !== 0) { - setTimeout(() => { - $('#importfileinput').focus(); - }, 100); - } else { - $('.exportlink').first().focus(); - } - }); - }); - - this.registerCommand('showusers', () => { - this.toggleDropDown('users', () => { - $('#myusernameedit').focus(); - }); - }); - - this.registerCommand('embed', () => { - this.setEmbedLinks(); - this.toggleDropDown('embed', () => { - $('#linkinput').focus().select(); - }); - }); - - this.registerCommand('savedRevision', () => { - padsavedrevs.saveNow(); - }); - - this.registerCommand('showTimeSlider', () => { - document.location = `${document.location.pathname}/timeslider`; - }); - - const aceAttributeCommand = (cmd, ace) => { - ace.ace_toggleAttributeOnSelection(cmd); - }; - this.registerAceCommand('bold', aceAttributeCommand); - this.registerAceCommand('italic', aceAttributeCommand); - this.registerAceCommand('underline', aceAttributeCommand); - this.registerAceCommand('strikethrough', aceAttributeCommand); - - this.registerAceCommand('undo', (cmd, ace) => { - ace.ace_doUndoRedo(cmd); - }); - - this.registerAceCommand('redo', (cmd, ace) => { - ace.ace_doUndoRedo(cmd); - }); - - this.registerAceCommand('insertunorderedlist', (cmd, ace) => { - ace.ace_doInsertUnorderedList(); - }); - - this.registerAceCommand('insertorderedlist', (cmd, ace) => { - ace.ace_doInsertOrderedList(); - }); - - this.registerAceCommand('indent', (cmd, ace) => { - if (!ace.ace_doIndentOutdent(false)) { - ace.ace_doInsertUnorderedList(); - } - }); - - this.registerAceCommand('outdent', (cmd, ace) => { - ace.ace_doIndentOutdent(true); - }); - - this.registerAceCommand('clearauthorship', (cmd, ace) => { - // If we have the whole document selected IE control A has been hit - const rep = ace.ace_getRep(); - let doPrompt = false; - const lastChar = rep.lines.atIndex(rep.lines.length() - 1).width - 1; - const lastLineIndex = rep.lines.length() - 1; - if (rep.selStart[0] === 0 && rep.selStart[1] === 0) { - // nesting intentionally here to make things readable - if (rep.selEnd[0] === lastLineIndex && rep.selEnd[1] === lastChar) { - doPrompt = true; - } - } - /* - * NOTICE: This command isn't fired on Control Shift C. - * I intentionally didn't create duplicate code because if you are hitting - * Control Shift C we make the assumption you are a "power user" - * and as such we assume you don't need the prompt to bug you each time! - * This does make wonder if it's worth having a checkbox to avoid being - * prompted again but that's probably overkill for this contribution. - */ - - // if we don't have any text selected, we have a caret or we have already said to prompt - if ((!(rep.selStart && rep.selEnd)) || ace.ace_isCaret() || doPrompt) { - if (window.confirm(html10n.get('pad.editbar.clearcolors'))) { - ace.ace_performDocumentApplyAttributesToCharRange(0, ace.ace_getRep().alltext.length, [ - ['author', ''], - ]); - } - } else { - ace.ace_setAttributeOnSelection('author', ''); - } - }); - - this.registerCommand('timeslider_returnToPad', (cmd) => { - if (document.referrer.length > 0 && - document.referrer.substring(document.referrer.lastIndexOf('/') - 1, - document.referrer.lastIndexOf('/')) === 'p') { - document.location = document.referrer; - } else { - document.location = document.location.href - .substring(0, document.location.href.lastIndexOf('/')); - } - }); + done: () => { + state = T_START; + animator.scheduleAnimation(); }, }; })(); -exports.padeditbar = padeditbar; +exports.padeditbar = { + _editbarPosition: 0, + + init() { + this.dropdowns = []; + + $('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE + this.enable(); + $('#editbar [data-key]').each((i, elt) => { + $(elt).unbind('click'); + new ToolbarItem($(elt)).bind((command, item) => { + this.triggerCommand(command, item); + }); + }); + + $('body:not(#editorcontainerbox)').on('keydown', (evt) => { + this._bodyKeyEvent(evt); + }); + + $('.show-more-icon-btn').click(() => { + $('.toolbar').toggleClass('full-icons'); + }); + this.checkAllIconsAreDisplayedInToolbar(); + $(window).resize(_.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100)); + + this._registerDefaultCommands(); + + hooks.callAll('postToolbarInit', { + toolbar: this, + ace: padeditor.ace, + }); + + /* + * On safari, the dropdown in the toolbar gets hidden because of toolbar + * overflow:hidden property. This is a bug from Safari: any children with + * position:fixed (like the dropdown) should be displayed no matter + * overflow:hidden on parent + */ + if (!browser.safari) { + $('select').niceSelect(); + } + + // When editor is scrolled, we add a class to style the editbar differently + $('iframe[name="ace_outer"]').contents().scroll((ev) => { + $('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2); + }); + }, + isEnabled: () => true, + disable: () => { + $('#editbar').addClass('disabledtoolbar').removeClass('enabledtoolbar'); + }, + enable: () => { + $('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar'); + }, + commands: {}, + registerCommand(cmd, callback) { + this.commands[cmd] = callback; + return this; + }, + registerDropdownCommand(cmd, dropdown) { + dropdown = dropdown || cmd; + this.dropdowns.push(dropdown); + this.registerCommand(cmd, () => { + this.toggleDropDown(dropdown); + }); + }, + registerAceCommand(cmd, callback) { + this.registerCommand(cmd, (cmd, ace, item) => { + ace.callWithAce((ace) => { + callback(cmd, ace, item); + }, cmd, true); + }); + }, + triggerCommand(cmd, item) { + if (this.isEnabled() && this.commands[cmd]) { + this.commands[cmd](cmd, padeditor.ace, item); + } + if (padeditor.ace) padeditor.ace.focus(); + }, + toggleDropDown(moduleName, cb) { + // do nothing if users are sticked + if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) { + return; + } + + $('.nice-select').removeClass('open'); + $('.toolbar-popup').removeClass('popup-show'); + + // hide all modules and remove highlighting of all buttons + if (moduleName === 'none') { + const returned = false; + for (const thisModuleName of this.dropdowns) { + // skip the userlist + if (thisModuleName === 'users') continue; + + const module = $(`#${thisModuleName}`); + + // skip any "force reconnect" message + const isAForceReconnectMessage = module.find('button#forcereconnect:visible').length > 0; + if (isAForceReconnectMessage) continue; + if (module.hasClass('popup-show')) { + $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); + module.removeClass('popup-show'); + } + } + + if (!returned && cb) return cb(); + } else { + // hide all modules that are not selected and remove highlighting + // respectively add highlighting to the corresponding button + for (const thisModuleName of this.dropdowns) { + const module = $(`#${thisModuleName}`); + + if (module.hasClass('popup-show')) { + $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); + module.removeClass('popup-show'); + } else if (thisModuleName === moduleName) { + $(`li[data-key=${thisModuleName}] > a`).addClass('selected'); + module.addClass('popup-show'); + if (cb) { + cb(); + } + } + } + } + }, + setSyncStatus: (status) => { + if (status === 'syncing') { + syncAnimation.syncing(); + } else if (status === 'done') { + syncAnimation.done(); + } + }, + setEmbedLinks: () => { + const padUrl = window.location.href.split('?')[0]; + const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false'; + const props = 'width="100%" height="600" frameborder="0"'; + + if ($('#readonlyinput').is(':checked')) { + const urlParts = padUrl.split('/'); + urlParts.pop(); + const readonlyLink = `${urlParts.join('/')}/${clientVars.readOnlyId}`; + $('#embedinput') + .val(``); + $('#linkinput').val(readonlyLink); + } else { + $('#embedinput') + .val(``); + $('#linkinput').val(padUrl); + } + }, + checkAllIconsAreDisplayedInToolbar: () => { + // reset style + $('.toolbar').removeClass('cropped'); + $('body').removeClass('mobile-layout'); + const menu_left = $('.toolbar .menu_left')[0]; + + // this is approximate, we cannot measure it because on mobile + // Layout it takes the full width on the bottom of the page + const menuRightWidth = 280; + if (menu_left && menu_left.scrollWidth > $('.toolbar').width() - menuRightWidth || + $('.toolbar').width() < 1000) { + $('body').addClass('mobile-layout'); + } + if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) { + $('.toolbar').addClass('cropped'); + } + }, + + _bodyKeyEvent(evt) { + // If the event is Alt F9 or Escape & we're already in the editbar menu + // Send the users focus back to the pad + if ((evt.keyCode === 120 && evt.altKey) || evt.keyCode === 27) { + if ($(':focus').parents('.toolbar').length === 1) { + // If we're in the editbar already.. + // Close any dropdowns we have open.. + this.toggleDropDown('none'); + // Check we're on a pad and not on the timeslider + // Or some other window I haven't thought about! + if (typeof pad === 'undefined') { + // Timeslider probably.. + // Shift focus away from any drop downs + $(':focus').blur(); // required to do not try to remove! + $('#editorcontainerbox').focus(); // Focus back onto the pad + } else { + // Shift focus away from any drop downs + $(':focus').blur(); // required to do not try to remove! + padeditor.ace.focus(); // Sends focus back to pad + // The above focus doesn't always work in FF, you have to hit enter afterwards + evt.preventDefault(); + } + } else { + // Focus on the editbar :) + const firstEditbarElement = parent.parent.$('#editbar button').first(); + + $(evt.currentTarget).blur(); + firstEditbarElement.focus(); + evt.preventDefault(); + } + } + // Are we in the toolbar?? + if ($(':focus').parents('.toolbar').length === 1) { + // On arrow keys go to next/previous button item in editbar + if (evt.keyCode !== 39 && evt.keyCode !== 37) return; + + // Get all the focusable items in the editbar + const focusItems = $('#editbar').find('button, select'); + + // On left arrow move to next button in editbar + if (evt.keyCode === 37) { + // If a dropdown is visible or we're in an input don't move to the next button + if ($('.popup').is(':visible') || evt.target.localName === 'input') return; + + this._editbarPosition--; + // Allow focus to shift back to end of row and start of row + if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1; + $(focusItems[this._editbarPosition]).focus(); + } + + // On right arrow move to next button in editbar + if (evt.keyCode === 39) { + // If a dropdown is visible or we're in an input don't move to the next button + if ($('.popup').is(':visible') || evt.target.localName === 'input') return; + + this._editbarPosition++; + // Allow focus to shift back to end of row and start of row + if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0; + $(focusItems[this._editbarPosition]).focus(); + } + } + }, + + _registerDefaultCommands() { + this.registerDropdownCommand('showusers', 'users'); + this.registerDropdownCommand('settings'); + this.registerDropdownCommand('connectivity'); + this.registerDropdownCommand('import_export'); + this.registerDropdownCommand('embed'); + + this.registerCommand('settings', () => { + this.toggleDropDown('settings', () => { + $('#options-stickychat').focus(); + }); + }); + + this.registerCommand('import_export', () => { + this.toggleDropDown('import_export', () => { + // If Import file input exists then focus on it.. + if ($('#importfileinput').length !== 0) { + setTimeout(() => { + $('#importfileinput').focus(); + }, 100); + } else { + $('.exportlink').first().focus(); + } + }); + }); + + this.registerCommand('showusers', () => { + this.toggleDropDown('users', () => { + $('#myusernameedit').focus(); + }); + }); + + this.registerCommand('embed', () => { + this.setEmbedLinks(); + this.toggleDropDown('embed', () => { + $('#linkinput').focus().select(); + }); + }); + + this.registerCommand('savedRevision', () => { + padsavedrevs.saveNow(); + }); + + this.registerCommand('showTimeSlider', () => { + document.location = `${document.location.pathname}/timeslider`; + }); + + const aceAttributeCommand = (cmd, ace) => { + ace.ace_toggleAttributeOnSelection(cmd); + }; + this.registerAceCommand('bold', aceAttributeCommand); + this.registerAceCommand('italic', aceAttributeCommand); + this.registerAceCommand('underline', aceAttributeCommand); + this.registerAceCommand('strikethrough', aceAttributeCommand); + + this.registerAceCommand('undo', (cmd, ace) => { + ace.ace_doUndoRedo(cmd); + }); + + this.registerAceCommand('redo', (cmd, ace) => { + ace.ace_doUndoRedo(cmd); + }); + + this.registerAceCommand('insertunorderedlist', (cmd, ace) => { + ace.ace_doInsertUnorderedList(); + }); + + this.registerAceCommand('insertorderedlist', (cmd, ace) => { + ace.ace_doInsertOrderedList(); + }); + + this.registerAceCommand('indent', (cmd, ace) => { + if (!ace.ace_doIndentOutdent(false)) { + ace.ace_doInsertUnorderedList(); + } + }); + + this.registerAceCommand('outdent', (cmd, ace) => { + ace.ace_doIndentOutdent(true); + }); + + this.registerAceCommand('clearauthorship', (cmd, ace) => { + // If we have the whole document selected IE control A has been hit + const rep = ace.ace_getRep(); + let doPrompt = false; + const lastChar = rep.lines.atIndex(rep.lines.length() - 1).width - 1; + const lastLineIndex = rep.lines.length() - 1; + if (rep.selStart[0] === 0 && rep.selStart[1] === 0) { + // nesting intentionally here to make things readable + if (rep.selEnd[0] === lastLineIndex && rep.selEnd[1] === lastChar) { + doPrompt = true; + } + } + /* + * NOTICE: This command isn't fired on Control Shift C. + * I intentionally didn't create duplicate code because if you are hitting + * Control Shift C we make the assumption you are a "power user" + * and as such we assume you don't need the prompt to bug you each time! + * This does make wonder if it's worth having a checkbox to avoid being + * prompted again but that's probably overkill for this contribution. + */ + + // if we don't have any text selected, we have a caret or we have already said to prompt + if ((!(rep.selStart && rep.selEnd)) || ace.ace_isCaret() || doPrompt) { + if (window.confirm(html10n.get('pad.editbar.clearcolors'))) { + ace.ace_performDocumentApplyAttributesToCharRange(0, ace.ace_getRep().alltext.length, [ + ['author', ''], + ]); + } + } else { + ace.ace_setAttributeOnSelection('author', ''); + } + }); + + this.registerCommand('timeslider_returnToPad', (cmd) => { + if (document.referrer.length > 0 && + document.referrer.substring(document.referrer.lastIndexOf('/') - 1, + document.referrer.lastIndexOf('/')) === 'p') { + document.location = document.referrer; + } else { + document.location = document.location.href + .substring(0, document.location.href.lastIndexOf('/')); + } + }); + }, +}; From fda34407f97f5c779bb4c0aa55dd6428522723b8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 15 Jul 2021 18:05:03 -0400 Subject: [PATCH 044/322] pad_editbar: Move `dropdowns` initialization to constructor This avoids null dereference if a buggy caller calls `toggleDropDown('none')` before `init()`. (Ideally the caller would be fixed, but this is not always feasible.) --- src/static/js/pad_editbar.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index f25418e80..e973dc080 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -124,10 +124,9 @@ const syncAnimation = (() => { exports.padeditbar = { _editbarPosition: 0, + dropdowns: [], init() { - this.dropdowns = []; - $('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE this.enable(); $('#editbar [data-key]').each((i, elt) => { From a4652d67a04e312f0157200e84aaa8434897ff0b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 15 Jul 2021 18:10:52 -0400 Subject: [PATCH 045/322] pad_editbar: Move `commands` up for readability --- src/static/js/pad_editbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index e973dc080..ea684283e 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -124,6 +124,7 @@ const syncAnimation = (() => { exports.padeditbar = { _editbarPosition: 0, + commands: {}, dropdowns: [], init() { @@ -175,7 +176,6 @@ exports.padeditbar = { enable: () => { $('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar'); }, - commands: {}, registerCommand(cmd, callback) { this.commands[cmd] = callback; return this; From 59d6a8b32130b36834d97dd15a7ae8ebdc4b185c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 15 Jul 2021 18:15:41 -0400 Subject: [PATCH 046/322] pad_editbar: Delete unnecessary `returned` variable --- src/static/js/pad_editbar.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index ea684283e..b64216ff0 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -211,7 +211,6 @@ exports.padeditbar = { // hide all modules and remove highlighting of all buttons if (moduleName === 'none') { - const returned = false; for (const thisModuleName of this.dropdowns) { // skip the userlist if (thisModuleName === 'users') continue; @@ -227,7 +226,7 @@ exports.padeditbar = { } } - if (!returned && cb) return cb(); + if (cb) return cb(); } else { // hide all modules that are not selected and remove highlighting // respectively add highlighting to the corresponding button From a1b924f7465638bbdd7844e725014a2eb781b192 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 07:08:20 -0400 Subject: [PATCH 047/322] pad_editbar: Don't pass a callback to `toggleDropDown()` The function is synchronous so there's no point. --- src/static/js/pad_editbar.js | 34 +++++++++++++++------------------- src/static/js/pad_modals.js | 13 ++++++------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index b64216ff0..f44855f7b 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -360,35 +360,31 @@ exports.padeditbar = { this.registerDropdownCommand('embed'); this.registerCommand('settings', () => { - this.toggleDropDown('settings', () => { - $('#options-stickychat').focus(); - }); + this.toggleDropDown('settings'); + $('#options-stickychat').focus(); }); this.registerCommand('import_export', () => { - this.toggleDropDown('import_export', () => { - // If Import file input exists then focus on it.. - if ($('#importfileinput').length !== 0) { - setTimeout(() => { - $('#importfileinput').focus(); - }, 100); - } else { - $('.exportlink').first().focus(); - } - }); + this.toggleDropDown('import_export'); + // If Import file input exists then focus on it.. + if ($('#importfileinput').length !== 0) { + setTimeout(() => { + $('#importfileinput').focus(); + }, 100); + } else { + $('.exportlink').first().focus(); + } }); this.registerCommand('showusers', () => { - this.toggleDropDown('users', () => { - $('#myusernameedit').focus(); - }); + this.toggleDropDown('users'); + $('#myusernameedit').focus(); }); this.registerCommand('embed', () => { this.setEmbedLinks(); - this.toggleDropDown('embed', () => { - $('#linkinput').focus().select(); - }); + this.toggleDropDown('embed'); + $('#linkinput').focus().select(); }); this.registerCommand('savedRevision', () => { diff --git a/src/static/js/pad_modals.js b/src/static/js/pad_modals.js index 99d4dd6ac..54bd83877 100644 --- a/src/static/js/pad_modals.js +++ b/src/static/js/pad_modals.js @@ -32,15 +32,14 @@ const padmodals = (() => { pad = _pad; }, showModal: (messageId) => { - padeditbar.toggleDropDown('none', () => { - $('#connectivity .visible').removeClass('visible'); - $(`#connectivity .${messageId}`).addClass('visible'); + padeditbar.toggleDropDown('none'); + $('#connectivity .visible').removeClass('visible'); + $(`#connectivity .${messageId}`).addClass('visible'); - const $modal = $(`#connectivity .${messageId}`); - automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad); + const $modal = $(`#connectivity .${messageId}`); + automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad); - padeditbar.toggleDropDown('connectivity'); - }); + padeditbar.toggleDropDown('connectivity'); }, showOverlay: () => { // Prevent the user to interact with the toolbar. Useful when user is disconnected for example From 07e05a92eb73d2b56f73fb1810d5ce77de5242ed Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 15 Jul 2021 18:24:36 -0400 Subject: [PATCH 048/322] pad_editbar: Call the callback after all work is done --- src/static/js/pad_editbar.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index f44855f7b..9320fd749 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -225,8 +225,6 @@ exports.padeditbar = { module.removeClass('popup-show'); } } - - if (cb) return cb(); } else { // hide all modules that are not selected and remove highlighting // respectively add highlighting to the corresponding button @@ -239,12 +237,10 @@ exports.padeditbar = { } else if (thisModuleName === moduleName) { $(`li[data-key=${thisModuleName}] > a`).addClass('selected'); module.addClass('popup-show'); - if (cb) { - cb(); - } } } } + if (cb) cb(); }, setSyncStatus: (status) => { if (status === 'syncing') { From 148e10821b6f0e09d0db826a23eaa368f76efe0d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 15 Jul 2021 18:57:59 -0400 Subject: [PATCH 049/322] pad_editbar: Always call the callback --- src/static/js/pad_editbar.js | 72 +++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 9320fd749..49ad47908 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -201,46 +201,52 @@ exports.padeditbar = { if (padeditor.ace) padeditor.ace.focus(); }, toggleDropDown(moduleName, cb) { - // do nothing if users are sticked - if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) { - return; - } + let cbErr = null; + try { + // do nothing if users are sticked + if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) { + return; + } - $('.nice-select').removeClass('open'); - $('.toolbar-popup').removeClass('popup-show'); + $('.nice-select').removeClass('open'); + $('.toolbar-popup').removeClass('popup-show'); - // hide all modules and remove highlighting of all buttons - if (moduleName === 'none') { - for (const thisModuleName of this.dropdowns) { - // skip the userlist - if (thisModuleName === 'users') continue; + // hide all modules and remove highlighting of all buttons + if (moduleName === 'none') { + for (const thisModuleName of this.dropdowns) { + // skip the userlist + if (thisModuleName === 'users') continue; - const module = $(`#${thisModuleName}`); + const module = $(`#${thisModuleName}`); - // skip any "force reconnect" message - const isAForceReconnectMessage = module.find('button#forcereconnect:visible').length > 0; - if (isAForceReconnectMessage) continue; - if (module.hasClass('popup-show')) { - $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); - module.removeClass('popup-show'); - } - } - } else { - // hide all modules that are not selected and remove highlighting - // respectively add highlighting to the corresponding button - for (const thisModuleName of this.dropdowns) { - const module = $(`#${thisModuleName}`); - - if (module.hasClass('popup-show')) { - $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); - module.removeClass('popup-show'); - } else if (thisModuleName === moduleName) { - $(`li[data-key=${thisModuleName}] > a`).addClass('selected'); - module.addClass('popup-show'); + // skip any "force reconnect" message + const isAForceReconnectMessage = module.find('button#forcereconnect:visible').length > 0; + if (isAForceReconnectMessage) continue; + if (module.hasClass('popup-show')) { + $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); + module.removeClass('popup-show'); + } + } + } else { + // hide all modules that are not selected and remove highlighting + // respectively add highlighting to the corresponding button + for (const thisModuleName of this.dropdowns) { + const module = $(`#${thisModuleName}`); + + if (module.hasClass('popup-show')) { + $(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); + module.removeClass('popup-show'); + } else if (thisModuleName === moduleName) { + $(`li[data-key=${thisModuleName}] > a`).addClass('selected'); + module.addClass('popup-show'); + } } } + } catch (err) { + cbErr = err || new Error(err); + } finally { + if (cb) cb(cbErr); } - if (cb) cb(); }, setSyncStatus: (status) => { if (status === 'syncing') { From c629ee09a885f39b8ae1fc93b2983f18291284ce Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 15 Jul 2021 18:58:58 -0400 Subject: [PATCH 050/322] pad_editbar: Call the callback asynchronously This follows JavaScript best practices. --- src/static/js/pad_editbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 49ad47908..46b4ad914 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -245,7 +245,7 @@ exports.padeditbar = { } catch (err) { cbErr = err || new Error(err); } finally { - if (cb) cb(cbErr); + if (cb) Promise.resolve().then(() => cb(cbErr)); } }, setSyncStatus: (status) => { From ee41de480917625a539b43975b07025aae26bc54 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 15 Jul 2021 18:59:29 -0400 Subject: [PATCH 051/322] pad_editbar: Deprecate the `toggleDropDown` callback --- doc/api/editbar.md | 2 +- src/static/js/pad_editbar.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/api/editbar.md b/doc/api/editbar.md index d297eb255..f448a218a 100644 --- a/doc/api/editbar.md +++ b/doc/api/editbar.md @@ -5,7 +5,7 @@ src/static/js/pad_editbar.js ## disable() -## toggleDropDown(dropdown, callback) +## toggleDropDown(dropdown) Shows the dropdown `div.popup` whose `id` equals `dropdown`. ## registerCommand(cmd, callback) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 46b4ad914..dd352fef9 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -200,7 +200,9 @@ exports.padeditbar = { } if (padeditor.ace) padeditor.ace.focus(); }, - toggleDropDown(moduleName, cb) { + + // cb is deprecated (this function is synchronous so a callback is unnecessary). + toggleDropDown(moduleName, cb = null) { let cbErr = null; try { // do nothing if users are sticked From 97ccf9e0826138406494bb455f1463a31860c64c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 07:04:26 -0400 Subject: [PATCH 052/322] pad_editbar: Factor out duplicate code --- src/static/js/pad_editbar.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index dd352fef9..695b6c1c2 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -301,16 +301,14 @@ exports.padeditbar = { // If we're in the editbar already.. // Close any dropdowns we have open.. this.toggleDropDown('none'); + // Shift focus away from any drop downs + $(':focus').blur(); // required to do not try to remove! // Check we're on a pad and not on the timeslider // Or some other window I haven't thought about! if (typeof pad === 'undefined') { // Timeslider probably.. - // Shift focus away from any drop downs - $(':focus').blur(); // required to do not try to remove! $('#editorcontainerbox').focus(); // Focus back onto the pad } else { - // Shift focus away from any drop downs - $(':focus').blur(); // required to do not try to remove! padeditor.ace.focus(); // Sends focus back to pad // The above focus doesn't always work in FF, you have to hit enter afterwards evt.preventDefault(); From 5478d2ce60a36ecfc014a77f6bc06aefe85b0f6d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 07:16:36 -0400 Subject: [PATCH 053/322] pad_editbar: Use ES6 class syntax for readability --- src/static/js/pad_editbar.js | 133 ++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 695b6c1c2..132fcc21a 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -30,52 +30,53 @@ const padsavedrevs = require('./pad_savedrevs'); const _ = require('underscore'); require('./vendors/nice-select'); -const ToolbarItem = function (element) { - this.$el = element; -}; - -ToolbarItem.prototype.getCommand = function () { - return this.$el.attr('data-key'); -}; - -ToolbarItem.prototype.getValue = function () { - if (this.isSelect()) { - return this.$el.find('select').val(); +class ToolbarItem { + constructor(element) { + this.$el = element; } -}; -ToolbarItem.prototype.setValue = function (val) { - if (this.isSelect()) { - return this.$el.find('select').val(val); + getCommand() { + return this.$el.attr('data-key'); } -}; - -ToolbarItem.prototype.getType = function () { - return this.$el.attr('data-type'); -}; - -ToolbarItem.prototype.isSelect = function () { - return this.getType() === 'select'; -}; - -ToolbarItem.prototype.isButton = function () { - return this.getType() === 'button'; -}; - -ToolbarItem.prototype.bind = function (callback) { - if (this.isButton()) { - this.$el.click((event) => { - $(':focus').blur(); - callback(this.getCommand(), this); - event.preventDefault(); - }); - } else if (this.isSelect()) { - this.$el.find('select').change(() => { - callback(this.getCommand(), this); - }); + getValue() { + if (this.isSelect()) { + return this.$el.find('select').val(); + } } -}; + + setValue(val) { + if (this.isSelect()) { + return this.$el.find('select').val(val); + } + } + + getType() { + return this.$el.attr('data-type'); + } + + isSelect() { + return this.getType() === 'select'; + } + + isButton() { + return this.getType() === 'button'; + } + + bind(callback) { + if (this.isButton()) { + this.$el.click((event) => { + $(':focus').blur(); + callback(this.getCommand(), this); + event.preventDefault(); + }); + } else if (this.isSelect()) { + this.$el.find('select').change(() => { + callback(this.getCommand(), this); + }); + } + } +} const syncAnimation = (() => { const SYNCING = -100; @@ -122,10 +123,12 @@ const syncAnimation = (() => { }; })(); -exports.padeditbar = { - _editbarPosition: 0, - commands: {}, - dropdowns: [], +exports.padeditbar = new class { + constructor() { + this._editbarPosition = 0; + this.commands = {}; + this.dropdowns = []; + } init() { $('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE @@ -168,38 +171,38 @@ exports.padeditbar = { $('iframe[name="ace_outer"]').contents().scroll((ev) => { $('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2); }); - }, - isEnabled: () => true, - disable: () => { + } + isEnabled() { return true; } + disable() { $('#editbar').addClass('disabledtoolbar').removeClass('enabledtoolbar'); - }, - enable: () => { + } + enable() { $('#editbar').addClass('enabledtoolbar').removeClass('disabledtoolbar'); - }, + } registerCommand(cmd, callback) { this.commands[cmd] = callback; return this; - }, + } registerDropdownCommand(cmd, dropdown) { dropdown = dropdown || cmd; this.dropdowns.push(dropdown); this.registerCommand(cmd, () => { this.toggleDropDown(dropdown); }); - }, + } registerAceCommand(cmd, callback) { this.registerCommand(cmd, (cmd, ace, item) => { ace.callWithAce((ace) => { callback(cmd, ace, item); }, cmd, true); }); - }, + } triggerCommand(cmd, item) { if (this.isEnabled() && this.commands[cmd]) { this.commands[cmd](cmd, padeditor.ace, item); } if (padeditor.ace) padeditor.ace.focus(); - }, + } // cb is deprecated (this function is synchronous so a callback is unnecessary). toggleDropDown(moduleName, cb = null) { @@ -249,15 +252,15 @@ exports.padeditbar = { } finally { if (cb) Promise.resolve().then(() => cb(cbErr)); } - }, - setSyncStatus: (status) => { + } + setSyncStatus(status) { if (status === 'syncing') { syncAnimation.syncing(); } else if (status === 'done') { syncAnimation.done(); } - }, - setEmbedLinks: () => { + } + setEmbedLinks() { const padUrl = window.location.href.split('?')[0]; const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false'; const props = 'width="100%" height="600" frameborder="0"'; @@ -274,8 +277,8 @@ exports.padeditbar = { .val(``); $('#linkinput').val(padUrl); } - }, - checkAllIconsAreDisplayedInToolbar: () => { + } + checkAllIconsAreDisplayedInToolbar() { // reset style $('.toolbar').removeClass('cropped'); $('body').removeClass('mobile-layout'); @@ -291,7 +294,7 @@ exports.padeditbar = { if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) { $('.toolbar').addClass('cropped'); } - }, + } _bodyKeyEvent(evt) { // If the event is Alt F9 or Escape & we're already in the editbar menu @@ -352,7 +355,7 @@ exports.padeditbar = { $(focusItems[this._editbarPosition]).focus(); } } - }, + } _registerDefaultCommands() { this.registerDropdownCommand('showusers', 'users'); @@ -474,5 +477,5 @@ exports.padeditbar = { .substring(0, document.location.href.lastIndexOf('/')); } }); - }, -}; + } +}(); From 4a1f21ce345012388bc6a994d9b091121056e16c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 07:24:48 -0400 Subject: [PATCH 054/322] pad_editbar: Convert snake case to camel case --- src/static/js/pad_editbar.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 132fcc21a..9d006d2c7 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -282,16 +282,16 @@ exports.padeditbar = new class { // reset style $('.toolbar').removeClass('cropped'); $('body').removeClass('mobile-layout'); - const menu_left = $('.toolbar .menu_left')[0]; + const menuLeft = $('.toolbar .menu_left')[0]; // this is approximate, we cannot measure it because on mobile // Layout it takes the full width on the bottom of the page const menuRightWidth = 280; - if (menu_left && menu_left.scrollWidth > $('.toolbar').width() - menuRightWidth || + if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width() - menuRightWidth || $('.toolbar').width() < 1000) { $('body').addClass('mobile-layout'); } - if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) { + if (menuLeft && menuLeft.scrollWidth > $('.toolbar').width()) { $('.toolbar').addClass('cropped'); } } From d4e74fd0385b9f9e5dbc418cb43f4c10d8dd6637 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 2 Aug 2021 19:30:43 -0400 Subject: [PATCH 055/322] Pad: Add missing `` and `` tags The comment "head and body had been removed intentionally" implies that the tags were causing some sort of problem, but the commit that removed them (57075d15453331cd1ff6ad1175cb67bd7852d4d2) didn't provide any rationale. I'm assuming it was a mistake. --- src/templates/pad.html | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/templates/pad.html b/src/templates/pad.html index 110e76158..43120fd67 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -5,11 +5,10 @@ ; %> - -<% e.begin_block("htmlHead"); %> -<% e.end_block(); %> - + + <% e.begin_block("htmlHead"); %> + <% e.end_block(); %> <%=settings.title%> - - - + + <% e.begin_block("body"); %> @@ -522,4 +520,5 @@ <% e.end_block(); %> + From b6fba9d66dd56d3aa66899d6974eee55ce0d09e9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 2 Aug 2021 18:45:36 -0400 Subject: [PATCH 056/322] Pad: Improve page load error handler * Install the error handler early. * Include stack trace. * Remove unnecessary escaping. * Improve formatting. * Move to a separate script file. --- src/static/js/basic_error_handler.js | 48 ++++++++++++++++++++++++++++ src/templates/pad.html | 22 +------------ 2 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 src/static/js/basic_error_handler.js diff --git a/src/static/js/basic_error_handler.js b/src/static/js/basic_error_handler.js new file mode 100644 index 000000000..ab400aa8a --- /dev/null +++ b/src/static/js/basic_error_handler.js @@ -0,0 +1,48 @@ +// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 + +/* Copyright 2021 Richard Hansen */ + +'use strict'; + +// Set up an error handler to display errors that happen during page load. This handler will be +// overridden with a nicer handler by setupGlobalExceptionHandler() in pad_utils.js. + +(() => { + const originalHandler = window.onerror; + window.onerror = (...args) => { + const [msg, url, line, col, err] = args; + + // Purge the existing HTML and styles for a consistent view. + document.body.textContent = ''; + for (const el of document.querySelectorAll('head style, head link[rel="stylesheet"]')) { + el.remove(); + } + + const box = document.body; + box.textContent = ''; + const summary = document.createElement('p'); + box.appendChild(summary); + summary.appendChild(document.createTextNode('An error occurred while loading the page:')); + const msgBlock = document.createElement('blockquote'); + box.appendChild(msgBlock); + msgBlock.style.fontWeight = 'bold'; + msgBlock.appendChild(document.createTextNode(msg)); + const loc = document.createElement('p'); + box.appendChild(loc); + loc.appendChild(document.createTextNode(`in ${url}`)); + loc.appendChild(document.createElement('br')); + loc.appendChild(document.createTextNode(`at line ${line}:${col}`)); + const stackSummary = document.createElement('p'); + box.appendChild(stackSummary); + stackSummary.appendChild(document.createTextNode('Stack trace:')); + const stackBlock = document.createElement('blockquote'); + box.appendChild(stackBlock); + const stack = document.createElement('pre'); + stackBlock.appendChild(stack); + stack.appendChild(document.createTextNode(err.stack || err.toString())); + + if (typeof originalHandler === 'function') originalHandler(...args); + }; +})(); + +// @license-end diff --git a/src/templates/pad.html b/src/templates/pad.html index 43120fd67..bcede8fd3 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -33,6 +33,7 @@ for the JavaScript code in this page.| */ + @@ -441,27 +442,6 @@ <% e.begin_block("scripts"); %> - - - From 1e22e0102d051aa0da5517d6f8cf5e079e96dc4d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 2 Aug 2021 19:41:42 -0400 Subject: [PATCH 057/322] Timeslider: Move `` and `<script>` inside `<head>` --- src/templates/timeslider.html | 48 +++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index fe43668c8..2070f56f5 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -4,31 +4,31 @@ %> <!doctype html> <html class="pad <%=settings.skinVariants%>"> -<title data-l10n-id="timeslider.pageTitle" data-l10n-args='{ "appTitle": "<%=settings.title%>" }'><%=settings.title%> Timeslider - + <%=settings.title%> Timeslider + From 0ca5a3459f808bad3edbc70bbf96344e695b3216 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 2 Aug 2021 19:57:33 -0400 Subject: [PATCH 058/322] Timeslider: Install an error handler --- src/static/js/pad_utils.js | 1 + src/static/js/timeslider.js | 2 ++ src/templates/timeslider.html | 1 + 3 files changed, 4 insertions(+) diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index fbb9f1637..7b2d50604 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -296,6 +296,7 @@ const padutils = { let globalExceptionHandler = null; padutils.setupGlobalExceptionHandler = () => { if (globalExceptionHandler == null) { + require('./vendors/gritter'); globalExceptionHandler = (e) => { let type; let err; diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 47246b4a1..2b226aef0 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -29,11 +29,13 @@ require('./vendors/jquery'); const Cookies = require('./pad_utils').Cookies; const randomString = require('./pad_utils').randomString; const hooks = require('./pluginfw/hooks'); +const padutils = require('./pad_utils').padutils; const socketio = require('./socketio'); let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider; const init = () => { + padutils.setupGlobalExceptionHandler(); $(document).ready(() => { // start the custom js if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 2070f56f5..dc351b1d0 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -29,6 +29,7 @@ for the JavaScript code in this page.| */ + From e581ee01f223fdc9599045e8630a9bcdfefe9163 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 15 Aug 2021 02:44:43 -0400 Subject: [PATCH 059/322] ace2_inner: Formatting improvements --- src/static/js/ace2_inner.js | 349 +++++++++++++----------------------- 1 file changed, 123 insertions(+), 226 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 32fc0a84f..32336d2a5 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -528,9 +528,7 @@ function Ace2Inner(editorInfo, cssManagers) { performDocumentApplyChangeset(changeset); performSelectionChange( - [0, rep.lines.atIndex(0).lineMarker], - [0, rep.lines.atIndex(0).lineMarker] - ); + [0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]); idleWorkTimer.atMost(100); @@ -1129,10 +1127,7 @@ function Ace2Inner(editorInfo, cssManagers) { splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]); netNumLinesChangeSoFar += (lines.length - (b - a)); } else if (b > a) { - splicesToDo.push([a + netNumLinesChangeSoFar, - b - a, - [], - []]); + splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [], []]); } i++; } @@ -1271,12 +1266,8 @@ function Ace2Inner(editorInfo, cssManagers) { }); }; - const isCaret = () => ( - rep.selStart && - rep.selEnd && - rep.selStart[0] === rep.selEnd[0] && - rep.selStart[1] === rep.selEnd[1] - ); + const isCaret = () => (rep.selStart && rep.selEnd && + rep.selStart[0] === rep.selEnd[0] && rep.selStart[1] === rep.selEnd[1]); editorInfo.ace_isCaret = isCaret; // prereq: isCaret() @@ -1473,12 +1464,9 @@ function Ace2Inner(editorInfo, cssManagers) { if (requiredSelectionSetting) { performSelectionChange( - lineAndColumnFromChar( - requiredSelectionSetting[0] - ), + lineAndColumnFromChar(requiredSelectionSetting[0]), lineAndColumnFromChar(requiredSelectionSetting[1]), - requiredSelectionSetting[2] - ); + requiredSelectionSetting[2]); } }; @@ -1553,8 +1541,8 @@ function Ace2Inner(editorInfo, cssManagers) { newText = newText.substring(0, newText.length - 1); } } - performDocumentReplaceRange(lineAndColumnFromChar(startChar), - lineAndColumnFromChar(endChar), newText); + performDocumentReplaceRange( + lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText); }; const performDocumentApplyAttributesToCharRange = (start, end, attribs) => { @@ -1697,10 +1685,7 @@ function Ace2Inner(editorInfo, cssManagers) { const attributeValue = selectionAllHasIt ? '' : 'true'; documentAttributeManager.setAttributesOnRange( - rep.selStart, - rep.selEnd, - [[attributeName, attributeValue]] - ); + rep.selStart, rep.selEnd, [[attributeName, attributeValue]]); if (attribIsFormattingStyle(attributeName)) { updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ... } @@ -1750,9 +1735,8 @@ function Ace2Inner(editorInfo, cssManagers) { const oldText = rep.alltext.substring(startOldChar, endOldChar); const oldAttribs = rep.alines.slice(startLine, startLine + deleteCount).join(''); const newAttribs = `${lineAttribs.join('|1+1')}|1+1`; // not valid in a changeset - const analysis = analyzeChange( - oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar - ); + const analysis = + analyzeChange(oldText, newText, oldAttribs, newAttribs, selStartHintChar, selEndHintChar); const commonStart = analysis[0]; let commonEnd = analysis[1]; let shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); @@ -1837,9 +1821,7 @@ function Ace2Inner(editorInfo, cssManagers) { return rep.apool.putAttrib([k, '']); } return false; - } - ) - ); + })); const builder1 = startBuilder(); if (shiftFinalNewlineToBeforeNewText) { @@ -2078,8 +2060,7 @@ function Ace2Inner(editorInfo, cssManagers) { isScrollableEditEvent(currentCallStack.type); const innerHeight = getInnerHeight(); scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary( - rep, isScrollableEvent, innerHeight * 2 - ); + rep, isScrollableEvent, innerHeight * 2); } return true; @@ -2087,11 +2068,7 @@ function Ace2Inner(editorInfo, cssManagers) { return false; }; - const isPadLoading = (eventType) => ( - eventType === 'setup') || - (eventType === 'setBaseText') || - (eventType === 'importText' - ); + const isPadLoading = (t) => t === 'setup' || t === 'setBaseText' || t === 'importText'; const updateStyleButtonState = (attribName, hasStyleOnRepSelection) => { const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a'); @@ -2475,11 +2452,10 @@ function Ace2Inner(editorInfo, cssManagers) { const doIndentOutdent = (isOut) => { if (!((rep.selStart && rep.selEnd) || - ((rep.selStart[0] === rep.selEnd[0]) && - (rep.selStart[1] === rep.selEnd[1]) && - rep.selEnd[1] > 1)) && - (isOut !== true) - ) { + (rep.selStart[0] === rep.selEnd[0] && + rep.selStart[1] === rep.selEnd[1] && + rep.selEnd[1] > 1)) && + isOut !== true) { return false; } @@ -2553,9 +2529,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (prevLineBlank && !prevLineListType) { // previous line is blank, remove it performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], - [theLine, 0], '' - ); + [theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); } else { // delistify performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); @@ -2563,15 +2537,11 @@ function Ace2Inner(editorInfo, cssManagers) { } else if (thisLineHasMarker && prevLineEntry) { // If the line has any attributes assigned, remove them by removing the marker '*' performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], - [theLine, lineEntry.lineMarker], '' - ); + [theLine - 1, prevLineEntry.text.length], [theLine, lineEntry.lineMarker], ''); } else if (theLine > 0) { // remove newline performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], - [theLine, 0], '' - ); + [theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); } } else { const docChar = caretDocChar(); @@ -2624,29 +2594,24 @@ function Ace2Inner(editorInfo, cssManagers) { // 224 is the command-key under Mac Firefox. // 91 is the Windows key in IE; it is ASCII for open-bracket but isn't the keycode for that key // 20 is capslock in IE. - const isModKey = ((!charCode) && - ((type === 'keyup') || (type === 'keydown')) && - ( - keyCode === 16 || keyCode === 17 || keyCode === 18 || - keyCode === 20 || keyCode === 224 || keyCode === 91 - )); + const isModKey = !charCode && (type === 'keyup' || type === 'keydown') && + (keyCode === 16 || keyCode === 17 || keyCode === 18 || + keyCode === 20 || keyCode === 224 || keyCode === 91); if (isModKey) return; // If the key is a keypress and the browser is opera and the key is enter, // do nothign at all as this fires twice. - if (keyCode === 13 && browser.opera && (type === 'keypress')) { + if (keyCode === 13 && browser.opera && type === 'keypress') { // This stops double enters in Opera but double Tabs still show on single // tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice return; } let specialHandled = false; - const isTypeForSpecialKey = ((browser.safari || - browser.chrome || - browser.firefox) ? (type === 'keydown') : (type === 'keypress')); - const isTypeForCmdKey = ((browser.safari || - browser.chrome || - browser.firefox) ? (type === 'keydown') : (type === 'keypress')); + const isTypeForSpecialKey = browser.safari || browser.chrome || browser.firefox + ? type === 'keydown' : type === 'keypress'; + const isTypeForCmdKey = browser.safari || browser.chrome || browser.firefox + ? type === 'keydown' : type === 'keypress'; let stopped = false; @@ -2679,13 +2644,9 @@ function Ace2Inner(editorInfo, cssManagers) { } const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; - if ( - (!specialHandled) && - altKey && - isTypeForSpecialKey && - keyCode === 120 && - padShortcutEnabled.altF9 - ) { + if (!specialHandled && isTypeForSpecialKey && + altKey && keyCode === 120 && + padShortcutEnabled.altF9) { // Alt F9 focuses on the File Menu and/or editbar. // Note that while most editors use Alt F10 this is not desirable // As ubuntu cannot use Alt F10.... @@ -2698,26 +2659,18 @@ function Ace2Inner(editorInfo, cssManagers) { firstEditbarElement.focus(); evt.preventDefault(); } - if ( - (!specialHandled) && - altKey && keyCode === 67 && - type === 'keydown' && - padShortcutEnabled.altC - ) { + if (!specialHandled && type === 'keydown' && + altKey && keyCode === 67 && + padShortcutEnabled.altC) { // Alt c focuses on the Chat window $(this).blur(); parent.parent.chat.show(); parent.parent.$('#chatinput').focus(); evt.preventDefault(); } - if ( - (!specialHandled) && - evt.ctrlKey && - shiftKey && - keyCode === 50 && - type === 'keydown' && - padShortcutEnabled.cmdShift2 - ) { + if (!specialHandled && type === 'keydown' && + evt.ctrlKey && shiftKey && keyCode === 50 && + padShortcutEnabled.cmdShift2) { // Control-Shift-2 shows a gritter popup showing a line author const lineNumber = rep.selEnd[0]; const alineAttrs = rep.alines[lineNumber]; @@ -2791,11 +2744,9 @@ function Ace2Inner(editorInfo, cssManagers) { time: '4000', }); } - if ((!specialHandled) && - isTypeForSpecialKey && - keyCode === 8 && - padShortcutEnabled.delete - ) { + if (!specialHandled && isTypeForSpecialKey && + keyCode === 8 && + padShortcutEnabled.delete) { // "delete" key; in mozilla, if we're at the beginning of a line, normalize now, // or else deleting a blank line can take two delete presses. // -- @@ -2808,11 +2759,9 @@ function Ace2Inner(editorInfo, cssManagers) { doDeleteKey(evt); specialHandled = true; } - if ((!specialHandled) && - isTypeForSpecialKey && - keyCode === 13 && - padShortcutEnabled.return - ) { + if (!specialHandled && isTypeForSpecialKey && + keyCode === 13 && + padShortcutEnabled.return) { // return key, handle specially; // note that in mozilla we need to do an incorporation for proper return behavior anyway. fastIncorp(4); @@ -2823,11 +2772,9 @@ function Ace2Inner(editorInfo, cssManagers) { }, 0); specialHandled = true; } - if ((!specialHandled) && - isTypeForSpecialKey && - keyCode === 27 && - padShortcutEnabled.esc - ) { + if (!specialHandled && isTypeForSpecialKey && + keyCode === 27 && + padShortcutEnabled.esc) { // prevent esc key; // in mozilla versions 14-19 avoid reconnecting pad. @@ -2838,15 +2785,11 @@ function Ace2Inner(editorInfo, cssManagers) { // close all gritters when the user hits escape key parent.parent.$.gritter.removeAll(); } - if ( - (!specialHandled) && - /* Do a saved revision on ctrl S */ - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 's' && - (evt.metaKey || evt.ctrlKey) && - !evt.altKey && - padShortcutEnabled.cmdS - ) { + if (!specialHandled && isTypeForCmdKey && + /* Do a saved revision on ctrl S */ + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 's' && + !evt.altKey && + padShortcutEnabled.cmdS) { evt.preventDefault(); const originalBackground = parent.parent.$('#revisionlink').css('background'); parent.parent.$('#revisionlink').css({background: 'lightyellow'}); @@ -2857,25 +2800,21 @@ function Ace2Inner(editorInfo, cssManagers) { parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); specialHandled = true; } - if ((!specialHandled) && - // tab - isTypeForSpecialKey && - keyCode === 9 && - !(evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.tab) { + if (!specialHandled && isTypeForSpecialKey && + // tab + keyCode === 9 && + !(evt.metaKey || evt.ctrlKey) && + padShortcutEnabled.tab) { fastIncorp(5); evt.preventDefault(); doTabKey(evt.shiftKey); specialHandled = true; } - if ((!specialHandled) && - // cmd-Z (undo) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'z' && - (evt.metaKey || evt.ctrlKey) && - !evt.altKey && - padShortcutEnabled.cmdZ - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-Z (undo) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'z' && + !evt.altKey && + padShortcutEnabled.cmdZ) { fastIncorp(6); evt.preventDefault(); if (evt.shiftKey) { @@ -2885,120 +2824,93 @@ function Ace2Inner(editorInfo, cssManagers) { } specialHandled = true; } - if ((!specialHandled) && - // cmd-Y (redo) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'y' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdY - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-Y (redo) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'y' && + padShortcutEnabled.cmdY) { fastIncorp(10); evt.preventDefault(); doUndoRedo('redo'); specialHandled = true; } - if ((!specialHandled) && - // cmd-B (bold) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'b' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdB) { + if (!specialHandled && isTypeForCmdKey && + // cmd-B (bold) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'b' && + padShortcutEnabled.cmdB) { fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('bold'); specialHandled = true; } - if ((!specialHandled) && - // cmd-I (italic) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'i' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdI - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-I (italic) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'i' && + padShortcutEnabled.cmdI) { fastIncorp(14); evt.preventDefault(); toggleAttributeOnSelection('italic'); specialHandled = true; } - if ((!specialHandled) && - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'u' && - (evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.cmdU - ) { - // cmd-U (underline) + if (!specialHandled && isTypeForCmdKey && + // cmd-U (underline) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'u' && + padShortcutEnabled.cmdU) { fastIncorp(15); evt.preventDefault(); toggleAttributeOnSelection('underline'); specialHandled = true; } - if ((!specialHandled) && - // cmd-5 (strikethrough) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === '5' && - (evt.metaKey || evt.ctrlKey) && - evt.altKey !== true && - padShortcutEnabled.cmd5 - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-5 (strikethrough) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === '5' && + evt.altKey !== true && + padShortcutEnabled.cmd5) { fastIncorp(13); evt.preventDefault(); toggleAttributeOnSelection('strikethrough'); specialHandled = true; } - if ((!specialHandled) && - // cmd-shift-L (unorderedlist) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'l' && - (evt.metaKey || evt.ctrlKey) && - evt.shiftKey && - padShortcutEnabled.cmdShiftL - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-shift-L (unorderedlist) + (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'l' && + evt.shiftKey && + padShortcutEnabled.cmdShiftL) { fastIncorp(9); evt.preventDefault(); doInsertUnorderedList(); specialHandled = true; } - if ((!specialHandled) && - // cmd-shift-N and cmd-shift-1 (orderedlist) - isTypeForCmdKey && - ( - (String.fromCharCode(which).toLowerCase() === 'n' && - padShortcutEnabled.cmdShiftN) || (String.fromCharCode(which) === '1' && - padShortcutEnabled.cmdShift1) - ) && (evt.metaKey || evt.ctrlKey) && - evt.shiftKey - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-shift-N and cmd-shift-1 (orderedlist) + (evt.metaKey || evt.ctrlKey) && evt.shiftKey && + ((String.fromCharCode(which).toLowerCase() === 'n' && padShortcutEnabled.cmdShiftN) || + (String.fromCharCode(which) === '1' && padShortcutEnabled.cmdShift1))) { fastIncorp(9); evt.preventDefault(); doInsertOrderedList(); specialHandled = true; } - if ((!specialHandled) && - // cmd-shift-C (clearauthorship) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'c' && - (evt.metaKey || evt.ctrlKey) && - evt.shiftKey && padShortcutEnabled.cmdShiftC - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-shift-C (clearauthorship) + (evt.metaKey || evt.ctrlKey) && evt.shiftKey && + String.fromCharCode(which).toLowerCase() === 'c' && + padShortcutEnabled.cmdShiftC) { fastIncorp(9); evt.preventDefault(); CMDS.clearauthorship(); } - if ((!specialHandled) && - // cmd-H (backspace) - isTypeForCmdKey && - String.fromCharCode(which).toLowerCase() === 'h' && - (evt.ctrlKey) && - padShortcutEnabled.cmdH - ) { + if (!specialHandled && isTypeForCmdKey && + // cmd-H (backspace) + (evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'h' && + padShortcutEnabled.cmdH) { fastIncorp(20); evt.preventDefault(); doDeleteKey(); specialHandled = true; } - if ((evt.which === 36 && evt.ctrlKey === true) && - // Control Home send to Y = 0 - padShortcutEnabled.ctrlHome) { + if (evt.ctrlKey === true && evt.which === 36 && + // Control Home send to Y = 0 + padShortcutEnabled.ctrlHome) { scroll.setScrollY(0); } if ((evt.which === 33 || evt.which === 34) && type === 'keydown' && !evt.ctrlKey) { @@ -3099,10 +3011,9 @@ function Ace2Inner(editorInfo, cssManagers) { thisKeyDoesntTriggerNormalize = true; } - if ((!specialHandled) && (!thisKeyDoesntTriggerNormalize) && (!inInternationalComposition)) { - if (type !== 'keyup') { - observeChangesAroundSelection(); - } + if (!specialHandled && !thisKeyDoesntTriggerNormalize && !inInternationalComposition && + type !== 'keyup') { + observeChangesAroundSelection(); } if (type === 'keyup') { @@ -3128,12 +3039,9 @@ function Ace2Inner(editorInfo, cssManagers) { } if (selectionInfo) { performSelectionChange( - lineAndColumnFromChar( - selectionInfo.selStart - ), + lineAndColumnFromChar(selectionInfo.selStart), lineAndColumnFromChar(selectionInfo.selEnd), - selectionInfo.selFocusAtStart - ); + selectionInfo.selFocusAtStart); } const oldEvent = currentCallStack.startNewEvent(oldEventType, true); return oldEvent; @@ -3173,15 +3081,13 @@ function Ace2Inner(editorInfo, cssManagers) { // with background doesn't seem to show up... if (isNodeText(p.node) && p.index === p.maxIndex) { let n = p.node; - while ((!n.nextSibling) && (n !== root) && (n.parentNode !== root)) { + while (!n.nextSibling && n !== root && n.parentNode !== root) { n = n.parentNode; } - if ( - n.nextSibling && - (!((typeof n.nextSibling.tagName) === 'string' && - n.nextSibling.tagName.toLowerCase() === 'br')) && - (n !== p.node) && (n !== root) && (n.parentNode !== root) - ) { + if (n.nextSibling && + !(typeof n.nextSibling.tagName === 'string' && + n.nextSibling.tagName.toLowerCase() === 'br') && + n !== p.node && n !== root && n.parentNode !== root) { // found a parent, go to next node and dive in p.node = n.nextSibling; p.maxIndex = nodeMaxIndex(p.node); @@ -3212,19 +3118,13 @@ function Ace2Inner(editorInfo, cssManagers) { if (browserSelection) { browserSelection.removeAllRanges(); if (selection) { - isCollapsed = ( - selection.startPoint.node === selection.endPoint.node && - selection.startPoint.index === selection.endPoint.index - ); + isCollapsed = (selection.startPoint.node === selection.endPoint.node && + selection.startPoint.index === selection.endPoint.index); const start = pointToRangeBound(selection.startPoint); const end = pointToRangeBound(selection.endPoint); - if ( - (!isCollapsed) && - selection.focusAtStart && - browserSelection.collapse && - browserSelection.extend - ) { + if (!isCollapsed && selection.focusAtStart && + browserSelection.collapse && browserSelection.extend) { // can handle "backwards"-oriented selection, shift-arrow-keys move start // of selection browserSelection.collapse(end.container, end.offset); @@ -3509,8 +3409,7 @@ function Ace2Inner(editorInfo, cssManagers) { let charsToLeft = index; let charsToRight = node.nodeValue.length - index; let n; - for (n = node.previousSibling; n && - isNodeText(n); n = n.previousSibling) { + for (n = node.previousSibling; n && isNodeText(n); n = n.previousSibling) { charsToLeft += n.nodeValue; } const leftEdge = (n ? rightOf(n) : leftOf(node.parentNode)); @@ -3560,9 +3459,8 @@ function Ace2Inner(editorInfo, cssManagers) { if (!doesWrap) { const browserSelection = getSelection(); if (browserSelection) { - const focusPoint = ( - browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint - ); + const focusPoint = + browserSelection.focusAtStart ? browserSelection.startPoint : browserSelection.endPoint; const selectionPointX = getSelectionPointX(focusPoint); scrollXHorizontallyIntoView(selectionPointX); fixView(); @@ -3681,8 +3579,7 @@ function Ace2Inner(editorInfo, cssManagers) { // extra margins/padding, but plugins might. h = docLine.nextSibling.offsetTop - parseInt( window.getComputedStyle(doc.body) - .getPropertyValue('padding-top').split('px')[0] - ); + .getPropertyValue('padding-top').split('px')[0]); } else { h = docLine.nextSibling.offsetTop - docLine.offsetTop; } From b5bfff43cfa301dacc4d433863a5e4dd1aca2689 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 29 Jul 2021 02:03:57 -0400 Subject: [PATCH 060/322] ace2_inner: Delete redundant class assignment --- src/static/js/ace2_inner.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 32336d2a5..42082f4ab 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -73,7 +73,6 @@ function Ace2Inner(editorInfo, cssManagers) { lineNumbersShown = 1; sideDiv.innerHTML = `${htmlOpen}${htmlClose}`; sideDivInner = outerWin.document.getElementById('sidedivinner'); - $(sideDiv).addClass('sidediv'); }; initLineNumbers(); From 7a8edc816bfee063bf7f7ead3ca9c8a248dcc398 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 29 Jul 2021 02:16:13 -0400 Subject: [PATCH 061/322] ace2_inner: Replace `initLineNumbers()` with an IIFE --- src/static/js/ace2_inner.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 42082f4ab..8467dd85a 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -65,17 +65,13 @@ function Ace2Inner(editorInfo, cssManagers) { const sideDiv = outerDoc.getElementById('sidediv'); const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv'); let lineNumbersShown; - let sideDivInner; - - const initLineNumbers = () => { + const sideDivInner = (() => { const htmlOpen = '
1'; const htmlClose = '
'; lineNumbersShown = 1; sideDiv.innerHTML = `${htmlOpen}${htmlClose}`; - sideDivInner = outerWin.document.getElementById('sidedivinner'); - }; - - initLineNumbers(); + return outerDoc.getElementById('sidedivinner'); + })(); const scroll = Scroll.init(outerWin); From b80295c22846d617a07ce659855692c65d5fa532 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 29 Jul 2021 02:29:17 -0400 Subject: [PATCH 062/322] ace2_inner: Combine declaration and initialization --- src/static/js/ace2_inner.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 8467dd85a..c7494cff9 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -64,14 +64,13 @@ function Ace2Inner(editorInfo, cssManagers) { const outerDoc = outerWin.document; const sideDiv = outerDoc.getElementById('sidediv'); const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv'); - let lineNumbersShown; const sideDivInner = (() => { const htmlOpen = '
1'; const htmlClose = '
'; - lineNumbersShown = 1; sideDiv.innerHTML = `${htmlOpen}${htmlClose}`; return outerDoc.getElementById('sidedivinner'); })(); + let lineNumbersShown = 1; const scroll = Scroll.init(outerWin); From 15b1d4cb75d3c961a03d01d124d062a9ed2ef2f8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 29 Jul 2021 02:33:51 -0400 Subject: [PATCH 063/322] ace2_inner: Build `sidedivinner` programmatically --- src/static/js/ace2_inner.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index c7494cff9..3b83591b6 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -64,11 +64,17 @@ function Ace2Inner(editorInfo, cssManagers) { const outerDoc = outerWin.document; const sideDiv = outerDoc.getElementById('sidediv'); const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv'); - const sideDivInner = (() => { - const htmlOpen = '
1'; - const htmlClose = '
'; - sideDiv.innerHTML = `${htmlOpen}${htmlClose}`; - return outerDoc.getElementById('sidedivinner'); + const sideDivInner = outerDoc.createElement('div'); + sideDivInner.id = 'sidedivinner'; + sideDivInner.classList.add('sidedivinner'); + sideDiv.appendChild(sideDivInner); + (() => { + const lineDiv = outerDoc.createElement('div'); + sideDivInner.appendChild(lineDiv); + const lineSpan = outerDoc.createElement('span'); + lineSpan.classList.add('line-number'); + lineSpan.appendChild(outerDoc.createTextNode('1')); + lineDiv.appendChild(lineSpan); })(); let lineNumbersShown = 1; From c7be4f9d2dbfa540eec0151d5891759facc94530 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 29 Jul 2021 02:37:58 -0400 Subject: [PATCH 064/322] ace2_inner: Move `sidedivinner` creation to ace.js --- src/static/js/ace.js | 4 ++++ src/static/js/ace2_inner.js | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 9339e46fd..efb38077f 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -229,6 +229,10 @@ const Ace2Editor = function () { sideDiv.id = 'sidediv'; sideDiv.classList.add('sidediv'); outerDocument.body.appendChild(sideDiv); + const sideDivInner = outerDocument.createElement('div'); + sideDivInner.id = 'sidedivinner'; + sideDivInner.classList.add('sidedivinner'); + sideDiv.appendChild(sideDivInner); const lineMetricsDiv = outerDocument.createElement('div'); lineMetricsDiv.id = 'linemetricsdiv'; lineMetricsDiv.appendChild(outerDocument.createTextNode('x')); diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 3b83591b6..b25f55672 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -64,10 +64,7 @@ function Ace2Inner(editorInfo, cssManagers) { const outerDoc = outerWin.document; const sideDiv = outerDoc.getElementById('sidediv'); const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv'); - const sideDivInner = outerDoc.createElement('div'); - sideDivInner.id = 'sidedivinner'; - sideDivInner.classList.add('sidedivinner'); - sideDiv.appendChild(sideDivInner); + const sideDivInner = outerDoc.getElementById('sidedivinner'); (() => { const lineDiv = outerDoc.createElement('div'); sideDivInner.appendChild(lineDiv); From 98c1ba5808af56d7eda5a2c17380a3304d903fef Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 21 Jun 2021 19:25:24 -0400 Subject: [PATCH 065/322] ace2_inner: Use destructuring assignment to simplify --- src/static/js/ace2_inner.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index b25f55672..b3af38008 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -2579,12 +2579,7 @@ function Ace2Inner(editorInfo, cssManagers) { const handleKeyEvent = (evt) => { if (!isEditable) return; - const type = evt.type; - const charCode = evt.charCode; - const keyCode = evt.keyCode; - const which = evt.which; - const altKey = evt.altKey; - const shiftKey = evt.shiftKey; + const {type, charCode, keyCode, which, altKey, shiftKey} = evt; // Don't take action based on modifier keys going up and down. // Modifier keys do not generate "keypress" events. From 11c86e677a69589555c2546690d8baa979608ad0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 22 Jun 2021 02:22:24 -0400 Subject: [PATCH 066/322] ace2_inner: Consistently use `outerWin` and `outerDoc` --- src/static/js/ace2_inner.js | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index b3af38008..90c08ab42 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3289,7 +3289,7 @@ function Ace2Inner(editorInfo, cssManagers) { $(document).on('keyup', handleKeyEvent); $(document).on('click', handleClick); // dropdowns on edit bar need to be closed on clicks on both pad inner and pad outer - $(outerWin.document).on('click', hideEditBarDropdowns); + $(outerDoc).on('click', hideEditBarDropdowns); // If non-nullish, pasting on a link should be suppressed. let suppressPasteOnLink = null; @@ -3414,11 +3414,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const getInnerHeight = () => { - const win = outerWin; - const odoc = win.document; - let h; - if (browser.opera) h = win.innerHeight; - else h = odoc.documentElement.clientHeight; + const h = browser.opera ? outerWin.innerHeight : outerDoc.documentElement.clientHeight; if (h) return h; // deal with case where iframe is hidden, hope that @@ -3426,20 +3422,15 @@ function Ace2Inner(editorInfo, cssManagers) { return Number(editorInfo.frame.parentNode.style.height.replace(/[^0-9]/g, '') || 0); }; - const getInnerWidth = () => { - const win = outerWin; - const odoc = win.document; - return odoc.documentElement.clientWidth; - }; + const getInnerWidth = () => outerDoc.documentElement.clientWidth; const scrollXHorizontallyIntoView = (pixelX) => { - const win = outerWin; - const distInsideLeft = pixelX - win.scrollX; - const distInsideRight = win.scrollX + getInnerWidth() - pixelX; + const distInsideLeft = pixelX - outerWin.scrollX; + const distInsideRight = outerWin.scrollX + getInnerWidth() - pixelX; if (distInsideLeft < 0) { - win.scrollBy(distInsideLeft, 0); + outerWin.scrollBy(distInsideLeft, 0); } else if (distInsideRight < 0) { - win.scrollBy(-distInsideRight + 1, 0); + outerWin.scrollBy(-distInsideRight + 1, 0); } }; @@ -3615,13 +3606,12 @@ function Ace2Inner(editorInfo, cssManagers) { if (newNumLines !== lineNumbersShown) { const container = sideDivInner; - const odoc = outerWin.document; - const fragment = odoc.createDocumentFragment(); + const fragment = outerDoc.createDocumentFragment(); // Create missing line and apply height while (lineNumbersShown < newNumLines) { lineNumbersShown++; - const div = odoc.createElement('DIV'); + const div = outerDoc.createElement('DIV'); if (lineOffsets[currentLine]) { div.style.height = `${lineOffsets[currentLine]}px`; div.style.lineHeight = `${lineHeights[currentLine]}px`; From ec63c15a400bcb2f981e83b8adbe2116504ad03d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 22:17:42 -0400 Subject: [PATCH 067/322] ace2_inner: Simplify document body selection --- src/static/js/ace2_inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 90c08ab42..c6e779478 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3543,7 +3543,7 @@ function Ace2Inner(editorInfo, cssManagers) { // but as it's non-text type the line-height/margins might not be present and it // could be that this breaks a theme that has a different default line height.. // So instead of using an integer here we get the value from the Editor CSS. - const innerdocbody = document.querySelector('#innerdocbody'); + const innerdocbody = document.body; const innerdocbodyStyles = getComputedStyle(innerdocbody); const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); From 4b4584c2646fcc9bb4c90515e1ad111387d406dc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 22 Jun 2021 02:30:24 -0400 Subject: [PATCH 068/322] ace2_inner: Delete unnecessary `doc` and `root` variables --- src/static/js/ace2_inner.js | 95 ++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index c6e779478..4ee9027a3 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -110,7 +110,6 @@ function Ace2Inner(editorInfo, cssManagers) { undoModule.apool = rep.apool; } - let root, doc; // set in init() let isEditable = true; let doesWrap = true; let hasLineNumbers = true; @@ -403,7 +402,7 @@ function Ace2Inner(editorInfo, cssManagers) { const setWraps = (newVal) => { doesWrap = newVal; - root.classList.toggle('doesWrap', doesWrap); + document.body.classList.toggle('doesWrap', doesWrap); scheduler.setTimeout(() => { inCallStackIfNecessary('setWraps', () => { fastIncorp(7); @@ -433,7 +432,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const setTextFace = (face) => { - root.style.fontFamily = face; + document.body.style.fontFamily = face; lineMetricsDiv.style.fontFamily = face; }; @@ -444,8 +443,8 @@ function Ace2Inner(editorInfo, cssManagers) { const setEditable = (newVal) => { isEditable = newVal; - root.contentEditable = isEditable ? 'true' : 'false'; - root.classList.toggle('static', !isEditable); + document.body.contentEditable = isEditable ? 'true' : 'false'; + document.body.classList.toggle('static', !isEditable); }; const enforceEditability = () => setEditable(isEditable); @@ -628,8 +627,8 @@ function Ace2Inner(editorInfo, cssManagers) { // These properties are exposed const setters = { wraps: setWraps, - showsauthorcolors: (val) => root.classList.toggle('authorColors', !!val), - showsuserselections: (val) => root.classList.toggle('userSelections', !!val), + showsauthorcolors: (val) => document.body.classList.toggle('authorColors', !!val), + showsuserselections: (val) => document.body.classList.toggle('userSelections', !!val), showslinenumbers: (value) => { hasLineNumbers = !!value; sideDiv.parentNode.classList.toggle('line-numbers-hidden', !hasLineNumbers); @@ -642,8 +641,8 @@ function Ace2Inner(editorInfo, cssManagers) { styled: setStyled, textface: setTextFace, rtlistrue: (value) => { - root.classList.toggle('rtl', value); - root.classList.toggle('ltr', !value); + document.body.classList.toggle('rtl', value); + document.body.classList.toggle('ltr', !value); document.documentElement.dir = value ? 'rtl' : 'ltr'; }, }; @@ -681,7 +680,7 @@ function Ace2Inner(editorInfo, cssManagers) { editorInfo.ace_getUnhandledErrors = () => caughtErrors.slice(); - editorInfo.ace_getDocument = () => doc; + editorInfo.ace_getDocument = () => document; editorInfo.ace_getDebugProperty = (prop) => { if (prop === 'debugger') { @@ -901,11 +900,11 @@ function Ace2Inner(editorInfo, cssManagers) { clearObservedChanges(); const getCleanNodeByKey = (key) => { - let n = doc.getElementById(key); + let n = document.getElementById(key); // copying and pasting can lead to duplicate ids while (n && isNodeDirty(n)) { n.id = ''; - n = doc.getElementById(key); + n = document.getElementById(key); } return n; }; @@ -987,11 +986,11 @@ function Ace2Inner(editorInfo, cssManagers) { const observeSuspiciousNodes = () => { // inspired by Firefox bug #473255, where pasting formatted text // causes the cursor to jump away, making the new HTML never found. - if (root.getElementsByTagName) { - const nds = root.getElementsByTagName('style'); + if (document.body.getElementsByTagName) { + const nds = document.body.getElementsByTagName('style'); for (let i = 0; i < nds.length; i++) { const n = topLevel(nds[i]); - if (n && n.parentNode === root) { + if (n && n.parentNode === document.body) { observeChangesAroundNode(n); } } @@ -1006,8 +1005,8 @@ function Ace2Inner(editorInfo, cssManagers) { if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; // returns true if dom changes were made - if (!root.firstChild) { - root.innerHTML = '
'; + if (!document.body.firstChild) { + document.body.innerHTML = '
'; } observeChangesAroundSelection(); @@ -1029,9 +1028,9 @@ function Ace2Inner(editorInfo, cssManagers) { j++; } if (!dirtyRangesCheckOut) { - const numBodyNodes = root.childNodes.length; + const numBodyNodes = document.body.childNodes.length; for (let k = 0; k < numBodyNodes; k++) { - const bodyNode = root.childNodes.item(k); + const bodyNode = document.body.childNodes.item(k); if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) { observeChangesAroundNode(bodyNode); } @@ -1053,11 +1052,11 @@ function Ace2Inner(editorInfo, cssManagers) { const range = dirtyRanges[i]; a = range[0]; b = range[1]; - let firstDirtyNode = (((a === 0) && root.firstChild) || + let firstDirtyNode = (((a === 0) && document.body.firstChild) || getCleanNodeByKey(rep.lines.atIndex(a - 1).key).nextSibling); firstDirtyNode = (firstDirtyNode && isNodeDirty(firstDirtyNode) && firstDirtyNode); - let lastDirtyNode = (((b === rep.lines.length()) && root.lastChild) || + let lastDirtyNode = (((b === rep.lines.length()) && document.body.lastChild) || getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); @@ -1159,7 +1158,7 @@ function Ace2Inner(editorInfo, cssManagers) { callstack: currentCallStack, editorInfo, rep, - root, + root: document.body, point: selection.startPoint, documentAttributeManager, }); @@ -1171,7 +1170,7 @@ function Ace2Inner(editorInfo, cssManagers) { callstack: currentCallStack, editorInfo, rep, - root, + root: document.body, point: selection.endPoint, documentAttributeManager, }); @@ -1253,9 +1252,9 @@ function Ace2Inner(editorInfo, cssManagers) { info.prepareForAdd(); entry.lineMarker = info.lineMarker; if (!nodeToAddAfter) { - root.insertBefore(node, root.firstChild); + document.body.insertBefore(node, document.body.firstChild); } else { - root.insertBefore(node, nodeToAddAfter.nextSibling); + document.body.insertBefore(node, nodeToAddAfter.nextSibling); } nodeToAddAfter = node; info.notifyAdded(); @@ -1352,7 +1351,7 @@ function Ace2Inner(editorInfo, cssManagers) { // Turn DOM node selection into [line,char] selection. // This method has to work when the DOM is not pristine, // assuming the point is not in a dirty node. - if (point.node === root) { + if (point.node === document.body) { if (point.index === 0) { return [0, 0]; } else { @@ -1371,7 +1370,7 @@ function Ace2Inner(editorInfo, cssManagers) { col = nodeText(n).length; } let parNode, prevSib; - while ((parNode = n.parentNode) !== root) { + while ((parNode = n.parentNode) !== document.body) { if ((prevSib = n.previousSibling)) { n = prevSib; col += nodeText(n).length; @@ -1424,7 +1423,7 @@ function Ace2Inner(editorInfo, cssManagers) { insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo)); keysToDelete.forEach((k) => { - const n = doc.getElementById(k); + const n = document.getElementById(k); n.parentNode.removeChild(n); }); @@ -2082,7 +2081,8 @@ function Ace2Inner(editorInfo, cssManagers) { }); }; - const doCreateDomLine = (nonEmpty) => domline.createDomLine(nonEmpty, doesWrap, browser, doc); + const doCreateDomLine = + (nonEmpty) => domline.createDomLine(nonEmpty, doesWrap, browser, document); const textify = (str) => str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); @@ -2141,7 +2141,7 @@ function Ace2Inner(editorInfo, cssManagers) { const a = cleanNodeForIndex(i - 1); const b = cleanNodeForIndex(i); if ((!a) || (!b)) return false; // violates precondition - if ((a === true) && (b === true)) return !root.firstChild; + if ((a === true) && (b === true)) return !document.body.firstChild; if ((a === true) && b.previousSibling) return false; if ((b === true) && a.nextSibling) return false; if ((a === true) || (b === true)) return true; @@ -2294,7 +2294,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const isNodeDirty = (n) => { - if (n.parentNode !== root) return true; + if (n.parentNode !== document.body) return true; const data = getAssoc(n, 'dirtiness'); if (!data) return true; if (n.id !== data.nodeId) return true; @@ -3073,13 +3073,13 @@ function Ace2Inner(editorInfo, cssManagers) { // with background doesn't seem to show up... if (isNodeText(p.node) && p.index === p.maxIndex) { let n = p.node; - while (!n.nextSibling && n !== root && n.parentNode !== root) { + while (!n.nextSibling && n !== document.body && n.parentNode !== document.body) { n = n.parentNode; } if (n.nextSibling && !(typeof n.nextSibling.tagName === 'string' && n.nextSibling.tagName.toLowerCase() === 'br') && - n !== p.node && n !== root && n.parentNode !== root) { + n !== p.node && n !== document.body && n.parentNode !== document.body) { // found a parent, go to next node and dive in p.node = n.nextSibling; p.maxIndex = nodeMaxIndex(p.node); @@ -3122,7 +3122,7 @@ function Ace2Inner(editorInfo, cssManagers) { browserSelection.collapse(end.container, end.offset); browserSelection.extend(start.container, start.offset); } else { - const range = doc.createRange(); + const range = document.createRange(); range.setStart(start.container, start.offset); range.setEnd(end.container, end.offset); browserSelection.removeAllRanges(); @@ -3199,7 +3199,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (!isInBody(container)) { // command-click in Firefox selects whole document, HEAD and BODY! return { - node: root, + node: document.body, index: 0, maxIndex: 1, }; @@ -3294,7 +3294,7 @@ function Ace2Inner(editorInfo, cssManagers) { // If non-nullish, pasting on a link should be suppressed. let suppressPasteOnLink = null; - $(root).on('auxclick', (e) => { + $(document.body).on('auxclick', (e) => { if (e.originalEvent.button === 1 && (e.target.a || e.target.localName === 'a')) { // The user middle-clicked on a link. Usually users do this to open a link in a new tab, but // in X11 (Linux) this will instead paste the contents of the primary selection at the mouse @@ -3316,7 +3316,7 @@ function Ace2Inner(editorInfo, cssManagers) { } }); - $(root).on('paste', (e) => { + $(document.body).on('paste', (e) => { if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) { scheduler.clearTimeout(suppressPasteOnLink); suppressPasteOnLink = null; @@ -3378,8 +3378,8 @@ function Ace2Inner(editorInfo, cssManagers) { }; const topLevel = (n) => { - if ((!n) || n === root) return null; - while (n.parentNode !== root) { + if ((!n) || n === document.body) return null; + while (n.parentNode !== document.body) { n = n.parentNode; } return n; @@ -3547,7 +3547,7 @@ function Ace2Inner(editorInfo, cssManagers) { const innerdocbodyStyles = getComputedStyle(innerdocbody); const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); - let docLine = doc.body.firstChild; + let docLine = document.body.firstChild; let currentLine = 0; let h = null; @@ -3561,7 +3561,7 @@ function Ace2Inner(editorInfo, cssManagers) { // included on the first line. The default stylesheet doesn't add // extra margins/padding, but plugins might. h = docLine.nextSibling.offsetTop - parseInt( - window.getComputedStyle(doc.body) + window.getComputedStyle(document.body) .getPropertyValue('padding-top').split('px')[0]); } else { h = docLine.nextSibling.offsetTop - docLine.offsetTop; @@ -3639,19 +3639,16 @@ function Ace2Inner(editorInfo, cssManagers) { this.init = async () => { await $.ready; - doc = document; // defined as a var in scope outside inCallStack('setup', () => { - const body = doc.getElementById('innerdocbody'); - root = body; // defined as a var in scope outside - if (browser.firefox) $(root).addClass('mozilla'); - if (browser.safari) $(root).addClass('safari'); - root.classList.toggle('authorColors', true); - root.classList.toggle('doesWrap', doesWrap); + if (browser.firefox) $(document.body).addClass('mozilla'); + if (browser.safari) $(document.body).addClass('safari'); + document.body.classList.toggle('authorColors', true); + document.body.classList.toggle('doesWrap', doesWrap); enforceEditability(); // set up dom and rep - while (root.firstChild) root.removeChild(root.firstChild); + while (document.body.firstChild) document.body.removeChild(document.body.firstChild); const oneEntry = createDomLineEntry(''); doRepLineSplice(0, rep.lines.length(), [oneEntry]); insertDomLines(null, [oneEntry.domInfo]); From 7d807d2fc5fe6566f2691543bf0ecda394ff5ebb Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 20:49:39 -0400 Subject: [PATCH 069/322] ace2_inner: Delete unnecessary `container` variable --- src/static/js/ace2_inner.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 4ee9027a3..95283a421 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3605,7 +3605,6 @@ function Ace2Inner(editorInfo, cssManagers) { } if (newNumLines !== lineNumbersShown) { - const container = sideDivInner; const fragment = outerDoc.createDocumentFragment(); // Create missing line and apply height @@ -3620,11 +3619,11 @@ function Ace2Inner(editorInfo, cssManagers) { fragment.appendChild(div); currentLine++; } - container.appendChild(fragment); + sideDivInner.appendChild(fragment); // Remove extra lines while (lineNumbersShown > newNumLines) { - container.removeChild(container.lastChild); + sideDivInner.removeChild(sideDivInner.lastChild); lineNumbersShown--; } } From e1a024847cf7285b6161dc14db299fd532f32e5d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 22:18:29 -0400 Subject: [PATCH 070/322] ace2_inner: Delete unnecessary `innerdocbody` variable --- src/static/js/ace2_inner.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 95283a421..5e9ba297a 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3543,8 +3543,7 @@ function Ace2Inner(editorInfo, cssManagers) { // but as it's non-text type the line-height/margins might not be present and it // could be that this breaks a theme that has a different default line height.. // So instead of using an integer here we get the value from the Editor CSS. - const innerdocbody = document.body; - const innerdocbodyStyles = getComputedStyle(innerdocbody); + const innerdocbodyStyles = getComputedStyle(document.body); const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); let docLine = document.body.firstChild; From 27363bf729d50c7f302981d42df111edaf76af41 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 20:51:18 -0400 Subject: [PATCH 071/322] ace2_inner: Add line number divs directly, not via fragment There's no layout thrashing so the fragment doesn't provide any benefit. --- src/static/js/ace2_inner.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 5e9ba297a..b0b0d3fb5 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3604,8 +3604,6 @@ function Ace2Inner(editorInfo, cssManagers) { } if (newNumLines !== lineNumbersShown) { - const fragment = outerDoc.createDocumentFragment(); - // Create missing line and apply height while (lineNumbersShown < newNumLines) { lineNumbersShown++; @@ -3615,10 +3613,9 @@ function Ace2Inner(editorInfo, cssManagers) { div.style.lineHeight = `${lineHeights[currentLine]}px`; } $(div).append($(`${String(lineNumbersShown)}`)); - fragment.appendChild(div); + sideDivInner.appendChild(div); currentLine++; } - sideDivInner.appendChild(fragment); // Remove extra lines while (lineNumbersShown > newNumLines) { From 1b890e3d4de9f18dcb3226d656fb68f0327d9b57 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 20:47:10 -0400 Subject: [PATCH 072/322] ace2_inner: Replace `lineNumbersShown` with number of children --- src/static/js/ace2_inner.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index b0b0d3fb5..ffe0ffe61 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -73,7 +73,6 @@ function Ace2Inner(editorInfo, cssManagers) { lineSpan.appendChild(outerDoc.createTextNode('1')); lineDiv.appendChild(lineSpan); })(); - let lineNumbersShown = 1; const scroll = Scroll.init(outerWin); @@ -3594,7 +3593,7 @@ function Ace2Inner(editorInfo, cssManagers) { // Apply height to existing sidediv lines currentLine = 0; - while (sidebarLine && currentLine <= lineNumbersShown) { + while (sidebarLine && currentLine <= sideDivInner.children.length) { if (lineOffsets[currentLine] != null) { sidebarLine.style.height = `${lineOffsets[currentLine]}px`; sidebarLine.style.lineHeight = `${lineHeights[currentLine]}px`; @@ -3603,24 +3602,22 @@ function Ace2Inner(editorInfo, cssManagers) { currentLine++; } - if (newNumLines !== lineNumbersShown) { + if (newNumLines !== sideDivInner.children.length) { // Create missing line and apply height - while (lineNumbersShown < newNumLines) { - lineNumbersShown++; + while (sideDivInner.children.length < newNumLines) { const div = outerDoc.createElement('DIV'); + sideDivInner.appendChild(div); if (lineOffsets[currentLine]) { div.style.height = `${lineOffsets[currentLine]}px`; div.style.lineHeight = `${lineHeights[currentLine]}px`; } - $(div).append($(`${String(lineNumbersShown)}`)); - sideDivInner.appendChild(div); + $(div).append($(`${sideDivInner.children.length}`)); currentLine++; } // Remove extra lines - while (lineNumbersShown > newNumLines) { + while (sideDivInner.children.length > newNumLines) { sideDivInner.removeChild(sideDivInner.lastChild); - lineNumbersShown--; } } }; From b238d9610af39ece0d00c2d7bbc15b1e2e7a09ba Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 22:35:22 -0400 Subject: [PATCH 073/322] ace2_inner: Factor out duplicate line height application --- src/static/js/ace2_inner.js | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index ffe0ffe61..c315a31d1 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3589,9 +3589,21 @@ function Ace2Inner(editorInfo, cssManagers) { let newNumLines = rep.lines.length(); if (newNumLines < 1) newNumLines = 1; - let sidebarLine = sideDivInner.firstChild; - // Apply height to existing sidediv lines + if (newNumLines !== sideDivInner.children.length) { + while (sideDivInner.children.length < newNumLines) { + const div = outerDoc.createElement('DIV'); + sideDivInner.appendChild(div); + $(div).append($(`${sideDivInner.children.length}`)); + } + + // Remove extra lines + while (sideDivInner.children.length > newNumLines) { + sideDivInner.removeChild(sideDivInner.lastChild); + } + } + + let sidebarLine = sideDivInner.firstChild; currentLine = 0; while (sidebarLine && currentLine <= sideDivInner.children.length) { if (lineOffsets[currentLine] != null) { @@ -3601,25 +3613,6 @@ function Ace2Inner(editorInfo, cssManagers) { sidebarLine = sidebarLine.nextSibling; currentLine++; } - - if (newNumLines !== sideDivInner.children.length) { - // Create missing line and apply height - while (sideDivInner.children.length < newNumLines) { - const div = outerDoc.createElement('DIV'); - sideDivInner.appendChild(div); - if (lineOffsets[currentLine]) { - div.style.height = `${lineOffsets[currentLine]}px`; - div.style.lineHeight = `${lineHeights[currentLine]}px`; - } - $(div).append($(`${sideDivInner.children.length}`)); - currentLine++; - } - - // Remove extra lines - while (sideDivInner.children.length > newNumLines) { - sideDivInner.removeChild(sideDivInner.lastChild); - } - } }; From 3237f8d123351c8525eecc9f86c88880326cf061 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 22:15:40 -0400 Subject: [PATCH 074/322] ace2_inner: Simplify iteration over line number divs --- src/static/js/ace2_inner.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index c315a31d1..a46978139 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3603,15 +3603,10 @@ function Ace2Inner(editorInfo, cssManagers) { } } - let sidebarLine = sideDivInner.firstChild; - currentLine = 0; - while (sidebarLine && currentLine <= sideDivInner.children.length) { - if (lineOffsets[currentLine] != null) { - sidebarLine.style.height = `${lineOffsets[currentLine]}px`; - sidebarLine.style.lineHeight = `${lineHeights[currentLine]}px`; - } - sidebarLine = sidebarLine.nextSibling; - currentLine++; + for (const [i, sideDivLine] of Array.prototype.entries.call(sideDivInner.children)) { + if (lineOffsets[i] == null) continue; + sideDivLine.style.height = `${lineOffsets[i]}px`; + sideDivLine.style.lineHeight = `${lineHeights[i]}px`; } }; From aad75e4661698902f89a3c4f1811c3c44613691c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 21:36:08 -0400 Subject: [PATCH 075/322] ace2_inner: Factor out duplicate line number div creation --- src/static/js/ace2_inner.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index a46978139..f384ddafd 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -65,14 +65,15 @@ function Ace2Inner(editorInfo, cssManagers) { const sideDiv = outerDoc.getElementById('sidediv'); const lineMetricsDiv = outerDoc.getElementById('linemetricsdiv'); const sideDivInner = outerDoc.getElementById('sidedivinner'); - (() => { + const appendNewSideDivLine = () => { const lineDiv = outerDoc.createElement('div'); sideDivInner.appendChild(lineDiv); const lineSpan = outerDoc.createElement('span'); lineSpan.classList.add('line-number'); - lineSpan.appendChild(outerDoc.createTextNode('1')); + lineSpan.appendChild(outerDoc.createTextNode(sideDivInner.children.length)); lineDiv.appendChild(lineSpan); - })(); + }; + appendNewSideDivLine(); const scroll = Scroll.init(outerWin); @@ -3591,12 +3592,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (newNumLines < 1) newNumLines = 1; if (newNumLines !== sideDivInner.children.length) { - while (sideDivInner.children.length < newNumLines) { - const div = outerDoc.createElement('DIV'); - sideDivInner.appendChild(div); - $(div).append($(`${sideDivInner.children.length}`)); - } - + while (sideDivInner.children.length < newNumLines) appendNewSideDivLine(); // Remove extra lines while (sideDivInner.children.length > newNumLines) { sideDivInner.removeChild(sideDivInner.lastChild); From a5f9c60a345ffc4a8ee52493c366be836dd1a1a4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 23:03:33 -0400 Subject: [PATCH 076/322] ace2_inner: Operate on Elements, not Nodes --- src/static/js/ace2_inner.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index f384ddafd..dd248a03e 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3546,24 +3546,25 @@ function Ace2Inner(editorInfo, cssManagers) { const innerdocbodyStyles = getComputedStyle(document.body); const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); - let docLine = document.body.firstChild; + let docLine = document.body.firstElementChild; let currentLine = 0; let h = null; // First loop to calculate the heights from doc body while (docLine) { - if (docLine.nextSibling) { + const nextDocLine = docLine.nextElementSibling; + if (nextDocLine) { if (currentLine === 0) { // It's the first line. For line number alignment purposes, its // height is taken to be the top offset of the next line. If we // didn't do this special case, we would miss out on any top margin // included on the first line. The default stylesheet doesn't add // extra margins/padding, but plugins might. - h = docLine.nextSibling.offsetTop - parseInt( + h = nextDocLine.offsetTop - parseInt( window.getComputedStyle(document.body) .getPropertyValue('padding-top').split('px')[0]); } else { - h = docLine.nextSibling.offsetTop - docLine.offsetTop; + h = nextDocLine.offsetTop - docLine.offsetTop; } } else { // last line @@ -3584,7 +3585,7 @@ function Ace2Inner(editorInfo, cssManagers) { } else { lineHeights.push(defaultLineHeight); } - docLine = docLine.nextSibling; + docLine = nextDocLine; currentLine++; } @@ -3593,10 +3594,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (newNumLines !== sideDivInner.children.length) { while (sideDivInner.children.length < newNumLines) appendNewSideDivLine(); - // Remove extra lines - while (sideDivInner.children.length > newNumLines) { - sideDivInner.removeChild(sideDivInner.lastChild); - } + while (sideDivInner.children.length > newNumLines) sideDivInner.lastElementChild.remove(); } for (const [i, sideDivLine] of Array.prototype.entries.call(sideDivInner.children)) { From 9fc613d3627a7d4d6ddb1fed659407cd3d003c84 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 14 Aug 2021 23:06:01 -0400 Subject: [PATCH 077/322] ace2_inner: Delete unnecessary checks --- src/static/js/ace2_inner.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index dd248a03e..415bcae64 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -2330,9 +2330,7 @@ function Ace2Inner(editorInfo, cssManagers) { }; const hideEditBarDropdowns = () => { - if (window.parent.parent.padeditbar) { // required in case its in an iframe should probably use parent.. See Issue 327 https://github.com/ether/etherpad-lite/issues/327 - window.parent.parent.padeditbar.toggleDropDown('none'); - } + window.parent.parent.padeditbar.toggleDropDown('none'); }; const renumberList = (lineNum) => { @@ -3525,8 +3523,6 @@ function Ace2Inner(editorInfo, cssManagers) { // We apply the height of a line in the doc body, to the corresponding sidediv line number const updateLineNumbers = () => { - if (!currentCallStack || !currentCallStack.domClean) return; - // Refs #4228, to avoid layout trashing, we need to first calculate all the heights, // and then apply at once all new height to div elements const lineOffsets = []; @@ -3591,14 +3587,9 @@ function Ace2Inner(editorInfo, cssManagers) { let newNumLines = rep.lines.length(); if (newNumLines < 1) newNumLines = 1; - - if (newNumLines !== sideDivInner.children.length) { - while (sideDivInner.children.length < newNumLines) appendNewSideDivLine(); - while (sideDivInner.children.length > newNumLines) sideDivInner.lastElementChild.remove(); - } - + while (sideDivInner.children.length < newNumLines) appendNewSideDivLine(); + while (sideDivInner.children.length > newNumLines) sideDivInner.lastElementChild.remove(); for (const [i, sideDivLine] of Array.prototype.entries.call(sideDivInner.children)) { - if (lineOffsets[i] == null) continue; sideDivLine.style.height = `${lineOffsets[i]}px`; sideDivLine.style.lineHeight = `${lineHeights[i]}px`; } From bf10e70f2e86efb1c9acf9f509fe73abcc91d6d6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 15 Aug 2021 00:35:23 -0400 Subject: [PATCH 078/322] ace2_inner: Delete unnecessary `currentLine` variable --- src/static/js/ace2_inner.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 415bcae64..8f00f6bb0 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3543,14 +3543,13 @@ function Ace2Inner(editorInfo, cssManagers) { const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); let docLine = document.body.firstElementChild; - let currentLine = 0; let h = null; // First loop to calculate the heights from doc body while (docLine) { const nextDocLine = docLine.nextElementSibling; if (nextDocLine) { - if (currentLine === 0) { + if (lineOffsets.length === 0) { // It's the first line. For line number alignment purposes, its // height is taken to be the top offset of the next line. If we // didn't do this special case, we would miss out on any top margin @@ -3582,7 +3581,6 @@ function Ace2Inner(editorInfo, cssManagers) { lineHeights.push(defaultLineHeight); } docLine = nextDocLine; - currentLine++; } let newNumLines = rep.lines.length(); From ca2e008e7b658d312c92c4f57f6d7e69e4e0db60 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 15 Aug 2021 00:37:03 -0400 Subject: [PATCH 079/322] ace2_inner: Move variable declarations to appropriate scope --- src/static/js/ace2_inner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 8f00f6bb0..1487349b1 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -2596,7 +2596,6 @@ function Ace2Inner(editorInfo, cssManagers) { // tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice return; } - let specialHandled = false; const isTypeForSpecialKey = browser.safari || browser.chrome || browser.firefox ? type === 'keydown' : type === 'keypress'; @@ -2619,6 +2618,7 @@ function Ace2Inner(editorInfo, cssManagers) { } else if (type === 'keydown') { outsideKeyDown(evt); } + let specialHandled = false; if (!stopped) { const specialHandledInHook = hooks.callAll('aceKeyEvent', { callstack: currentCallStack, @@ -3543,10 +3543,10 @@ function Ace2Inner(editorInfo, cssManagers) { const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); let docLine = document.body.firstElementChild; - let h = null; // First loop to calculate the heights from doc body while (docLine) { + let h; const nextDocLine = docLine.nextElementSibling; if (nextDocLine) { if (lineOffsets.length === 0) { From 2d50a8aa956e8eccb104d6875afe4d55f05281fe Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 15 Aug 2021 01:15:39 -0400 Subject: [PATCH 080/322] ace2_inner: Fix efficiency of `rangeForLine()` Returning `true` or `false` has no effect when iterating using `Array.prototype.forEach`. This fixes a bug introduced in commit b28bfe8e31cf4a95b1242bc35b47201f7560a0a3. --- src/static/js/ace2_inner.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 1487349b1..5f1885969 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -2162,16 +2162,13 @@ function Ace2Inner(editorInfo, cssManagers) { [-1, N + 1], ]; + // returns index of cleanRange containing i, or -1 if none const rangeForLine = (i) => { - // returns index of cleanRange containing i, or -1 if none - let answer = -1; - cleanRanges.forEach((r, idx) => { - if (i >= r[1]) return false; // keep looking - if (i < r[0]) return true; // not found, stop looking - answer = idx; - return true; // found, stop looking - }); - return answer; + for (const [idx, r] of cleanRanges.entries()) { + if (i < r[0]) return -1; + if (i < r[1]) return idx; + } + return -1; }; const removeLineFromRange = (rng, line) => { From f06307cb4cd4373d522287d469709088a45ba874 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 15 Aug 2021 02:48:26 -0400 Subject: [PATCH 081/322] ace2_inner: Fix for..in iteration See commit c38c34bef47a74467394797b34165641a87ac620. --- src/static/js/ace2_inner.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 5f1885969..32946fbbf 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -2263,13 +2263,11 @@ function Ace2Inner(editorInfo, cssManagers) { detectChangesAroundLine(0, 1); detectChangesAroundLine(N - 1, 1); - for (const k in observedChanges.cleanNodesNearChanges) { - if (observedChanges.cleanNodesNearChanges[k]) { - const key = k.substring(1); - if (rep.lines.containsKey(key)) { - const line = rep.lines.indexOfKey(key); - detectChangesAroundLine(line, 2); - } + for (const k of Object.keys(observedChanges.cleanNodesNearChanges)) { + const key = k.substring(1); + if (rep.lines.containsKey(key)) { + const line = rep.lines.indexOfKey(key); + detectChangesAroundLine(line, 2); } } } From bc6428025a2a591248d03390ebe64aa68988b0f5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 15 Aug 2021 00:37:50 -0400 Subject: [PATCH 082/322] ace2_inner: Use for..of iteration to improve readability --- src/static/js/ace2_inner.js | 87 ++++++++++++++----------------------- 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 32946fbbf..58570f079 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -137,7 +137,7 @@ function Ace2Inner(editorInfo, cssManagers) { 'profileEnd', ]; console = {}; - for (let i = 0; i < names.length; ++i) console[names[i]] = noop; + for (const name of names) console[name] = noop; } const scheduler = parent; // hack for opera required @@ -275,9 +275,9 @@ function Ace2Inner(editorInfo, cssManagers) { applyChangesToBase: 1, }; - hooks.callAll('aceRegisterNonScrollableEditEvents').forEach((eventType) => { + for (const eventType of hooks.callAll('aceRegisterNonScrollableEditEvents')) { _nonScrollableEditEvents[eventType] = 1; - }); + } const isScrollableEditEvent = (eventType) => !_nonScrollableEditEvents[eventType]; @@ -987,9 +987,9 @@ function Ace2Inner(editorInfo, cssManagers) { // inspired by Firefox bug #473255, where pasting formatted text // causes the cursor to jump away, making the new HTML never found. if (document.body.getElementsByTagName) { - const nds = document.body.getElementsByTagName('style'); - for (let i = 0; i < nds.length; i++) { - const n = topLevel(nds[i]); + const elts = document.body.getElementsByTagName('style'); + for (const elt of elts) { + const n = topLevel(elt); if (n && n.parentNode === document.body) { observeChangesAroundNode(n); } @@ -1028,9 +1028,7 @@ function Ace2Inner(editorInfo, cssManagers) { j++; } if (!dirtyRangesCheckOut) { - const numBodyNodes = document.body.childNodes.length; - for (let k = 0; k < numBodyNodes; k++) { - const bodyNode = document.body.childNodes.item(k); + for (const bodyNode of document.body.childNodes) { if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) { observeChangesAroundNode(bodyNode); } @@ -1106,17 +1104,14 @@ function Ace2Inner(editorInfo, cssManagers) { const entries = []; const nodeToAddAfter = lastDirtyNode; - const lineNodeInfos = new Array(lines.length); - for (let k = 0; k < lines.length; k++) { - const lineString = lines[k]; + const lineNodeInfos = []; + for (const lineString of lines) { const newEntry = createDomLineEntry(lineString); entries.push(newEntry); - lineNodeInfos[k] = newEntry.domInfo; + lineNodeInfos.push(newEntry.domInfo); } domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - dirtyNodes.forEach((n) => { - toDeleteAtEnd.push(n); - }); + for (const n of dirtyNodes) toDeleteAtEnd.push(n); const spliceHints = {}; if (selStart) spliceHints.selStart = selStart; if (selEnd) spliceHints.selEnd = selEnd; @@ -1131,20 +1126,18 @@ function Ace2Inner(editorInfo, cssManagers) { const domChanges = (splicesToDo.length > 0); // update the representation - splicesToDo.forEach((splice) => { + for (const splice of splicesToDo) { doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); - }); + } // do DOM inserts - domInsertsNeeded.forEach((ins) => { - insertDomLines(ins[0], ins[1]); - }); + for (const ins of domInsertsNeeded) insertDomLines(ins[0], ins[1]); // delete old dom nodes - toDeleteAtEnd.forEach((n) => { + for (const n of toDeleteAtEnd) { // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) if (n.parentNode) n.parentNode.removeChild(n); - }); + } // needed to stop chrome from breaking the ui when long strings without spaces are pasted if (scrollToTheLeftNeeded) { @@ -1227,9 +1220,7 @@ function Ace2Inner(editorInfo, cssManagers) { const insertDomLines = (nodeToAddAfter, infoStructs) => { let lastEntry; let lineStartOffset; - if (infoStructs.length < 1) return; - - infoStructs.forEach((info) => { + for (const info of infoStructs) { const node = info.node; const key = uniqueId(node); let entry; @@ -1259,7 +1250,7 @@ function Ace2Inner(editorInfo, cssManagers) { nodeToAddAfter = node; info.notifyAdded(); markNodeClean(node); - }); + } }; const isCaret = () => (rep.selStart && rep.selEnd && @@ -1422,10 +1413,10 @@ function Ace2Inner(editorInfo, cssManagers) { insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo)); - keysToDelete.forEach((k) => { + for (const k of keysToDelete) { const n = document.getElementById(k); n.parentNode.removeChild(n); - }); + } if ( (rep.selStart && @@ -1696,9 +1687,7 @@ function Ace2Inner(editorInfo, cssManagers) { // Change the abstract representation of the document to have a different set of lines. // Must be called after rep.alltext is set. const doRepLineSplice = (startLine, deleteCount, newLineEntries) => { - newLineEntries.forEach((entry) => { - entry.width = entry.text.length + 1; - }); + for (const entry of newLineEntries) entry.width = entry.text.length + 1; const startOldChar = rep.lines.offsetOfIndex(startLine); const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); @@ -2074,11 +2063,11 @@ function Ace2Inner(editorInfo, cssManagers) { const attribIsFormattingStyle = (attribName) => FORMATTING_STYLES.indexOf(attribName) !== -1; const selectFormattingButtonIfLineHasStyleApplied = (rep) => { - FORMATTING_STYLES.forEach((style) => { + for (const style of FORMATTING_STYLES) { const hasStyleOnRepSelection = documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); updateStyleButtonState(style, hasStyleOnRepSelection); - }); + } }; const doCreateDomLine = @@ -2096,9 +2085,7 @@ function Ace2Inner(editorInfo, cssManagers) { ul: 1, }; - hooks.callAll('aceRegisterBlockElements').forEach((element) => { - _blockElems[element] = 1; - }); + for (const element of hooks.callAll('aceRegisterBlockElements')) _blockElems[element] = 1; const isBlockElement = (n) => !!_blockElems[(n.tagName || '').toLowerCase()]; editorInfo.ace_isBlockElement = isBlockElement; @@ -2469,9 +2456,7 @@ function Ace2Inner(editorInfo, cssManagers) { } } - mods.forEach((mod) => { - setLineListType(mod[0], mod[1]); - }); + for (const mod of mods) setLineListType(mod[0], mod[1]); return true; }; editorInfo.ace_doIndentOutdent = doIndentOutdent; @@ -2689,8 +2674,8 @@ function Ace2Inner(editorInfo, cssManagers) { // Known authors info, both current and historical const padAuthors = parent.parent.pad.userList(); let authorObj = {}; - authors.forEach((authorId) => { - padAuthors.forEach((padAuthor) => { + for (const authorId of authors) { + for (const padAuthor of padAuthors) { // If the person doing the lookup is the author.. if (padAuthor.userId === authorId) { if (parent.parent.clientVars.userId === authorId) { @@ -2701,15 +2686,15 @@ function Ace2Inner(editorInfo, cssManagers) { authorObj = padAuthor; } } - }); + } if (!authorObj) { author = 'Unknown'; - return; + continue; } author = authorObj.name; if (!author) author = 'Unknown'; authorNames.push(author); - }); + } } if (authors.length === 1) { authorString = `The author of this line is ${authorNames[0]}`; @@ -3271,7 +3256,7 @@ function Ace2Inner(editorInfo, cssManagers) { const _teardownActions = []; - const teardown = () => _teardownActions.forEach((a) => a()); + const teardown = () => { for (const a of _teardownActions) a(); }; let inInternationalComposition = null; editorInfo.ace_getInInternationalComposition = () => inInternationalComposition; @@ -3501,9 +3486,7 @@ function Ace2Inner(editorInfo, cssManagers) { } } - mods.forEach((mod) => { - setLineListType(mod[0], mod[1]); - }); + for (const mod of mods) setLineListType(mod[0], mod[1]); }; const doInsertUnorderedList = () => { @@ -3537,10 +3520,7 @@ function Ace2Inner(editorInfo, cssManagers) { const innerdocbodyStyles = getComputedStyle(document.body); const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']); - let docLine = document.body.firstElementChild; - - // First loop to calculate the heights from doc body - while (docLine) { + for (const docLine of document.body.children) { let h; const nextDocLine = docLine.nextElementSibling; if (nextDocLine) { @@ -3575,7 +3555,6 @@ function Ace2Inner(editorInfo, cssManagers) { } else { lineHeights.push(defaultLineHeight); } - docLine = nextDocLine; } let newNumLines = rep.lines.length(); From 88057eade218fbddc9f6c89276cb5107e85f8134 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 23 Jun 2021 03:27:50 -0400 Subject: [PATCH 083/322] ace2_inner: Readability improvements --- src/static/js/ace2_inner.js | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 58570f079..b04abffe6 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1125,19 +1125,9 @@ function Ace2Inner(editorInfo, cssManagers) { const domChanges = (splicesToDo.length > 0); - // update the representation - for (const splice of splicesToDo) { - doIncorpLineSplice(splice[0], splice[1], splice[2], splice[3], splice[4]); - } - - // do DOM inserts - for (const ins of domInsertsNeeded) insertDomLines(ins[0], ins[1]); - - // delete old dom nodes - for (const n of toDeleteAtEnd) { - // parent of n may not be "root" in IE due to non-tree-shaped DOM (wtf) - if (n.parentNode) n.parentNode.removeChild(n); - } + for (const splice of splicesToDo) doIncorpLineSplice(...splice); + for (const ins of domInsertsNeeded) insertDomLines(...ins); + for (const n of toDeleteAtEnd) n.remove(); // needed to stop chrome from breaking the ui when long strings without spaces are pasted if (scrollToTheLeftNeeded) { @@ -2269,10 +2259,7 @@ function Ace2Inner(editorInfo, cssManagers) { const markNodeClean = (n) => { // clean nodes have knownHTML that matches their innerHTML - const dirtiness = {}; - dirtiness.nodeId = uniqueId(n); - dirtiness.knownHTML = n.innerHTML; - setAssoc(n, 'dirtiness', dirtiness); + setAssoc(n, 'dirtiness', {nodeId: uniqueId(n), knownHTML: n.innerHTML}); }; const isNodeDirty = (n) => { From fed950e8094fa8b50d8c98ca3259cd0ccbbe7a4e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 16 Aug 2021 00:22:43 -0400 Subject: [PATCH 084/322] ace2_inner: Simplify handler for Ctrl-@ (show authors) --- src/static/js/ace2_inner.js | 60 +++++++------------------------------ 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index b04abffe6..03dd925bb 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -2637,67 +2637,27 @@ function Ace2Inner(editorInfo, cssManagers) { // TODO: Still work when authorship colors have been cleared // TODO: i18n // TODO: There appears to be a race condition or so. - const authors = []; - let author = null; + const authorIds = new Set(); if (alineAttrs) { const opIter = Changeset.opIterator(alineAttrs); - while (opIter.hasNext()) { const op = opIter.next(); const authorId = Changeset.opAttributeValue(op, 'author', apool); - - // Only push unique authors and ones with values - if (authors.indexOf(authorId) === -1 && authorId !== '') { - authors.push(authorId); - } + if (authorId !== '') authorIds.add(authorId); } } - - let authorString; - const authorNames = []; - if (authors.length === 0) { - authorString = 'No author information is available'; - } else { - // Known authors info, both current and historical - const padAuthors = parent.parent.pad.userList(); - let authorObj = {}; - for (const authorId of authors) { - for (const padAuthor of padAuthors) { - // If the person doing the lookup is the author.. - if (padAuthor.userId === authorId) { - if (parent.parent.clientVars.userId === authorId) { - authorObj = { - name: 'Me', - }; - } else { - authorObj = padAuthor; - } - } - } - if (!authorObj) { - author = 'Unknown'; - continue; - } - author = authorObj.name; - if (!author) author = 'Unknown'; - authorNames.push(author); - } - } - if (authors.length === 1) { - authorString = `The author of this line is ${authorNames[0]}`; - } - if (authors.length > 1) { - authorString = `The authors of this line are ${authorNames.join(' & ')}`; - } + const idToName = new Map(parent.parent.pad.userList().map((a) => [a.userId, a.name])); + const myId = parent.parent.clientVars.userId; + const authors = + [...authorIds].map((id) => id === myId ? 'me' : idToName.get(id) || 'unknown'); parent.parent.$.gritter.add({ - // (string | mandatory) the heading of the notification title: 'Line Authors', - // (string | mandatory) the text inside the notification - text: authorString, - // (bool | optional) if you want it to fade out on its own or just sit there + text: + authors.length === 0 ? 'No author information is available' + : authors.length === 1 ? `The author of this line is ${authors[0]}` + : `The authors of this line are ${authors.join(' & ')}`, sticky: false, - // (int | optional) the time you want it to be alive for before fading out time: '4000', }); } From 05182d1d30d33d3c8f113c1e86ccac4e2556d0b9 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 19 Aug 2021 07:35:17 +0000 Subject: [PATCH 085/322] fix: upgrade mime-types from 2.1.31 to 2.1.32 Snyk has created this PR to upgrade mime-types from 2.1.31 to 2.1.32. See this package in npm: https://www.npmjs.com/package/mime-types See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 14 +++++++------- src/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index b3457e898..e0a53233d 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3738,16 +3738,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" }, "mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", "requires": { - "mime-db": "1.48.0" + "mime-db": "1.49.0" } }, "mimic-response": { diff --git a/src/package.json b/src/package.json index 8050b167e..2ed7dafda 100644 --- a/src/package.json +++ b/src/package.json @@ -52,7 +52,7 @@ "lodash.clonedeep": "4.5.0", "log4js": "0.6.38", "measured-core": "1.51.1", - "mime-types": "^2.1.27", + "mime-types": "^2.1.32", "nodeify": "1.0.1", "npm": "6.14.13", "openapi-backend": "^3.9.1", From f08a4434976a5de0a3aecb93223fe444ab4e9322 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sat, 14 Aug 2021 08:32:33 +0000 Subject: [PATCH 086/322] fix: upgrade rate-limiter-flexible from 2.2.3 to 2.2.4 Snyk has created this PR to upgrade rate-limiter-flexible from 2.2.3 to 2.2.4. See this package in npm: https://www.npmjs.com/package/rate-limiter-flexible See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e0a53233d..dd17a46e4 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -7887,9 +7887,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.3.tgz", - "integrity": "sha512-NxY71f78yZ62bmJeAznRdCDKy6fByoa9uBinCsZHzr22Wub2fGyfVnJVKcbU3U+FW97Gg9Akv6x9nuh/Pa7fvQ==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.4.tgz", + "integrity": "sha512-8u4k5b1afuBcfydX0L0l3J2PNjgcuo3zua8plhvIisyDqOBldrCwfSFut/Fj00LAB1nxJYVM9jeszr2rZyDhQw==" }, "raw-body": { "version": "2.4.0", diff --git a/src/package.json b/src/package.json index 2ed7dafda..0ea39497e 100644 --- a/src/package.json +++ b/src/package.json @@ -57,7 +57,7 @@ "npm": "6.14.13", "openapi-backend": "^3.9.1", "proxy-addr": "^2.0.6", - "rate-limiter-flexible": "^2.2.3", + "rate-limiter-flexible": "^2.2.4", "rehype": "^10.0.0", "rehype-minify-whitespace": "^4.0.5", "request": "2.88.2", From b683cdfe0f808b25324d9c766a792d2d02dbcfee Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 23 Aug 2021 13:03:03 +0200 Subject: [PATCH 087/322] Localisation updates from https://translatewiki.net. --- src/locales/uk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/uk.json b/src/locales/uk.json index e01af194e..e13cd0e7f 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -162,7 +162,7 @@ "timeslider.month.december": "Грудень", "timeslider.unnamedauthors": "{{num}} {[plural(num) one: безіменний автор, few: безіменні автори, many: безіменних авторів, other: безіменних авторів]}", "pad.savedrevs.marked": "Цю версію помічено збереженою версією", - "pad.savedrevs.timeslider": "Ви можете побачити збережені ревізії, відвідавши \"Слайдер Змін Ревізій\"", + "pad.savedrevs.timeslider": "Ви можете побачити збережені ревізії, відвідавши «Слайдер Змін Ревізій»", "pad.userlist.entername": "Введіть ваше ім'я", "pad.userlist.unnamed": "безіменний", "pad.editbar.clearcolors": "Очистити кольори у всьому документі? Це не можна буде відкотити", From 0d65dc8a440c9d9089b85728fac3a468b1318928 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 25 Aug 2021 14:54:07 -0400 Subject: [PATCH 088/322] pad: Add `clientVars` to `postAceInit` hook context This allows plugins to avoid the `clientVars` global variable. --- CHANGELOG.md | 6 ++++++ doc/api/hooks_client-side.md | 5 ++++- src/static/js/pad.js | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2604b73ed..ffe8d785e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ [citizenos/ep_image_upload#49](https://github.com/citizenos/ep_image_upload/pull/49) for an example fix. +### Notable enhancements + +* For plugin authors: + * `clientVars` was added to the context for the `postAceInit` client-side + hook. Plugins should use this instead of the `clientVars` global variable. + # 1.8.14 ### Security fixes diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 2559a4e08..6f5f6e065 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -229,7 +229,10 @@ Called from: src/static/js/pad.js Things in context: 1. ace - the ace object that is applied to this editor. -2. pad - the pad object of the current pad. +2. clientVars - Object containing client-side configuration such as author ID + and plugin settings. Your plugin can manipulate this object via the + `clientVars` server-side hook. +3. pad - the pad object of the current pad. ## postToolbarInit diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 0ea4ce227..ccf29cc57 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -501,7 +501,7 @@ const pad = { $('#editorcontainer').addClass('initialized'); - hooks.aCallAll('postAceInit', {ace: padeditor.ace, pad}); + hooks.aCallAll('postAceInit', {ace: padeditor.ace, clientVars, pad}); }; // order of inits is important here: From 3c159ef75ed3f472a451f33af29d67b60116aa96 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 5 Aug 2021 08:37:38 +0000 Subject: [PATCH 089/322] fix: upgrade wtfnode from 0.9.0 to 0.9.1 Snyk has created this PR to upgrade wtfnode from 0.9.0 to 0.9.1. See this package in npm: https://www.npmjs.com/package/wtfnode See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 15 +++------------ src/package.json | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index dd17a46e4..7069dfcc5 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1353,11 +1353,6 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, - "coffeescript": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", - "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -9424,13 +9419,9 @@ "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" }, "wtfnode": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.0.tgz", - "integrity": "sha512-IKHfNAFZwfm0uCt/zuFADN3mHyoB+ZrmwFpRGOxKPIXV0tifqpIaTH3NvImA7yy7GimsAayZGTaNvOmavKzE+A==", - "requires": { - "coffeescript": "^2.5.1", - "source-map-support": "^0.5.19" - } + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.1.tgz", + "integrity": "sha512-Ip6C2KeQPl/F3aP1EfOnPoQk14Udd9lffpoqWDNH3Xt78svxPbv53ngtmtfI0q2Te3oTq79XKTnRNXVIn/GsPA==" }, "xml-name-validator": { "version": "3.0.0", diff --git a/src/package.json b/src/package.json index 0ea39497e..d61b9ab23 100644 --- a/src/package.json +++ b/src/package.json @@ -72,7 +72,7 @@ "ueberdb2": "^1.4.13", "underscore": "1.13.1", "unorm": "1.6.0", - "wtfnode": "^0.9.0" + "wtfnode": "^0.9.1" }, "bin": { "etherpad-lite": "node/server.js" From cca3ba94f9dfc29bd5e0013a8b22028e6db25ce2 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 27 Aug 2021 07:05:25 +0000 Subject: [PATCH 090/322] fix: upgrade async from 3.2.0 to 3.2.1 Snyk has created this PR to upgrade async from 3.2.0 to 3.2.1. See this package in npm: https://www.npmjs.com/package/async See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 7069dfcc5..e89367839 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -904,9 +904,9 @@ "dev": true }, "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", + "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" }, "async-stacktrace": { "version": "0.0.2", diff --git a/src/package.json b/src/package.json index d61b9ab23..ff5844f5f 100644 --- a/src/package.json +++ b/src/package.json @@ -30,7 +30,7 @@ } ], "dependencies": { - "async": "^3.2.0", + "async": "^3.2.1", "async-stacktrace": "0.0.2", "channels": "0.0.4", "clean-css": "4.2.3", From 3225abc3c781f95cb9b9c0746c455ce7d6cd23e7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 19:23:43 -0400 Subject: [PATCH 091/322] deps: Bump eslint to 7.32.0 --- src/package-lock.json | 97 +++++++++++++++++++++++++++++-------------- src/package.json | 2 +- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e89367839..7e10fa513 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -377,9 +377,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", "dev": true }, "@babel/highlight": { @@ -394,9 +394,9 @@ } }, "@eslint/eslintrc": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", - "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -420,9 +420,9 @@ } }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { "ms": "2.1.2" @@ -458,6 +458,40 @@ } } }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, "@js-joda/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-3.2.0.tgz", @@ -684,9 +718,9 @@ } }, "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, "acorn-walk": { @@ -1913,13 +1947,14 @@ } }, "eslint": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.28.0.tgz", - "integrity": "sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "requires": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.2", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1984,9 +2019,9 @@ } }, "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -2009,9 +2044,9 @@ "dev": true }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { "ms": "2.1.2" @@ -2544,9 +2579,9 @@ } }, "flatted": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", - "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", "dev": true }, "follow-redirects": { @@ -2703,9 +2738,9 @@ } }, "globals": { - "version": "13.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz", - "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", + "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -8788,9 +8823,9 @@ }, "dependencies": { "ajv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", - "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", diff --git a/src/package.json b/src/package.json index ff5844f5f..7a64173eb 100644 --- a/src/package.json +++ b/src/package.json @@ -78,7 +78,7 @@ "etherpad-lite": "node/server.js" }, "devDependencies": { - "eslint": "^7.28.0", + "eslint": "^7.32.0", "eslint-config-etherpad": "^2.0.0", "eslint-plugin-cypress": "^2.11.3", "eslint-plugin-eslint-comments": "^3.2.0", From 912e72a8ac114ce7956c40a5e47c23480dcee8c3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 19:33:03 -0400 Subject: [PATCH 092/322] deps: Bump clean-css to 5.1.5 --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 7e10fa513..6308ae895 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1299,9 +1299,9 @@ "optional": true }, "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.1.5.tgz", + "integrity": "sha512-9dr/cU/LjMpU57PXlSvDkVRh0rPxJBXiBtD0+SgYt8ahTCsXtfKjCkNYgIoTC6mBg8CFr5EKhW3DKCaGMUbUfQ==", "requires": { "source-map": "~0.6.0" } diff --git a/src/package.json b/src/package.json index 7a64173eb..bd001e929 100644 --- a/src/package.json +++ b/src/package.json @@ -33,7 +33,7 @@ "async": "^3.2.1", "async-stacktrace": "0.0.2", "channels": "0.0.4", - "clean-css": "4.2.3", + "clean-css": "^5.1.5", "cookie-parser": "1.4.5", "cross-spawn": "^7.0.3", "ejs": "^3.1.6", From ea43c92fe959d5aab2763522d6d7e8801a3ffab6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 20:02:54 -0400 Subject: [PATCH 093/322] deps: Bump etherpad-cli-client to 0.1.12 --- src/package-lock.json | 88 ++++++++++++++++++++++++++++++++++++++++--- src/package.json | 2 +- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 6308ae895..5e66a91fe 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2355,13 +2355,85 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "etherpad-cli-client": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-0.0.9.tgz", - "integrity": "sha1-A+5+fNzA4EZLTu/djn7gzwUaVDs=", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-0.1.12.tgz", + "integrity": "sha512-7Cz9Ofd2xa4OJwOHNHyWdzKhRLLa17Mqbav2IV2old+DoVPUiFyOXz6YXaqBvkj09bS8BuTEQVajqo/rQ6N0LA==", "dev": true, "requires": { - "async": "*", - "socket.io-client": "*" + "async": "^3.2.1", + "socket.io-client": "^2.3.0", + "superagent": "^6.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "superagent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + } + } } }, "etherpad-require-kernel": { @@ -2494,6 +2566,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-safe-stringify": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz", + "integrity": "sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==", + "dev": true + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", diff --git a/src/package.json b/src/package.json index bd001e929..5ed26653e 100644 --- a/src/package.json +++ b/src/package.json @@ -87,7 +87,7 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", - "etherpad-cli-client": "0.0.9", + "etherpad-cli-client": "^0.1.12", "mocha": "7.1.2", "mocha-froth": "^0.2.10", "openapi-schema-validation": "^0.4.2", From 7dbd278d1d420849e0ecd18a508eac3d821a4ab1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 20:34:07 -0400 Subject: [PATCH 094/322] deps: Bump js-cookie to 3.0.0 --- src/node/utils/tar.json | 8 ++++---- src/package-lock.json | 6 +++--- src/package.json | 2 +- src/static/js/pad_utils.js | 23 ++++++++++++----------- src/tests/frontend/index.html | 2 +- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index 7f1fe0135..e6caa3e67 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -2,7 +2,7 @@ "pad.js": [ "pad.js" , "pad_utils.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "security.js" , "$security.js" , "vendors/browser.js" @@ -21,7 +21,7 @@ , "pad_connectionstatus.js" , "chat.js" , "vendors/gritter.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "$tinycon/tinycon.js" , "vendors/farbtastic.js" , "skin_variants.js" @@ -33,7 +33,7 @@ , "colorutils.js" , "draggable.js" , "pad_utils.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "vendors/browser.js" , "pad_cookie.js" , "pad_editor.js" @@ -73,7 +73,7 @@ , "scroll.js" , "caretPosition.js" , "pad_utils.js" - , "$js-cookie/src/js.cookie.js" + , "$js-cookie/dist/js.cookie.js" , "security.js" , "$security.js" ] diff --git a/src/package-lock.json b/src/package-lock.json index 5e66a91fe..e30e67e83 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3372,9 +3372,9 @@ } }, "js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.0.tgz", + "integrity": "sha512-oUbbplKuH07/XX2YD2+Q+GMiPpnVXaRz8npE7suhBH9QEkJe2W7mQ6rwuMXHue3fpfcftQwzgyvGzIHyfCSngQ==" }, "js-tokens": { "version": "4.0.0", diff --git a/src/package.json b/src/package.json index 5ed26653e..8eaa4924a 100644 --- a/src/package.json +++ b/src/package.json @@ -45,7 +45,7 @@ "find-root": "1.1.0", "formidable": "1.2.2", "http-errors": "1.8.0", - "js-cookie": "^2.2.1", + "js-cookie": "^3.0.0", "jsdom": "^16.7.0", "jsonminify": "0.4.1", "languages4translatewiki": "0.1.3", diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 7b2d50604..6fb46fde0 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -383,17 +383,18 @@ const inThirdPartyIframe = () => { // This file is included from Node so that it can reuse randomString, but Node doesn't have a global // window object. if (typeof window !== 'undefined') { - exports.Cookies = require('js-cookie/src/js.cookie'); - // Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case - // use `SameSite=None`. For iframes from another site, only `None` has a chance of working - // because the cookies are third-party (not same-site). Many browsers/users block third-party - // cookies, but maybe blocked is better than definitely blocked (which would happen with `Lax` - // or `Strict`). Note: `None` will not work unless secure is true. - // - // `Strict` is not used because it has few security benefits but significant usability drawbacks - // vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. - exports.Cookies.defaults.sameSite = inThirdPartyIframe() ? 'None' : 'Lax'; - exports.Cookies.defaults.secure = window.location.protocol === 'https:'; + exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({ + // Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case + // use `SameSite=None`. For iframes from another site, only `None` has a chance of working + // because the cookies are third-party (not same-site). Many browsers/users block third-party + // cookies, but maybe blocked is better than definitely blocked (which would happen with `Lax` + // or `Strict`). Note: `None` will not work unless secure is true. + // + // `Strict` is not used because it has few security benefits but significant usability drawbacks + // vs. `Lax`. See https://stackoverflow.com/q/41841880 for discussion. + sameSite: inThirdPartyIframe() ? 'None' : 'Lax', + secure: window.location.protocol === 'https:', + }); } exports.randomString = randomString; exports.padutils = padutils; diff --git a/src/tests/frontend/index.html b/src/tests/frontend/index.html index 9f542ee13..22c42ab15 100644 --- a/src/tests/frontend/index.html +++ b/src/tests/frontend/index.html @@ -18,7 +18,7 @@ - + From 7db3e4273d048fdddc5a021ffe41e93e97f08fc0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 20:36:20 -0400 Subject: [PATCH 095/322] deps: Bump jsdom to 17.0.0 --- src/package-lock.json | 52 +++++++++++++++++++++++-------------------- src/package.json | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e30e67e83..b6dffda92 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1505,9 +1505,9 @@ } }, "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, "cssstyle": { "version": "2.3.0", @@ -1533,13 +1533,13 @@ } }, "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.0.tgz", + "integrity": "sha512-4AefxbTTdFtxDUdh0BuMBs2qJVL25Mow2zlcuuePegQwgD6GEmQao42LLEeksOui8nL4RcNEugIpFP7eRd33xg==", "requires": { "abab": "^2.0.3", "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "whatwg-url": "^9.0.0" } }, "date-utils": { @@ -3401,20 +3401,20 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-17.0.0.tgz", + "integrity": "sha512-MUq4XdqwtNurZDVeKScENMPHnkgmdIvMzZ1r1NSwHkDuaqI6BouPjr+17COo4/19oLNnmdpFDPOHVpgIZmZ+VA==", "requires": { "abab": "^2.0.5", - "acorn": "^8.2.4", + "acorn": "^8.4.1", "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", + "cssom": "^0.5.0", "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", + "data-urls": "^3.0.0", + "decimal.js": "^10.3.1", "domexception": "^2.0.1", "escodegen": "^2.0.0", - "form-data": "^3.0.0", + "form-data": "^4.0.0", "html-encoding-sniffer": "^2.0.1", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", @@ -3429,8 +3429,8 @@ "webidl-conversions": "^6.1.0", "whatwg-encoding": "^1.0.5", "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", + "whatwg-url": "^9.0.0", + "ws": "^8.0.0", "xml-name-validator": "^3.0.0" }, "dependencies": { @@ -3440,9 +3440,9 @@ "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" }, "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3463,6 +3463,11 @@ "punycode": "^2.1.1", "universalify": "^0.1.2" } + }, + "ws": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.1.tgz", + "integrity": "sha512-XkgWpJU3sHU7gX8f13NqTn6KQ85bd1WU7noBHTT8fSohx7OS1TPY8k+cyRPCzFkia7C4mM229yeHr1qK9sM4JQ==" } } }, @@ -9421,11 +9426,10 @@ "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" }, "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz", + "integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==", "requires": { - "lodash": "^4.7.0", "tr46": "^2.1.0", "webidl-conversions": "^6.1.0" } diff --git a/src/package.json b/src/package.json index 8eaa4924a..cbbae09ed 100644 --- a/src/package.json +++ b/src/package.json @@ -46,7 +46,7 @@ "formidable": "1.2.2", "http-errors": "1.8.0", "js-cookie": "^3.0.0", - "jsdom": "^16.7.0", + "jsdom": "^17.0.0", "jsonminify": "0.4.1", "languages4translatewiki": "0.1.3", "lodash.clonedeep": "4.5.0", From 72b22f7c020bf5f08004cc0ae566c7a97ee52766 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 20:39:13 -0400 Subject: [PATCH 096/322] deps: Bump measured-core to 2.0.0 --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index b6dffda92..462f5b02f 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3816,9 +3816,9 @@ } }, "measured-core": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-1.51.1.tgz", - "integrity": "sha512-DZQP9SEwdqqYRvT2slMK81D/7xwdxXosZZBtLVfPSo6y5P672FBTbzHVdN4IQyUkUpcVOR9pIvtUy5Ryl7NKyg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-2.0.0.tgz", + "integrity": "sha512-SIzGtX1WGDvR59FqcJaGEAqDueBvLBh6W4T/gQaHr5ufcqvQkUHGcfQhlmq77mkeF5Mo+UpD+8hm69CwUVibGw==", "requires": { "binary-search": "^1.3.3", "optional-js": "^2.0.0" diff --git a/src/package.json b/src/package.json index cbbae09ed..f440f600c 100644 --- a/src/package.json +++ b/src/package.json @@ -51,7 +51,7 @@ "languages4translatewiki": "0.1.3", "lodash.clonedeep": "4.5.0", "log4js": "0.6.38", - "measured-core": "1.51.1", + "measured-core": "^2.0.0", "mime-types": "^2.1.32", "nodeify": "1.0.1", "npm": "6.14.13", From 0accdf0a077c15a91d2d577f6441d5abc16ae93f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 20:49:56 -0400 Subject: [PATCH 097/322] deps: Bump mocha to 9.1.1 --- src/package-lock.json | 806 +++++++++++++++++------------------------- src/package.json | 2 +- 2 files changed, 329 insertions(+), 479 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 462f5b02f..4943bff1c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -675,6 +675,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -1225,9 +1231,9 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "dev": true }, "caseless": { @@ -1277,19 +1283,19 @@ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==" }, "chokidar": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", - "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "dev": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.1", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.2.0" + "readdirp": "~3.6.0" } }, "chownr": { @@ -1307,52 +1313,46 @@ } }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } @@ -1556,9 +1556,9 @@ } }, "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, "decimal.js": { @@ -1586,15 +1586,6 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1628,9 +1619,9 @@ "optional": true }, "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, "dirty": { @@ -1833,54 +1824,11 @@ "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz", "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" }, - "es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - } - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true }, "escape-html": { "version": "1.0.3", @@ -2618,22 +2566,20 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, "flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true }, "flat-cache": { "version": "3.0.4", @@ -2718,9 +2664,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "optional": true }, @@ -2866,12 +2812,6 @@ "ansi-regex": "^2.0.0" } }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true - }, "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", @@ -3175,12 +3115,6 @@ "is-decimal": "^1.0.0" } }, - "is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", - "dev": true - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3190,26 +3124,11 @@ "binary-extensions": "^2.0.0" } }, - "is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, "is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" }, - "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", - "dev": true - }, "is-core-module": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", @@ -3218,12 +3137,6 @@ "has": "^1.0.3" } }, - "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", - "dev": true - }, "is-decimal": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", @@ -3262,24 +3175,12 @@ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", - "dev": true - }, "is-observable": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", @@ -3300,36 +3201,17 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" }, - "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", - "dev": true - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -3681,13 +3563,12 @@ } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^5.0.0" } }, "lodash": { @@ -3753,12 +3634,64 @@ "dev": true }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "chalk": "^2.4.2" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "log4js": { @@ -3880,7 +3813,8 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "optional": true }, "minipass": { "version": "2.9.0", @@ -3905,6 +3839,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -3916,113 +3851,86 @@ "optional": true }, "mocha": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz", - "integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", "dev": true, "requires": { - "ansi-colors": "3.2.3", + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.3.0", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", + "chokidar": "3.5.2", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.7", "growl": "1.10.5", "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", "minimatch": "3.0.4", - "mkdirp": "0.5.5", - "ms": "2.1.1", - "node-environment-flags": "1.0.6", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", + "ms": "2.1.3", + "nanoid": "3.1.23", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", "wide-align": "1.1.3", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" + "workerpool": "6.1.5", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" }, "dependencies": { - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { - "has-flag": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" + "has-flag": "^4.0.0" } } } @@ -4173,6 +4081,12 @@ } } }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "dev": true + }, "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -4267,16 +4181,6 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "optional": true }, - "node-environment-flags": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", - "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", @@ -7546,35 +7450,6 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", - "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2" - } - }, "observable-fns": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz", @@ -7713,29 +7588,23 @@ } }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^3.0.2" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -7777,9 +7646,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "path-is-absolute": { @@ -7994,6 +7863,15 @@ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8057,12 +7935,12 @@ } }, "readdirp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", - "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { - "picomatch": "^2.0.4" + "picomatch": "^2.2.1" } }, "redis": { @@ -8198,12 +8076,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -8345,6 +8217,15 @@ } } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -8359,7 +8240,8 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true }, "set-cookie-parser": { "version": "2.4.8", @@ -8739,26 +8621,6 @@ "strip-ansi": "^3.0.0" } }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8790,7 +8652,8 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "optional": true }, "superagent": { "version": "3.8.3", @@ -9264,18 +9127,6 @@ "random-bytes": "~1.0.0" } }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, "underscore": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", @@ -9442,25 +9293,6 @@ "isexe": "^2.0.0" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -9474,53 +9306,77 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, + "workerpool": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", + "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "dev": true + }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } @@ -9585,9 +9441,9 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yallist": { @@ -9597,88 +9453,82 @@ "optional": true }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } }, "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true }, "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" } }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/src/package.json b/src/package.json index f440f600c..35ad6de1c 100644 --- a/src/package.json +++ b/src/package.json @@ -88,7 +88,7 @@ "eslint-plugin-promise": "^5.1.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", "etherpad-cli-client": "^0.1.12", - "mocha": "7.1.2", + "mocha": "^9.1.1", "mocha-froth": "^0.2.10", "openapi-schema-validation": "^0.4.2", "selenium-webdriver": "^4.0.0-beta.3", From 529d2f6b7d6629663d39f9f1397015c15e7d557f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 23:08:15 -0400 Subject: [PATCH 098/322] deps: Bump rehype to 10.0.0 --- src/package-lock.json | 144 ++++++++++++++++++++---------------------- src/package.json | 2 +- 2 files changed, 69 insertions(+), 77 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 4943bff1c..e6627affd 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -590,6 +590,14 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, + "@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "requires": { + "@types/unist": "*" + } + }, "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -626,6 +634,11 @@ } } }, + "@types/parse5": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", + "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" + }, "@types/request": { "version": "2.48.7", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.7.tgz", @@ -671,9 +684,9 @@ } }, "@types/unist": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", - "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, "@ungap/promise-all-settled": { "version": "1.1.2", @@ -2857,15 +2870,16 @@ } }, "hast-util-from-parse5": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz", - "integrity": "sha512-gOc8UB99F6eWVWFtM9jUikjN7QkWxB3nY0df5Z0Zq1/Nkwl5V4hAAsl0tmwlgWl/1shlTF8DnNYLO8X6wRV9pA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz", + "integrity": "sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==", "requires": { - "ccount": "^1.0.3", - "hastscript": "^5.0.0", + "@types/parse5": "^5.0.0", + "hastscript": "^6.0.0", "property-information": "^5.0.0", - "web-namespaces": "^1.1.2", - "xtend": "^4.0.1" + "vfile": "^4.0.0", + "vfile-location": "^3.2.0", + "web-namespaces": "^1.0.0" } }, "hast-util-is-element": { @@ -2879,20 +2893,20 @@ "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==" }, "hast-util-to-html": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-6.1.0.tgz", - "integrity": "sha512-IlC+LG2HGv0Y8js3wqdhg9O2sO4iVpRDbHOPwXd7qgeagpGsnY49i8yyazwqS35RA35WCzrBQE/n0M6GG/ewxA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz", + "integrity": "sha512-yk2+1p3EJTEE9ZEUkgHsUSVhIpCsL/bvT8E5GzmWc+N1Po5gBw+0F8bo7dpxXR0nu0bQVxVZGX2lBGF21CmeDw==", "requires": { "ccount": "^1.0.0", - "comma-separated-tokens": "^1.0.1", + "comma-separated-tokens": "^1.0.0", "hast-util-is-element": "^1.0.0", "hast-util-whitespace": "^1.0.0", "html-void-elements": "^1.0.0", - "property-information": "^5.2.0", + "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0", - "stringify-entities": "^2.0.0", - "unist-util-is": "^3.0.0", - "xtend": "^4.0.1" + "stringify-entities": "^3.0.1", + "unist-util-is": "^4.0.0", + "xtend": "^4.0.0" } }, "hast-util-whitespace": { @@ -2901,10 +2915,11 @@ "integrity": "sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==" }, "hastscript": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz", - "integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", "requires": { + "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", @@ -3101,20 +3116,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, - "is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==" - }, - "is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3137,11 +3138,6 @@ "has": "^1.0.3" } }, - "is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" - }, "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -3170,11 +3166,6 @@ "is-extglob": "^2.1.1" } }, - "is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7626,9 +7617,9 @@ } }, "parse5": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, "parseqs": { "version": "0.0.6", @@ -7979,12 +7970,12 @@ "dev": true }, "rehype": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-10.0.0.tgz", - "integrity": "sha512-0W8M4Y91b2QuzDSTjkZgBOJo79bP089YbSQNPMqebuUVrp6iveoi+Ra6/H7fJwUxq8FCHGCGzkLaq3fvO9XnVg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-11.0.0.tgz", + "integrity": "sha512-qXqRqiCFJD5CJ61CSJuNImTFrm3zVkOU9XywHDwrUuvWN74MWt72KJ67c5CM5x8g0vGcOkRVCrYj85vqkmHulQ==", "requires": { - "rehype-parse": "^6.0.0", - "rehype-stringify": "^6.0.0", + "rehype-parse": "^7.0.0", + "rehype-stringify": "^8.0.0", "unified": "^9.0.0" } }, @@ -8007,22 +7998,20 @@ } }, "rehype-parse": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-6.0.2.tgz", - "integrity": "sha512-0S3CpvpTAgGmnz8kiCyFLGuW5yA4OQhyNTm/nwPopZ7+PI11WnGl1TTWTGv/2hPEe/g2jRLlhVVSsoDH8waRug==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-7.0.1.tgz", + "integrity": "sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw==", "requires": { - "hast-util-from-parse5": "^5.0.0", - "parse5": "^5.0.0", - "xtend": "^4.0.0" + "hast-util-from-parse5": "^6.0.0", + "parse5": "^6.0.0" } }, "rehype-stringify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-6.0.1.tgz", - "integrity": "sha512-JfEPRDD4DiG7jet4md7sY07v6ACeb2x+9HWQtRPm2iA6/ic31hCv1SNBUtpolJASxQ/D8gicXiviW4TJKEMPKQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-8.0.0.tgz", + "integrity": "sha512-VkIs18G0pj2xklyllrPSvdShAV36Ff3yE5PUO9u36f6+2qJFnn22Z5gKwBOwgXviux4UC7K+/j13AnZfPICi/g==", "requires": { - "hast-util-to-html": "^6.0.0", - "xtend": "^4.0.0" + "hast-util-to-html": "^7.1.1" } }, "request": { @@ -8630,15 +8619,13 @@ } }, "stringify-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-2.0.0.tgz", - "integrity": "sha512-fqqhZzXyAM6pGD9lky/GOPq6V4X0SeTAFBl0iXb/BzOegl40gpf/bV3QQP7zULNYvjr6+Dx8SCaDULjVoOru0A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz", + "integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==", "requires": { "character-entities-html4": "^1.0.0", "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.2", - "is-hexadecimal": "^1.0.0" + "xtend": "^4.0.0" } }, "strip-ansi": { @@ -9133,9 +9120,9 @@ "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" }, "unified": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.1.tgz", - "integrity": "sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", "requires": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -9146,9 +9133,9 @@ } }, "unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==" }, "unist-util-stringify-position": { "version": "2.0.3", @@ -9228,6 +9215,11 @@ "vfile-message": "^2.0.0" } }, + "vfile-location": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz", + "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==" + }, "vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", diff --git a/src/package.json b/src/package.json index 35ad6de1c..ad9369ace 100644 --- a/src/package.json +++ b/src/package.json @@ -58,7 +58,7 @@ "openapi-backend": "^3.9.1", "proxy-addr": "^2.0.6", "rate-limiter-flexible": "^2.2.4", - "rehype": "^10.0.0", + "rehype": "^11.0.0", "rehype-minify-whitespace": "^4.0.5", "request": "2.88.2", "resolve": "1.20.0", From 550c7365c285f4f181fdf8f70783ee60eb0907cc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 21:10:16 -0400 Subject: [PATCH 099/322] deps: Bump openapi-backend to 4.2.0 --- src/package-lock.json | 102 +++++++++++++++++++++++++++++++++--------- src/package.json | 2 +- 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index e6627affd..ce93e218b 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -599,9 +599,9 @@ } }, "@types/json-schema": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", - "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, "@types/long": { "version": "4.0.1", @@ -839,6 +839,32 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -7477,21 +7503,37 @@ } }, "openapi-backend": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.2.tgz", - "integrity": "sha512-+IqhtObMGeRf4aDB6L5Lc3nZYPHB9JRkTiOaNHKx26SDWcaMAof6RnABbgLDNVRRiz+fbJPmizWcFSkCPX8qeQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-4.2.0.tgz", + "integrity": "sha512-eqdgJAjDbVZ7zhiIF68mlItFxqE48OPAM9nHHYx6BJMoGK2xInSBc2Oqp4dzsrsLIzoY8nVzK/vUtYktyXGb9Q==", "requires": { "@apidevtools/json-schema-ref-parser": "^9.0.7", - "ajv": "^6.10.0", + "ajv": "^8.5.0", "bath-es5": "^3.0.3", "cookie": "^0.4.0", "lodash": "^4.17.15", "mock-json-schema": "^1.0.7", - "openapi-schema-validator": "^7.0.1", - "openapi-types": "^7.0.1", + "openapi-schema-validator": "^9.2.0", + "openapi-types": "^9.2.0", "qs": "^6.9.3" }, "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "qs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", @@ -7514,20 +7556,38 @@ } }, "openapi-schema-validator": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-7.2.3.tgz", - "integrity": "sha512-XT8NM5e/zBBa/cydTS1IeYkCPzJp9oixvt9Y1lEx+2gsCTOooNxw9x/KEivtWMSokne7X1aR+VtsYHQtNNOSyA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-9.2.0.tgz", + "integrity": "sha512-BzXm4Kz78pw9BMxW2whd9nKnYjLFxqFWEkH8Eh1/bZ2aOeCfIgPkYvU9Ai/fAPNQAW0y+oDSl1BoqGKENlO6sA==", "requires": { - "ajv": "^6.5.2", + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", "lodash.merge": "^4.6.1", - "openapi-types": "^7.2.3", - "swagger-schema-official": "2.0.0-bab6bed" + "openapi-types": "^9.2.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } } }, "openapi-types": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-7.2.3.tgz", - "integrity": "sha512-olbaNxz12R27+mTyJ/ZAFEfUruauHH27AkeQHDHRq5AF0LdNkK1SSV7EourXQDK+4aX7dv2HtyirAGK06WMAsA==" + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.2.0.tgz", + "integrity": "sha512-3x0gg8DxhpZ5MVki7AK6jmMdVIZASmVGo9CoUtD+nksLdkqz7EzWKdfS9Oxxq1J7idnZV0b3LjqcvizfKFySpQ==" }, "optional-js": { "version": "2.3.0", @@ -8062,8 +8122,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "resolve": { "version": "1.20.0", @@ -8734,7 +8793,8 @@ "swagger-schema-official": { "version": "2.0.0-bab6bed", "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", - "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=" + "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=", + "dev": true }, "symbol-tree": { "version": "3.2.4", diff --git a/src/package.json b/src/package.json index ad9369ace..fe484b8d5 100644 --- a/src/package.json +++ b/src/package.json @@ -55,7 +55,7 @@ "mime-types": "^2.1.32", "nodeify": "1.0.1", "npm": "6.14.13", - "openapi-backend": "^3.9.1", + "openapi-backend": "^4.2.0", "proxy-addr": "^2.0.6", "rate-limiter-flexible": "^2.2.4", "rehype": "^11.0.0", From c33a2682f9c88edaeb25821b490b05c142c708b4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 21:13:44 -0400 Subject: [PATCH 100/322] deps: Bump semver to 7.3.5 --- src/package-lock.json | 32 ++++++++++++++++++++++++++------ src/package.json | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index ce93e218b..07d7063dd 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3438,6 +3438,11 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -3752,7 +3757,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" }, @@ -3760,8 +3764,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -4185,6 +4188,14 @@ "optional": true, "requires": { "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + } } }, "node-abort-controller": { @@ -4268,6 +4279,12 @@ "osenv": "^0.1.4" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + }, "tar": { "version": "4.4.15", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.15.tgz", @@ -8222,9 +8239,12 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } }, "send": { "version": "0.17.1", diff --git a/src/package.json b/src/package.json index fe484b8d5..d042cf12d 100644 --- a/src/package.json +++ b/src/package.json @@ -63,7 +63,7 @@ "request": "2.88.2", "resolve": "1.20.0", "security": "1.0.0", - "semver": "5.7.1", + "semver": "^7.3.5", "socket.io": "^2.4.1", "terser": "^4.7.0", "threads": "^1.4.0", From 96e66aab171d53e062f1dc3a33eb9bb1730980a9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 21:16:30 -0400 Subject: [PATCH 101/322] deps: Bump sinon to 11.1.2 --- src/package-lock.json | 44 +++++++++++++++++++------------------------ src/package.json | 2 +- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 07d7063dd..3b01c57d1 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -550,18 +550,18 @@ } }, "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" } }, "@sinonjs/samsam": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", - "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -4158,13 +4158,13 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "nise": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", - "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/fake-timers": "^7.0.4", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" @@ -8400,25 +8400,19 @@ } }, "sinon": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", - "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", "dev": true, "requires": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/samsam": "^5.3.1", - "diff": "^4.0.2", - "nise": "^4.0.4", - "supports-color": "^7.1.0" + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.2", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" }, "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/src/package.json b/src/package.json index d042cf12d..f0a36380b 100644 --- a/src/package.json +++ b/src/package.json @@ -93,7 +93,7 @@ "openapi-schema-validation": "^0.4.2", "selenium-webdriver": "^4.0.0-beta.3", "set-cookie-parser": "^2.4.6", - "sinon": "^9.2.0", + "sinon": "^11.1.2", "split-grid": "^1.0.11", "superagent": "^3.8.3", "supertest": "4.0.2" From 327989ef0b926ed1d763ca23ac948552fa470cd5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 21:28:06 -0400 Subject: [PATCH 102/322] deps: Bump superagent to 6.1.0 --- src/package-lock.json | 108 +++++++++++++++++++++++++++++++++--------- src/package.json | 2 +- 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 3b01c57d1..4700a73a3 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8716,21 +8716,75 @@ "optional": true }, "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", "dev": true, "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "supertest": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", + "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" }, "dependencies": { "debug": { @@ -8783,19 +8837,27 @@ "requires": { "safe-buffer": "~5.1.0" } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } } } }, - "supertest": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", - "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/src/package.json b/src/package.json index f0a36380b..44fcaeff3 100644 --- a/src/package.json +++ b/src/package.json @@ -95,7 +95,7 @@ "set-cookie-parser": "^2.4.6", "sinon": "^11.1.2", "split-grid": "^1.0.11", - "superagent": "^3.8.3", + "superagent": "^6.1.0", "supertest": "4.0.2" }, "eslintConfig": { From f5657510be425c1fa131ce2feccbce58f027969b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 21:57:28 -0400 Subject: [PATCH 103/322] deps: Bump supertest to 6.1.6 --- src/package-lock.json | 79 +++---------------------------------------- src/package.json | 2 +- 2 files changed, 5 insertions(+), 76 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 4700a73a3..61e90b9c7 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8778,84 +8778,13 @@ } }, "supertest": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", - "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.1.6.tgz", + "integrity": "sha512-0hACYGNJ8OHRg8CRITeZOdbjur7NLuNs0mBjVhdpxi7hP6t3QIbOzLON5RTUmZcy2I9riuII3+Pr2C7yztrIIg==", "dev": true, "requires": { "methods": "^1.1.2", - "superagent": "^3.8.3" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - } - } + "superagent": "^6.1.0" } }, "supports-color": { diff --git a/src/package.json b/src/package.json index 44fcaeff3..ca6954970 100644 --- a/src/package.json +++ b/src/package.json @@ -96,7 +96,7 @@ "sinon": "^11.1.2", "split-grid": "^1.0.11", "superagent": "^6.1.0", - "supertest": "4.0.2" + "supertest": "^6.1.6" }, "eslintConfig": { "ignorePatterns": [ From 1e20936b5bf3cc749c31b14e4dab859aac7957d9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 29 Aug 2021 22:05:16 -0400 Subject: [PATCH 104/322] deps: Bump terser to 5.7.2 --- src/package-lock.json | 23 +++++++++++++++-------- src/package.json | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 61e90b9c7..bde7d5fe1 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1236,9 +1236,9 @@ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "buffer-writer": { "version": "2.0.0", @@ -8982,13 +8982,20 @@ } }, "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.2.tgz", + "integrity": "sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw==", "requires": { "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } } }, "text-table": { diff --git a/src/package.json b/src/package.json index ca6954970..de7410616 100644 --- a/src/package.json +++ b/src/package.json @@ -65,7 +65,7 @@ "security": "1.0.0", "semver": "^7.3.5", "socket.io": "^2.4.1", - "terser": "^4.7.0", + "terser": "^5.7.2", "threads": "^1.4.0", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", From 942b686f2dd46e9dd50d416afa7e463130430a53 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 30 Aug 2021 01:22:44 -0400 Subject: [PATCH 105/322] deps: Bump npm to 6.14.15 --- src/package-lock.json | 34 +++++++++++++++++++++------------- src/package.json | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index bde7d5fe1..a9cee97fd 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -4327,9 +4327,9 @@ "dev": true }, "npm": { - "version": "6.14.13", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.13.tgz", - "integrity": "sha512-SRl4jJi0EBHY2xKuu98FLRMo3VhYQSA6otyLnjSEiHoSG/9shXCFNJy9tivpUJvtkN9s6VDdItHa5Rn+fNBzag==", + "version": "6.14.15", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.15.tgz", + "integrity": "sha512-dkcQc4n+DiJAMYG2haNAMyJbmuvevjXz+WC9dCUzodw8EovwTIc6CATSsTEplCY6c0jG4OshxFGFJsrnKJguWA==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -4440,7 +4440,7 @@ "sorted-union-stream": "~2.1.3", "ssri": "^6.0.2", "stringify-package": "^1.0.1", - "tar": "^4.4.13", + "tar": "^4.4.19", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "uid-number": "0.0.6", @@ -6448,7 +6448,7 @@ "bundled": true }, "path-parse": { - "version": "1.0.6", + "version": "1.0.7", "bundled": true }, "performance-now": { @@ -6999,16 +6999,16 @@ } }, "tar": { - "version": "4.4.13", + "version": "4.4.19", "bundled": true, "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" }, "dependencies": { "minipass": { @@ -7018,6 +7018,14 @@ "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } + }, + "safe-buffer": { + "version": "5.2.1", + "bundled": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true } } }, diff --git a/src/package.json b/src/package.json index de7410616..88fdee849 100644 --- a/src/package.json +++ b/src/package.json @@ -54,7 +54,7 @@ "measured-core": "^2.0.0", "mime-types": "^2.1.32", "nodeify": "1.0.1", - "npm": "6.14.13", + "npm": "^6.14.15", "openapi-backend": "^4.2.0", "proxy-addr": "^2.0.6", "rate-limiter-flexible": "^2.2.4", From 67dfb640950899c424f7668a224a9dabd9ff9529 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 30 Aug 2021 01:49:02 -0400 Subject: [PATCH 106/322] deps: Bump ueberdb2 to 1.4.15 --- src/package-lock.json | 400 +++++++++++++++++++----------------------- src/package.json | 2 +- 2 files changed, 179 insertions(+), 223 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index a9cee97fd..592bce8c1 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -38,27 +38,38 @@ } }, "@azure/core-client": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.2.2.tgz", - "integrity": "sha512-VYFR2qiczjBrSfpQSbo5s8FJhXaJFz2tP01MOrpNJaOqnSNEKcY35I79b1Ty7s8qHGvc5/YMJ745l3B7abncFQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.3.0.tgz", + "integrity": "sha512-4ricu3aM1TQP2vglBcvFX8KgbWVe+7hl1jVAw6BzIGG4CTAvO3ygDS6th3O+zFwGN9xkgXFHa7Tp3u9za8ciIA==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.1.0", - "@azure/core-tracing": "1.0.0-preview.12", + "@azure/core-tracing": "1.0.0-preview.13", "tslib": "^2.2.0" + }, + "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "requires": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + } + } } }, "@azure/core-http": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-1.2.6.tgz", - "integrity": "sha512-odtH7UMKtekc5YQ86xg9GlVHNXR6pq2JgJ5FBo7/jbOjNGdBqcrIVrZx2bevXVJz/uUTSx6vUf62gzTXTfqYSQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.1.0.tgz", + "integrity": "sha512-Pzj87F4b1RH4PFDUpxkZqCdDZ35c5AjDCt3lsTn3i7yCtrXasEm6PVJYhjwsvYYmtgM7aDZIXexcu/qLLf7kyA==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", "@types/tunnel": "^0.0.1", @@ -73,20 +84,14 @@ }, "dependencies": { "@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" } }, - "@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" - }, "form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -115,31 +120,24 @@ } }, "@azure/core-lro": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-1.0.5.tgz", - "integrity": "sha512-0EFCFZxARrIoLWMIRt4vuqconRVIO2Iin7nFBfJiYCCbKp5eEmxutNk8uqudPmG0XFl5YqlVh68/al/vbE5OOg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.0.tgz", + "integrity": "sha512-TJo95eNT1dwYOPCb0m1C2zyxVlHuRRkKGeg9TKu8XMF2qh4v6c1weD63r9RVIrLdHdnSqS0n6PTXBpWoB8NqMw==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-http": "^1.2.0", - "@azure/core-tracing": "1.0.0-preview.11", - "events": "^3.0.0", - "tslib": "^2.0.0" + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" }, "dependencies": { "@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" } - }, - "@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" } } }, @@ -152,13 +150,13 @@ } }, "@azure/core-rest-pipeline": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.1.1.tgz", - "integrity": "sha512-ObF8iTEDXIG7/NlL28ni9bR3XLJwgm2S3GWO4aNW6CsTCFVoY9HMdbBtN7xOB+pUQwifehifXNnootbzzuwJnw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.2.0.tgz", + "integrity": "sha512-oOd8feRcuoSUwflPNLPO8x6v+m4TcJ9DmazlouuG9d64zJJEwaU757ovpRss9zaL8cggUAdm84C4EbtZ/ltMAw==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.12", + "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "form-data": "^3.0.0", "http-proxy-agent": "^4.0.1", @@ -167,6 +165,15 @@ "uuid": "^8.3.0" }, "dependencies": { + "@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "requires": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + } + }, "form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -194,9 +201,9 @@ } }, "@azure/identity": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.0.tgz", - "integrity": "sha512-djgywuWtX6720seqNOPmGM1hY54oHnjRT0MLIOzacMARTZuEtAIaFFvMPBlUIMQdtSGhdjH+/MS1/9PE8j83eA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.1.tgz", + "integrity": "sha512-ENYdcHT72PwEb+aiL2G6WIXxdm8mO0LNLZVPXaSRZYNsIshre72MF1H/rnJvcVGX9uVDVClSbNPxXwY5MJPLjw==", "requires": { "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.0.0", @@ -225,33 +232,27 @@ } }, "@azure/keyvault-keys": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.2.2.tgz", - "integrity": "sha512-SWRx0Z8ShLYnuqCitIOi3DqLSLBTI6G1F+Wv5/hy1w6ZXRnOwc74deQ8kfO0Tbm3n09SpaN4ytp6dmg6C2cRmQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.3.0.tgz", + "integrity": "sha512-OEosl0/rE/mKD5Ji9KaQN7UH+yQnV5MS0MRhGqQIiJrG+qAvAla0MYudJzv3XvBlplpGk0+MVgyL9H3KX/UAwQ==", "requires": { "@azure/abort-controller": "^1.0.0", - "@azure/core-http": "^1.2.0", - "@azure/core-lro": "^1.0.2", + "@azure/core-http": "^2.0.0", + "@azure/core-lro": "^2.0.0", "@azure/core-paging": "^1.1.1", - "@azure/core-tracing": "1.0.0-preview.11", + "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" }, "dependencies": { "@azure/core-tracing": { - "version": "1.0.0-preview.11", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.11.tgz", - "integrity": "sha512-frF0pJc9HTmKncVokhBxCqipjbql02DThQ1ZJ9wLi7SDMLdPAFyDI5xZNzX5guLz+/DtPkY+SGK2li9FIXqshQ==", + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", "requires": { - "@opencensus/web-types": "0.0.7", - "@opentelemetry/api": "1.0.0-rc.0", - "tslib": "^2.0.0" + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" } - }, - "@opentelemetry/api": { - "version": "1.0.0-rc.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.0-rc.0.tgz", - "integrity": "sha512-iXKByCMfrlO5S6Oh97BuM56tM2cIBB0XsL/vWF/AtJrJEKx4MC/Xdu0xDsGXMGcNWpqF7ujMsjjnp0+UHBwnDQ==" } } }, @@ -269,9 +270,9 @@ "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "@azure/ms-rest-js": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.5.3.tgz", - "integrity": "sha512-OZ7qJwazS2nSRtZOA6+0k7x+RJ9D2P0IyUl9iHycyjgtQlINALNRutGqQeBirhIEx2IRQs9TMnnxoh/yRkFEAw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.6.0.tgz", + "integrity": "sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g==", "requires": { "@azure/core-auth": "^1.1.4", "abort-controller": "^3.0.0", @@ -327,9 +328,9 @@ } }, "@azure/msal-common": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.5.0.tgz", - "integrity": "sha512-eXI3GamyjU29Mttn+5mWU47Su4DtgAZF6xmIrnIq271dz6VKJDBmU1dEbUUYqZeVSzxdcraX7BR85kKof11Tig==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.5.1.tgz", + "integrity": "sha512-/i5dXM+QAtO+6atYd5oHGBAx48EGSISkXNXViheliOQe+SIFMDo3gSq3lL54W0suOSAsVPws3XnTaIHlla0PIQ==", "requires": { "debug": "^4.1.1" }, @@ -530,15 +531,10 @@ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, - "@opencensus/web-types": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@opencensus/web-types/-/web-types-0.0.7.tgz", - "integrity": "sha512-xB+w7ZDAu3YBzqH44rCmG9/RlrOmFuDPt/bpf17eJr8eZSrLt7nc7LnWdxM9Mmoj/YKMHpxRg28txu3TcpiL+g==" - }, "@opentelemetry/api": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.1.tgz", - "integrity": "sha512-H5Djcc2txGAINgf3TNaq4yFofYSIK3722PM89S/3R8FuI/eqi1UscajlXk7EBkG9s2pxss/q6SHlpturaavXaw==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.2.tgz", + "integrity": "sha512-DCF9oC89ao8/EJUqrp/beBlDR8Bp2R43jqtzayqCoomIvkwTuPfLcHdVhIGRR69GFlkykFjcDW+V92t0AS7Tww==" }, "@sinonjs/commons": { "version": "1.8.3", @@ -585,11 +581,6 @@ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, "@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -609,9 +600,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "16.4.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.6.tgz", - "integrity": "sha512-FKyawK3o5KL16AwbeFajen8G4K3mmqUrQsehn5wNKs8IzlKHE8TfnSmILXVMVziAEcnB23u1RCFU1NT6hSyr7Q==" + "version": "16.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.6.tgz", + "integrity": "sha512-VESVNFoa/ahYA62xnLBjo5ur6gPsgEE5cNRy8SrdnkZ2nwJSW0kJ4ufbFr2zuU9ALtHM8juY53VcRoTA7htXSg==" }, "@types/node-fetch": { "version": "2.5.12", @@ -639,29 +630,6 @@ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, - "@types/request": { - "version": "2.48.7", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.7.tgz", - "integrity": "sha512-GWP9AZW7foLd4YQxyFZDBepl0lPsWLMEXDZUjQ/c1gqVPDPECrRZyEzuhJdnPWioFCq3Tv0qoGpMD6U+ygd4ZA==", - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } - } - }, "@types/stoppable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz", @@ -1015,6 +983,15 @@ "follow-redirects": "^1.10.0" } }, + "axios-cookiejar-support": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz", + "integrity": "sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig==", + "requires": { + "is-redirect": "^1.0.0", + "pify": "^5.0.0" + } + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -1204,11 +1181,6 @@ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, - "browser-request": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", - "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" - }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -1396,31 +1368,6 @@ } } }, - "cloudant-follow": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/cloudant-follow/-/cloudant-follow-0.18.2.tgz", - "integrity": "sha512-qu/AmKxDqJds+UmT77+0NbM7Yab2K3w0qSeJRzsq5dRWJTEJdWeb+XpG4OpKuTE9RKOa/Awn2gR3TTnvNr3TeA==", - "requires": { - "browser-request": "~0.3.0", - "debug": "^4.0.1", - "request": "^2.88.0" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -1637,9 +1584,9 @@ "optional": true }, "denque": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", - "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", + "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" }, "depd": { "version": "1.1.2", @@ -1664,9 +1611,9 @@ "dev": true }, "dirty": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.2.tgz", - "integrity": "sha512-bf2Iw8z2spf6eitAPVLERYrNNB85itJXOa/gctg1mSjDBt0qt8E8212+XrXcYGlERXmqq6EfvQJKsrplVY8/5A==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.3.tgz", + "integrity": "sha512-PlnV9+KeJ6bh8o5qQZqRnD80Wegijyr47dpwxCIuJ6SzwJ6/deO+NRTEnq/mubIYtBvBBgWznlE6dZ+nQsS/og==" }, "doctrine": { "version": "3.0.0", @@ -1858,11 +1805,6 @@ "ansi-colors": "^4.1.1" } }, - "errs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz", - "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk=" - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2648,9 +2590,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.2.tgz", + "integrity": "sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==" }, "forever-agent": { "version": "0.6.1", @@ -2810,9 +2752,9 @@ } }, "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "optional": true }, "growl": { @@ -3218,6 +3160,11 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -3290,9 +3237,9 @@ } }, "jsbi": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.5.tgz", - "integrity": "sha512-w2BY0VOYC1ahe+w6Qhl4SFoPvPsZ9NPHY4bwass+LCgU7RK3PBoVQlQ3G1s7vI8W3CYyJiEXcbKF7FIM/L8q3Q==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.1.tgz", + "integrity": "sha512-NzcT09wuJReIO829enrY3yRdHFz+ciVIq01PCGPkHlwIv5Dj9v2F4daQ4akwuPHf6xO/oii4Jrd3BsPepOxXrg==" }, "jsbn": { "version": "0.1.1", @@ -3970,9 +3917,9 @@ } }, "mongodb": { - "version": "3.6.10", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.10.tgz", - "integrity": "sha512-fvIBQBF7KwCJnDZUnFFy4WqEFP8ibdXeFANnylW19+vOwdjOAvqIzPdsNCEMT6VKTHnYu4K64AWRih0mkFms6Q==", + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.11.tgz", + "integrity": "sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", @@ -4003,15 +3950,15 @@ } }, "mssql": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.2.0.tgz", - "integrity": "sha512-nlFq6UmYmJTT6pSo0x2NCigXuOi42WM6d6A8By3ZxMRznY9wVw7KQ9IfRhC72KaRj4G9oACHeCHVCucoSwcFFg==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.2.1.tgz", + "integrity": "sha512-kq0hVeD1tR+ikZqmLwgQqLGSavOhrrwaiYsYxdUQASifc3oIOFRx2IHpuWk+8oLI6Ab/s3o3JfpFX1v1Nf2sxA==", "requires": { "@tediousjs/connection-string": "^0.3.0", - "debug": "^4", + "debug": "^4.3.2", "rfdc": "^1.3.0", "tarn": "^3.0.1", - "tedious": "^11.0.7" + "tedious": "^11.4.0" }, "dependencies": { "debug": { @@ -4075,29 +4022,34 @@ } }, "nano": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/nano/-/nano-8.2.3.tgz", - "integrity": "sha512-nubyTQeZ/p+xf3ZFFMd7WrZwpcy9tUDrbaXw9HFBsM6zBY5gXspvOjvG2Zz3emT6nfJtP/h7F2/ESfsVVXnuMw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/nano/-/nano-9.0.3.tgz", + "integrity": "sha512-NFI8+6q5ihnozH6qK+BJ+ilnPfZzBhlUswaFgqUvSp2EN5eJ2BMxbzkYiBsN+waa+N95FculCdbneDmzLWfXaQ==", "requires": { - "@types/request": "^2.48.4", - "cloudant-follow": "^0.18.2", - "debug": "^4.1.1", - "errs": "^0.3.2", - "request": "^2.88.0" + "@types/tough-cookie": "^4.0.0", + "axios": "^0.21.1", + "axios-cookiejar-support": "^1.0.1", + "qs": "^6.9.4", + "tough-cookie": "^4.0.0" }, "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", "requires": { - "ms": "2.1.2" + "side-channel": "^1.0.4" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + } } } }, @@ -4125,9 +4077,9 @@ "dev": true }, "needle": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.8.0.tgz", - "integrity": "sha512-ZTq6WYkN/3782H1393me3utVYdq2XyqNUFBsprEE3VMAT0+hP/cItpnITpqsY6ep2yeFE4Tqtqwc74VqUlUYtw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.0.tgz", + "integrity": "sha512-UBLC4P8w9to3rAhWOQYXIXzTUio9yVnDzIeKxfGbF+Hngy+2bXTqqFK+6nF42EAQKfJdezXK6vzMsefUa1Y3ag==", "optional": true, "requires": { "debug": "^3.2.6", @@ -4286,18 +4238,18 @@ "optional": true }, "tar": { - "version": "4.4.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.15.tgz", - "integrity": "sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA==", + "version": "4.4.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", + "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", "optional": true, "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" + "chownr": "^1.1.4", + "fs-minipass": "^1.2.7", + "minipass": "^2.9.0", + "minizlib": "^1.3.3", + "mkdirp": "^0.5.5", + "safe-buffer": "^5.2.1", + "yallist": "^3.1.1" } } } @@ -7620,9 +7572,9 @@ "integrity": "sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw==" }, "optional-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.1.tgz", - "integrity": "sha512-EnUe33GTAltyZlIsQ2l93KzBC9zi8BsxLvKP3wxALOsz/YIakVojyuZsv5PFFk8y8e6r+SbaPIsNmyPoSK0OHw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.7.tgz", + "integrity": "sha512-cIeRZocXsZnZYn+SevbtSqNlLbeoS4mLzuNn4fvXRMDRNhTGg0sxuKXl0FnZCtnew85LorNxIbZp5OeliILhMw==", "requires": { "require-at": "^1.0.6" } @@ -7812,6 +7764,11 @@ "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, + "pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==" + }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -7836,9 +7793,9 @@ } }, "prebuild-install": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.3.tgz", - "integrity": "sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", + "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", "optional": true, "requires": { "detect-libc": "^1.0.3", @@ -8383,9 +8340,9 @@ } }, "simple-git": { - "version": "2.41.1", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.41.1.tgz", - "integrity": "sha512-n1STz1tfnemvYndzWakgKa0JB4s/LrUG4btXMetWB9N9ZoIAJQd0ZtWj9sBwWxIZ/X/tYdA/tq+KHfFNAGzZhQ==", + "version": "2.45.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.45.0.tgz", + "integrity": "sha512-wu/Ujs9IXn0HuyYm4HyRvne+EKsjJSWKEMkB3wQa3gNHSMHt7y3oeNX9zRQ3UBPk7bRRMLLHAdIZCZfHT9ehPg==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", @@ -8935,9 +8892,9 @@ "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" }, "tedious": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.4.0.tgz", - "integrity": "sha512-A7DYRH0EJL5b4vj8X8KVRyWVk7OZCXkHltMlkmMgOJEfPPMv41X+iNIeOCZ7zJ4/fvaJHkvfsKYuOzL+Eda+TQ==", + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-11.8.0.tgz", + "integrity": "sha512-GtFrO694x/7CRiUBt0AI4jrMtrkXV+ywifiOrDy4K0ufJLeKB4rgmPjy5Ws366fCaBaKlqQ9RnJ+sCJ1Jbd1lw==", "requires": { "@azure/identity": "^1.3.0", "@azure/keyvault-keys": "^4.1.0", @@ -8951,7 +8908,6 @@ "native-duplexpair": "^1.0.0", "node-abort-controller": "^2.0.0", "punycode": "^2.1.0", - "readable-stream": "^3.6.0", "sprintf-js": "^1.1.2" }, "dependencies": { @@ -9114,9 +9070,9 @@ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==" }, "tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "tunnel": { "version": "0.0.6", @@ -9167,23 +9123,23 @@ } }, "ueberdb2": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.13.tgz", - "integrity": "sha512-z6hn0c8Fcpr2N7fiD5WyS1yLVRswoD9Gj6GqNUB42o6UYwFwgvTEEdRoqVC2JHpv1VgYSR+B5O48nVFCi5Rdxw==", + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.15.tgz", + "integrity": "sha512-Gk+aj/sVFrGe+f+dd4m8Z58FwUbHONsKPmrFyFdhB4GnUQaHRya31bCU3Ax1Kk2g14HSWlOy3Eub/xL9X5zbpA==", "requires": { - "async": "^3.2.0", - "cassandra-driver": "^4.5.1", - "dirty": "^1.1.1", + "async": "^3.2.1", + "cassandra-driver": "^4.6.3", + "dirty": "^1.1.3", "elasticsearch": "^16.7.1", - "mongodb": "^3.6.3", - "mssql": "^7.0.0-beta.2", + "mongodb": "^3.6.11", + "mssql": "^7.2.1", "mysql": "2.18.1", - "nano": "^8.2.2", - "pg": "^8.0.3", + "nano": "^9.0.3", + "pg": "^8.7.1", "redis": "^3.1.2", "rethinkdb": "^2.4.2", - "simple-git": "^2.4.0", - "sqlite3": "^5.0.1" + "simple-git": "^2.45.0", + "sqlite3": "^5.0.2" } }, "uid-safe": { diff --git a/src/package.json b/src/package.json index 88fdee849..189c66e71 100644 --- a/src/package.json +++ b/src/package.json @@ -69,7 +69,7 @@ "threads": "^1.4.0", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", - "ueberdb2": "^1.4.13", + "ueberdb2": "^1.4.15", "underscore": "1.13.1", "unorm": "1.6.0", "wtfnode": "^0.9.1" From 348bc0c26969293b8dbda073ad1d1d6ddc98914d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 30 Aug 2021 02:02:35 -0400 Subject: [PATCH 107/322] tests: Delete overly aggressive timeouts See https://github.com/ether/etherpad-lite/issues/4988 for rationale. --- src/tests/backend/specs/api/chat.js | 5 -- .../backend/specs/api/importexportGetPost.js | 6 --- src/tests/backend/specs/api/pad.js | 54 ------------------- src/tests/backend/specs/socketio.js | 26 --------- 4 files changed, 91 deletions(-) diff --git a/src/tests/backend/specs/api/chat.js b/src/tests/backend/specs/api/chat.js index 17f5bf4ef..fcc69a363 100644 --- a/src/tests/backend/specs/api/chat.js +++ b/src/tests/backend/specs/api/chat.js @@ -39,7 +39,6 @@ describe(__filename, function () { */ describe('createPad', function () { - this.timeout(400); it('creates a new Pad', function (done) { agent.get(`${endPoint('createPad')}&padID=${padID}`) .expect((res) => { @@ -51,7 +50,6 @@ describe(__filename, function () { }); describe('createAuthor', function () { - this.timeout(100); it('Creates an author with a name set', function (done) { agent.get(endPoint('createAuthor')) .expect((res) => { @@ -66,7 +64,6 @@ describe(__filename, function () { }); describe('appendChatMessage', function () { - this.timeout(100); it('Adds a chat message to the pad', function (done) { agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` + `&authorID=${authorID}&time=${timestamp}`) @@ -80,7 +77,6 @@ describe(__filename, function () { describe('getChatHead', function () { - this.timeout(100); it('Gets the head of chat', function (done) { agent.get(`${endPoint('getChatHead')}&padID=${padID}`) .expect((res) => { @@ -94,7 +90,6 @@ describe(__filename, function () { }); describe('getChatHistory', function () { - this.timeout(40); it('Gets Chat History of a Pad', function (done) { agent.get(`${endPoint('getChatHistory')}&padID=${padID}`) .expect((res) => { diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.js index a68ba4011..54d085eda 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.js @@ -31,7 +31,6 @@ describe(__filename, function () { describe('Connectivity', function () { it('can connect', async function () { - this.timeout(250); await agent.get('/api/') .expect(200) .expect('Content-Type', /json/); @@ -40,7 +39,6 @@ describe(__filename, function () { describe('API Versioning', function () { it('finds the version tag', async function () { - this.timeout(250); await agent.get('/api/') .expect(200) .expect((res) => assert(res.body.currentVersion)); @@ -96,7 +94,6 @@ describe(__filename, function () { }); it('creates a new Pad, imports content to it, checks that content', async function () { - this.timeout(500); await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/) @@ -111,7 +108,6 @@ describe(__filename, function () { for (const authn of [false, true]) { it(`can export from read-only pad ID, authn ${authn}`, async function () { - this.timeout(250); settings.requireAuthentication = authn; const get = (ep) => { let req = agent.get(ep); @@ -129,8 +125,6 @@ describe(__filename, function () { } describe('Import/Export tests requiring AbiWord/LibreOffice', function () { - this.timeout(10000); - before(async function () { if ((!settings.abiword || settings.abiword.indexOf('/') === -1) && (!settings.soffice || settings.soffice.indexOf('/') === -1)) { diff --git a/src/tests/backend/specs/api/pad.js b/src/tests/backend/specs/api/pad.js index bb0ecdd9a..e50f9a6ab 100644 --- a/src/tests/backend/specs/api/pad.js +++ b/src/tests/backend/specs/api/pad.js @@ -51,7 +51,6 @@ describe(__filename, function () { describe('Connectivity', function () { it('can connect', function (done) { - this.timeout(200); agent.get('/api/') .expect('Content-Type', /json/) .expect(200, done); @@ -60,7 +59,6 @@ describe(__filename, function () { describe('API Versioning', function () { it('finds the version tag', function (done) { - this.timeout(150); agent.get('/api/') .expect((res) => { apiVersion = res.body.currentVersion; @@ -73,7 +71,6 @@ describe(__filename, function () { describe('Permission', function () { it('errors with invalid APIKey', function (done) { - this.timeout(150); // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway const permErrorURL = `/api/${apiVersion}/createPad?apikey=password&padID=test`; @@ -126,7 +123,6 @@ describe(__filename, function () { describe('deletePad', function () { it('deletes a Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect('Content-Type', /json/) .expect(200, done); // @TODO: we shouldn't expect 200 here since the pad may not exist @@ -135,7 +131,6 @@ describe(__filename, function () { describe('createPad', function () { it('creates a new Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to create new Pad'); @@ -147,7 +142,6 @@ describe(__filename, function () { describe('getRevisionsCount', function () { it('gets revision count of Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to get Revision Count'); @@ -160,7 +154,6 @@ describe(__filename, function () { describe('getSavedRevisionsCount', function () { it('gets saved revisions count of Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); @@ -175,7 +168,6 @@ describe(__filename, function () { describe('listSavedRevisions', function () { it('gets saved revision list of Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); @@ -188,7 +180,6 @@ describe(__filename, function () { describe('getHTML', function () { it('get the HTML of Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.html.length <= 1) throw new Error('Unable to get the HTML'); @@ -200,7 +191,6 @@ describe(__filename, function () { describe('listAllPads', function () { it('list all pads', function (done) { - this.timeout(150); agent.get(endPoint('listAllPads')) .expect((res) => { if (res.body.data.padIDs.includes(testPadId) !== true) { @@ -214,7 +204,6 @@ describe(__filename, function () { describe('deletePad', function () { it('deletes a Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Deletion failed'); @@ -226,7 +215,6 @@ describe(__filename, function () { describe('listAllPads', function () { it('list all pads', function (done) { - this.timeout(150); agent.get(endPoint('listAllPads')) .expect((res) => { if (res.body.data.padIDs.includes(testPadId) !== false) { @@ -240,7 +228,6 @@ describe(__filename, function () { describe('getHTML', function () { it('get the HTML of a Pad -- Should return a failure', function (done) { - this.timeout(150); agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 1) throw new Error('Pad deletion failed'); @@ -252,7 +239,6 @@ describe(__filename, function () { describe('createPad', function () { it('creates a new Pad with text', function (done) { - this.timeout(200); agent.get(`${endPoint('createPad')}&padID=${testPadId}&text=testText`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Creation failed'); @@ -264,7 +250,6 @@ describe(__filename, function () { describe('getText', function () { it('gets the Pad text and expect it to be testText with \n which is a line break', function (done) { - this.timeout(150); agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.text !== 'testText\n') throw new Error('Pad Creation with text'); @@ -276,7 +261,6 @@ describe(__filename, function () { describe('setText', function () { it('creates a new Pad with text', function (done) { - this.timeout(200); agent.post(endPoint('setText')) .send({ padID: testPadId, @@ -292,7 +276,6 @@ describe(__filename, function () { describe('getText', function () { it('gets the Pad text', function (done) { - this.timeout(150); agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.text !== 'testTextTwo\n') throw new Error('Setting Text'); @@ -304,7 +287,6 @@ describe(__filename, function () { describe('getRevisionsCount', function () { it('gets Revision Count of a Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.revisions !== 1) throw new Error('Unable to get text revision count'); @@ -316,7 +298,6 @@ describe(__filename, function () { describe('saveRevision', function () { it('saves Revision', function (done) { - this.timeout(150); agent.get(`${endPoint('saveRevision')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to save Revision'); @@ -328,7 +309,6 @@ describe(__filename, function () { describe('getSavedRevisionsCount', function () { it('gets saved revisions count of Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); @@ -343,7 +323,6 @@ describe(__filename, function () { describe('listSavedRevisions', function () { it('gets saved revision list of Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); @@ -355,7 +334,6 @@ describe(__filename, function () { }); describe('padUsersCount', function () { it('gets User Count of a Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('padUsersCount')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.padUsersCount !== 0) throw new Error('Incorrect Pad User count'); @@ -367,7 +345,6 @@ describe(__filename, function () { describe('getReadOnlyID', function () { it('Gets the Read Only ID of a Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) .expect((res) => { if (!res.body.data.readOnlyID) throw new Error('No Read Only ID for Pad'); @@ -379,7 +356,6 @@ describe(__filename, function () { describe('listAuthorsOfPad', function () { it('Get Authors of the Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('listAuthorsOfPad')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.authorIDs.length !== 0) { @@ -393,7 +369,6 @@ describe(__filename, function () { describe('getLastEdited', function () { it('Get When Pad was left Edited', function (done) { - this.timeout(150); agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect((res) => { if (!res.body.data.lastEdited) { @@ -409,7 +384,6 @@ describe(__filename, function () { describe('setText', function () { it('creates a new Pad with text', function (done) { - this.timeout(200); agent.post(endPoint('setText')) .send({ padID: testPadId, @@ -425,7 +399,6 @@ describe(__filename, function () { describe('getLastEdited', function () { it('Get When Pad was left Edited', function (done) { - this.timeout(150); agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.lastEdited <= lastEdited) { @@ -439,7 +412,6 @@ describe(__filename, function () { describe('padUsers', function () { it('gets User Count of a Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('padUsers')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.padUsers.length !== 0) throw new Error('Incorrect Pad Users'); @@ -451,7 +423,6 @@ describe(__filename, function () { describe('deletePad', function () { it('deletes a Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Deletion failed'); @@ -466,7 +437,6 @@ describe(__filename, function () { describe('createPad', function () { it('creates a new Pad with text', function (done) { - this.timeout(200); agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Creation failed'); @@ -478,7 +448,6 @@ describe(__filename, function () { describe('setText', function () { it('Sets text on a pad Id', function (done) { - this.timeout(150); agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text}) .expect((res) => { @@ -491,7 +460,6 @@ describe(__filename, function () { describe('getText', function () { it('Gets text on a pad Id', function (done) { - this.timeout(200); agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Get Text failed'); @@ -504,7 +472,6 @@ describe(__filename, function () { describe('setText', function () { it('Sets text on a pad Id including an explicit newline', function (done) { - this.timeout(200); agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text: `${text}\n`}) .expect((res) => { @@ -517,7 +484,6 @@ describe(__filename, function () { describe('getText', function () { it("Gets text on a pad Id and doesn't have an excess newline", function (done) { - this.timeout(150); agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Get Text failed'); @@ -530,7 +496,6 @@ describe(__filename, function () { describe('getLastEdited', function () { it('Gets when pad was last edited', function (done) { - this.timeout(150); agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect((res) => { if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); @@ -542,7 +507,6 @@ describe(__filename, function () { describe('movePad', function () { it('Move a Pad to a different Pad ID', function (done) { - this.timeout(200); agent.get(`${endPoint('movePad')}&sourceID=${testPadId}&destinationID=${newPadId}&force=true`) .expect((res) => { if (res.body.code !== 0) throw new Error('Moving Pad Failed'); @@ -554,7 +518,6 @@ describe(__filename, function () { describe('getText', function () { it('Gets text on a pad Id', function (done) { - this.timeout(150); agent.get(`${endPoint('getText')}&padID=${newPadId}`) .expect((res) => { if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); @@ -566,7 +529,6 @@ describe(__filename, function () { describe('movePad', function () { it('Move a Pad to a different Pad ID', function (done) { - this.timeout(200); agent.get(`${endPoint('movePad')}&sourceID=${newPadId}&destinationID=${testPadId}` + '&force=false') .expect((res) => { @@ -579,7 +541,6 @@ describe(__filename, function () { describe('getText', function () { it('Gets text on a pad Id', function (done) { - this.timeout(150); agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); @@ -591,7 +552,6 @@ describe(__filename, function () { describe('getLastEdited', function () { it('Gets when pad was last edited', function (done) { - this.timeout(150); agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect((res) => { if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); @@ -603,7 +563,6 @@ describe(__filename, function () { describe('appendText', function () { it('Append text to a pad Id', function (done) { - this.timeout(150); agent.get(`${endPoint('appendText', '1.2.13')}&padID=${testPadId}&text=hello`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Append Text failed'); @@ -615,7 +574,6 @@ describe(__filename, function () { describe('getText', function () { it('Gets text on a pad Id', function (done) { - this.timeout(150); agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Pad Get Text failed'); @@ -631,7 +589,6 @@ describe(__filename, function () { describe('setHTML', function () { it('Sets the HTML of a Pad attempting to pass ugly HTML', function (done) { - this.timeout(200); const html = '
Hello HTML
'; agent.post(endPoint('setHTML')) .send({ @@ -650,7 +607,6 @@ describe(__filename, function () { describe('setHTML', function () { it('Sets the HTML of a Pad with complex nested lists of different types', function (done) { - this.timeout(200); agent.post(endPoint('setHTML')) .send({ padID: testPadId, @@ -666,7 +622,6 @@ describe(__filename, function () { describe('getHTML', function () { it('Gets back the HTML of a Pad with complex nested lists of different types', function (done) { - this.timeout(150); agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect((res) => { const receivedHtml = res.body.data.html.replace('
', '').toLowerCase(); @@ -690,7 +645,6 @@ describe(__filename, function () { describe('setHTML', function () { it('Sets the HTML of a Pad with white space between list items', function (done) { - this.timeout(200); agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) .expect((res) => { if (res.body.code !== 0) throw new Error('List HTML cant be imported'); @@ -702,7 +656,6 @@ describe(__filename, function () { describe('getHTML', function () { it('Gets back the HTML of a Pad with complex nested lists of different types', function (done) { - this.timeout(150); agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect((res) => { const receivedHtml = res.body.data.html.replace('
', '').toLowerCase(); @@ -725,7 +678,6 @@ describe(__filename, function () { describe('createPad', function () { it('errors if pad can be created', function (done) { - this.timeout(150); const badUrlChars = ['/', '%23', '%3F', '%26']; async.map( badUrlChars, @@ -743,7 +695,6 @@ describe(__filename, function () { describe('copyPad', function () { it('copies the content of a existent pad', function (done) { - this.timeout(200); agent.get(`${endPoint('copyPad')}&sourceID=${testPadId}&destinationID=${copiedPadId}` + '&force=true') .expect((res) => { @@ -767,7 +718,6 @@ describe(__filename, function () { }); it('returns a successful response', function (done) { - this.timeout(200); agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + `&destinationID=${newPad}&force=false`) .expect((res) => { @@ -779,7 +729,6 @@ describe(__filename, function () { // this test validates if the source pad's text and attributes are kept it('creates a new pad with the same content as the source pad', function (done) { - this.timeout(200); agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + `&destinationID=${newPad}&force=false`) .expect((res) => { @@ -811,7 +760,6 @@ describe(__filename, function () { const padId = makeid(); const padWithNonExistentGroup = `notExistentGroup$${padId}`; it('throws an error', function (done) { - this.timeout(150); agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}&` + `destinationID=${padWithNonExistentGroup}&force=true`) .expect((res) => { @@ -831,7 +779,6 @@ describe(__filename, function () { context('and force is false', function () { it('throws an error', function (done) { - this.timeout(150); agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + `&destinationID=${padIdExistent}&force=false`) .expect((res) => { @@ -844,7 +791,6 @@ describe(__filename, function () { context('and force is true', function () { it('returns a successful response', function (done) { - this.timeout(200); agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + `&destinationID=${padIdExistent}&force=true`) .expect((res) => { diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 9b9e2101b..8412e0e1d 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -142,27 +142,23 @@ describe(__filename, function () { describe('Normal accesses', function () { it('!authn anonymous cookie /p/pad -> 200, ok', async function () { - this.timeout(600); const res = await agent.get('/p/pad').expect(200); socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('!authn !cookie -> ok', async function () { - this.timeout(400); socket = await connect(null); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('!authn user /p/pad -> 200, ok', async function () { - this.timeout(400); const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); const clientVars = await handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('authn user /p/pad -> 200, ok', async function () { - this.timeout(400); settings.requireAuthentication = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -173,7 +169,6 @@ describe(__filename, function () { for (const authn of [false, true]) { const desc = authn ? 'authn user' : '!authn anonymous'; it(`${desc} read-only /p/pad -> 200, ok`, async function () { - this.timeout(400); const get = (ep) => { let res = agent.get(ep); if (authn) res = res.auth('user', 'user-password'); @@ -197,7 +192,6 @@ describe(__filename, function () { } it('authz user /p/pad -> 200, ok', async function () { - this.timeout(400); settings.requireAuthentication = true; settings.requireAuthorization = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -206,7 +200,6 @@ describe(__filename, function () { assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('supports pad names with characters that must be percent-encoded', async function () { - this.timeout(400); settings.requireAuthentication = true; // requireAuthorization is set to true here to guarantee that the user's padAuthorizations // object is populated. Technically this isn't necessary because the user's padAuthorizations @@ -223,7 +216,6 @@ describe(__filename, function () { describe('Abnormal access attempts', function () { it('authn anonymous /p/pad -> 401, error', async function () { - this.timeout(400); settings.requireAuthentication = true; const res = await agent.get('/p/pad').expect(401); // Despite the 401, try to create the pad via a socket.io connection anyway. @@ -233,7 +225,6 @@ describe(__filename, function () { }); it('authn anonymous read-only /p/pad -> 401, error', async function () { - this.timeout(400); settings.requireAuthentication = true; let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -250,14 +241,12 @@ describe(__filename, function () { }); it('authn !cookie -> error', async function () { - this.timeout(400); settings.requireAuthentication = true; socket = await connect(null); const message = await handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('authorization bypass attempt -> error', async function () { - this.timeout(400); // Only allowed to access /p/pad. authorize = (req) => req.path === '/p/pad'; settings.requireAuthentication = true; @@ -278,7 +267,6 @@ describe(__filename, function () { }); it("level='create' -> can create", async function () { - this.timeout(400); authorize = () => 'create'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -287,7 +275,6 @@ describe(__filename, function () { assert.equal(clientVars.data.readonly, false); }); it('level=true -> can create', async function () { - this.timeout(400); authorize = () => true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -296,7 +283,6 @@ describe(__filename, function () { assert.equal(clientVars.data.readonly, false); }); it("level='modify' -> can modify", async function () { - this.timeout(400); await padManager.getPad('pad'); // Create the pad. authorize = () => 'modify'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -306,7 +292,6 @@ describe(__filename, function () { assert.equal(clientVars.data.readonly, false); }); it("level='create' settings.editOnly=true -> unable to create", async function () { - this.timeout(400); authorize = () => 'create'; settings.editOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -315,7 +300,6 @@ describe(__filename, function () { assert.equal(message.accessStatus, 'deny'); }); it("level='modify' settings.editOnly=false -> unable to create", async function () { - this.timeout(400); authorize = () => 'modify'; settings.editOnly = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -324,7 +308,6 @@ describe(__filename, function () { assert.equal(message.accessStatus, 'deny'); }); it("level='readOnly' -> unable to create", async function () { - this.timeout(400); authorize = () => 'readOnly'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -332,7 +315,6 @@ describe(__filename, function () { assert.equal(message.accessStatus, 'deny'); }); it("level='readOnly' -> unable to modify", async function () { - this.timeout(400); await padManager.getPad('pad'); // Create the pad. authorize = () => 'readOnly'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -349,7 +331,6 @@ describe(__filename, function () { }); it('user.canCreate = true -> can create and modify', async function () { - this.timeout(400); settings.users.user.canCreate = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -358,7 +339,6 @@ describe(__filename, function () { assert.equal(clientVars.data.readonly, false); }); it('user.canCreate = false -> unable to create', async function () { - this.timeout(400); settings.users.user.canCreate = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -366,7 +346,6 @@ describe(__filename, function () { assert.equal(message.accessStatus, 'deny'); }); it('user.readOnly = true -> unable to create', async function () { - this.timeout(400); settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -374,7 +353,6 @@ describe(__filename, function () { assert.equal(message.accessStatus, 'deny'); }); it('user.readOnly = true -> unable to modify', async function () { - this.timeout(400); await padManager.getPad('pad'); // Create the pad. settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -384,7 +362,6 @@ describe(__filename, function () { assert.equal(clientVars.data.readonly, true); }); it('user.readOnly = false -> can create and modify', async function () { - this.timeout(400); settings.users.user.readOnly = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); socket = await connect(res); @@ -393,7 +370,6 @@ describe(__filename, function () { assert.equal(clientVars.data.readonly, false); }); it('user.readOnly = true, user.canCreate = true -> unable to create', async function () { - this.timeout(400); settings.users.user.canCreate = true; settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -410,7 +386,6 @@ describe(__filename, function () { }); it('authorize hook does not elevate level from user settings', async function () { - this.timeout(400); settings.users.user.readOnly = true; authorize = () => 'create'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); @@ -419,7 +394,6 @@ describe(__filename, function () { assert.equal(message.accessStatus, 'deny'); }); it('user settings does not elevate level from authorize hook', async function () { - this.timeout(400); settings.users.user.readOnly = false; settings.users.user.canCreate = true; authorize = () => 'readOnly'; From 15964345f78484820421fd382a950cc529541d7c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:51:34 +0200 Subject: [PATCH 108/322] fixed typo --- doc/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/docker.md b/doc/docker.md index d7fb76b37..2992370ea 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -19,7 +19,7 @@ All of the following instructions are as a member of the `docker` group. By default, the Etherpad Docker image is built and run in `production` mode: no development dependencies are installed, and asset bundling speeds up page load time. ### Rebuilding with custom settings -Edit `/settings.json.docker` at your will. When rebuilding the image, this file will be copied inside your image and renamed to `setting.json`. +Edit `/settings.json.docker` at your will. When rebuilding the image, this file will be copied inside your image and renamed to `settings.json`. **Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. From 42e59ff2cdb3de11b9aa7bdd1c96168bc4191fa0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 5 Sep 2021 19:23:01 -0400 Subject: [PATCH 109/322] installDeps.sh: Simplify log message --- src/bin/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh index 94d8630cc..ee3f4ae01 100755 --- a/src/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -34,7 +34,7 @@ if [ ! -f "$settings" ]; then cp settings.json.template "$settings" || exit 1 fi -log "Ensure that all dependencies are up to date... If this is the first time you have run Etherpad please be patient." +log "Installing dependencies..." ( mkdir -p node_modules cd node_modules From 8b89cb3f6fac9d647973c14017aa613874a6e94e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 5 Sep 2021 19:24:17 -0400 Subject: [PATCH 110/322] installDeps.sh: Wrap long lines --- src/bin/installDeps.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh index ee3f4ae01..be7be1f36 100755 --- a/src/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -15,10 +15,12 @@ is_cmd node || fatal "Please install node.js ( https://nodejs.org )" is_cmd npm || fatal "Please install npm ( https://npmjs.org )" # Check npm version -require_minimal_version "npm" $(get_program_version "npm") "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" +require_minimal_version "npm" $(get_program_version "npm") \ + "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" # Check node version -require_minimal_version "nodejs" $(get_program_version "node") "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR" +require_minimal_version "nodejs" $(get_program_version "node") \ + "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR" # Get the name of the settings file settings="settings.json" From cf7cf8be69904f6ca8bd16caa94262e67b9d2224 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 5 Sep 2021 19:28:58 -0400 Subject: [PATCH 111/322] installDeps.sh: Quote underquoted expansions --- src/bin/installDeps.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh index be7be1f36..3fe82a346 100755 --- a/src/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -15,11 +15,11 @@ is_cmd node || fatal "Please install node.js ( https://nodejs.org )" is_cmd npm || fatal "Please install npm ( https://npmjs.org )" # Check npm version -require_minimal_version "npm" $(get_program_version "npm") \ +require_minimal_version "npm" "$(get_program_version "npm")" \ "$REQUIRED_NPM_MAJOR" "$REQUIRED_NPM_MINOR" # Check node version -require_minimal_version "nodejs" $(get_program_version "node") \ +require_minimal_version "nodejs" "$(get_program_version "node")" \ "$REQUIRED_NODE_MAJOR" "$REQUIRED_NODE_MINOR" # Get the name of the settings file From 258b8366e202e50a064ba42915e1514247bd6dc2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 5 Sep 2021 19:31:06 -0400 Subject: [PATCH 112/322] installDeps.sh: Ensure that `ep_etherpad-lite` is a directory --- src/bin/installDeps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh index 3fe82a346..a3e593f31 100755 --- a/src/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -40,7 +40,7 @@ log "Installing dependencies..." ( mkdir -p node_modules cd node_modules - [ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite + [ -d ep_etherpad-lite ] || ln -sf ../src ep_etherpad-lite cd ep_etherpad-lite npm ci --no-optional ) || { From 2ba85dba0e92068ceb69f4e7020d5d2cc4225d79 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 5 Sep 2021 19:36:05 -0400 Subject: [PATCH 113/322] installDeps.sh: Handle errors --- src/bin/installDeps.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh index a3e593f31..25a677b3a 100755 --- a/src/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -38,10 +38,10 @@ fi log "Installing dependencies..." ( - mkdir -p node_modules - cd node_modules - [ -d ep_etherpad-lite ] || ln -sf ../src ep_etherpad-lite - cd ep_etherpad-lite + mkdir -p node_modules && + cd node_modules && + { [ -d ep_etherpad-lite ] || ln -sf ../src ep_etherpad-lite; } && + cd ep_etherpad-lite && npm ci --no-optional ) || { rm -rf src/node_modules From 4a09000ca6007676ed53d18fb14fd41dd92fe5ec Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 5 Sep 2021 19:38:18 -0400 Subject: [PATCH 114/322] installDeps.sh: Don't nuke `src/node_modules` on error Rationale: * Clearing out `src/node_modules` is unlikely to bring future success. * If there is an error, it's better to leave the filesystem alone so that the user can investigate the cause. * Deleting the directory on error is a surprising behavior. --- src/bin/installDeps.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh index 25a677b3a..66ad895c2 100755 --- a/src/bin/installDeps.sh +++ b/src/bin/installDeps.sh @@ -43,10 +43,7 @@ log "Installing dependencies..." { [ -d ep_etherpad-lite ] || ln -sf ../src ep_etherpad-lite; } && cd ep_etherpad-lite && npm ci --no-optional -) || { - rm -rf src/node_modules - exit 1 -} +) || exit 1 # Remove all minified data to force node creating it new log "Clearing minified cache..." From b9609a749d8ad2ebf2846362416dc285c3c7a45d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 6 Sep 2021 02:59:40 -0400 Subject: [PATCH 115/322] SocketIORouter: Rename variables to improve readability --- src/node/handler/SocketIORouter.js | 40 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index cad53d173..855bb7d7d 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -21,9 +21,10 @@ */ const log4js = require('log4js'); -const messageLogger = log4js.getLogger('message'); const stats = require('../stats'); +const logger = log4js.getLogger('message'); + /** * Saves all components * key is the component name @@ -31,7 +32,7 @@ const stats = require('../stats'); */ const components = {}; -let socket; +let io; /** * adds a component @@ -39,45 +40,42 @@ let socket; exports.addComponent = (moduleName, module) => { // save the component components[moduleName] = module; - - // give the module the socket - module.setSocketIO(socket); + module.setSocketIO(io); }; /** * sets the socket.io and adds event functions for routing */ -exports.setSocketIO = (_socket) => { - // save this socket internaly - socket = _socket; +exports.setSocketIO = (_io) => { + io = _io; - socket.sockets.on('connection', (client) => { + io.sockets.on('connection', (socket) => { // wrap the original send function to log the messages - client._send = client.send; - client.send = (message) => { - messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`); - client._send(message); + socket._send = socket.send; + socket.send = (message) => { + logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`); + socket._send(message); }; // tell all components about this connect for (const i of Object.keys(components)) { - components[i].handleConnect(client); + components[i].handleConnect(socket); } - client.on('message', async (message) => { + socket.on('message', async (message) => { if (message.protocolVersion && message.protocolVersion !== 2) { - messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); + logger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); return; } if (!message.component || !components[message.component]) { - messageLogger.error(`Can't route the message: ${JSON.stringify(message)}`); + logger.error(`Can't route the message: ${JSON.stringify(message)}`); return; } - messageLogger.debug(`from ${client.id}: ${JSON.stringify(message)}`); - await components[message.component].handleMessage(client, message); + logger.debug(`from ${socket.id}: ${JSON.stringify(message)}`); + await components[message.component].handleMessage(socket, message); }); - client.on('disconnect', () => { + socket.on('disconnect', () => { // store the lastDisconnect as a timestamp, this is useful if you want to know // when the last user disconnected. If your activePads is 0 and totalUsers is 0 // you can say, if there has been no active pads or active users for 10 minutes @@ -85,7 +83,7 @@ exports.setSocketIO = (_socket) => { stats.gauge('lastDisconnect', () => Date.now()); // tell all components about this disconnect for (const i of Object.keys(components)) { - components[i].handleDisconnect(client); + components[i].handleDisconnect(socket); } }); }); From 2e93fca6995f10e61ebf7384cf1e3f80a6603157 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 6 Sep 2021 03:08:52 -0400 Subject: [PATCH 116/322] SocketIORouter: Logging improvements --- src/node/handler/SocketIORouter.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 855bb7d7d..e6120d366 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -21,9 +21,10 @@ */ const log4js = require('log4js'); +const settings = require('../utils/Settings'); const stats = require('../stats'); -const logger = log4js.getLogger('message'); +const logger = log4js.getLogger('socket.io'); /** * Saves all components @@ -50,6 +51,9 @@ exports.setSocketIO = (_io) => { io = _io; io.sockets.on('connection', (socket) => { + const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip; + logger.debug(`${socket.id} connected from IP ${ip}`); + // wrap the original send function to log the messages socket._send = socket.send; socket.send = (message) => { @@ -75,7 +79,8 @@ exports.setSocketIO = (_io) => { await components[message.component].handleMessage(socket, message); }); - socket.on('disconnect', () => { + socket.on('disconnect', (reason) => { + logger.debug(`${socket.id} disconnected: ${reason}`); // store the lastDisconnect as a timestamp, this is useful if you want to know // when the last user disconnected. If your activePads is 0 and totalUsers is 0 // you can say, if there has been no active pads or active users for 10 minutes From 94f71bd5e9117eff513055cc897400058b0bf0f4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 6 Sep 2021 03:17:39 -0400 Subject: [PATCH 117/322] SocketIORouter: Add ability to unregister handler This will make it easier to add tests. --- src/node/handler/SocketIORouter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index e6120d366..1a53602dc 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -39,11 +39,13 @@ let io; * adds a component */ exports.addComponent = (moduleName, module) => { - // save the component + if (module == null) return exports.deleteComponent(moduleName); components[moduleName] = module; module.setSocketIO(io); }; +exports.deleteComponent = (moduleName) => { delete components[moduleName]; }; + /** * sets the socket.io and adds event functions for routing */ From 320e5c110952452362c1e02b71758725aa67d78f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 6 Sep 2021 04:11:30 -0400 Subject: [PATCH 118/322] SocketIORouter: Add unit tests --- src/tests/backend/specs/socketio.js | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 8412e0e1d..3f04e566d 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -8,6 +8,7 @@ const plugins = require('../../../static/js/pluginfw/plugin_defs'); const readOnlyManager = require('../../../node/db/ReadOnlyManager'); const setCookieParser = require('set-cookie-parser'); const settings = require('../../../node/utils/Settings'); +const socketIoRouter = require('../../../node/handler/SocketIORouter'); const logger = common.logger; @@ -403,4 +404,81 @@ describe(__filename, function () { assert.equal(message.accessStatus, 'deny'); }); }); + + describe('SocketIORouter.js', function () { + const Module = class { + setSocketIO(io) {} + handleConnect(socket) {} + handleDisconnect(socket) {} + handleMessage(socket, message) {} + }; + + afterEach(async function () { + socketIoRouter.deleteComponent(this.test.fullTitle()); + socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`); + }); + + it('setSocketIO', async function () { + let ioServer; + socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { + setSocketIO(io) { ioServer = io; } + }()); + assert(ioServer != null); + }); + + it('handleConnect', async function () { + let serverSocket; + socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { + handleConnect(socket) { serverSocket = socket; } + }()); + socket = await connect(); + assert(serverSocket != null); + }); + + it('handleDisconnect', async function () { + let resolveConnected; + const connected = new Promise((resolve) => resolveConnected = resolve); + let resolveDisconnected; + const disconnected = new Promise((resolve) => resolveDisconnected = resolve); + socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { + handleConnect(socket) { + this._socket = socket; + resolveConnected(); + } + handleDisconnect(socket) { + assert(socket != null); + // There might be lingering disconnect events from sockets created by other tests. + if (this._socket == null || socket.id !== this._socket.id) return; + assert.equal(socket, this._socket); + resolveDisconnected(); + } + }()); + socket = await connect(); + await connected; + socket.close(); + socket = null; + await disconnected; + }); + + it('handleMessage', async function () { + let serverSocket; + const want = { + component: this.test.fullTitle(), + protocolVersion: 2, + foo: {bar: 'asdf'}, + }; + let rx; + const got = new Promise((resolve) => { rx = resolve; }); + socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { + handleConnect(socket) { serverSocket = socket; } + handleMessage(socket, message) { assert.equal(socket, serverSocket); rx(message); } + }()); + socketIoRouter.addComponent(`${this.test.fullTitle()} #2`, new class extends Module { + handleMessage(socket, message) { assert.fail('wrong handler called'); } + }()); + socket = await connect(); + socket.send(want); + assert.deepEqual(await got, want); + }); + }); }); From 9f9adb369b3d6a29ad012158131c5ad96536ccce Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 6 Sep 2021 05:51:57 -0400 Subject: [PATCH 119/322] SocketIORouter: Don't crash if message handler throws --- src/node/handler/SocketIORouter.js | 6 +++++- src/tests/backend/specs/socketio.js | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 1a53602dc..a4bfb41c4 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -78,7 +78,11 @@ exports.setSocketIO = (_io) => { return; } logger.debug(`from ${socket.id}: ${JSON.stringify(message)}`); - await components[message.component].handleMessage(socket, message); + try { + await components[message.component].handleMessage(socket, message); + } catch (err) { + logger.error(`Error while handling message from ${socket.id}: ${err.stack || err}`); + } }); socket.on('disconnect', (reason) => { diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 3f04e566d..7a99e0e34 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -460,7 +460,7 @@ describe(__filename, function () { await disconnected; }); - it('handleMessage', async function () { + it('handleMessage (success)', async function () { let serverSocket; const want = { component: this.test.fullTitle(), @@ -480,5 +480,24 @@ describe(__filename, function () { socket.send(want); assert.deepEqual(await got, want); }); + + it('handleMessage (error)', async function () { + let receive; + const received = new Promise((resolve) => receive = resolve); + socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { + handleMessage(socket, message) { + if (message.throw) throw new Error('injected'); + receive(); + } + }()); + socket = await connect(); + const tx = (msg = {}) => { + msg = Object.assign({component: this.test.fullTitle(), protocolVersion: 2}, msg); + socket.send(msg); + }; + tx({throw: true}); + tx(); + await received; + }); }); }); From bc9cdd695794a4149df5102a1ef483f20c3ba9fb Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 6 Sep 2021 05:42:47 -0400 Subject: [PATCH 120/322] SocketIORouter: Add acknowledgement support --- src/node/handler/SocketIORouter.js | 13 +++++----- src/tests/backend/specs/socketio.js | 37 +++++++++++++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index a4bfb41c4..0fa18755e 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -68,7 +68,7 @@ exports.setSocketIO = (_io) => { components[i].handleConnect(socket); } - socket.on('message', async (message) => { + socket.on('message', (message, ack = () => {}) => { if (message.protocolVersion && message.protocolVersion !== 2) { logger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); return; @@ -78,11 +78,12 @@ exports.setSocketIO = (_io) => { return; } logger.debug(`from ${socket.id}: ${JSON.stringify(message)}`); - try { - await components[message.component].handleMessage(socket, message); - } catch (err) { - logger.error(`Error while handling message from ${socket.id}: ${err.stack || err}`); - } + (async () => await components[message.component].handleMessage(socket, message))().then( + (val) => ack(null, val), + (err) => { + logger.error(`Error while handling message from ${socket.id}: ${err.stack || err}`); + ack({name: err.name, message: err.message}); + }); }); socket.on('disconnect', (reason) => { diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 7a99e0e34..30ef82748 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -481,23 +481,34 @@ describe(__filename, function () { assert.deepEqual(await got, want); }); - it('handleMessage (error)', async function () { - let receive; - const received = new Promise((resolve) => receive = resolve); + const tx = async (socket, message = {}) => await new Promise((resolve, reject) => { + message = Object.assign({protocolVersion: 2}, message); + const AckErr = class extends Error { + constructor(name, ...args) { super(...args); this.name = name; } + }; + socket.send(message, + (errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val)); + }); + + it('handleMessage with ack (success)', async function () { + const want = 'value'; socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleMessage(socket, message) { - if (message.throw) throw new Error('injected'); - receive(); - } + handleMessage(socket, msg) { return want; } }()); socket = await connect(); - const tx = (msg = {}) => { - msg = Object.assign({component: this.test.fullTitle(), protocolVersion: 2}, msg); - socket.send(msg); + const got = await tx(socket, {component: this.test.fullTitle()}); + assert.equal(got, want); + }); + + it('handleMessage with ack (error)', async function () { + const InjectedError = class extends Error { + constructor() { super('injected test error'); this.name = 'InjectedError'; } }; - tx({throw: true}); - tx(); - await received; + socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { + handleMessage(socket, msg) { throw new InjectedError(); } + }()); + socket = await connect(); + await assert.rejects(tx(socket, {component: this.test.fullTitle()}), new InjectedError()); }); }); }); From ebe05f8e632f72c90ad3114e702ee3fe97d97ba9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 30 Aug 2021 02:48:02 -0400 Subject: [PATCH 121/322] deps: Delete unused `async-stacktrace` dependency --- src/package-lock.json | 5 ----- src/package.json | 1 - 2 files changed, 6 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 592bce8c1..f6be62f02 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -955,11 +955,6 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" }, - "async-stacktrace": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/async-stacktrace/-/async-stacktrace-0.0.2.tgz", - "integrity": "sha1-i7uXh+OzjINscpp+nXwIYw210e8=" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/src/package.json b/src/package.json index 189c66e71..dfdba997f 100644 --- a/src/package.json +++ b/src/package.json @@ -31,7 +31,6 @@ ], "dependencies": { "async": "^3.2.1", - "async-stacktrace": "0.0.2", "channels": "0.0.4", "clean-css": "^5.1.5", "cookie-parser": "1.4.5", From 59ebaa78fdcca05a6d521beea6b03cad9ba4a2dc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 30 Aug 2021 03:20:51 -0400 Subject: [PATCH 122/322] deps: Update `src/package.json` versions to match lock file --- src/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package.json b/src/package.json index dfdba997f..fa939bdac 100644 --- a/src/package.json +++ b/src/package.json @@ -55,7 +55,7 @@ "nodeify": "1.0.1", "npm": "^6.14.15", "openapi-backend": "^4.2.0", - "proxy-addr": "^2.0.6", + "proxy-addr": "^2.0.7", "rate-limiter-flexible": "^2.2.4", "rehype": "^11.0.0", "rehype-minify-whitespace": "^4.0.5", @@ -65,7 +65,7 @@ "semver": "^7.3.5", "socket.io": "^2.4.1", "terser": "^5.7.2", - "threads": "^1.4.0", + "threads": "^1.6.5", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", "ueberdb2": "^1.4.15", @@ -90,8 +90,8 @@ "mocha": "^9.1.1", "mocha-froth": "^0.2.10", "openapi-schema-validation": "^0.4.2", - "selenium-webdriver": "^4.0.0-beta.3", - "set-cookie-parser": "^2.4.6", + "selenium-webdriver": "^4.0.0-beta.4", + "set-cookie-parser": "^2.4.8", "sinon": "^11.1.2", "split-grid": "^1.0.11", "superagent": "^6.1.0", From 03275ba227c21457724ca3759fdbdb1b341c0e11 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 30 Aug 2021 03:13:29 -0400 Subject: [PATCH 123/322] deps: Regenerate `src/package-lock.json` --- src/package-lock.json | 833 +++++++++++++++++++----------------------- 1 file changed, 384 insertions(+), 449 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index f6be62f02..ab0bdc87c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -102,16 +102,6 @@ "mime-types": "^2.1.12" } }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - } - }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -142,11 +132,12 @@ } }, "@azure/core-paging": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.1.3.tgz", - "integrity": "sha512-his7Ah40ThEYORSpIAwuh6B8wkGwO/zG7gqVtmSE4WAJ46e36zUDXTKReUCLBDc6HmjjApQQxxcRFy5FruG79A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.2.0.tgz", + "integrity": "sha512-ZX1bCjm/MjKPCN6kQD/9GJErYSoKA8YWp6YWoo5EIzcTWlSBLXu3gNaBTUl8usGl+UShiKo7b4Gdy1NSTIlpZg==", "requires": { - "@azure/core-asynciterator-polyfill": "^1.0.0" + "@azure/core-asynciterator-polyfill": "^1.0.0", + "tslib": "^2.2.0" } }, "@azure/core-rest-pipeline": { @@ -411,6 +402,18 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -439,6 +442,12 @@ "esprima": "^4.0.0" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -600,9 +609,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "16.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.6.tgz", - "integrity": "sha512-VESVNFoa/ahYA62xnLBjo5ur6gPsgEE5cNRy8SrdnkZ2nwJSW0kJ4ufbFr2zuU9ALtHM8juY53VcRoTA7htXSg==" + "version": "16.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.8.tgz", + "integrity": "sha512-8upnoQU0OPzbIkm+ZMM0zCeFCkw2s3mS0IWdx0+AAaWqm4fkBb0UJp8Edl7FVKRamYbpJC/aVsHpKWBIbiC7Zg==" }, "@types/node-fetch": { "version": "2.5.12", @@ -691,9 +700,9 @@ } }, "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" }, "acorn-globals": { "version": "6.0.0", @@ -702,6 +711,13 @@ "requires": { "acorn": "^7.1.1", "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + } } }, "acorn-jsx": { @@ -797,13 +813,13 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", "requires": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, @@ -813,24 +829,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "requires": { "ajv": "^8.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - } } }, "ansi-colors": { @@ -899,12 +897,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -1072,13 +1064,6 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "string_decoder": { @@ -1087,13 +1072,6 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } } } @@ -1145,11 +1123,6 @@ "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, @@ -1431,13 +1404,6 @@ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "requires": { "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "content-type": { @@ -1748,6 +1714,11 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" } } }, @@ -1776,6 +1747,11 @@ "requires": { "ms": "2.0.0" } + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" } } }, @@ -1826,48 +1802,6 @@ "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - } } }, "eslint": { @@ -1918,6 +1852,18 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -1998,21 +1944,48 @@ "esprima": "^4.0.0" } }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2042,6 +2015,15 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } } } }, @@ -2174,6 +2156,14 @@ "requires": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } } }, "eslint-utils": { @@ -2215,6 +2205,12 @@ "eslint-visitor-keys": "^1.3.0" }, "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, "eslint-visitor-keys": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", @@ -2235,14 +2231,6 @@ "dev": true, "requires": { "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } } }, "esrecurse": { @@ -2252,21 +2240,12 @@ "dev": true, "requires": { "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } } }, "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" }, "esutils": { "version": "2.0.3", @@ -2287,77 +2266,6 @@ "async": "^3.2.1", "socket.io-client": "^2.3.0", "superagent": "^6.1.0" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "superagent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", - "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", - "dev": true, - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.2", - "debug": "^4.1.1", - "fast-safe-stringify": "^2.0.7", - "form-data": "^3.0.0", - "formidable": "^1.2.2", - "methods": "^1.1.2", - "mime": "^2.4.6", - "qs": "^6.9.4", - "readable-stream": "^3.6.0", - "semver": "^7.3.2" - } - } } }, "etherpad-require-kernel": { @@ -2424,13 +2332,6 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } } }, "express-rate-limit": { @@ -2462,6 +2363,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, @@ -2595,12 +2501,12 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -2770,6 +2676,24 @@ "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + } } }, "has": { @@ -2920,6 +2844,11 @@ "toidentifier": "1.0.0" }, "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3059,9 +2988,9 @@ } }, "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.8", @@ -3094,9 +3023,9 @@ "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" }, "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", "requires": { "has": "^1.0.3" } @@ -3273,43 +3202,6 @@ "whatwg-url": "^9.0.0", "ws": "^8.0.0", "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", - "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - } - }, - "ws": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.1.tgz", - "integrity": "sha512-XkgWpJU3sHU7gX8f13NqTn6KQ85bd1WU7noBHTT8fSohx7OS1TPY8k+cyRPCzFkia7C4mM229yeHr1qK9sM4JQ==" - } } }, "json-schema": { @@ -3318,9 +3210,9 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -3400,9 +3292,9 @@ } }, "jszip": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", - "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "dev": true, "requires": { "lie": "~3.3.0", @@ -3432,12 +3324,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3508,13 +3394,12 @@ "integrity": "sha1-xDYgbgUtIUkLEQF6RNURj5Ih5ds=" }, "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "lie": { @@ -3667,26 +3552,10 @@ "semver": "~4.3.3" }, "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "semver": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" } } }, @@ -3701,13 +3570,6 @@ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "requires": { "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } } }, "measured-core": { @@ -3786,6 +3648,14 @@ "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "optional": true + } } }, "minizlib": { @@ -3912,9 +3782,9 @@ } }, "mongodb": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.11.tgz", - "integrity": "sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw==", + "version": "3.6.12", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.12.tgz", + "integrity": "sha512-ErHpF4P4disEIQB8Nns2twIMVXcvmlwjpKqfVnyB/hhd/L5We48LfoBYjBjuUSiSqL6ffmcygPTgjvpy2LETRQ==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", @@ -4001,11 +3871,6 @@ "util-deprecate": "~1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4035,16 +3900,6 @@ "requires": { "side-channel": "^1.0.4" } - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - } } } }, @@ -4226,6 +4081,12 @@ "osenv": "^0.1.4" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "optional": true + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -4246,6 +4107,12 @@ "safe-buffer": "^5.2.1", "yallist": "^3.1.1" } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "optional": true } } }, @@ -7435,9 +7302,9 @@ "optional": true }, "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" }, "observable-fns": { "version": "0.6.1", @@ -7490,22 +7357,6 @@ "qs": "^6.9.3" }, "dependencies": { - "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "qs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", @@ -7528,38 +7379,20 @@ } }, "openapi-schema-validator": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-9.2.0.tgz", - "integrity": "sha512-BzXm4Kz78pw9BMxW2whd9nKnYjLFxqFWEkH8Eh1/bZ2aOeCfIgPkYvU9Ai/fAPNQAW0y+oDSl1BoqGKENlO6sA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-9.3.0.tgz", + "integrity": "sha512-KlvgZMWTu+H1FHFSZNAGj369uXl3BD1nXSIq+sXlG6P+OrsAHd3YORx0ZEZ3WGdu2LQrPGmtowGQavYXL+PLwg==", "requires": { "ajv": "^8.1.0", "ajv-formats": "^2.0.2", "lodash.merge": "^4.6.1", - "openapi-types": "^9.2.0" - }, - "dependencies": { - "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - } + "openapi-types": "^9.3.0" } }, "openapi-types": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.2.0.tgz", - "integrity": "sha512-3x0gg8DxhpZ5MVki7AK6jmMdVIZASmVGo9CoUtD+nksLdkqz7EzWKdfS9Oxxq1J7idnZV0b3LjqcvizfKFySpQ==" + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.0.tgz", + "integrity": "sha512-sR23YjmuwDSMsQVZDHbV9mPgi0RyniQlqR0AQxTC2/F3cpSjRFMH3CFPjoWvNqhC4OxPkDYNb2l8Mc1Me6D/KQ==" }, "optional-js": { "version": "2.3.0", @@ -7575,17 +7408,16 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" } }, "os-homedir": { @@ -7809,10 +7641,9 @@ } }, "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "process": { "version": "0.11.10", @@ -7932,11 +7763,6 @@ "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, @@ -7953,13 +7779,14 @@ } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, "readdirp": { @@ -8025,13 +7852,6 @@ "hast-util-is-element": "^1.0.0", "hast-util-whitespace": "^1.0.4", "unist-util-is": "^4.0.0" - }, - "dependencies": { - "unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==" - } } }, "rehype-parse": { @@ -8078,10 +7898,29 @@ "uuid": "^3.3.2" }, "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } } } }, @@ -8139,9 +7978,9 @@ } }, "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", @@ -8238,6 +8077,11 @@ "toidentifier": "1.0.0" } }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -8584,6 +8428,31 @@ "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "requires": { "readable-stream": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, "sprintf-js": { @@ -8644,12 +8513,9 @@ } }, "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "stringify-entities": { "version": "3.1.0", @@ -8734,6 +8600,32 @@ "requires": { "side-channel": "^1.0.4" } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } } } }, @@ -8780,18 +8672,6 @@ "strip-ansi": "^6.0.0" }, "dependencies": { - "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -8804,12 +8684,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -8877,6 +8751,40 @@ "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "optional": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "optional": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" } } } @@ -8937,6 +8845,34 @@ "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } } } }, @@ -8976,9 +8912,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -9043,12 +8979,13 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" } }, "tr46": { @@ -9088,12 +9025,11 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "requires": { - "prelude-ls": "^1.2.1" + "prelude-ls": "~1.1.2" } }, "type-detect": { @@ -9410,9 +9346,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.1.tgz", + "integrity": "sha512-XkgWpJU3sHU7gX8f13NqTn6KQ85bd1WU7noBHTT8fSohx7OS1TPY8k+cyRPCzFkia7C4mM229yeHr1qK9sM4JQ==" }, "wtfnode": { "version": "0.9.1", @@ -9470,10 +9406,9 @@ "dev": true }, "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "16.2.0", From d58b649c76e98c3908fe02b60561eac1f85e17fc Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 9 Sep 2021 13:02:49 +0200 Subject: [PATCH 124/322] Localisation updates from https://translatewiki.net. --- src/locales/bn.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/locales/bn.json b/src/locales/bn.json index 035661449..b46ed8bdb 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -25,9 +25,18 @@ "admin_plugins.installed_uninstall.value": "আনইনস্টল করুন", "admin_plugins.last-update": "সর্বশেষ হালনাগাদ", "admin_plugins.name": "নাম", + "admin_plugins.page-title": "প্লাগিন ব্যবস্থাপনা - ইথারপ্যাড", "admin_plugins.version": "সংস্করণ", + "admin_plugins_info": "সমস্যা সমাধানের তথ্য", + "admin_plugins_info.hooks": "ইন্সটলকৃত হুক", + "admin_plugins_info.hooks_client": "গ্রাহক পার্শ্বের হুক", + "admin_plugins_info.hooks_server": "সার্ভার পার্শ্বের হুক", + "admin_plugins_info.parts": "ইন্সটলকৃত অংশ", + "admin_plugins_info.plugins": "ইন্সটলকৃত প্লাগিন", + "admin_plugins_info.page-title": "প্লাগিন তথ্য - ইথারপ্যাড", "admin_plugins_info.version": "ইথারপ্যাড সংস্করণ", "admin_plugins_info.version_latest": "সাম্প্রতিক উপলব্ধ সংস্করণ", + "admin_plugins_info.version_number": "সংস্করণ সংখ্যা", "admin_settings": "সেটিংসমূহ", "admin_settings.current": "বর্তমান কনফিগারেশন", "admin_settings.current_restart.value": "ইথারপ্যাড পুনরায় চালু করুন", From 58bd96ce8f1e054e10e769506660ccb103c22a44 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sat, 3 Jul 2021 03:58:33 +0200 Subject: [PATCH 125/322] padreadonly: Remove dead `/ro/:id` handling Read-only pads are accessed using `/p/` path since commit ba3430ebb7347e378cfb5512f24dd2c9968e2210. --- src/ep.json | 6 ------ src/node/hooks/express/padreadonly.js | 27 --------------------------- 2 files changed, 33 deletions(-) delete mode 100644 src/node/hooks/express/padreadonly.js diff --git a/src/ep.json b/src/ep.json index 5642f8c12..b917aa1f3 100644 --- a/src/ep.json +++ b/src/ep.json @@ -50,12 +50,6 @@ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize" } }, - { - "name": "padreadonly", - "hooks": { - "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padreadonly" - } - }, { "name": "webaccess", "hooks": { diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js deleted file mode 100644 index 4dda67b1f..000000000 --- a/src/node/hooks/express/padreadonly.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const readOnlyManager = require('../../db/ReadOnlyManager'); -const hasPadAccess = require('../../padaccess'); -const exporthtml = require('../../utils/ExportHtml'); - -exports.expressCreateServer = (hookName, args, cb) => { - // serve read only pad - args.app.get('/ro/:id', async (req, res) => { - // translate the read only pad to a padId - const padId = await readOnlyManager.getPadId(req.params.id); - if (padId == null) { - res.status(404).send('404 - Not Found'); - return; - } - - // we need that to tell hasPadAcess about the pad - req.params.pad = padId; - - if (await hasPadAccess(req, res)) { - // render the html document - const html = await exporthtml.getPadHTMLDocument(padId, null); - res.send(html); - } - }); - return cb(); -}; From dbd76f0c5d77481877fba32720f6d5dc9b8187a5 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Tue, 29 Jun 2021 19:13:10 +0200 Subject: [PATCH 126/322] export: Don't leak writeable pad ID when exporting Co-authored-by: Richard Hansen --- CHANGELOG.md | 14 ++++++++++++++ src/node/handler/ExportHandler.js | 4 ++-- src/node/utils/ExportEtherpad.js | 28 ++++++++++++---------------- src/node/utils/ExportHtml.js | 4 ++-- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe8d785e..86f3a4ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Next Release +### Security fixes + +* Fixed leak of the writable pad ID when exporting from the pad's read-only ID. + This only matters if you treat the writeable pad IDs as secret (e.g., you are + not using [ep_padlist2](https://www.npmjs.com/package/ep_padlist2)) and you + share the pad's read-only ID with untrusted users. Instead of treating + writeable pad IDs as secret, you are encouraged to take advantage of + Etherpad's authentication and authorization mechanisms (e.g., use + [ep_openid_connect](https://www.npmjs.com/package/ep_openid_connect) with + [ep_readonly_guest](https://www.npmjs.com/package/ep_readonly_guest), or write + your own + [authentication](https://etherpad.org/doc/v1.8.14/#index_authenticate) and + [authorization](https://etherpad.org/doc/v1.8.14/#index_authorize) plugins). + ### Compatibility changes * For plugin authors: diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 0bf75a17f..f3fde047c 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -56,14 +56,14 @@ exports.doExport = async (req, res, padId, readOnlyId, type) => { // if this is a plain text export, we can do this directly // We have to over engineer this because tabs are stored as attributes and not plain text if (type === 'etherpad') { - const pad = await exportEtherpad.getPadRaw(padId); + const pad = await exportEtherpad.getPadRaw(padId, readOnlyId); res.send(pad); } else if (type === 'txt') { const txt = await exporttxt.getPadTXTDocument(padId, req.params.rev); res.send(txt); } else { // render the html document - let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev); + let html = await exporthtml.getPadHTMLDocument(padId, req.params.rev, readOnlyId); // decide what to do with the html export diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index 48c850af9..45683bc65 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -19,23 +19,18 @@ const db = require('../db/DB'); const hooks = require('../../static/js/pluginfw/hooks'); -exports.getPadRaw = async (padId) => { - const padKey = `pad:${padId}`; - const padcontent = await db.get(padKey); +exports.getPadRaw = async (padId, readOnlyId) => { + const keyPrefixRead = `pad:${padId}`; + const keyPrefixWrite = readOnlyId ? `pad:${readOnlyId}` : keyPrefixRead; + const padcontent = await db.get(keyPrefixRead); - const records = [padKey]; - for (let i = 0; i <= padcontent.head; i++) { - records.push(`${padKey}:revs:${i}`); - } - - for (let i = 0; i <= padcontent.chatHead; i++) { - records.push(`${padKey}:chat:${i}`); - } + const keySuffixes = ['']; + for (let i = 0; i <= padcontent.head; i++) keySuffixes.push(`:revs:${i}`); + for (let i = 0; i <= padcontent.chatHead; i++) keySuffixes.push(`:chat:${i}`); const data = {}; - for (const key of records) { - // For each piece of info about a pad. - const entry = data[key] = await db.get(key); + for (const keySuffix of keySuffixes) { + const entry = data[keyPrefixWrite + keySuffix] = await db.get(keyPrefixRead + keySuffix); // Get the Pad Authors if (entry.pool && entry.pool.numToAttrib) { @@ -50,7 +45,7 @@ exports.getPadRaw = async (padId) => { if (authorEntry) { data[`globalAuthor:${authorId}`] = authorEntry; if (authorEntry.padIDs) { - authorEntry.padIDs = padId; + authorEntry.padIDs = readOnlyId || padId; } } } @@ -63,7 +58,8 @@ exports.getPadRaw = async (padId) => { const prefixes = await hooks.aCallAll('exportEtherpadAdditionalContent'); await Promise.all(prefixes.map(async (prefix) => { const key = `${prefix}:${padId}`; - data[key] = await db.get(key); + const writeKey = readOnlyId ? `${prefix}:${readOnlyId}` : key; + data[writeKey] = await db.get(key); })); return data; diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 38e5fb1a6..bc50da77b 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -457,7 +457,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { return pieces.join(''); }; -exports.getPadHTMLDocument = async (padId, revNum) => { +exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => { const pad = await padManager.getPad(padId); // Include some Styles into the Head for Export @@ -475,7 +475,7 @@ exports.getPadHTMLDocument = async (padId, revNum) => { return eejs.require('ep_etherpad-lite/templates/export_html.html', { body: html, - padId: Security.escapeHTML(padId), + padId: Security.escapeHTML(readOnlyId || padId), extraCSS: stylesForExportCSS, }); }; From 0f5a4bd1f8fc7ab7a0465ca235717617a51a413b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 15 Sep 2021 05:08:05 -0400 Subject: [PATCH 127/322] tests: Restructure read-only pad export tests This also adds coverage for `.etherpad` exports. --- .../backend/specs/api/importexportGetPost.js | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.js index 54d085eda..daa8435b3 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.js @@ -106,23 +106,52 @@ describe(__filename, function () { .expect((res) => assert.equal(res.body.data.text, padText.toString())); }); - for (const authn of [false, true]) { - it(`can export from read-only pad ID, authn ${authn}`, async function () { - settings.requireAuthentication = authn; - const get = (ep) => { - let req = agent.get(ep); - if (authn) req = req.auth('user', 'user-password'); - return req.expect(200); - }; - const ro = await get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) - .expect((res) => assert.ok(JSON.parse(res.text).data.readOnlyID)); - const readOnlyId = JSON.parse(ro.text).data.readOnlyID; - await get(`/p/${readOnlyId}/export/html`) - .expect((res) => assert(res.text.indexOf('This is the') !== -1)); - await get(`/p/${readOnlyId}/export/txt`) - .expect((res) => assert(res.text.indexOf('This is the') !== -1)); + describe('export from read-only pad ID', function () { + let readOnlyId; + + // This ought to be before(), but it must run after the top-level beforeEach() above. + beforeEach(async function () { + if (readOnlyId != null) return; + await agent.post(`/p/${testPadId}/import`) + .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'}) + .expect(200); + const res = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => assert.equal(res.body.code, 0)); + readOnlyId = res.body.data.readOnlyID; }); - } + + for (const authn of [false, true]) { + describe(`requireAuthentication = ${authn}`, function () { + // This ought to be before(), but it must run after the top-level beforeEach() above. + beforeEach(async function () { + settings.requireAuthentication = authn; + }); + + for (const exportType of ['html', 'txt', 'etherpad']) { + describe(`export to ${exportType}`, function () { + let text; + + // This ought to be before(), but it must run after the top-level beforeEach() above. + beforeEach(async function () { + if (text != null) return; + let req = agent.get(`/p/${readOnlyId}/export/${exportType}`); + if (authn) req = req.auth('user', 'user-password'); + const res = await req + .expect(200) + .buffer(true).parse(superagent.parse.text); + text = res.text; + }); + + it('export OK', async function () { + assert.match(text, /This is the/); + }); + }); + } + }); + } + }); describe('Import/Export tests requiring AbiWord/LibreOffice', function () { before(async function () { From b683dc300d7bcaade09c59c5434ceaa5c22a4cbd Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 14 Sep 2021 15:25:40 +0100 Subject: [PATCH 128/322] tests: Check for leak of read-write pad ID when exporting --- src/tests/backend/specs/api/importexportGetPost.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.js index daa8435b3..9dd4defc8 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.js @@ -147,6 +147,10 @@ describe(__filename, function () { it('export OK', async function () { assert.match(text, /This is the/); }); + + it('writable pad ID is not leaked', async function () { + assert(!text.includes(testPadId)); + }); }); } }); From 70c16bb1b562dcfb55f5f3d9de2f9efd2f3d8725 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 15 Sep 2021 05:05:56 -0400 Subject: [PATCH 129/322] tests: Check import of export of read-only pad ID --- .../backend/specs/api/importexportGetPost.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.js index 9dd4defc8..584341cc0 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.js @@ -151,6 +151,28 @@ describe(__filename, function () { it('writable pad ID is not leaked', async function () { assert(!text.includes(testPadId)); }); + + it('re-import to read-only pad ID gives 403 forbidden', async function () { + let req = agent.post(`/p/${readOnlyId}/import`) + .attach('file', Buffer.from(text), { + filename: `/test.${exportType}`, + contentType: 'text/plain', + }); + if (authn) req = req.auth('user', 'user-password'); + await req.expect(403); + }); + + it('re-import to read-write pad ID gives 200 OK', async function () { + // The new pad ID must differ from testPadId because Etherpad refuses to import + // .etherpad files on top of a pad that already has edits. + let req = agent.post(`/p/${testPadId}_import/import`) + .attach('file', Buffer.from(text), { + filename: `/test.${exportType}`, + contentType: 'text/plain', + }); + if (authn) req = req.auth('user', 'user-password'); + await req.expect(200); + }); }); } }); From 73cb698ba00f60082daf9b5367b0c9b19e85f350 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 15 Sep 2021 19:40:47 -0400 Subject: [PATCH 130/322] tests: Update selenium-webdriver to 4.0.0-rc-1 --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index ab0bdc87c..b6968a987 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8015,9 +8015,9 @@ "integrity": "sha1-gRwwAxNoYTPvAAcSXjsO1wCXiBU=" }, "selenium-webdriver": { - "version": "4.0.0-beta.4", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-beta.4.tgz", - "integrity": "sha512-+s/CIYkWzmnC9WASBxxVj7Lm0dcyl6OaFxwIJaFCT5WCuACiimEEr4lUnOOFP/QlKfkDQ56m+aRczaq2EvJEJg==", + "version": "4.0.0-rc-1", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz", + "integrity": "sha512-bcrwFPRax8fifRP60p7xkWDGSJJoMkPAzufMlk5K2NyLPht/YZzR2WcIk1+3gR8VOCLlst1P2PI+MXACaFzpIw==", "dev": true, "requires": { "jszip": "^3.6.0", diff --git a/src/package.json b/src/package.json index fa939bdac..265a1950d 100644 --- a/src/package.json +++ b/src/package.json @@ -90,7 +90,7 @@ "mocha": "^9.1.1", "mocha-froth": "^0.2.10", "openapi-schema-validation": "^0.4.2", - "selenium-webdriver": "^4.0.0-beta.4", + "selenium-webdriver": "^4.0.0-rc-1", "set-cookie-parser": "^2.4.8", "sinon": "^11.1.2", "split-grid": "^1.0.11", From 15f17b523733827caffa1e439831113480165104 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 15 Sep 2021 19:42:11 -0400 Subject: [PATCH 131/322] tests: Avoid deprecated `Builder.withCapabilities()` method --- src/tests/frontend/travis/remote_runner.js | 70 ++++++++-------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/src/tests/frontend/travis/remote_runner.js b/src/tests/frontend/travis/remote_runner.js index 264d8032c..74aefa62e 100644 --- a/src/tests/frontend/travis/remote_runner.js +++ b/src/tests/frontend/travis/remote_runner.js @@ -26,7 +26,7 @@ const log = (msg, pfx = '') => { const finishedRegex = /FINISHED.*[0-9]+ tests passed, ([0-9]+) tests failed/; -const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { +const sauceTestWorker = async.queue(async ({name, pfx, browser, version, platform}) => { const chromeOptions = new swdChrome.Options() .addArguments('use-fake-device-for-media-stream', 'use-fake-ui-for-media-stream'); const edgeOptions = new swdEdge.Options() @@ -34,25 +34,24 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { const firefoxOptions = new swdFirefox.Options() .setPreference('media.navigator.permission.disabled', true) .setPreference('media.navigator.streams.fake', true); - const driver = await new swd.Builder() + const builder = new swd.Builder() .usingServer('https://ondemand.saucelabs.com/wd/hub') - .withCapabilities(Object.assign({ - 'sauce:options': { - username: process.env.SAUCE_USERNAME, - accessKey: process.env.SAUCE_ACCESS_KEY, - name: [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '), - public: true, - build: process.env.GIT_HASH, - // console.json can be downloaded via saucelabs, - // don't know how to print them into output of the tests - extendedDebugging: true, - tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, - }, - }, testSettings)) + .forBrowser(browser, version, platform) .setChromeOptions(chromeOptions) .setEdgeOptions(edgeOptions) - .setFirefoxOptions(firefoxOptions) - .build(); + .setFirefoxOptions(firefoxOptions); + builder.getCapabilities().set('sauce:options', { + username: process.env.SAUCE_USERNAME, + accessKey: process.env.SAUCE_ACCESS_KEY, + name: [process.env.GIT_HASH].concat(process.env.SAUCE_NAME || [], name).join(' - '), + public: true, + build: process.env.GIT_HASH, + // console.json can be downloaded via saucelabs, + // don't know how to print them into output of the tests + extendedDebugging: true, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, + }); + const driver = await builder.build(); const url = `https://saucelabs.com/jobs/${(await driver.getSession()).getId()}`; try { await driver.get('http://localhost:9001/tests/frontend/'); @@ -94,39 +93,18 @@ const sauceTestWorker = async.queue(async ({name, pfx, testSettings}) => { }, 6); // run 6 tests in parrallel Promise.all([ - { - platformName: 'macOS 11.00', - browserName: 'safari', - browserVersion: 'latest', - }, + {browser: 'safari', version: 'latest', platform: 'macOS 11.00'}, ...(isAdminRunner ? [] : [ - { - platformName: 'Windows 10', - browserName: 'firefox', - browserVersion: 'latest', - }, - { - platformName: 'Windows 10', - browserName: 'MicrosoftEdge', - browserVersion: 'latest', - }, - { - platformName: 'Windows 10', - browserName: 'chrome', - browserVersion: 'latest', - }, - { - platformName: 'Windows 7', - browserName: 'chrome', - browserVersion: '55.0', - }, + {browser: 'firefox', version: 'latest', platform: 'Windows 10'}, + {browser: 'MicrosoftEdge', version: 'latest', platform: 'Windows 10'}, + {browser: 'chrome', version: 'latest', platform: 'Windows 10'}, + {browser: 'chrome', version: '55.0', platform: 'Windows 7'}, ]), -].map(async (testSettings) => { - const name = - `${testSettings.browserName} ${testSettings.browserVersion}, ${testSettings.platformName}`; +].map(async ({browser, version, platform}) => { + const name = `${browser} ${version}, ${platform}`; const pfx = `[${name}] `; try { - await sauceTestWorker.push({name, pfx, testSettings}); + await sauceTestWorker.push({name, pfx, browser, version, platform}); } catch (err) { log(`[red]FAILED[clear] ${err.stack || err}`, pfx); process.exitCode = 1; From fe7d223b6e7455f170e6389160e7930c977fbb8a Mon Sep 17 00:00:00 2001 From: JustAnotherArchivist Date: Thu, 16 Sep 2021 18:48:05 +0000 Subject: [PATCH 132/322] Docker: Expose socketIo.maxHttpBufferSize and dumpOnUncleanExit settings as environment variables --- doc/docker.md | 2 ++ settings.json.docker | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/docker.md b/doc/docker.md index 2992370ea..91caab97e 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -211,7 +211,9 @@ For the editor container, you can also make it full width by adding `full-width- | `FOCUS_LINE_PERCENTAGE_ARROW_UP` | Percentage of viewport height to be additionally scrolled when user presses arrow up in the line of the top of the viewport. Set to 0 to let the scroll to be handled as default by Etherpad | `0` | | `FOCUS_LINE_DURATION` | Time (in milliseconds) used to animate the scroll transition. Set to 0 to disable animation | `0` | | `FOCUS_LINE_CARET_SCROLL` | Flag to control if it should scroll when user places the caret in the last line of the viewport | `false` | +| `SOCKETIO_MAX_HTTP_BUFFER_SIZE` | The maximum size (in bytes) of a single message accepted via Socket.IO. If a client sends a larger message, its connection gets closed to prevent DoS (memory exhaustion) attacks. | `10000` | | `LOAD_TEST` | Allow Load Testing tools to hit the Etherpad Instance. WARNING: this will disable security on the instance. | `false` | +| `DUMP_ON_UNCLEAN_EXIT` | Enable dumping objects preventing a clean exit of Node.js. WARNING: this has a significant performance impact. | `false` | | `EXPOSE_VERSION` | Expose Etherpad version in the web interface and in the Server http header. Do not enable on production machines. | `false` | diff --git a/settings.json.docker b/settings.json.docker index b69af7663..40872339d 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -482,7 +482,7 @@ * value to work properly, but increasing the value increases susceptibility * to denial of service attacks (malicious clients can exhaust memory). */ - "maxHttpBufferSize": 10000 + "maxHttpBufferSize": "${SOCKETIO_MAX_HTTP_BUFFER_SIZE:10000}" }, /* @@ -495,7 +495,7 @@ /** * Disable dump of objects preventing a clean exit */ - "dumpOnUncleanExit": false, + "dumpOnUncleanExit": "${DUMP_ON_UNCLEAN_EXIT:false}", /* * Disable indentation on new line when previous line ends with some special From 4637b2b729a480dd5fb8de71546226530de737d5 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 23 Sep 2021 08:10:28 +0000 Subject: [PATCH 133/322] fix: upgrade js-cookie from 3.0.0 to 3.0.1 Snyk has created this PR to upgrade js-cookie from 3.0.0 to 3.0.1. See this package in npm: https://www.npmjs.com/package/js-cookie See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index b6968a987..80e21496c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3142,9 +3142,9 @@ } }, "js-cookie": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.0.tgz", - "integrity": "sha512-oUbbplKuH07/XX2YD2+Q+GMiPpnVXaRz8npE7suhBH9QEkJe2W7mQ6rwuMXHue3fpfcftQwzgyvGzIHyfCSngQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", + "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==" }, "js-tokens": { "version": "4.0.0", diff --git a/src/package.json b/src/package.json index 265a1950d..8e1f17414 100644 --- a/src/package.json +++ b/src/package.json @@ -44,7 +44,7 @@ "find-root": "1.1.0", "formidable": "1.2.2", "http-errors": "1.8.0", - "js-cookie": "^3.0.0", + "js-cookie": "^3.0.1", "jsdom": "^17.0.0", "jsonminify": "0.4.1", "languages4translatewiki": "0.1.3", From 4d2839457a903d210e9790e43e0b353fa9e1eb6c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 25 Sep 2021 18:32:42 -0400 Subject: [PATCH 134/322] CSS: Fix underscore and clear authorship icon alignment --- src/static/css/pad/icons.css | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/static/css/pad/icons.css b/src/static/css/pad/icons.css index 0ae7ed2db..eb1016dca 100644 --- a/src/static/css/pad/icons.css +++ b/src/static/css/pad/icons.css @@ -55,9 +55,6 @@ } .buttonicon-clearauthorship:before { content: "\e843"; - left: -9px; - position: absolute; - top: -9px; } .buttonicon-settings:before { content: "\e851"; @@ -87,9 +84,9 @@ .ep_font_color .buttonicon:before { content: '\e84e' !important; border-bottom: solid 2px #e42a2a; } .buttonicon-underline:before { - top: -8px; - left: -8px; - position: absolute; + /* The baseline of the underscore glyph seems off. Compensate for it here. */ + top: 0.1em; + position: relative; } /* COPY CSS GENERATED BY FONTELLO HERE */ From 653dbb344940974bd4cb9574e3f986410bbd243f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 25 Sep 2021 22:03:54 -0400 Subject: [PATCH 135/322] tests: Wait for pad init before returning from `helper.aNewPad()` This should make it easier to avoid race conditions. --- src/tests/frontend/helper.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 37d5d548c..7da7fff04 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -132,9 +132,11 @@ const helper = {}; if (opts.padPrefs) { helper.setPadPrefCookie(opts.padPrefs); } + const $loading = helper.padChrome$('#editorloadingbox'); + const $container = helper.padChrome$('#editorcontainer'); try { await helper.waitForPromise( - () => !$iframe.contents().find('#editorloadingbox').is(':visible'), 10000); + () => !$loading.is(':visible') && $container.hasClass('initialized'), 10000); } catch (err) { if (opts._retry++ >= 4) throw new Error('Pad never loaded'); return await helper.aNewPad(opts); From 7653dc650d3ce1bda1bdba8c19c15e207e599b85 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 15 Sep 2021 23:59:17 -0400 Subject: [PATCH 136/322] settings: Use a log4js logger instead of `console` --- src/node/utils/Settings.js | 75 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 40576c345..7ecd9f197 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -39,9 +39,11 @@ const suppressDisableMsg = ' -- To suppress these warning messages change ' + 'suppressErrorsInPadText to true in your settings.json\n'; const _ = require('underscore'); +const logger = log4js.getLogger('settings'); + /* Root path of the installation */ exports.root = absolutePaths.findEtherpadRoot(); -console.log('All relative paths will be interpreted relative to the identified ' + +logger.info('All relative paths will be interpreted relative to the identified ' + `Etherpad base dir: ${exports.root}`); /** @@ -450,7 +452,7 @@ exports.getGitCommit = () => { } version = version.substring(0, 7); } catch (e) { - console.warn(`Can't get git version for server header\n${e.message}`); + logger.warn(`Can't get git version for server header\n${e.message}`); } return version; }; @@ -469,7 +471,7 @@ const storeSettings = (settingsObj) => { for (const i of Object.keys(settingsObj || {})) { // test if the setting starts with a lowercase character if (i.charAt(0).search('[a-z]') !== 0) { - console.warn(`Settings should start with a lowercase character: '${i}'`); + logger.warn(`Settings should start with a lowercase character: '${i}'`); } // we know this setting, so we overwrite it @@ -482,7 +484,7 @@ const storeSettings = (settingsObj) => { } } else { // this setting is unknown, output a warning and throw it away - console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); + logger.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); } } }; @@ -598,10 +600,10 @@ const lookupEnvironmentVariables = (obj) => { const defaultValue = match[3]; if ((envVarValue === undefined) && (defaultValue === undefined)) { - console.warn(`Environment variable "${envVarName}" does not contain any value for ` + - `configuration key "${key}", and no default was given. Using null. ` + - 'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' + - 'explicitly use "null" as the default if you want to continue to use null.'); + logger.warn(`Environment variable "${envVarName}" does not contain any value for ` + + `configuration key "${key}", and no default was given. Using null. ` + + 'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' + + 'explicitly use "null" as the default if you want to continue to use null.'); /* * We have to return null, because if we just returned undefined, the @@ -611,8 +613,8 @@ const lookupEnvironmentVariables = (obj) => { } if ((envVarValue === undefined) && (defaultValue !== undefined)) { - console.debug(`Environment variable "${envVarName}" not found for ` + - `configuration key "${key}". Falling back to default value.`); + logger.debug(`Environment variable "${envVarName}" not found for ` + + `configuration key "${key}". Falling back to default value.`); return coerceValue(defaultValue); } @@ -623,7 +625,7 @@ const lookupEnvironmentVariables = (obj) => { * For numeric and boolean strings let's convert it to proper types before * returning it, in order to maintain backward compatibility. */ - console.debug( + logger.debug( `Configuration key "${key}" will be read from environment variable "${envVarName}"`); return coerceValue(envVarValue); @@ -650,11 +652,11 @@ const parseSettings = (settingsFilename, isSettings) => { if (isSettings) { settingsType = 'settings'; notFoundMessage = 'Continuing using defaults!'; - notFoundFunction = console.warn; + notFoundFunction = logger.warn.bind(logger); } else { settingsType = 'credentials'; notFoundMessage = 'Ignoring.'; - notFoundFunction = console.info; + notFoundFunction = logger.info.bind(logger); } try { @@ -672,14 +674,14 @@ const parseSettings = (settingsFilename, isSettings) => { const settings = JSON.parse(settingsStr); - console.info(`${settingsType} loaded from: ${settingsFilename}`); + logger.info(`${settingsType} loaded from: ${settingsFilename}`); const replacedSettings = lookupEnvironmentVariables(settings); return replacedSettings; } catch (e) { - console.error(`There was an error processing your ${settingsType} ` + - `file from ${settingsFilename}: ${e.message}`); + logger.error(`There was an error processing your ${settingsType} ` + + `file from ${settingsFilename}: ${e.message}`); process.exit(1); } @@ -706,8 +708,8 @@ exports.reloadSettings = () => { log4js.replaceConsole(); if (!exports.skinName) { - console.warn('No "skinName" parameter found. Please check out settings.json.template and ' + - 'update your settings.json. Falling back to the default "colibris".'); + logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + + 'update your settings.json. Falling back to the default "colibris".'); exports.skinName = 'colibris'; } @@ -717,8 +719,8 @@ exports.reloadSettings = () => { const countPieces = exports.skinName.split(path.sep).length; if (countPieces !== 1) { - console.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + - `not valid: "${exports.skinName}". Falling back to the default "colibris".`); + logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + + `not valid: "${exports.skinName}". Falling back to the default "colibris".`); exports.skinName = 'colibris'; } @@ -728,21 +730,20 @@ exports.reloadSettings = () => { // what if someone sets skinName == ".." or "."? We catch him! if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { - console.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + - 'Falling back to the default "colibris".'); + logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + + 'Falling back to the default "colibris".'); exports.skinName = 'colibris'; skinPath = path.join(skinBasePath, exports.skinName); } if (fs.existsSync(skinPath) === false) { - console.error( - `Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); + logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); exports.skinName = 'colibris'; skinPath = path.join(skinBasePath, exports.skinName); } - console.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); + logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); } if (exports.abiword) { @@ -754,7 +755,7 @@ exports.reloadSettings = () => { if (!exports.suppressErrorsInPadText) { exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; } - console.error(`${abiwordError} File location: ${exports.abiword}`); + logger.error(`${abiwordError} File location: ${exports.abiword}`); exports.abiword = null; } }); @@ -770,7 +771,7 @@ exports.reloadSettings = () => { if (!exports.suppressErrorsInPadText) { exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; } - console.error(`${sofficeError} File location: ${exports.soffice}`); + logger.error(`${sofficeError} File location: ${exports.soffice}`); exports.soffice = null; } }); @@ -780,18 +781,18 @@ exports.reloadSettings = () => { const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); try { exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); - console.info(`Session key loaded from: ${sessionkeyFilename}`); + logger.info(`Session key loaded from: ${sessionkeyFilename}`); } catch (e) { - console.info( + logger.info( `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); exports.sessionKey = randomString(32); fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); } } else { - console.warn('Declaring the sessionKey in the settings.json is deprecated. ' + - 'This value is auto-generated now. Please remove the setting from the file. -- ' + - 'If you are seeing this error after restarting using the Admin User ' + - 'Interface then you can ignore this message.'); + logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' + + 'This value is auto-generated now. Please remove the setting from the file. -- ' + + 'If you are seeing this error after restarting using the Admin User ' + + 'Interface then you can ignore this message.'); } if (exports.dbType === 'dirty') { @@ -801,13 +802,13 @@ exports.reloadSettings = () => { } exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - console.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); + logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); } if (exports.ip === '') { // using Unix socket for connectivity - console.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + - '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); + logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + + '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); } /* @@ -822,7 +823,7 @@ exports.reloadSettings = () => { * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead */ exports.randomVersionString = randomString(4); - console.log(`Random string used for versioning assets: ${exports.randomVersionString}`); + logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); }; exports.exportedForTestingOnly = { From b3b6c94b761b2db10b690d72055f7c71f1922807 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 23 Sep 2021 03:05:42 -0400 Subject: [PATCH 137/322] log4js: Initialize as early as possible --- src/node/utils/Settings.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 7ecd9f197..78235de0e 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -41,6 +41,21 @@ const _ = require('underscore'); const logger = log4js.getLogger('settings'); +// This is a function to make it easy to create a new instance. It is important to not reuse a +// config object after passing it to log4js.configure() because that method mutates the object. :( +const defaultLogConfig = () => ({appenders: [{type: 'console'}]}); +const defaultLogLevel = 'INFO'; + +const initLogging = (logLevel, config) => { + log4js.configure(config); + log4js.setGlobalLogLevel(logLevel); + log4js.replaceConsole(); +}; + +// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized +// with the user's chosen log level and logger config after the settings have been loaded. +initLogging(defaultLogLevel, defaultLogConfig()); + /* Root path of the installation */ exports.root = absolutePaths.findEtherpadRoot(); logger.info('All relative paths will be interpreted relative to the identified ' + @@ -236,7 +251,7 @@ exports.allowUnknownFileEnds = true; /** * The log level of log4js */ -exports.loglevel = 'INFO'; +exports.loglevel = defaultLogLevel; /** * Disable IP logging @@ -266,7 +281,7 @@ exports.indentationOnNewLine = true; /* * log4js appender configuration */ -exports.logconfig = {appenders: [{type: 'console'}]}; +exports.logconfig = defaultLogConfig(); /* * Session Key, do not sure this. @@ -703,9 +718,7 @@ exports.reloadSettings = () => { storeSettings(settings); storeSettings(credentials); - log4js.configure(exports.logconfig);// Configure the logging appenders - log4js.setGlobalLogLevel(exports.loglevel);// set loglevel - log4js.replaceConsole(); + initLogging(exports.loglevel, exports.logconfig); if (!exports.skinName) { logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + From aec619cc0bd043e8a921b599d1deecd2ef09b898 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 16 Sep 2021 00:24:40 -0400 Subject: [PATCH 138/322] log4js: Deprecate the `logconfig` setting This will make it possible to upgrade log4js in a future version. --- CHANGELOG.md | 1 + settings.json.docker | 52 -------------------------------------- settings.json.template | 52 -------------------------------------- src/node/utils/Settings.js | 5 ++++ src/package.json | 1 + 5 files changed, 7 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f3a4ca5..14114580a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Compatibility changes +* The `logconfig` setting is deprecated. * For plugin authors: * Etherpad now uses [jsdom](https://github.com/jsdom/jsdom) instead of [cheerio](https://cheerio.js.org/) for processing HTML imports. There are diff --git a/settings.json.docker b/settings.json.docker index 40872339d..ed1be901d 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -586,58 +586,6 @@ */ "loglevel": "${LOGLEVEL:INFO}", - /* - * Logging configuration. See log4js documentation for further information: - * https://github.com/nomiddlename/log4js-node - * - * You can add as many appenders as you want here. - */ - "logconfig" : - { "appenders": [ - { "type": "console" - //, "category": "access"// only logs pad access - } - - /* - , { "type": "file" - , "filename": "your-log-file-here.log" - , "maxLogSize": 1024 - , "backups": 3 // how many log files there're gonna be at max - //, "category": "test" // only log a specific category - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "warn" // filters out all log messages that have a lower level than "error" - , "appender": - { Use whatever appender you want here } - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "error" // filters out all log messages that have a lower level than "error" - , "appender": - { "type": "smtp" - , "subject": "An error occurred in your EPL instance!" - , "recipients": "bar@blurdybloop.com, baz@blurdybloop.com" - , "sendInterval": 300 // 60 * 5 = 5 minutes -- will buffer log messages; set to 0 to send a mail for every message - , "transport": "SMTP", "SMTP": { // see https://github.com/andris9/Nodemailer#possible-transport-methods - "host": "smtp.example.com", "port": 465, - "secureConnection": true, - "auth": { - "user": "foo@example.com", - "pass": "bar_foo" - } - } - } - } - */ - - ] - }, // logconfig - /* Override any strings found in locale directories */ "customLocaleStrings": {} } diff --git a/settings.json.template b/settings.json.template index 2d7f119a2..8b8766be8 100644 --- a/settings.json.template +++ b/settings.json.template @@ -590,58 +590,6 @@ */ "loglevel": "INFO", - /* - * Logging configuration. See log4js documentation for further information: - * https://github.com/nomiddlename/log4js-node - * - * You can add as many appenders as you want here. - */ - "logconfig" : - { "appenders": [ - { "type": "console" - //, "category": "access"// only logs pad access - } - - /* - , { "type": "file" - , "filename": "your-log-file-here.log" - , "maxLogSize": 1024 - , "backups": 3 // how many log files there're gonna be at max - //, "category": "test" // only log a specific category - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "warn" // filters out all log messages that have a lower level than "error" - , "appender": - { Use whatever appender you want here } - } - */ - - /* - , { "type": "logLevelFilter" - , "level": "error" // filters out all log messages that have a lower level than "error" - , "appender": - { "type": "smtp" - , "subject": "An error occurred in your EPL instance!" - , "recipients": "bar@blurdybloop.com, baz@blurdybloop.com" - , "sendInterval": 300 // 60 * 5 = 5 minutes -- will buffer log messages; set to 0 to send a mail for every message - , "transport": "SMTP", "SMTP": { // see https://github.com/andris9/Nodemailer#possible-transport-methods - "host": "smtp.example.com", "port": 465, - "secureConnection": true, - "auth": { - "user": "foo@example.com", - "pass": "bar_foo" - } - } - } - } - */ - - ] - }, // logconfig - /* Override any strings found in locale directories */ "customLocaleStrings": {}, diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 78235de0e..1a8bb000e 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -28,6 +28,7 @@ */ const absolutePaths = require('./AbsolutePaths'); +const deepEqual = require('fast-deep-equal/es6'); const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -47,9 +48,13 @@ const defaultLogConfig = () => ({appenders: [{type: 'console'}]}); const defaultLogLevel = 'INFO'; const initLogging = (logLevel, config) => { + // log4js.configure() modifies exports.logconfig so check for equality first. + const logConfigIsDefault = deepEqual(config, defaultLogConfig()); log4js.configure(config); log4js.setGlobalLogLevel(logLevel); log4js.replaceConsole(); + // Log the warning after configuring log4js to increase the chances the user will see it. + if (!logConfigIsDefault) logger.warn('The logconfig setting is deprecated.'); }; // Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized diff --git a/src/package.json b/src/package.json index 8e1f17414..8e441058a 100644 --- a/src/package.json +++ b/src/package.json @@ -41,6 +41,7 @@ "express": "4.17.1", "express-rate-limit": "5.3.0", "express-session": "1.17.2", + "fast-deep-equal": "^3.1.3", "find-root": "1.1.0", "formidable": "1.2.2", "http-errors": "1.8.0", From 0e311184cffa8708bae04ebed69b0093d5a0c2b4 Mon Sep 17 00:00:00 2001 From: Hossein Date: Tue, 28 Sep 2021 20:46:42 +0330 Subject: [PATCH 139/322] fix: change directory to etherpad root --- src/bin/installOnWindows.bat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/installOnWindows.bat b/src/bin/installOnWindows.bat index 3c6bf58c0..971335c20 100644 --- a/src/bin/installOnWindows.bat +++ b/src/bin/installOnWindows.bat @@ -1,7 +1,7 @@ @echo off :: Change directory to etherpad-lite root -cd /D "%~dp0\.." +cd /D "%~dp0\..\.." :: Is node installed? cmd /C node -e "" || ( echo "Please install node.js ( https://nodejs.org )" && exit /B 1 ) @@ -16,7 +16,7 @@ mklink /D "ep_etherpad-lite" "..\src" cd /D "ep_etherpad-lite" cmd /C npm ci || exit /B 1 -cd /D "%~dp0\.." +cd /D "%~dp0\..\.." echo _ echo Clearing cache... From d262e31bbf7243e0d8ab915428929413e47f2b0b Mon Sep 17 00:00:00 2001 From: Hossein Date: Tue, 28 Sep 2021 20:48:51 +0330 Subject: [PATCH 140/322] fix: install all dependencies and symlink --- .github/workflows/backend-tests.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index e88b32731..9e33cbe10 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -116,9 +116,7 @@ jobs: node-version: 12 - name: Install all dependencies and symlink for ep_etherpad-lite - run: | - cd src - npm ci --no-optional + run: src/bin/installOnWindows.bat - name: Fix up the settings.json run: | @@ -172,9 +170,7 @@ jobs: # if npm correctly hoists the dependencies, the hoisting seems to confuse # tools such as `npm outdated`, `npm update`, and some ESLint rules. - name: Install all dependencies and symlink for ep_etherpad-lite - run: | - cd src - npm ci --no-optional + run: src/bin/installOnWindows.bat - name: Fix up the settings.json run: | From cc6fda691657ef544ed866967d50302f16fbab78 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 16 Jul 2021 20:46:04 +0200 Subject: [PATCH 141/322] adminsettings test: Fix restart detection We cannot guarantee that the system time on SauceLabs and Github is in sync. In case the SauceLabs runner's clock is slow the test would have failed. --- src/tests/frontend/specs/adminsettings.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/frontend/specs/adminsettings.js b/src/tests/frontend/specs/adminsettings.js index 3eb6e41a6..2ac5f8e0b 100644 --- a/src/tests/frontend/specs/adminsettings.js +++ b/src/tests/frontend/specs/adminsettings.js @@ -69,15 +69,15 @@ describe('Admin > Settings', function () { return null; } }; + let oldStartTime; await helper.waitForPromise(async () => { - const startTime = await getStartTime(); - return startTime != null && startTime > 0 && Date.now() > startTime; + oldStartTime = await getStartTime(); + return oldStartTime != null && oldStartTime > 0; }, 1000, 500); - const clickTime = Date.now(); helper.admin$('#restartEtherpad').click(); await helper.waitForPromise(async () => { const startTime = await getStartTime(); - return startTime != null && startTime >= clickTime; + return startTime != null && startTime > oldStartTime; }, 60000, 500); }); }); From 91d3974a0dec07acf4b1c088d99afd4424ef19be Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 16 Jul 2021 20:46:04 +0200 Subject: [PATCH 142/322] adminsettings test: Be a little stricter for some assertions --- src/tests/frontend/specs/adminsettings.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tests/frontend/specs/adminsettings.js b/src/tests/frontend/specs/adminsettings.js index 2ac5f8e0b..da9904ccd 100644 --- a/src/tests/frontend/specs/adminsettings.js +++ b/src/tests/frontend/specs/adminsettings.js @@ -38,7 +38,8 @@ describe('Admin > Settings', function () { // reset it to the old value helper.newAdmin('settings'); await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.settings').val().length > 0, 20000); + () => helper.admin$ && + helper.admin$('.settings').val().length === settingsLength + 11, 20000); // replace the test value with a line break helper.admin$('.settings').val((_, text) => text.replace('/* test */\n', '')); @@ -50,12 +51,11 @@ describe('Admin > Settings', function () { // settings should have the old value helper.newAdmin('settings'); await helper.waitForPromise( - () => helper.admin$ && helper.admin$('.settings').val().length > 0, 36000); - expect(settings).to.be(helper.admin$('.settings').val()); + () => helper.admin$ && helper.admin$('.settings').val().length === settingsLength && + settings === helper.admin$('.settings').val(), 20000); }); it('restart works', async function () { - this.timeout(60000); const getStartTime = async () => { try { const {httpStartTime} = await $.ajax({ @@ -66,6 +66,8 @@ describe('Admin > Settings', function () { }); return httpStartTime; } catch (err) { + document.getElementById('console').append( + `an error occurred: ${err.message} of type ${err.name}\n`); return null; } }; From d3890bc2c26642d69077a22648e8b597af245d7b Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Mon, 19 Jul 2021 03:59:58 +0200 Subject: [PATCH 143/322] admin tests: Increase `maxHttpBufferSize` to fit `settings.json` --- .github/workflows/frontend-admin-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 44cb697f2..5fb0f39c2 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -57,6 +57,9 @@ jobs: - name: Write custom settings.json that enables the Admin UI tests run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json" + - name: increase maxHttpBufferSize + run: "sed -i 's/\"maxHttpBufferSize\": 10000/\"maxHttpBufferSize\": 100000/' settings.json" + - name: Remove standard frontend test files, so only admin tests are run run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs From bb0ca91dc2bc0dcfd9cb571f0804fded041beb55 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Mon, 19 Jul 2021 07:39:49 +0200 Subject: [PATCH 144/322] adminupdateplugins test: swap assertions, increase timeout --- src/tests/frontend/specs/adminupdateplugins.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend/specs/adminupdateplugins.js b/src/tests/frontend/specs/adminupdateplugins.js index 6bcf9cafc..7ab472858 100755 --- a/src/tests/frontend/specs/adminupdateplugins.js +++ b/src/tests/frontend/specs/adminupdateplugins.js @@ -28,8 +28,8 @@ describe('Plugins page', function () { it('Searches for plugin', async function () { helper.admin$('#search-query').val('ep_font_color'); - await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 5000); - await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 5000); + await helper.waitForPromise(() => helper.admin$('.results').children().length > 0, 10000); + await helper.waitForPromise(() => helper.admin$('.results').children().length < 300, 10000); }); it('Attempt to Update a plugin', async function () { From 0de41ee0874162d1e820528115b67747969d0609 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Mon, 19 Jul 2021 21:42:38 +0200 Subject: [PATCH 145/322] adminsettings test: Fix save detection race condition Use MutationObserver to detect if a saveProgress event was received, which will trigger an animation. Before this, `helper.admin$('#response').is(':visible')` was true after the page loaded and before clicking the Save button, so there was a possibility that after clicking Save, but before sending the socketio message to the server, the visibility is checked and returns true, so the page gets reloaded before the changed settings have been saved. --- src/tests/frontend/specs/adminsettings.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/tests/frontend/specs/adminsettings.js b/src/tests/frontend/specs/adminsettings.js index da9904ccd..a2ee02046 100644 --- a/src/tests/frontend/specs/adminsettings.js +++ b/src/tests/frontend/specs/adminsettings.js @@ -21,6 +21,16 @@ describe('Admin > Settings', function () { }); it('Are Settings visible, populated, does save work', async function () { + const save = async () => { + const p = new Promise((resolve) => { + const observer = new MutationObserver(() => { resolve(); observer.disconnect(); }); + observer.observe( + helper.admin$('#response')[0], {attributes: true, childList: false, subtree: false}); + }); + helper.admin$('#saveSettings').click(); + await p; + }; + // save old value const settings = helper.admin$('.settings').val(); const settingsLength = settings.length; @@ -29,10 +39,7 @@ describe('Admin > Settings', function () { helper.admin$('.settings').val((_, text) => `/* test */\n${text}`); await helper.waitForPromise( () => settingsLength + 11 === helper.admin$('.settings').val().length, 5000); - - // saves - helper.admin$('#saveSettings').click(); - await helper.waitForPromise(() => helper.admin$('#response').is(':visible'), 5000); + await save(); // new value for settings.json should now be saved // reset it to the old value @@ -44,9 +51,7 @@ describe('Admin > Settings', function () { // replace the test value with a line break helper.admin$('.settings').val((_, text) => text.replace('/* test */\n', '')); await helper.waitForPromise(() => settingsLength === helper.admin$('.settings').val().length); - - helper.admin$('#saveSettings').click(); // saves - await helper.waitForPromise(() => helper.admin$('#response').is(':visible')); + await save(); // settings should have the old value helper.newAdmin('settings'); From 4d8ae3475dfb6f7da67cd73f41609bf7b6249bdd Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sat, 23 Jan 2016 12:57:48 +0100 Subject: [PATCH 146/322] Changeset: Add documentation for `textLinesMutator()` --- src/static/js/Changeset.js | 109 ++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index d985e504f..7cfb48ce2 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -550,9 +550,15 @@ exports.textLinesMutator = (lines) => { // is not actually a newline, but for the purposes of N and L values, // the caller should pretend it is, and for things to work right in that case, the input // to insert() should be a single line with no newlines. + + // The splice holds information which lines are to be deleted or changed. + // curSplice[0] is an index into the lines array + // curSplice[1] is the number of lines that will be removed from lines + // the other elements represent mutated (changed by ops) lines or new lines (added by ops) const curSplice = [0, 0]; let inSplice = false; - // position in document after curSplice is applied: + + // position in lines after curSplice is applied: let curLine = 0; let curCol = 0; // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && @@ -560,12 +566,21 @@ exports.textLinesMutator = (lines) => { // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then // curCol == 0 + /** + * Adds and/or removes entries at a specific offset in lines array + * It is called when leaving the splice + * @param {Array} s curSplice + */ const lines_applySplice = (s) => { lines.splice.apply(lines, s); }; const lines_toSource = () => lines.toSource(); + /** + * Get a line from lines at given index + * @param {Number} idx an index + */ const lines_get = (idx) => { if (lines.get) { return lines.get(idx); @@ -575,6 +590,11 @@ exports.textLinesMutator = (lines) => { }; // can be unimplemented if removeLines's return value not needed + /** + * Return a slice from lines array + * @param {Number} start the start index + * @param {Number} end the end index + */ const lines_slice = (start, end) => { if (lines.slice) { return lines.slice(start, end); @@ -583,6 +603,9 @@ exports.textLinesMutator = (lines) => { } }; + /** + * Return the length of lines array + */ const lines_length = () => { if ((typeof lines.length) === 'number') { return lines.length; @@ -591,15 +614,25 @@ exports.textLinesMutator = (lines) => { } }; + /** + * Starts a new splice. + */ const enterSplice = () => { curSplice[0] = curLine; curSplice[1] = 0; + // TODO(doc) when is this the case? + // check all enterSplice calls and changes to curCol if (curCol > 0) { putCurLineInSplice(); } inSplice = true; }; + /** + * Changes the lines array according to the values in curSplice + * and resets curSplice. + * This is called via close or TODO(doc) + */ const leaveSplice = () => { lines_applySplice(curSplice); curSplice.length = 2; @@ -607,26 +640,45 @@ exports.textLinesMutator = (lines) => { inSplice = false; }; + /** + * Indicates if curLine is already in the splice. This is necessary because the last element in + * curSplice is curLine when this line is currently worked on (e.g. when skipping are inserting) + * + * TODO(doc) why aren't removals considered? + * @returns {Boolean} true if curLine is in splice + */ const isCurLineInSplice = () => (curLine - curSplice[0] < (curSplice.length - 2)); const debugPrint = (typ) => { /* eslint-disable-line no-unused-vars */ print(`${typ}: ${curSplice.toSource()} / ${curLine},${curCol} / ${lines_toSource()}`); }; + /** + * Incorporates current line into the splice + * and marks its old position to be deleted. + * + * @returns {Number} the index of the added line in curSplice + */ const putCurLineInSplice = () => { if (!isCurLineInSplice()) { curSplice.push(lines_get(curSplice[0] + curSplice[1])); curSplice[1]++; } - return 2 + curLine - curSplice[0]; + return 2 + curLine - curSplice[0]; // TODO should be the same as curSplice.length - 1 }; + /** + * It will skip some newlines by putting them into the splice. + * + * @param {Boolean} includeInSplice indicates if attributes are present + */ const skipLines = (L, includeInSplice) => { if (L) { if (includeInSplice) { if (!inSplice) { enterSplice(); } + // TODO(doc) should this count the number of characters that are skipped to check? for (let i = 0; i < L; i++) { curCol = 0; putCurLineInSplice(); @@ -635,6 +687,7 @@ exports.textLinesMutator = (lines) => { } else { if (inSplice) { if (L > 1) { + // TODO(doc) figure out why single lines are incorporated into splice instead of ignored leaveSplice(); } else { putCurLineInSplice(); @@ -647,6 +700,13 @@ exports.textLinesMutator = (lines) => { } }; + /** + * Skip some characters. Can contain newlines. + * + * @param {Number} N number of characters to skip + * @param {Number} L number of newlines to skip + * @param {Boolean} includeInSplice indicates if attributes are present + */ const skip = (N, L, includeInSplice) => { if (N) { if (L) { @@ -656,6 +716,8 @@ exports.textLinesMutator = (lines) => { enterSplice(); } if (inSplice) { + // although the line is put into splice curLine is not increased, because + // only some chars are skipped, not the whole line putCurLineInSplice(); } curCol += N; @@ -663,6 +725,11 @@ exports.textLinesMutator = (lines) => { } }; + /** + * Remove whole lines from lines array + * + * @param {Number} L number of lines to be removed + */ const removeLines = (L) => { let removed = ''; if (L) { @@ -670,6 +737,12 @@ exports.textLinesMutator = (lines) => { enterSplice(); } + /** + * Gets a string of joined lines after the end of the splice + * + * @param k {Number} number of lines + * @returns {String} joined lines + */ const nextKLinesText = (k) => { const m = curSplice[0] + curSplice[1]; return lines_slice(m, m + k).join(''); @@ -697,6 +770,12 @@ exports.textLinesMutator = (lines) => { return removed; }; + /** + * Remove text from lines array + * + * @param N {Number} characters to delete + * @param L {Number} lines to delete + */ const remove = (N, L) => { let removed = ''; if (N) { @@ -706,6 +785,8 @@ exports.textLinesMutator = (lines) => { if (!inSplice) { enterSplice(); } + // although the line is put into splice, curLine is not increased, because + // only some chars are removed not the whole line const sline = putCurLineInSplice(); removed = curSplice[sline].substring(curCol, curCol + N); curSplice[sline] = curSplice[sline].substring(0, curCol) + @@ -715,6 +796,12 @@ exports.textLinesMutator = (lines) => { return removed; }; + /** + * Inserts text into lines array. + * + * @param text {String} the text to insert + * @param L {Number} number of newlines in text + */ const insert = (text, L) => { if (text) { if (!inSplice) { @@ -726,18 +813,25 @@ exports.textLinesMutator = (lines) => { const sline = curSplice.length - 1; const theLine = curSplice[sline]; const lineCol = curCol; + // insert the first new line curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; curLine++; newLines.splice(0, 1); + // insert the remaining new lines Array.prototype.push.apply(curSplice, newLines); curLine += newLines.length; + // insert the remaining chars from the "old" line (e.g. the line we were in + // when we started to insert new lines) curSplice.push(theLine.substring(lineCol)); - curCol = 0; + curCol = 0; // TODO(doc) why is this not set to the length of last line? } else { Array.prototype.push.apply(curSplice, newLines); curLine += newLines.length; } } else { + // there are no additional lines + // although the line is put into splice, curLine is not increased, because + // there may be more chars in the line (newline is not reached) const sline = putCurLineInSplice(); if (!curSplice[sline]) { console.error('curSplice[sline] not populated, actual curSplice contents is ', curSplice, '. Possibly related to https://github.com/ether/etherpad-lite/issues/2802'); @@ -749,6 +843,12 @@ exports.textLinesMutator = (lines) => { } }; + /** + * Checks if curLine (the line we are in when curSplice is applied) is the last line + * in lines. + * + * @return {Boolean} indicates if there are lines left + */ const hasMore = () => { let docLines = lines_length(); if (inSplice) { @@ -757,6 +857,9 @@ exports.textLinesMutator = (lines) => { return curLine < docLines; }; + /** + * Closes the splice + */ const close = () => { if (inSplice) { leaveSplice(); From 0c31940b09d4f0dbd2afd45ae2ce6d3f55b4b4b0 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 30 Sep 2021 13:02:24 +0200 Subject: [PATCH 147/322] Localisation updates from https://translatewiki.net. --- src/locales/my.json | 169 ++++++++++++++++++++++++++++++++++++++++++++ src/locales/sw.json | 169 ++++++++++++++++++++++++++++++++++++++++++++ src/locales/th.json | 36 ++++++++++ 3 files changed, 374 insertions(+) create mode 100644 src/locales/my.json create mode 100644 src/locales/sw.json diff --git a/src/locales/my.json b/src/locales/my.json new file mode 100644 index 000000000..d427bbcb5 --- /dev/null +++ b/src/locales/my.json @@ -0,0 +1,169 @@ +{ + "@metadata": { + "authors": [ + "Andibecker", + "Dr Lotus Black" + ] + }, + "admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad", + "admin_plugins": "ပလပ်အင်မန်နေဂျာ", + "admin_plugins.available": "ရနိုင်သော plugins များ", + "admin_plugins.available_not-found": "ပလပ်အင်များမတွေ့ပါ။", + "admin_plugins.available_fetching": "ရယူနေသည်…", + "admin_plugins.available_install.value": "အင်စတော လုပ်ပါ", + "admin_plugins.available_search.placeholder": "အင်စတောလုပ်ဖို့ plugins များကိုရှာပါ", + "admin_plugins.description": "ဖော်ပြချက်", + "admin_plugins.installed": "plugins များထည့်သွင်းထားသည်", + "admin_plugins.installed_fetching": "ထည့်သွင်းထားသောပလပ်အင်များကိုရယူနေသည်…", + "admin_plugins.installed_nothing": "သင်မည်သည့် plugins ကိုမျှမထည့်သွင်းရသေးပါ။", + "admin_plugins.installed_uninstall.value": "ဖြုတ်ပါ", + "admin_plugins.last-update": "နောက်ဆုံးအပ်ဒိတ်", + "admin_plugins.name": "နာမည်", + "admin_plugins.page-title": "ပလပ်အင်မန်နေဂျာ - Etherpad", + "admin_plugins.version": "ဗားရှင်း", + "admin_plugins_info": "သတင်းအချက်အလက်ပြဿနာဖြေရှင်းခြင်း", + "admin_plugins_info.hooks": "ချိတ်များတပ်ဆင်ထားသည်", + "admin_plugins_info.hooks_client": "Client-side ချိတ်", + "admin_plugins_info.hooks_server": "Server-side ချိတ်", + "admin_plugins_info.parts": "တပ်ဆင်ထားသော အစိတ်အပိုင်းများ", + "admin_plugins_info.plugins": "plugins များထည့်သွင်းထားသည်", + "admin_plugins_info.page-title": "ပလပ်အင်အချက်အလက် - Etherpad", + "admin_plugins_info.version": "Etherpad ဗားရှင်း", + "admin_plugins_info.version_latest": "နောက်ဆုံးရနိုင်သောဗားရှင်း", + "admin_plugins_info.version_number": "ဗားရှင်းနံပါတ်", + "admin_settings": "အပြင်အဆင်များ", + "admin_settings.current": "လက်ရှိဖွဲ့စည်းမှု", + "admin_settings.current_example-devel": "နမူနာဖွံ့ဖြိုးတိုးတက်မှုဆက်တင်နမူနာ", + "admin_settings.current_example-prod": "နမူနာထုတ်လုပ်မှုဆက်တင်ပုံစံ", + "admin_settings.current_restart.value": "Etherpad ကိုပြန်လည်စတင်ပါ", + "admin_settings.current_save.value": "ဆက်တင်များကိုသိမ်းပါ", + "admin_settings.page-title": "ဆက်တင်များ - Etherpad", + "index.newPad": "Pad အသစ်", + "index.createOpenPad": "သို့မဟုတ် Pad နှင့်နာမည်ဖွင့်ပါ။", + "index.openPad": "ရှိပြီးသား Pad ကိုနာမည်နှင့်ဖွင့်ပါ။", + "pad.toolbar.bold.title": "စာလုံးအကြီး (Ctrl+B)", + "pad.toolbar.italic.title": "စာလုံးစောင်း (Ctrl+I)", + "pad.toolbar.underline.title": "မျဉ်းသားရန် (Ctrl+U)", + "pad.toolbar.strikethrough.title": "ဖြတ်တောက်ခြင်း (Ctrl+5)", + "pad.toolbar.ol.title": "အမှာစာစာရင်း (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Unordered စာရင်း (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "အင်တင်း (TAB)", + "pad.toolbar.unindent.title": "အပြင် (Shift+TAB)", + "pad.toolbar.undo.title": "ပြန်လုပ်ရန် (Ctrl+Z)", + "pad.toolbar.redo.title": "ပြန်လုပ်ရန် (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "စာရေးသူအရောင်များကိုရှင်းလင်းပါ (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "ကွဲပြားခြားနားသောဖိုင်အမျိုးအစားများမှ/သွင်းကုန်/တင်ပို့ပါ", + "pad.toolbar.timeslider.title": "Timeslider", + "pad.toolbar.savedRevision.title": "ပြန်လည်တည်းဖြတ်ပါ", + "pad.toolbar.settings.title": "အပြင်အဆင်များ", + "pad.toolbar.embed.title": "ဒီ pad ကို Share လုပ်ပြီးမြှုပ်လိုက်ပါ", + "pad.toolbar.showusers.title": "ဤ pad ပေါ်တွင်အသုံးပြုသူများကိုပြပါ", + "pad.colorpicker.save": "သိမ်းရန်", + "pad.colorpicker.cancel": "မလုပ်တော့ပါ", + "pad.loading": "ဝန်ဆွဲတင်နေသည်...", + "pad.noCookie": "ကွတ်ကီးကိုရှာမတွေ့ပါ။ ကျေးဇူးပြု၍ သင်၏ browser တွင် cookies များကိုခွင့်ပြုပါ။ လည်ပတ်မှုများအကြားသင်၏အစည်းအဝေးနှင့်ဆက်တင်များကိုသိမ်းဆည်းမည်မဟုတ်ပါ။ ၎င်းသည်အချို့သောဘရောင်ဇာများတွင် iFrame တွင် iFrame တွင်ထည့်သွင်းခံရခြင်းကြောင့်ဖြစ်နိုင်သည်။ Etherpad သည် parent iFrame ကဲ့သို့တူညီသော subdomain/domain ပေါ်တွင်သေချာပါစေ", + "pad.permissionDenied": "သင်ဤ pad ကိုသုံးခွင့်မရှိပါ", + "pad.settings.padSettings": "Pad ဆက်တင်များ", + "pad.settings.myView": "ငါ့အမြင်", + "pad.settings.stickychat": "ဖန်သားပြင်ပေါ်တွင်အမြဲစကားပြောပါ", + "pad.settings.chatandusers": "ချတ်နှင့်အသုံးပြုသူများကိုပြပါ", + "pad.settings.colorcheck": "စာရေးသူအရောင်များ", + "pad.settings.linenocheck": "လိုင်းနံပါတ်များ", + "pad.settings.rtlcheck": "အကြောင်းအရာကိုညာမှဘယ်သို့ဖတ်ပါ။", + "pad.settings.fontType": "ဖောင့်အမျိုးအစား", + "pad.settings.fontType.normal": "သာမန်", + "pad.settings.language": "ဘာသာစကား:", + "pad.settings.about": "အကြောင်း", + "pad.settings.poweredBy": "မှပံ့ပိုးသည်", + "pad.importExport.import_export": "သွင်းကုန်/ပို့ကုန်", + "pad.importExport.import": "မည်သည့်စာသားဖိုင်သို့မဆိုစာရွက်စာတမ်းတင်ပါ", + "pad.importExport.importSuccessful": "အောင်မြင်သည်။", + "pad.importExport.export": "လက်ရှိ pad ကိုအောက်ပါအတိုင်းတင်ပို့ပါ။", + "pad.importExport.exportetherpad": "Etherpad ပါ", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "ရိုးရိုးစာသား", + "pad.importExport.exportword": "Microsoft Word", + "pad.importExport.exportpdf": "ပီဒီအက်ဖ်", + "pad.importExport.exportopen": "ODF (စာရွက်စာတမ်းဖွင့်ပုံစံ)", + "pad.importExport.abiword.innerHTML": "သင်ရိုးရိုးစာသားများ (သို့) HTML ပုံစံများဖြင့်သာတင်သွင်းနိုင်သည်။ ပိုမိုအဆင့်မြင့်သောသွင်းကုန်အင်္ဂါရပ်များအတွက် ကျေးဇူးပြု၍ ကျေးဇူးပြု၍ AbiWord သို့မဟုတ် LibreOffice ကို install လုပ်ပါ။", + "pad.modals.connected": "ချိတ်ဆက်ထားသည်။", + "pad.modals.reconnecting": "သင်၏ pad သို့ပြန်လည်ချိတ်ဆက်နေသည်…", + "pad.modals.forcereconnect": "ပြန်လည်ချိတ်ဆက်ခိုင်းပါ", + "pad.modals.reconnecttimer": "ပြန်လည်ချိတ်ဆက်ရန်ကြိုးစားနေသည်", + "pad.modals.cancel": "မလုပ်တော့ပါ", + "pad.modals.userdup": "ပယ်ဖျက်", + "pad.modals.userdup.explanation": "ဤ pad ကိုဤကွန်ပျူတာရှိ browser window တစ်ခုထက်ပိုဖွင့်ထားပုံရသည်။", + "pad.modals.userdup.advice": "၎င်းအစားဤဝင်းဒိုးကိုသုံးရန်ပြန်လည်ချိတ်ဆက်ပါ။", + "pad.modals.unauth": "လုပ်ပိုင်ခွင့်မရှိပါ", + "pad.modals.unauth.explanation": "ဤစာမျက်နှာကိုကြည့်နေစဉ်သင်၏ခွင့်ပြုချက်များပြောင်းသွားသည်။ ပြန်လည်ချိတ်ဆက်ရန်ကြိုးစားပါ။", + "pad.modals.looping.explanation": "synchronization server နှင့်ဆက်သွယ်မှုပြဿနာများရှိသည်။", + "pad.modals.looping.cause": "သဟဇာတမဖြစ်သည့် firewall (သို့) proxy မှတဆင့်သင်ဆက်သွယ်နိုင်သည်။", + "pad.modals.initsocketfail": "ဆာဗာကို ဆက်သွယ်၍ မရပါ။", + "pad.modals.initsocketfail.explanation": "ထပ်တူပြုခြင်းဆာဗာသို့မချိတ်ဆက်နိုင်ခဲ့ပါ။", + "pad.modals.initsocketfail.cause": "၎င်းသည်သင်၏ browser (သို့) သင်၏အင်တာနက်ဆက်သွယ်မှုပြဿနာကြောင့်ဖြစ်နိုင်သည်။", + "pad.modals.slowcommit.explanation": "ဆာဗာကမတုံ့ပြန်ပါ။", + "pad.modals.slowcommit.cause": "၎င်းသည်ကွန်ယက်ချိတ်ဆက်မှုဆိုင်ရာပြဿနာများကြောင့်ဖြစ်နိုင်သည်။", + "pad.modals.badChangeset.explanation": "သင်ပြုလုပ်သောတည်းဖြတ်မှုကို synchronization server မှတရားမ ၀ င်ခွဲခြားခဲ့သည်။", + "pad.modals.badChangeset.cause": "၎င်းသည်မှားယွင်းသော server ဖွဲ့စည်းမှုပုံစံ (သို့) အခြားမမျှော်လင့်သောအပြုအမူများကြောင့်ဖြစ်နိုင်သည်။ ဤအရာသည်မှားယွင်းမှုတစ်ခုဟုသင်ခံစားရပါက ၀ န်ဆောင်မှုစီမံခန့်ခွဲသူအားဆက်သွယ်ပါ။ တည်းဖြတ်မှုကိုဆက်လက်လုပ်ဆောင်နိုင်ရန်ပြန်လည်ချိတ်ဆက်ကြည့်ပါ။", + "pad.modals.corruptPad.explanation": "သင်ရယူရန်ကြိုးစားနေသော pad သည်ယိုယွင်းနေသည်။", + "pad.modals.corruptPad.cause": "၎င်းသည်မှားယွင်းသော server ဖွဲ့စည်းမှုပုံစံ (သို့) အခြားမမျှော်လင့်သောအပြုအမူများကြောင့်ဖြစ်နိုင်သည်။ ကျေးဇူးပြု၍ ၀ န်ဆောင်မှုစီမံခန့်ခွဲသူကိုဆက်သွယ်ပါ။", + "pad.modals.deleted": "ဖျက်လိုက်သည်။", + "pad.modals.deleted.explanation": "ဒီအကွက်ကိုဖယ်ရှားပြီးပါပြီ။", + "pad.modals.rateLimited": "နှုန်းကန့်သတ်။", + "pad.modals.rateLimited.explanation": "မင်းဒီအဆက်အသွယ်ကိုဒီ pad မှာအရမ်းများတဲ့မက်ဆေ့ဂျ်တွေပို့ခဲ့တယ်။", + "pad.modals.rejected.explanation": "ဆာဗာသည်သင်၏ဘရောင်ဇာမှပေးပို့သောစာကိုငြင်းပယ်ခဲ့သည်။", + "pad.modals.rejected.cause": "သင် pad ကိုကြည့်နေစဉ်ဆာဗာကိုမွမ်းမံခဲ့ပေမည်၊ သို့မဟုတ် Etherpad တွင်ချို့ယွင်းချက်တစ်ခုရှိနေနိုင်သည်။ စာမျက်နှာကိုပြန်တင်ကြည့်ပါ။", + "pad.modals.disconnected": "မင်းအဆက်အသွယ်ဖြတ်လိုက်ပြီ။", + "pad.modals.disconnected.explanation": "ဆာဗာနှင့်ချိတ်ဆက်မှုပြတ်တောက်သွားသည်", + "pad.modals.disconnected.cause": "ဆာဗာမရနိုင်ပါ။ ဤသို့ဆက်ဖြစ်နေပါက ၀န်ဆောင်မှုစီမံခန့်ခွဲသူအား အကြောင်းကြားပါ။", + "pad.share": "ဒီစာရွက်ကိုမျှဝေပါ", + "pad.share.readonly": "ဖတ်သာကြည့်ပါ", + "pad.share.link": "လင့်", + "pad.share.emebdcode": "URL ထည့်ပါ", + "pad.chat": "စကားပြောမယ်", + "pad.chat.title": "ဒီ pad အတွက်စကားပြောခန်းကိုဖွင့်ပါ။", + "pad.chat.loadmessages": "နောက်ထပ်မက်ဆေ့ခ်ျများတင်ပါ", + "pad.chat.stick.title": "ချတ်ကိုမျက်နှာပြင်သို့ကပ်ပါ", + "pad.chat.writeMessage.placeholder": "မင်းရဲ့စာကိုဒီမှာရေးပါ", + "timeslider.followContents": "pad အကြောင်းအရာနောက်ဆုံးသတင်းများကိုလိုက်နာပါ", + "timeslider.pageTitle": "{{appTitle}} Timeslider", + "timeslider.toolbar.returnbutton": "ပလက်ဖောင်းသို့ပြန်သွားရန်", + "timeslider.toolbar.authors": "ရေးသားသူ -", + "timeslider.toolbar.authorsList": "စာရေးသူမရှိပါ", + "timeslider.toolbar.exportlink.title": "တင်ပို့သည်", + "timeslider.exportCurrent": "လက်ရှိဗားရှင်းအဖြစ်", + "timeslider.version": "ဗားရှင်း {{version}}", + "timeslider.saved": "သိမ်းထားသည် {{month}} {{day}}, {{year}}", + "timeslider.playPause": "Pad အကြောင်းအရာများပြန်ဖွင့်ခြင်း / ခဏရပ်ခြင်း", + "timeslider.backRevision": "ဤ Pad ရှိပြန်လည်သုံးသပ်ခြင်းကိုပြန်သွားပါ", + "timeslider.forwardRevision": "ဤ Pad ၌တည်းဖြတ်မှုတစ်ခုကိုရှေ့ဆက်ပါ", + "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}: {{minutes}}: {{seconds}}", + "timeslider.month.january": "ဇန်နဝါရီ", + "timeslider.month.february": "ဖေဖော်ဝါရီ", + "timeslider.month.march": "မတ်", + "timeslider.month.april": "ဧပြီ", + "timeslider.month.may": "မေ", + "timeslider.month.june": "ဇွန်", + "timeslider.month.july": "ဇူလိုင်", + "timeslider.month.august": "ဩဂုတ်", + "timeslider.month.september": "စက်တင်ဘာ", + "timeslider.month.october": "အောက်တိုဘာ", + "timeslider.month.november": "နို​ဝင်​ဘာ​", + "timeslider.month.december": "ဒီဇင်ဘာ", + "timeslider.unnamedauthors": "{{num}} အမည်မဖော်လိုသူ {[အများကိန်း (num) one: author၊ အခြား: author]}", + "pad.savedrevs.marked": "ယခုပြန်လည်တည်းဖြတ်မှုအားသိမ်းဆည်းထားသောတည်းဖြတ်မှုတစ်ခုအဖြစ်အမှတ်အသားပြုထားသည်", + "pad.savedrevs.timeslider": "timeslider ကိုသွားခြင်းဖြင့်သိမ်းဆည်းထားသောပြန်လည်တည်းဖြတ်ချက်များကိုသင်မြင်နိုင်သည်", + "pad.userlist.entername": "မင်းနာမည်ထည့်ပါ", + "pad.userlist.unnamed": "အမည်မဲ့", + "pad.editbar.clearcolors": "စာရွက်စာတမ်းတစ်ခုလုံးတွင်စာရေးသူအရောင်များကိုရှင်းလိုပါသလား။ ဒါကိုပြန် ပြင်၍ မရပါ", + "pad.impexp.importbutton": "ယခုတင်သွင်းပါ", + "pad.impexp.importing": "တင်သွင်းနေသည် ...", + "pad.impexp.confirmimport": "ဖိုင်တစ်ခုတင်သွင်းခြင်းသည် pad ၏လက်ရှိစာသားကိုထပ်ရေးလိမ့်မည်။ သင်ရှေ့ဆက်လိုသည်မှာသေချာသလား။", + "pad.impexp.convertFailed": "ဤဖိုင်ကိုကျွန်ုပ်တို့မတင်သွင်းနိုင်ခဲ့ပါ။ ကျေးဇူးပြု၍ အခြားစာရွက်စာတမ်းပုံစံတစ်ခုကိုသုံးပါသို့မဟုတ်ကိုယ်တိုင်ကူးယူပါ", + "pad.impexp.padHasData": "ဤ Pad သည်အပြောင်းအလဲများရှိနေပြီးဖြစ်သောကြောင့် ကျေးဇူးပြု၍ ဤဖိုင်ကိုတင်သွင်းနိုင်ခဲ့ခြင်းမရှိပါ၊ ကျေးဇူးပြု၍ pad အသစ်သို့တင်သွင်းပါ", + "pad.impexp.uploadFailed": "အပ်လုဒ်တင်ခြင်းမအောင်မြင်ပါ၊ ကျေးဇူးပြု၍ ထပ်ကြိုးစားပါ", + "pad.impexp.importfailed": "တင်သွင်းမှုမအောင်မြင်ပါ", + "pad.impexp.copypaste": "ကျေးဇူးပြု၍ ကူးထည့်ပါ", + "pad.impexp.exportdisabled": "{{type}} ပုံစံအဖြစ်ထုတ်ယူခြင်းကိုပိတ်ထားသည်။ အသေးစိတ်အတွက် ကျေးဇူးပြု၍ သင်၏စနစ်စီမံခန့်ခွဲသူကိုဆက်သွယ်ပါ။", + "pad.impexp.maxFileSize": "ဖိုင်ဆိုဒ်အရမ်းကြီးတယ်။ သွင်းကုန်အတွက်ခွင့်ပြုထားသောဖိုင်အရွယ်အစားကိုမြှင့်ရန်သင်၏ site စီမံခန့်ခွဲသူနှင့်ဆက်သွယ်ပါ" +} diff --git a/src/locales/sw.json b/src/locales/sw.json new file mode 100644 index 000000000..33d24add2 --- /dev/null +++ b/src/locales/sw.json @@ -0,0 +1,169 @@ +{ + "@metadata": { + "authors": [ + "Andibecker", + "Edwingudfriend", + "Muddyb" + ] + }, + "admin.page-title": "Dashibodi ya Usimamizi - Etherpad", + "admin_plugins": "Meneja wa programu-jalizi", + "admin_plugins.available": "Programu-jalizi zinazopatikana", + "admin_plugins.available_not-found": "Hakuna programu-jalizi zilizopatikana.", + "admin_plugins.available_fetching": "Inaleta…", + "admin_plugins.available_install.value": "Sakinisha", + "admin_plugins.available_search.placeholder": "Tafuta programu-jalizi ili usakinishe", + "admin_plugins.description": "Maelezo", + "admin_plugins.installed": "Programu-jalizi zilizosanikishwa", + "admin_plugins.installed_fetching": "Inaleta programu-jalizi zilizosakinishwa…", + "admin_plugins.installed_nothing": "Bado hujasakinisha programu-jalizi yoyote.", + "admin_plugins.installed_uninstall.value": "Ondoa", + "admin_plugins.last-update": "Sasisho la mwisho", + "admin_plugins.name": "Jina", + "admin_plugins.page-title": "Meneja wa programu-jalizi - Etherpad", + "admin_plugins.version": "Toleo", + "admin_plugins_info": "Maelezo ya utatuzi", + "admin_plugins_info.hooks": "Ndoano zilizowekwa", + "admin_plugins_info.hooks_client": "Kulabu za mteja", + "admin_plugins_info.hooks_server": "Kulabu za upande wa seva", + "admin_plugins_info.parts": "Sehemu zilizowekwa", + "admin_plugins_info.plugins": "Programu-jalizi zilizosanikishwa", + "admin_plugins_info.page-title": "Habari ya programu-jalizi - Etherpad", + "admin_plugins_info.version": "Toleo la Etherpad", + "admin_plugins_info.version_latest": "Toleo la hivi karibuni linalopatikana", + "admin_plugins_info.version_number": "Nambari ya toleo", + "admin_settings": "Mipangilio", + "admin_settings.current": "Usanidi wa sasa", + "admin_settings.current_example-devel": "Mfano mipangilio ya mipangilio ya maendeleo", + "admin_settings.current_example-prod": "Mfano mipangilio ya mipangilio ya uzalishaji", + "admin_settings.current_restart.value": "Anzisha upya Etherpad", + "admin_settings.current_save.value": "Hifadhi Mipangilio", + "admin_settings.page-title": "Mipangilio - Etherpad", + "index.newPad": "Pad Mpya", + "index.createOpenPad": "au tunga/fungua Pad yenye jina:", + "index.openPad": "fungua Pad iliyopo na jina:", + "pad.toolbar.bold.title": "Koozesha (Ctrl+B)", + "pad.toolbar.italic.title": "Mlalo (Ctrl+I)", + "pad.toolbar.underline.title": "Pigia mstari (Ctrl+U)", + "pad.toolbar.strikethrough.title": "Kata (Ctrl+5)", + "pad.toolbar.ol.title": "Orodha iliyopangliwa (Ctrl+Shift+N)", + "pad.toolbar.ul.title": "Orodha isiyopangiliwa (Ctrl+Shift+L)", + "pad.toolbar.indent.title": "Jongeza (TAB)", + "pad.toolbar.unindent.title": "Punguza (Shift+TAB)", + "pad.toolbar.undo.title": "Tengua (Ctrl+Z)", + "pad.toolbar.redo.title": "Fanyaupya (Ctrl+Y)", + "pad.toolbar.clearAuthorship.title": "Futa Rangi za Uandishi (Ctrl+Shift+C)", + "pad.toolbar.import_export.title": "Ingiza/Toa kutoka/kwa muundo wa faili tofauti", + "pad.toolbar.timeslider.title": "Kiburuzawakati", + "pad.toolbar.savedRevision.title": "Hifadhi Mapitio", + "pad.toolbar.settings.title": "Marekebisho", + "pad.toolbar.embed.title": "Shiriki na Pachika pedi hii", + "pad.toolbar.showusers.title": "Onyesha watumiaji kwenye pedi hii", + "pad.colorpicker.save": "Okoa", + "pad.colorpicker.cancel": "Ghairi", + "pad.loading": "Inapakiwa...", + "pad.noCookie": "Kuki haikuweza kupatikana. Tafadhali ruhusu kuki katika kivinjari chako! Kikao na mipangilio yako haitahifadhiwa kati ya ziara. Hii inaweza kuwa ni kutokana na Etherpad kuingizwa katika iFrame katika Vivinjari vingine. Tafadhali hakikisha Etherpad iko kwenye kikoa / kikoa sawa na iFrame ya mzazi", + "pad.permissionDenied": "Huna ruhusa ya kufikia pedi hii", + "pad.settings.padSettings": "Mipangilio ya pedi", + "pad.settings.myView": "Mtazamo Wangu", + "pad.settings.stickychat": "Ongea kila wakati kwenye skrini", + "pad.settings.chatandusers": "Onyesha Gumzo na Watumiaji", + "pad.settings.colorcheck": "Rangi za uandishi", + "pad.settings.linenocheck": "Nambari za laini", + "pad.settings.rtlcheck": "Soma yaliyomo kutoka kulia kwenda kushoto?", + "pad.settings.fontType": "Aina ya herufi:", + "pad.settings.language": "Lugha:", + "pad.settings.about": "Kuhusu", + "pad.settings.poweredBy": "Kinatumia", + "pad.importExport.import_export": "Ingiza / Hamisha", + "pad.importExport.import": "Pakia faili yoyote ya maandishi au hati", + "pad.importExport.importSuccessful": "Imefanikiwa!", + "pad.importExport.export": "Hamisha pedi ya sasa kama:", + "pad.importExport.exportetherpad": "Etherpad", + "pad.importExport.exporthtml": "HTML", + "pad.importExport.exportplain": "Maandishi wazi", + "pad.importExport.exportword": "Neno la Microsoft", + "pad.importExport.exportpdf": "PDF", + "pad.importExport.exportopen": "ODF (Fungua Fomati ya Hati)", + "pad.importExport.abiword.innerHTML": "Unaweza kuagiza tu kutoka kwa maandishi wazi au fomati za HTML. Kwa vipengee vya hali ya juu zaidi tafadhali weka AbiWord au LibreOffice .", + "pad.modals.connected": "Imeunganishwa", + "pad.modals.reconnecting": "Inaunganisha tena pedi yako…", + "pad.modals.forcereconnect": "Lazimisha kuunganisha tena", + "pad.modals.reconnecttimer": "Kujaribu kuungana tena", + "pad.modals.cancel": "Ghairi", + "pad.modals.userdup": "Imefunguliwa kwenye dirisha lingine", + "pad.modals.userdup.explanation": "Pedi hii inaonekana kufunguliwa katika zaidi ya dirisha moja la kivinjari kwenye kompyuta hii.", + "pad.modals.userdup.advice": "Unganisha tena ili utumie dirisha hili badala yake.", + "pad.modals.unauth": "Haijaidhinishwa", + "pad.modals.unauth.explanation": "Ruhusa zako zimebadilika wakati wa kutazama ukurasa huu. Jaribu kuunganisha tena.", + "pad.modals.looping.explanation": "Kuna shida za mawasiliano na seva ya maingiliano.", + "pad.modals.looping.cause": "Labda uliunganisha kupitia firewall isiyokubaliana au wakala.", + "pad.modals.initsocketfail": "Seva haipatikani.", + "pad.modals.initsocketfail.explanation": "Imeshindwa kuunganisha kwenye seva ya usawazishaji.", + "pad.modals.initsocketfail.cause": "Labda hii ni kwa sababu ya shida na kivinjari chako au muunganisho wako wa mtandao.", + "pad.modals.slowcommit.explanation": "Seva haijibu.", + "pad.modals.slowcommit.cause": "Hii inaweza kuwa ni kwa sababu ya shida na muunganisho wa mtandao.", + "pad.modals.badChangeset.explanation": "Hariri uliyoifanya iliainishwa kuwa haramu na seva ya maingiliano.", + "pad.modals.badChangeset.cause": "Hii inaweza kuwa ni kwa sababu ya usanidi mbaya wa seva au tabia zingine zisizotarajiwa. Tafadhali wasiliana na msimamizi wa huduma, ikiwa unahisi hii ni kosa. Jaribu kuunganisha tena ili uendelee kuhariri.", + "pad.modals.corruptPad.explanation": "Pedi unayojaribu kufikia ni mbovu.", + "pad.modals.corruptPad.cause": "Hii inaweza kuwa ni kwa sababu ya usanidi mbaya wa seva au tabia zingine zisizotarajiwa. Tafadhali wasiliana na msimamizi wa huduma.", + "pad.modals.deleted": "Imefutwa.", + "pad.modals.deleted.explanation": "Pedi hii imeondolewa.", + "pad.modals.rateLimited": "Kiwango kidogo.", + "pad.modals.rateLimited.explanation": "Ulituma ujumbe mwingi kwenye pedi hii kwa hivyo ikakukata.", + "pad.modals.rejected.explanation": "Seva ilikataa ujumbe ambao ulitumwa na kivinjari chako.", + "pad.modals.rejected.cause": "Seva inaweza kuwa imesasishwa wakati unatazama pedi, au labda kuna mdudu katika Etherpad. Jaribu kupakia upya ukurasa.", + "pad.modals.disconnected": "Umetenganishwa", + "pad.modals.disconnected.explanation": "Muunganisho wa seva ulipotea", + "pad.modals.disconnected.cause": "Huenda seva haipatikani. Tafadhali mjulishe msimamizi wa huduma ikiwa hii itaendelea kutokea.", + "pad.share": "Shiriki pedi hii", + "pad.share.readonly": "Soma tu", + "pad.share.link": "Kiungo", + "pad.share.emebdcode": "Pachika URL", + "pad.chat": "Ongea", + "pad.chat.title": "Fungua gumzo kwa pedi hii.", + "pad.chat.loadmessages": "Pakia ujumbe zaidi", + "pad.chat.stick.title": "Funga mazungumzo kwenye skrini", + "pad.chat.writeMessage.placeholder": "Andika ujumbe wako hapa", + "timeslider.followContents": "Fuata sasisho za yaliyomo kwenye pedi", + "timeslider.pageTitle": "{{appTitle}} Mpangaji Nyakati", + "timeslider.toolbar.returnbutton": "Rudi kwenye pedi", + "timeslider.toolbar.authors": "Waandishi", + "timeslider.toolbar.authorsList": "Hakuna Waandishi", + "timeslider.toolbar.exportlink.title": "Hamisha", + "timeslider.exportCurrent": "Hamisha toleo la sasa kama:", + "timeslider.version": "Toleo {{version}}", + "timeslider.saved": "Imehifadhiwa {{month}} {{day}}, {{year}}", + "timeslider.playPause": "Uchezaji / Sitisha Yaliyomo ya Pad", + "timeslider.backRevision": "Rudi nyuma kwenye toleo hili", + "timeslider.forwardRevision": "Nenda mbele kwa marekebisho katika Pad hii", + "timeslider.dateformat": "{{month}} / {{day}} / {{year}} {{hours}}: {{minutes}}: {{seconds}}", + "timeslider.month.january": "Januari", + "timeslider.month.february": "Februari", + "timeslider.month.march": "Machi", + "timeslider.month.april": "Aprili", + "timeslider.month.may": "Mei", + "timeslider.month.june": "Juni", + "timeslider.month.july": "Julai", + "timeslider.month.august": "Agosti", + "timeslider.month.september": "Septemba", + "timeslider.month.october": "Oktoba", + "timeslider.month.november": "Novemba", + "timeslider.month.december": "Desemba", + "timeslider.unnamedauthors": "{{num}} haijatajwa jina {[wingi (num) moja: mwandishi, mwingine: waandishi]}", + "pad.savedrevs.marked": "Marekebisho haya sasa yamewekwa alama kama marekebisho yaliyohifadhiwa", + "pad.savedrevs.timeslider": "Unaweza kuona marekebisho yaliyohifadhiwa kwa kutembelea mpangilio wa nyakati", + "pad.userlist.entername": "Ingiza jina lako", + "pad.userlist.unnamed": "bila jina", + "pad.editbar.clearcolors": "Futa rangi za uandishi kwenye hati nzima? Hii haiwezi kutenduliwa", + "pad.impexp.importbutton": "Ingiza Sasa", + "pad.impexp.importing": "Inaleta ...", + "pad.impexp.confirmimport": "Kuingiza faili kutaondoa maandishi ya sasa ya pedi. Je! Una uhakika unataka kuendelea?", + "pad.impexp.convertFailed": "Hatukuweza kuleta faili hii. Tafadhali tumia fomati ya hati tofauti au nakili ubandike mwenyewe", + "pad.impexp.padHasData": "Hatukuweza kuagiza faili hii kwa sababu pedi hii tayari imekuwa na mabadiliko, tafadhali ingiza kwa pedi mpya", + "pad.impexp.uploadFailed": "Upakiaji umeshindwa, tafadhali jaribu tena", + "pad.impexp.importfailed": "Uingizaji haukufaulu", + "pad.impexp.copypaste": "Tafadhali nakili kuweka", + "pad.impexp.exportdisabled": "Kuhamisha kama muundo wa {{type}} kumezimwa. Tafadhali wasiliana na msimamizi wako wa mfumo kwa maelezo.", + "pad.impexp.maxFileSize": "Faili kubwa sana. Wasiliana na msimamizi wa wavuti yako ili kuongeza saizi iliyoruhusiwa ya kuagiza" +} diff --git a/src/locales/th.json b/src/locales/th.json index 3905b8f40..573c7a761 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -2,9 +2,43 @@ "@metadata": { "authors": [ "Aefgh39622", + "Andibecker", "Patsagorn Y." ] }, + "admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad", + "admin_plugins": "ตัวจัดการปลั๊กอิน", + "admin_plugins.available": "ปลั๊กอินที่มีอยู่", + "admin_plugins.available_not-found": "ไม่พบปลั๊กอิน", + "admin_plugins.available_fetching": "กำลังเรียก…", + "admin_plugins.available_install.value": "ติดตั้ง", + "admin_plugins.available_search.placeholder": "ค้นหาปลั๊กอินที่จะติดตั้ง", + "admin_plugins.description": "คำอธิบาย", + "admin_plugins.installed": "ปลั๊กอินที่ติดตั้ง", + "admin_plugins.installed_fetching": "กำลังเรียกปลั๊กอินที่ติดตั้ง…", + "admin_plugins.installed_nothing": "คุณยังไม่ได้ติดตั้งปลั๊กอินใดๆ", + "admin_plugins.installed_uninstall.value": "ถอนการติดตั้ง", + "admin_plugins.last-update": "การปรับปรุงครั้งล่าสุด", + "admin_plugins.name": "ชื่อ", + "admin_plugins.page-title": "ตัวจัดการปลั๊กอิน - Etherpad", + "admin_plugins.version": "เวอร์ชั่น", + "admin_plugins_info": "ข้อมูลการแก้ไขปัญหา", + "admin_plugins_info.hooks": "ติดตั้งตะขอ", + "admin_plugins_info.hooks_client": "ตะขอฝั่งไคลเอ็นต์", + "admin_plugins_info.hooks_server": "ตะขอฝั่งเซิร์ฟเวอร์", + "admin_plugins_info.parts": "ชิ้นส่วนที่ติดตั้ง", + "admin_plugins_info.plugins": "ปลั๊กอินที่ติดตั้ง", + "admin_plugins_info.page-title": "ข้อมูลปลั๊กอิน - Etherpad", + "admin_plugins_info.version": "รุ่น Etherpad", + "admin_plugins_info.version_latest": "เวอร์ชันล่าสุดที่มีอยู่", + "admin_plugins_info.version_number": "หมายเลขเวอร์ชัน", + "admin_settings": "การตั้งค่า", + "admin_settings.current": "การกำหนดค่าปัจจุบัน", + "admin_settings.current_example-devel": "ตัวอย่างเทมเพลตการตั้งค่าการพัฒนา", + "admin_settings.current_example-prod": "ตัวอย่างเทมเพลตการตั้งค่าการผลิต", + "admin_settings.current_restart.value": "รีสตาร์ท Etherpad", + "admin_settings.current_save.value": "บันทึกการตั้งค่า", + "admin_settings.page-title": "การตั้งค่า - Etherpad", "index.newPad": "สร้างแผ่นจดบันทึกใหม่", "index.createOpenPad": "หรือสร้าง/เปิดแผ่นจดบันทึกที่มีชื่อ:", "index.openPad": "เปิดแพดที่มีอยู่แล้วด้วยชื่อ:", @@ -77,6 +111,8 @@ "pad.modals.deleted.explanation": "แผ่นจดบันทึกนี้ได้ถูกลบออกแล้ว", "pad.modals.rateLimited": "ถึงขีดจำกัด", "pad.modals.rateLimited.explanation": "คณส่งข้อความถึงแพดนี้มากเกินไปจึงถูกตัดการเชื่อมโดยโปรแกรมอัตโนมัติ", + "pad.modals.rejected.explanation": "เซิร์ฟเวอร์ปฏิเสธข้อความที่ส่งโดยเบราว์เซอร์ของคุณ", + "pad.modals.rejected.cause": "เซิร์ฟเวอร์อาจได้รับการอัปเดตในขณะที่คุณกำลังดูแพด หรืออาจมีข้อบกพร่องใน Etherpad ลองโหลดหน้านี้ใหม่", "pad.modals.disconnected": "คุณได้ตัดการเชื่อมต่อแล้ว", "pad.modals.disconnected.explanation": "การเชื่อมต่อกับเซิร์ฟเวอร์ถูกตัด", "pad.modals.disconnected.cause": "เซิร์ฟเวอร์อาจใช้ไม่ได้ชั่วคราว โปรดแจ้งให้ผู้ดูแลการให้บริการทราบถ้าปัญหานี้ยังคงเกิดขึ้น", From 76374bc489611d8ca12a7277ca796366b9f040df Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 29 Sep 2021 21:33:46 -0400 Subject: [PATCH 148/322] LibreOffice: Close stdin right away This should prevent LibreOffice from hanging if it attempts to read from stdin (it'll get EOF and probably exit with an error instead). --- src/node/utils/LibreOffice.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.js index 276bc3003..e6e9c0522 100644 --- a/src/node/utils/LibreOffice.js +++ b/src/node/utils/LibreOffice.js @@ -44,10 +44,11 @@ const doConvertTask = async (task) => { '--outdir', tmpDir, ]); + soffice.stdin.end(); // Soffice/libreoffice is buggy and often hangs. // To remedy this we kill the spawned process after a while. + // TODO: Use the timeout option once support for Node.js < v15.13.0 is dropped. const hangTimeout = setTimeout(() => { - soffice.stdin.pause(); // required to kill hanging threads soffice.kill(); }, 120000); let stdoutBuffer = ''; From e42e5457c12ec793f012ff64f1b7346cd5076084 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 29 Sep 2021 23:10:40 -0400 Subject: [PATCH 149/322] LibreOffice: Improve logging --- src/node/utils/LibreOffice.js | 48 ++++++++++++++++------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.js index e6e9c0522..339209194 100644 --- a/src/node/utils/LibreOffice.js +++ b/src/node/utils/LibreOffice.js @@ -22,17 +22,15 @@ const fs = require('fs').promises; const log4js = require('log4js'); const os = require('os'); const path = require('path'); +const runCmd = require('./run_cmd'); const settings = require('./Settings'); -const spawn = require('child_process').spawn; -const libreOfficeLogger = log4js.getLogger('LibreOffice'); +const logger = log4js.getLogger('LibreOffice'); const doConvertTask = async (task) => { const tmpDir = os.tmpdir(); - - libreOfficeLogger.debug( - `Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`); - const soffice = spawn(settings.soffice, [ + const p = runCmd([ + settings.soffice, '--headless', '--invisible', '--nologo', @@ -43,34 +41,32 @@ const doConvertTask = async (task) => { task.srcFile, '--outdir', tmpDir, - ]); - soffice.stdin.end(); + ], {stdio: [ + null, + (line) => logger.info(`[${p.child.pid}] stdout: ${line}`), + (line) => logger.error(`[${p.child.pid}] stderr: ${line}`), + ]}); + logger.info(`[${p.child.pid}] Converting ${task.srcFile} to ${task.type} in ${tmpDir}`); // Soffice/libreoffice is buggy and often hangs. // To remedy this we kill the spawned process after a while. // TODO: Use the timeout option once support for Node.js < v15.13.0 is dropped. const hangTimeout = setTimeout(() => { - soffice.kill(); + logger.error(`[${p.child.pid}] Conversion timed out; killing LibreOffice...`); + p.child.kill(); }, 120000); - let stdoutBuffer = ''; - soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); - soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - await new Promise((resolve, reject) => { - soffice.on('exit', (code) => { - clearTimeout(hangTimeout); - if (code !== 0) { - const err = - new Error(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`); - libreOfficeLogger.error(err.stack); - return reject(err); - } - resolve(); - }); - }); - + try { + await p; + } catch (err) { + logger.error(`[${p.child.pid}] Conversion failed: ${err.stack || err}`); + throw err; + } finally { + clearTimeout(hangTimeout); + } + logger.info(`[${p.child.pid}] Conversion done.`); const filename = path.basename(task.srcFile); const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`; const sourcePath = path.join(tmpDir, sourceFile); - libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`); + logger.debug(`Renaming ${sourcePath} to ${task.destFile}`); await fs.rename(sourcePath, task.destFile); }; From 37a33042d24648185d90a48593d981fb6024365c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 2 Oct 2021 02:02:04 -0400 Subject: [PATCH 150/322] ace2_inner: Improve rep documentation --- src/static/js/ace2_inner.js | 49 ++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 03dd925bb..4b1d66317 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -81,24 +81,45 @@ function Ace2Inner(editorInfo, cssManagers) { let outsideKeyPress = (e) => true; let outsideNotifyDirty = noop; - // Document representation. + /** + * Document representation. + */ const rep = { - // Each entry in this skip list is an object created by createDomLineEntry(). The object - // represents a line (paragraph) of content. + /** + * The contents of the document. Each entry in this skip list is an object representing a + * line (actually paragraph) of text. The line objects are created by createDomLineEntry(). + */ lines: new SkipList(), - // Points at the start of the selection. Represented as [zeroBasedLineNumber, - // zeroBasedColumnNumber]. - // TODO: If the selection starts at the beginning of a line, I think this could be either - // [lineNumber, 0] or [previousLineNumber, previousLineLength]. Need to confirm. + /** + * Start of the selection. Represented as an array of two non-negative numbers that point to the + * first character of the selection: [zeroBasedLineNumber, zeroBasedColumnNumber]. Notes: + * - There is an implicit newline character (not actually stored) at the end of every line. + * Because of this, a selection that starts at the end of a line (column number equals the + * number of characters in the line, not including the implicit newline) is not equivalent + * to a selection that starts at the beginning of the next line. The same goes for the + * selection end. + * - If there are N lines, [N, 0] is valid for the start of the selection. [N, 0] indicates + * that the selection starts just after the implicit newline at the end of the document's + * last line (if the document has any lines). The same goes for the end of the selection. + * - If a line starts with a line marker, a selection that starts at the beginning of the line + * may start either immediately before (column = 0) or immediately after (column = 1) the + * line marker, and the two are considered to be semantically equivalent. For safety, all + * code should be written to accept either but only produce selections that start after the + * line marker (the column number should be 1, not 0, when there is a line marker). The same + * goes for the end of the selection. + */ selStart: null, - // Points at the character just past the last selected character. Same representation as - // selStart. - // TODO: If the last selected character is the last character of a line, I think this could be - // either [lineNumber, lineLength] or [lineNumber+1, 0]. Need to confirm. + /** + * End of the selection. Represented as an array of two non-negative numbers that point to the + * character just after the end of the selection: [zeroBasedLineNumber, zeroBasedColumnNumber]. + * See the above notes for selStart. + */ selEnd: null, - // Whether the selection extends "backwards", so that the focus point (controlled with the arrow - // keys) is at the beginning. This is not supported in IE, though native IE selections have that - // behavior (which we try not to interfere with). Must be false if selection is collapsed! + /** + * Whether the selection extends "backwards", so that the focus point (controlled with the arrow + * keys) is at the beginning. This is not supported in IE, though native IE selections have that + * behavior (which we try not to interfere with). Must be false if selection is collapsed! + */ selFocusAtStart: false, alltext: '', alines: [], From c361df52d236fab92b9ba9ae68c5ad2cd8eeb84e Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 24 Sep 2021 16:46:53 +0100 Subject: [PATCH 151/322] bugfix: Allow selection to start/end before line marker --- src/static/js/AttributeManager.js | 8 +++--- .../frontend/specs/clear_authorship_colors.js | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index a2ea15b6c..124434031 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -103,12 +103,12 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ const markerWidth = this.lineHasMarker(row) ? 1 : 0; if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`); - const startCol = row === start[0] ? start[1] : markerWidth; - if (startCol - markerWidth < 0) throw new RangeError('selection starts before line start'); + if (start[1] < 0) throw new RangeError('selection starts at negative column'); + const startCol = Math.max(markerWidth, row === start[0] ? start[1] : 0); if (startCol > lineLength) throw new RangeError('selection starts after line end'); - const endCol = row === end[0] ? end[1] : lineLength; - if (endCol - markerWidth < 0) throw new RangeError('selection ends before line start'); + if (end[1] < 0) throw new RangeError('selection ends at negative column'); + const endCol = Math.max(markerWidth, row === end[0] ? end[1] : lineLength); if (endCol > lineLength) throw new RangeError('selection ends after line end'); if (startCol > endCol) throw new RangeError('selection ends before it starts'); diff --git a/src/tests/frontend/specs/clear_authorship_colors.js b/src/tests/frontend/specs/clear_authorship_colors.js index 58ce93704..20c358b49 100644 --- a/src/tests/frontend/specs/clear_authorship_colors.js +++ b/src/tests/frontend/specs/clear_authorship_colors.js @@ -1,9 +1,11 @@ 'use strict'; describe('clear authorship colors button', function () { + let padId; + // create a new pad before each test run beforeEach(async function () { - await helper.aNewPad(); + padId = await helper.aNewPad(); }); it('makes text clear authorship colors', async function () { @@ -98,4 +100,26 @@ describe('clear authorship colors button', function () { await helper.waitForPromise( () => chrome$('div.disconnected').attr('class').indexOf('visible') === -1); }); + + // Test for https://github.com/ether/etherpad-lite/issues/5128 + it('clears authorship when first line has line attributes', async function () { + // override the confirm dialogue function + helper.padChrome$.window.confirm = () => true; + + // Make sure there is text with author info. The first line must have a line attribute. + await helper.clearPad(); + await helper.edit('Hello'); + helper.padChrome$('.buttonicon-insertunorderedlist').click(); + await helper.waitForPromise(() => helper.padInner$('[class*="author-"]').length > 0); + + const nCommits = helper.commits.length; + helper.padChrome$('.buttonicon-clearauthorship').click(); + await helper.waitForPromise(() => helper.padInner$('[class*="author-"]').length === 0); + + // Make sure the change was actually accepted by reloading the pad and looking for authorship. + // Before the pad can be reloaded the server might need some time to accept the change. + await helper.waitForPromise(() => helper.commits.length > nCommits); + await helper.aNewPad({id: padId}); + expect(helper.padInner$('[class*="author-"]').length).to.be(0); + }); }); From 1c49970fda1e2af1d7b760f43ce4958e8c360a7f Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 3 Oct 2021 11:22:00 +0100 Subject: [PATCH 152/322] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e0c110b3f..bb0b4743b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha * [Images](https://image.etherpad.com) - Plugins to improve provide Image support within a pad. * [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad. * [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads. -* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation. * [Scale](https://shard.etherpad.com) - Etherpad running at scale with pad sharding which allows Etherpad to scale to ∞ number of Active Pads with up to ~20,000 edits per second, per pad. # Project Status From 90a2b553d18c1b6255ce4ef9ac095520423abe87 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 3 Oct 2021 11:41:14 +0100 Subject: [PATCH 153/322] Revert "Update README.md" This reverts commit 1c49970fda1e2af1d7b760f43ce4958e8c360a7f. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bb0b4743b..e0c110b3f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha * [Images](https://image.etherpad.com) - Plugins to improve provide Image support within a pad. * [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad. * [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads. +* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation. * [Scale](https://shard.etherpad.com) - Etherpad running at scale with pad sharding which allows Etherpad to scale to ∞ number of Active Pads with up to ~20,000 edits per second, per pad. # Project Status From 39a971e3b92ba54da94d19e4f6ef2bb5f4672f4e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 2 Oct 2021 19:09:12 -0400 Subject: [PATCH 154/322] tests: Remove overly aggressive timeouts --- .../backend/specs/api/characterEncoding.js | 6 ---- src/tests/backend/specs/api/instance.js | 2 -- .../backend/specs/api/sessionsAndGroups.js | 29 ------------------- 3 files changed, 37 deletions(-) diff --git a/src/tests/backend/specs/api/characterEncoding.js b/src/tests/backend/specs/api/characterEncoding.js index 3080425ec..300cf2d17 100644 --- a/src/tests/backend/specs/api/characterEncoding.js +++ b/src/tests/backend/specs/api/characterEncoding.js @@ -21,7 +21,6 @@ describe(__filename, function () { describe('Connectivity For Character Encoding', function () { it('can connect', function (done) { - this.timeout(250); agent.get('/api/') .expect('Content-Type', /json/) .expect(200, done); @@ -29,7 +28,6 @@ describe(__filename, function () { }); describe('API Versioning', function () { - this.timeout(150); it('finds the version tag', function (done) { agent.get('/api/') .expect((res) => { @@ -43,7 +41,6 @@ describe(__filename, function () { describe('Permission', function () { it('errors with invalid APIKey', function (done) { - this.timeout(150); // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway const permErrorURL = `/api/${apiVersion}/createPad?apikey=password&padID=test`; @@ -54,7 +51,6 @@ describe(__filename, function () { describe('createPad', function () { it('creates a new Pad', function (done) { - this.timeout(150); agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect((res) => { if (res.body.code !== 0) throw new Error('Unable to create new Pad'); @@ -66,7 +62,6 @@ describe(__filename, function () { describe('setHTML', function () { it('Sets the HTML of a Pad attempting to weird utf8 encoded content', function (done) { - this.timeout(1000); fs.readFile('tests/backend/specs/api/emojis.html', 'utf8', (err, html) => { agent.post(endPoint('setHTML')) .send({ @@ -84,7 +79,6 @@ describe(__filename, function () { describe('getHTML', function () { it('get the HTML of Pad with emojis', function (done) { - this.timeout(400); agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect((res) => { if (res.body.data.html.indexOf('🇼') === -1) { diff --git a/src/tests/backend/specs/api/instance.js b/src/tests/backend/specs/api/instance.js index 64a9ea5e0..f0180e203 100644 --- a/src/tests/backend/specs/api/instance.js +++ b/src/tests/backend/specs/api/instance.js @@ -18,7 +18,6 @@ describe(__filename, function () { describe('Connectivity for instance-level API tests', function () { it('can connect', function (done) { - this.timeout(150); agent.get('/api/') .expect('Content-Type', /json/) .expect(200, done); @@ -27,7 +26,6 @@ describe(__filename, function () { describe('getStats', function () { it('Gets the stats of a running instance', function (done) { - this.timeout(100); agent.get(endPoint('getStats')) .expect((res) => { if (res.body.code !== 0) throw new Error('getStats() failed'); diff --git a/src/tests/backend/specs/api/sessionsAndGroups.js b/src/tests/backend/specs/api/sessionsAndGroups.js index 238353d0d..83fdac698 100644 --- a/src/tests/backend/specs/api/sessionsAndGroups.js +++ b/src/tests/backend/specs/api/sessionsAndGroups.js @@ -18,7 +18,6 @@ describe(__filename, function () { describe('API Versioning', function () { it('errors if can not connect', async function () { - this.timeout(200); await agent.get('/api/') .expect(200) .expect((res) => { @@ -60,7 +59,6 @@ describe(__filename, function () { describe('API: Group creation and deletion', function () { it('createGroup', async function () { - this.timeout(100); await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) @@ -72,7 +70,6 @@ describe(__filename, function () { }); it('listSessionsOfGroup for empty group', async function () { - this.timeout(100); await agent.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -83,7 +80,6 @@ describe(__filename, function () { }); it('deleteGroup', async function () { - this.timeout(100); await agent.get(`${endPoint('deleteGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -93,7 +89,6 @@ describe(__filename, function () { }); it('createGroupIfNotExistsFor', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupIfNotExistsFor')}&groupMapper=management`) .expect(200) .expect('Content-Type', /json/) @@ -106,7 +101,6 @@ describe(__filename, function () { // Test coverage for https://github.com/ether/etherpad-lite/issues/4227 // Creates a group, creates 2 sessions, 2 pads and then deletes the group. it('createGroup', async function () { - this.timeout(100); await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) @@ -118,7 +112,6 @@ describe(__filename, function () { }); it('createAuthor', async function () { - this.timeout(100); await agent.get(endPoint('createAuthor')) .expect(200) .expect('Content-Type', /json/) @@ -130,7 +123,6 @@ describe(__filename, function () { }); it('createSession', async function () { - this.timeout(100); await agent.get(`${endPoint('createSession')}&authorID=${authorID}&groupID=${groupID}` + '&validUntil=999999999999') .expect(200) @@ -143,7 +135,6 @@ describe(__filename, function () { }); it('createSession', async function () { - this.timeout(100); await agent.get(`${endPoint('createSession')}&authorID=${authorID}&groupID=${groupID}` + '&validUntil=999999999999') .expect(200) @@ -156,7 +147,6 @@ describe(__filename, function () { }); it('createGroupPad', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x1234567`) .expect(200) .expect('Content-Type', /json/) @@ -166,7 +156,6 @@ describe(__filename, function () { }); it('createGroupPad', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x12345678`) .expect(200) .expect('Content-Type', /json/) @@ -176,7 +165,6 @@ describe(__filename, function () { }); it('deleteGroup', async function () { - this.timeout(100); await agent.get(`${endPoint('deleteGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -189,7 +177,6 @@ describe(__filename, function () { describe('API: Author creation', function () { it('createGroup', async function () { - this.timeout(100); await agent.get(endPoint('createGroup')) .expect(200) .expect('Content-Type', /json/) @@ -201,7 +188,6 @@ describe(__filename, function () { }); it('createAuthor', async function () { - this.timeout(100); await agent.get(endPoint('createAuthor')) .expect(200) .expect('Content-Type', /json/) @@ -212,7 +198,6 @@ describe(__filename, function () { }); it('createAuthor with name', async function () { - this.timeout(100); await agent.get(`${endPoint('createAuthor')}&name=john`) .expect(200) .expect('Content-Type', /json/) @@ -224,7 +209,6 @@ describe(__filename, function () { }); it('createAuthorIfNotExistsFor', async function () { - this.timeout(100); await agent.get(`${endPoint('createAuthorIfNotExistsFor')}&authorMapper=chris`) .expect(200) .expect('Content-Type', /json/) @@ -235,7 +219,6 @@ describe(__filename, function () { }); it('getAuthorName', async function () { - this.timeout(100); await agent.get(`${endPoint('getAuthorName')}&authorID=${authorID}`) .expect(200) .expect('Content-Type', /json/) @@ -248,7 +231,6 @@ describe(__filename, function () { describe('API: Sessions', function () { it('createSession', async function () { - this.timeout(100); await agent.get(`${endPoint('createSession')}&authorID=${authorID}&groupID=${groupID}` + '&validUntil=999999999999') .expect(200) @@ -261,7 +243,6 @@ describe(__filename, function () { }); it('getSessionInfo', async function () { - this.timeout(100); await agent.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) @@ -274,7 +255,6 @@ describe(__filename, function () { }); it('listSessionsOfGroup', async function () { - this.timeout(100); await agent.get(`${endPoint('listSessionsOfGroup')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -285,7 +265,6 @@ describe(__filename, function () { }); it('deleteSession', async function () { - this.timeout(100); await agent.get(`${endPoint('deleteSession')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) @@ -295,7 +274,6 @@ describe(__filename, function () { }); it('getSessionInfo of deleted session', async function () { - this.timeout(100); await agent.get(`${endPoint('getSessionInfo')}&sessionID=${sessionID}`) .expect(200) .expect('Content-Type', /json/) @@ -307,7 +285,6 @@ describe(__filename, function () { describe('API: Group pad management', function () { it('listPads', async function () { - this.timeout(100); await agent.get(`${endPoint('listPads')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -318,7 +295,6 @@ describe(__filename, function () { }); it('createGroupPad', async function () { - this.timeout(100); await agent.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=${padID}`) .expect(200) .expect('Content-Type', /json/) @@ -329,7 +305,6 @@ describe(__filename, function () { }); it('listPads after creating a group pad', async function () { - this.timeout(100); await agent.get(`${endPoint('listPads')}&groupID=${groupID}`) .expect(200) .expect('Content-Type', /json/) @@ -342,7 +317,6 @@ describe(__filename, function () { describe('API: Pad security', function () { it('getPublicStatus', async function () { - this.timeout(100); await agent.get(`${endPoint('getPublicStatus')}&padID=${padID}`) .expect(200) .expect('Content-Type', /json/) @@ -353,7 +327,6 @@ describe(__filename, function () { }); it('setPublicStatus', async function () { - this.timeout(100); await agent.get(`${endPoint('setPublicStatus')}&padID=${padID}&publicStatus=true`) .expect(200) .expect('Content-Type', /json/) @@ -363,7 +336,6 @@ describe(__filename, function () { }); it('getPublicStatus after changing public status', async function () { - this.timeout(100); await agent.get(`${endPoint('getPublicStatus')}&padID=${padID}`) .expect(200) .expect('Content-Type', /json/) @@ -380,7 +352,6 @@ describe(__filename, function () { describe('API: Misc', function () { it('listPadsOfAuthor', async function () { - this.timeout(100); await agent.get(`${endPoint('listPadsOfAuthor')}&authorID=${authorID}`) .expect(200) .expect('Content-Type', /json/) From dd37251da4d3b0783d69198dd4961585a452ecc9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 2 Oct 2021 19:17:47 -0400 Subject: [PATCH 155/322] tests: Promisify some backend tests --- .../backend/specs/api/characterEncoding.js | 77 +- src/tests/backend/specs/api/pad.js | 773 ++++++++---------- 2 files changed, 360 insertions(+), 490 deletions(-) diff --git a/src/tests/backend/specs/api/characterEncoding.js b/src/tests/backend/specs/api/characterEncoding.js index 300cf2d17..2e833d12c 100644 --- a/src/tests/backend/specs/api/characterEncoding.js +++ b/src/tests/backend/specs/api/characterEncoding.js @@ -8,6 +8,7 @@ const common = require('../../common'); const fs = require('fs'); +const fsp = fs.promises; let agent; const apiKey = common.apiKey; @@ -20,73 +21,59 @@ describe(__filename, function () { before(async function () { agent = await common.init(); }); describe('Connectivity For Character Encoding', function () { - it('can connect', function (done) { - agent.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done); + it('can connect', async function () { + await agent.get('/api/') + .expect(200) + .expect('Content-Type', /json/); }); }); describe('API Versioning', function () { - it('finds the version tag', function (done) { - agent.get('/api/') - .expect((res) => { - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); - return; - }) - .expect(200, done); + it('finds the version tag', async function () { + const res = await agent.get('/api/') + .expect(200); + apiVersion = res.body.currentVersion; + if (!res.body.currentVersion) throw new Error('No version set in API'); }); }); describe('Permission', function () { - it('errors with invalid APIKey', function (done) { + it('errors with invalid APIKey', async function () { // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway - const permErrorURL = `/api/${apiVersion}/createPad?apikey=password&padID=test`; - agent.get(permErrorURL) - .expect(401, done); + await agent.get(`/api/${apiVersion}/createPad?apikey=password&padID=test`) + .expect(401); }); }); describe('createPad', function () { - it('creates a new Pad', function (done) { - agent.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to create new Pad'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('creates a new Pad', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to create new Pad'); }); }); describe('setHTML', function () { - it('Sets the HTML of a Pad attempting to weird utf8 encoded content', function (done) { - fs.readFile('tests/backend/specs/api/emojis.html', 'utf8', (err, html) => { - agent.post(endPoint('setHTML')) - .send({ - padID: testPadId, - html, - }) - .expect((res) => { - if (res.body.code !== 0) throw new Error("Can't set HTML properly"); - }) - .expect('Content-Type', /json/) - .expect(200, done); - }); + it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () { + const res = await agent.post(endPoint('setHTML')) + .send({ + padID: testPadId, + html: await fsp.readFile('tests/backend/specs/api/emojis.html', 'utf8'), + }) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error("Can't set HTML properly"); }); }); describe('getHTML', function () { - it('get the HTML of Pad with emojis', function (done) { - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.html.indexOf('🇼') === -1) { - throw new Error('Unable to get the HTML'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('get the HTML of Pad with emojis', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.html.indexOf('🇼') === -1) throw new Error('Unable to get the HTML'); }); }); }); diff --git a/src/tests/backend/specs/api/pad.js b/src/tests/backend/specs/api/pad.js index e50f9a6ab..9e53961f9 100644 --- a/src/tests/backend/specs/api/pad.js +++ b/src/tests/backend/specs/api/pad.js @@ -8,7 +8,6 @@ */ const assert = require('assert').strict; -const async = require('async'); const common = require('../../common'); let agent; @@ -50,32 +49,28 @@ describe(__filename, function () { before(async function () { agent = await common.init(); }); describe('Connectivity', function () { - it('can connect', function (done) { - agent.get('/api/') - .expect('Content-Type', /json/) - .expect(200, done); + it('can connect', async function () { + await agent.get('/api/') + .expect(200) + .expect('Content-Type', /json/); }); }); describe('API Versioning', function () { - it('finds the version tag', function (done) { - agent.get('/api/') - .expect((res) => { - apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); - return; - }) - .expect(200, done); + it('finds the version tag', async function () { + const res = await agent.get('/api/') + .expect(200); + apiVersion = res.body.currentVersion; + if (!apiVersion) throw new Error('No version set in API'); }); }); describe('Permission', function () { - it('errors with invalid APIKey', function (done) { + it('errors with invalid APIKey', async function () { // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway - const permErrorURL = `/api/${apiVersion}/createPad?apikey=password&padID=test`; - agent.get(permErrorURL) - .expect(401, done); + await agent.get(`/api/${apiVersion}/createPad?apikey=password&padID=test`) + .expect(401); }); }); @@ -122,313 +117,262 @@ describe(__filename, function () { */ describe('deletePad', function () { - it('deletes a Pad', function (done) { - agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) - .expect('Content-Type', /json/) - .expect(200, done); // @TODO: we shouldn't expect 200 here since the pad may not exist + it('deletes a Pad', async function () { + await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) + .expect(200) // @TODO: we shouldn't expect 200 here since the pad may not exist + .expect('Content-Type', /json/); }); }); describe('createPad', function () { - it('creates a new Pad', function (done) { - agent.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to create new Pad'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('creates a new Pad', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to create new Pad'); }); }); describe('getRevisionsCount', function () { - it('gets revision count of Pad', function (done) { - agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Revision Count'); - if (res.body.data.revisions !== 0) throw new Error('Incorrect Revision Count'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets revision count of Pad', async function () { + const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to get Revision Count'); + if (res.body.data.revisions !== 0) throw new Error('Incorrect Revision Count'); }); }); describe('getSavedRevisionsCount', function () { - it('gets saved revisions count of Pad', function (done) { - agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); - if (res.body.data.savedRevisions !== 0) { - throw new Error('Incorrect Saved Revisions Count'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revisions count of Pad', async function () { + const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); + if (res.body.data.savedRevisions !== 0) throw new Error('Incorrect Saved Revisions Count'); }); }); describe('listSavedRevisions', function () { - it('gets saved revision list of Pad', function (done) { - agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); - assert.deepEqual(res.body.data.savedRevisions, []); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revision list of Pad', async function () { + const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); + assert.deepEqual(res.body.data.savedRevisions, []); }); }); describe('getHTML', function () { - it('get the HTML of Pad', function (done) { - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.html.length <= 1) throw new Error('Unable to get the HTML'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('get the HTML of Pad', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.html.length <= 1) throw new Error('Unable to get the HTML'); }); }); describe('listAllPads', function () { - it('list all pads', function (done) { - agent.get(endPoint('listAllPads')) - .expect((res) => { - if (res.body.data.padIDs.includes(testPadId) !== true) { - throw new Error('Unable to find pad in pad list'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('list all pads', async function () { + const res = await agent.get(endPoint('listAllPads')) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.padIDs.includes(testPadId) !== true) { + throw new Error('Unable to find pad in pad list'); + } }); }); describe('deletePad', function () { - it('deletes a Pad', function (done) { - agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Deletion failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('deletes a Pad', async function () { + const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Deletion failed'); }); }); describe('listAllPads', function () { - it('list all pads', function (done) { - agent.get(endPoint('listAllPads')) - .expect((res) => { - if (res.body.data.padIDs.includes(testPadId) !== false) { - throw new Error('Test pad should not be in pads list'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('list all pads', async function () { + const res = await agent.get(endPoint('listAllPads')) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.padIDs.includes(testPadId) !== false) { + throw new Error('Test pad should not be in pads list'); + } }); }); describe('getHTML', function () { - it('get the HTML of a Pad -- Should return a failure', function (done) { - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 1) throw new Error('Pad deletion failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('get the HTML of a Pad -- Should return a failure', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 1) throw new Error('Pad deletion failed'); }); }); describe('createPad', function () { - it('creates a new Pad with text', function (done) { - agent.get(`${endPoint('createPad')}&padID=${testPadId}&text=testText`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Creation failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('creates a new Pad with text', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}&text=testText`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Creation failed'); }); }); describe('getText', function () { - it('gets the Pad text and expect it to be testText with \n which is a line break', function (done) { - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.text !== 'testText\n') throw new Error('Pad Creation with text'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets the Pad text and expect it to be testText with \n which is a line break', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.text !== 'testText\n') throw new Error('Pad Creation with text'); }); }); describe('setText', function () { - it('creates a new Pad with text', function (done) { - agent.post(endPoint('setText')) + it('creates a new Pad with text', async function () { + const res = await agent.post(endPoint('setText')) .send({ padID: testPadId, text: 'testTextTwo', }) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad setting text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad setting text failed'); }); }); describe('getText', function () { - it('gets the Pad text', function (done) { - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.text !== 'testTextTwo\n') throw new Error('Setting Text'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets the Pad text', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.text !== 'testTextTwo\n') throw new Error('Setting Text'); }); }); describe('getRevisionsCount', function () { - it('gets Revision Count of a Pad', function (done) { - agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.revisions !== 1) throw new Error('Unable to get text revision count'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets Revision Count of a Pad', async function () { + const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.revisions !== 1) throw new Error('Unable to get text revision count'); }); }); describe('saveRevision', function () { - it('saves Revision', function (done) { - agent.get(`${endPoint('saveRevision')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to save Revision'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('saves Revision', async function () { + const res = await agent.get(`${endPoint('saveRevision')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to save Revision'); }); }); describe('getSavedRevisionsCount', function () { - it('gets saved revisions count of Pad', function (done) { - agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); - if (res.body.data.savedRevisions !== 1) { - throw new Error('Incorrect Saved Revisions Count'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revisions count of Pad', async function () { + const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); + if (res.body.data.savedRevisions !== 1) { + throw new Error('Incorrect Saved Revisions Count'); + } }); }); describe('listSavedRevisions', function () { - it('gets saved revision list of Pad', function (done) { - agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); - assert.deepEqual(res.body.data.savedRevisions, [1]); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets saved revision list of Pad', async function () { + const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); + assert.deepEqual(res.body.data.savedRevisions, [1]); }); }); + describe('padUsersCount', function () { - it('gets User Count of a Pad', function (done) { - agent.get(`${endPoint('padUsersCount')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.padUsersCount !== 0) throw new Error('Incorrect Pad User count'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets User Count of a Pad', async function () { + const res = await agent.get(`${endPoint('padUsersCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.padUsersCount !== 0) throw new Error('Incorrect Pad User count'); }); }); describe('getReadOnlyID', function () { - it('Gets the Read Only ID of a Pad', function (done) { - agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) - .expect((res) => { - if (!res.body.data.readOnlyID) throw new Error('No Read Only ID for Pad'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets the Read Only ID of a Pad', async function () { + const res = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (!res.body.data.readOnlyID) throw new Error('No Read Only ID for Pad'); }); }); describe('listAuthorsOfPad', function () { - it('Get Authors of the Pad', function (done) { - agent.get(`${endPoint('listAuthorsOfPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.authorIDs.length !== 0) { - throw new Error('# of Authors of pad is not 0'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get Authors of the Pad', async function () { + const res = await agent.get(`${endPoint('listAuthorsOfPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.authorIDs.length !== 0) { + throw new Error('# of Authors of pad is not 0'); + } }); }); describe('getLastEdited', function () { - it('Get When Pad was left Edited', function (done) { - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (!res.body.data.lastEdited) { - throw new Error('# of Authors of pad is not 0'); - } else { - lastEdited = res.body.data.lastEdited; - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get When Pad was left Edited', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (!res.body.data.lastEdited) { + throw new Error('# of Authors of pad is not 0'); + } else { + lastEdited = res.body.data.lastEdited; + } }); }); describe('setText', function () { - it('creates a new Pad with text', function (done) { - agent.post(endPoint('setText')) + it('creates a new Pad with text', async function () { + const res = await agent.post(endPoint('setText')) .send({ padID: testPadId, text: 'testTextTwo', }) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad setting text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad setting text failed'); }); }); describe('getLastEdited', function () { - it('Get When Pad was left Edited', function (done) { - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.lastEdited <= lastEdited) { - throw new Error('Editing A Pad is not updating when it was last edited'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Get When Pad was left Edited', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.lastEdited <= lastEdited) { + throw new Error('Editing A Pad is not updating when it was last edited'); + } }); }); describe('padUsers', function () { - it('gets User Count of a Pad', function (done) { - agent.get(`${endPoint('padUsers')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.padUsers.length !== 0) throw new Error('Incorrect Pad Users'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('gets User Count of a Pad', async function () { + const res = await agent.get(`${endPoint('padUsers')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.padUsers.length !== 0) throw new Error('Incorrect Pad Users'); }); }); describe('deletePad', function () { - it('deletes a Pad', function (done) { - agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Deletion failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('deletes a Pad', async function () { + const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Deletion failed'); }); }); @@ -436,198 +380,169 @@ describe(__filename, function () { const copiedPadId = makeid(); describe('createPad', function () { - it('creates a new Pad with text', function (done) { - agent.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Creation failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('creates a new Pad with text', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Creation failed'); }); }); describe('setText', function () { - it('Sets text on a pad Id', function (done) { - agent.post(`${endPoint('setText')}&padID=${testPadId}`) + it('Sets text on a pad Id', async function () { + const res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text}) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Set Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Set Text failed'); }); }); describe('getText', function () { - it('Gets text on a pad Id', function (done) { - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets text on a pad Id', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Get Text failed'); + if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); }); }); describe('setText', function () { - it('Sets text on a pad Id including an explicit newline', function (done) { - agent.post(`${endPoint('setText')}&padID=${testPadId}`) + it('Sets text on a pad Id including an explicit newline', async function () { + const res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text: `${text}\n`}) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Set Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Set Text failed'); }); }); describe('getText', function () { - it("Gets text on a pad Id and doesn't have an excess newline", function (done) { - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it("Gets text on a pad Id and doesn't have an excess newline", async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Get Text failed'); + if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); }); }); describe('getLastEdited', function () { - it('Gets when pad was last edited', function (done) { - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets when pad was last edited', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); }); }); describe('movePad', function () { - it('Move a Pad to a different Pad ID', function (done) { - agent.get(`${endPoint('movePad')}&sourceID=${testPadId}&destinationID=${newPadId}&force=true`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Moving Pad Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Move a Pad to a different Pad ID', async function () { + const res = await agent.get( + `${endPoint('movePad')}&sourceID=${testPadId}&destinationID=${newPadId}&force=true`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Moving Pad Failed'); }); }); describe('getText', function () { - it('Gets text on a pad Id', function (done) { - agent.get(`${endPoint('getText')}&padID=${newPadId}`) - .expect((res) => { - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets text on a pad Id', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${newPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); }); }); describe('movePad', function () { - it('Move a Pad to a different Pad ID', function (done) { - agent.get(`${endPoint('movePad')}&sourceID=${newPadId}&destinationID=${testPadId}` + - '&force=false') - .expect((res) => { - if (res.body.code !== 0) throw new Error('Moving Pad Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Move a Pad to a different Pad ID', async function () { + const res = await agent.get( + `${endPoint('movePad')}&sourceID=${newPadId}&destinationID=${testPadId}&force=false`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Moving Pad Failed'); }); }); describe('getText', function () { - it('Gets text on a pad Id', function (done) { - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets text on a pad Id', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); }); }); describe('getLastEdited', function () { - it('Gets when pad was last edited', function (done) { - agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets when pad was last edited', async function () { + const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); }); }); describe('appendText', function () { - it('Append text to a pad Id', function (done) { - agent.get(`${endPoint('appendText', '1.2.13')}&padID=${testPadId}&text=hello`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Append Text failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Append text to a pad Id', async function () { + const res = await agent.get( + `${endPoint('appendText', '1.2.13')}&padID=${testPadId}&text=hello`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Append Text failed'); }); }); describe('getText', function () { - it('Gets text on a pad Id', function (done) { - agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}hello\n`) { - throw new Error('Pad Text not set properly'); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Gets text on a pad Id', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Pad Get Text failed'); + if (res.body.data.text !== `${text}hello\n`) { + throw new Error('Pad Text not set properly'); + } }); }); - describe('setHTML', function () { - it('Sets the HTML of a Pad attempting to pass ugly HTML', function (done) { + it('Sets the HTML of a Pad attempting to pass ugly HTML', async function () { const html = '
Hello HTML
'; - agent.post(endPoint('setHTML')) + const res = await agent.post(endPoint('setHTML')) .send({ padID: testPadId, html, }) - .expect((res) => { - if (res.body.code !== 0) { - throw new Error("Crappy HTML Can't be Imported[we weren't able to sanitize it']"); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) { + throw new Error("Crappy HTML Can't be Imported[we weren't able to sanitize it']"); + } }); }); describe('setHTML', function () { - it('Sets the HTML of a Pad with complex nested lists of different types', function (done) { - agent.post(endPoint('setHTML')) + it('Sets the HTML of a Pad with complex nested lists of different types', async function () { + const res = await agent.post(endPoint('setHTML')) .send({ padID: testPadId, html: ulHtml, }) - .expect((res) => { - if (res.body.code !== 0) throw new Error('List HTML cant be imported'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('List HTML cant be imported'); }); }); describe('getHTML', function () { - it('Gets back the HTML of a Pad with complex nested lists of different types', function (done) { - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - const receivedHtml = res.body.data.html.replace('
', '').toLowerCase(); - - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. + it('Gets back the HTML of a Pad with complex nested lists of different types', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + const receivedHtml = res.body.data.html.replace('
', '').toLowerCase(); + if (receivedHtml !== expectedHtml) { + throw new Error(`HTML received from export is not the one we were expecting. Received: ${receivedHtml} @@ -636,31 +551,27 @@ describe(__filename, function () { Which is a slightly modified version of the originally imported one: ${ulHtml}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + } }); }); describe('setHTML', function () { - it('Sets the HTML of a Pad with white space between list items', function (done) { - agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('List HTML cant be imported'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('Sets the HTML of a Pad with white space between list items', async function () { + const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('List HTML cant be imported'); }); }); describe('getHTML', function () { - it('Gets back the HTML of a Pad with complex nested lists of different types', function (done) { - agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect((res) => { - const receivedHtml = res.body.data.html.replace('
', '').toLowerCase(); - if (receivedHtml !== expectedSpaceHtml) { - throw new Error(`HTML received from export is not the one we were expecting. + it('Gets back the HTML of a Pad with complex nested lists of different types', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + const receivedHtml = res.body.data.html.replace('
', '').toLowerCase(); + if (receivedHtml !== expectedSpaceHtml) { + throw new Error(`HTML received from export is not the one we were expecting. Received: ${receivedHtml} @@ -669,39 +580,27 @@ describe(__filename, function () { Which is a slightly modified version of the originally imported one: ${ulSpaceHtml}`); - } - }) - .expect('Content-Type', /json/) - .expect(200, done); + } }); }); describe('createPad', function () { - it('errors if pad can be created', function (done) { - const badUrlChars = ['/', '%23', '%3F', '%26']; - async.map( - badUrlChars, - (badUrlChar, cb) => { - agent.get(`${endPoint('createPad')}&padID=${badUrlChar}`) - .expect((res) => { - if (res.body.code !== 1) throw new Error('Pad with bad characters was created'); - }) - .expect('Content-Type', /json/) - .end(cb); - }, - done); + it('errors if pad can be created', async function () { + await Promise.all(['/', '%23', '%3F', '%26'].map(async (badUrlChar) => { + const res = await agent.get(`${endPoint('createPad')}&padID=${badUrlChar}`) + .expect('Content-Type', /json/); + if (res.body.code !== 1) throw new Error('Pad with bad characters was created'); + })); }); }); describe('copyPad', function () { - it('copies the content of a existent pad', function (done) { - agent.get(`${endPoint('copyPad')}&sourceID=${testPadId}&destinationID=${copiedPadId}` + - '&force=true') - .expect((res) => { - if (res.body.code !== 0) throw new Error('Copy Pad Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('copies the content of a existent pad', async function () { + const res = await agent.get( + `${endPoint('copyPad')}&sourceID=${testPadId}&destinationID=${copiedPadId}&force=true`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Copy Pad Failed'); }); }); @@ -709,39 +608,32 @@ describe(__filename, function () { const sourcePadId = makeid(); let newPad; - before(function (done) { - createNewPadWithHtml(sourcePadId, ulHtml, done); + before(async function () { + await createNewPadWithHtml(sourcePadId, ulHtml); }); beforeEach(async function () { newPad = makeid(); }); - it('returns a successful response', function (done) { - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${newPad}&force=false`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); - }) - .expect('Content-Type', /json/) - .expect(200, done); + it('returns a successful response', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + + `&destinationID=${newPad}&force=false`) + .expect(200) + .expect('Content-Type', /json/); + if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); }); // this test validates if the source pad's text and attributes are kept - it('creates a new pad with the same content as the source pad', function (done) { - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${newPad}&force=false`) - .expect((res) => { - if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); - }) - .end(() => { - agent.get(`${endPoint('getHTML')}&padID=${newPad}`) - .expect((res) => { - const receivedHtml = - res.body.data.html.replace('

', '').toLowerCase(); - - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. + it('creates a new pad with the same content as the source pad', async function () { + let res = await agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + + `&destinationID=${newPad}&force=false`); + if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); + res = await agent.get(`${endPoint('getHTML')}&padID=${newPad}`) + .expect(200); + const receivedHtml = res.body.data.html.replace('

', '').toLowerCase(); + if (receivedHtml !== expectedHtml) { + throw new Error(`HTML received from export is not the one we were expecting. Received: ${receivedHtml} @@ -750,56 +642,50 @@ describe(__filename, function () { Which is a slightly modified version of the originally imported one: ${ulHtml}`); - } - }) - .expect(200, done); - }); + } }); context('when try copy a pad with a group that does not exist', function () { const padId = makeid(); const padWithNonExistentGroup = `notExistentGroup$${padId}`; - it('throws an error', function (done) { - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}&` + - `destinationID=${padWithNonExistentGroup}&force=true`) - .expect((res) => { - // code 1, it means an error has happened - if (res.body.code !== 1) throw new Error('It should report an error'); - }) - .expect(200, done); + it('throws an error', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padWithNonExistentGroup}&force=true`) + .expect(200); + // code 1, it means an error has happened + if (res.body.code !== 1) throw new Error('It should report an error'); }); }); context('when try copy a pad and destination pad already exist', function () { const padIdExistent = makeid(); - before(function (done) { - createNewPadWithHtml(padIdExistent, ulHtml, done); + before(async function () { + await createNewPadWithHtml(padIdExistent, ulHtml); }); context('and force is false', function () { - it('throws an error', function (done) { - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${padIdExistent}&force=false`) - .expect((res) => { - // code 1, it means an error has happened - if (res.body.code !== 1) throw new Error('It should report an error'); - }) - .expect(200, done); + it('throws an error', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padIdExistent}&force=false`) + .expect(200); + // code 1, it means an error has happened + if (res.body.code !== 1) throw new Error('It should report an error'); }); }); context('and force is true', function () { - it('returns a successful response', function (done) { - agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + - `&destinationID=${padIdExistent}&force=true`) - .expect((res) => { - // code 1, it means an error has happened - if (res.body.code !== 0) { - throw new Error('Copy pad without history with force true failed'); - } - }) - .expect(200, done); + it('returns a successful response', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padIdExistent}&force=true`) + .expect(200); + // code 1, it means an error has happened + if (res.body.code !== 0) { + throw new Error('Copy pad without history with force true failed'); + } }); }); }); @@ -811,15 +697,12 @@ describe(__filename, function () { */ -const createNewPadWithHtml = (padId, html, cb) => { - agent.get(`${endPoint('createPad')}&padID=${padId}`) - .end(() => { - agent.post(endPoint('setHTML')) - .send({ - padID: padId, - html, - }) - .end(cb); +const createNewPadWithHtml = async (padId, html) => { + await agent.get(`${endPoint('createPad')}&padID=${padId}`); + await agent.post(endPoint('setHTML')) + .send({ + padID: padId, + html, }); }; From 72b12bc97b1f33fc9d997fd3804149a70c8793bb Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 3 Oct 2021 20:06:30 -0400 Subject: [PATCH 156/322] tests: Slight backend test reorganization * Delete some useless uses of `describe()` * Combine some dependent tests * Rename some tests to avoid duplicate names --- .../backend/specs/api/characterEncoding.js | 12 +- src/tests/backend/specs/api/pad.js | 196 ++++-------------- 2 files changed, 45 insertions(+), 163 deletions(-) diff --git a/src/tests/backend/specs/api/characterEncoding.js b/src/tests/backend/specs/api/characterEncoding.js index 2e833d12c..028e4f5ea 100644 --- a/src/tests/backend/specs/api/characterEncoding.js +++ b/src/tests/backend/specs/api/characterEncoding.js @@ -20,24 +20,20 @@ const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?api describe(__filename, function () { before(async function () { agent = await common.init(); }); - describe('Connectivity For Character Encoding', function () { + describe('Sanity checks', function () { it('can connect', async function () { await agent.get('/api/') .expect(200) .expect('Content-Type', /json/); }); - }); - describe('API Versioning', function () { it('finds the version tag', async function () { const res = await agent.get('/api/') .expect(200); apiVersion = res.body.currentVersion; if (!res.body.currentVersion) throw new Error('No version set in API'); }); - }); - describe('Permission', function () { it('errors with invalid APIKey', async function () { // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway @@ -46,16 +42,14 @@ describe(__filename, function () { }); }); - describe('createPad', function () { + describe('Tests', function () { it('creates a new Pad', async function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Unable to create new Pad'); }); - }); - describe('setHTML', function () { it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () { const res = await agent.post(endPoint('setHTML')) .send({ @@ -66,9 +60,7 @@ describe(__filename, function () { .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error("Can't set HTML properly"); }); - }); - describe('getHTML', function () { it('get the HTML of Pad with emojis', async function () { const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) diff --git a/src/tests/backend/specs/api/pad.js b/src/tests/backend/specs/api/pad.js index 9e53961f9..15d591b1b 100644 --- a/src/tests/backend/specs/api/pad.js +++ b/src/tests/backend/specs/api/pad.js @@ -14,6 +14,8 @@ let agent; const apiKey = common.apiKey; let apiVersion = 1; const testPadId = makeid(); +const newPadId = makeid(); +const copiedPadId = makeid(); let lastEdited = ''; const text = generateLongText(); @@ -48,24 +50,20 @@ const expectedSpaceHtml = '
  • one describe(__filename, function () { before(async function () { agent = await common.init(); }); - describe('Connectivity', function () { + describe('Sanity checks', function () { it('can connect', async function () { await agent.get('/api/') .expect(200) .expect('Content-Type', /json/); }); - }); - describe('API Versioning', function () { it('finds the version tag', async function () { const res = await agent.get('/api/') .expect(200); apiVersion = res.body.currentVersion; if (!apiVersion) throw new Error('No version set in API'); }); - }); - describe('Permission', function () { it('errors with invalid APIKey', async function () { // This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // If your APIKey is password you deserve to fail all tests anyway @@ -116,24 +114,20 @@ describe(__filename, function () { */ - describe('deletePad', function () { - it('deletes a Pad', async function () { + describe('Tests', function () { + it('deletes a Pad that does not exist', async function () { await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect(200) // @TODO: we shouldn't expect 200 here since the pad may not exist .expect('Content-Type', /json/); }); - }); - describe('createPad', function () { it('creates a new Pad', async function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Unable to create new Pad'); }); - }); - describe('getRevisionsCount', function () { it('gets revision count of Pad', async function () { const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) .expect(200) @@ -141,9 +135,7 @@ describe(__filename, function () { if (res.body.code !== 0) throw new Error('Unable to get Revision Count'); if (res.body.data.revisions !== 0) throw new Error('Incorrect Revision Count'); }); - }); - describe('getSavedRevisionsCount', function () { it('gets saved revisions count of Pad', async function () { const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) .expect(200) @@ -151,9 +143,7 @@ describe(__filename, function () { if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); if (res.body.data.savedRevisions !== 0) throw new Error('Incorrect Saved Revisions Count'); }); - }); - describe('listSavedRevisions', function () { it('gets saved revision list of Pad', async function () { const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) .expect(200) @@ -161,18 +151,14 @@ describe(__filename, function () { if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); assert.deepEqual(res.body.data.savedRevisions, []); }); - }); - describe('getHTML', function () { it('get the HTML of Pad', async function () { const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.html.length <= 1) throw new Error('Unable to get the HTML'); }); - }); - describe('listAllPads', function () { it('list all pads', async function () { const res = await agent.get(endPoint('listAllPads')) .expect(200) @@ -181,19 +167,15 @@ describe(__filename, function () { throw new Error('Unable to find pad in pad list'); } }); - }); - describe('deletePad', function () { - it('deletes a Pad', async function () { + it('deletes the Pad', async function () { const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Deletion failed'); }); - }); - describe('listAllPads', function () { - it('list all pads', async function () { + it('list all pads again', async function () { const res = await agent.get(endPoint('listAllPads')) .expect(200) .expect('Content-Type', /json/); @@ -201,37 +183,29 @@ describe(__filename, function () { throw new Error('Test pad should not be in pads list'); } }); - }); - describe('getHTML', function () { it('get the HTML of a Pad -- Should return a failure', async function () { const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 1) throw new Error('Pad deletion failed'); }); - }); - describe('createPad', function () { it('creates a new Pad with text', async function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}&text=testText`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Creation failed'); }); - }); - describe('getText', function () { - it('gets the Pad text and expect it to be testText with \n which is a line break', async function () { + it('gets the Pad text and expect it to be testText with trailing \\n', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.text !== 'testText\n') throw new Error('Pad Creation with text'); }); - }); - describe('setText', function () { - it('creates a new Pad with text', async function () { + it('set text', async function () { const res = await agent.post(endPoint('setText')) .send({ padID: testPadId, @@ -241,37 +215,29 @@ describe(__filename, function () { .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad setting text failed'); }); - }); - describe('getText', function () { it('gets the Pad text', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.text !== 'testTextTwo\n') throw new Error('Setting Text'); }); - }); - describe('getRevisionsCount', function () { it('gets Revision Count of a Pad', async function () { const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.revisions !== 1) throw new Error('Unable to get text revision count'); }); - }); - describe('saveRevision', function () { it('saves Revision', async function () { const res = await agent.get(`${endPoint('saveRevision')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Unable to save Revision'); }); - }); - describe('getSavedRevisionsCount', function () { - it('gets saved revisions count of Pad', async function () { + it('gets saved revisions count of Pad again', async function () { const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); @@ -280,37 +246,29 @@ describe(__filename, function () { throw new Error('Incorrect Saved Revisions Count'); } }); - }); - describe('listSavedRevisions', function () { - it('gets saved revision list of Pad', async function () { + it('gets saved revision list of Pad again', async function () { const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); assert.deepEqual(res.body.data.savedRevisions, [1]); }); - }); - describe('padUsersCount', function () { it('gets User Count of a Pad', async function () { const res = await agent.get(`${endPoint('padUsersCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.padUsersCount !== 0) throw new Error('Incorrect Pad User count'); }); - }); - describe('getReadOnlyID', function () { it('Gets the Read Only ID of a Pad', async function () { const res = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (!res.body.data.readOnlyID) throw new Error('No Read Only ID for Pad'); }); - }); - describe('listAuthorsOfPad', function () { it('Get Authors of the Pad', async function () { const res = await agent.get(`${endPoint('listAuthorsOfPad')}&padID=${testPadId}`) .expect(200) @@ -319,9 +277,7 @@ describe(__filename, function () { throw new Error('# of Authors of pad is not 0'); } }); - }); - describe('getLastEdited', function () { it('Get When Pad was left Edited', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) @@ -332,10 +288,8 @@ describe(__filename, function () { lastEdited = res.body.data.lastEdited; } }); - }); - describe('setText', function () { - it('creates a new Pad with text', async function () { + it('set text again', async function () { const res = await agent.post(endPoint('setText')) .send({ padID: testPadId, @@ -345,10 +299,8 @@ describe(__filename, function () { .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad setting text failed'); }); - }); - describe('getLastEdited', function () { - it('Get When Pad was left Edited', async function () { + it('Get When Pad was left Edited again', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); @@ -356,39 +308,28 @@ describe(__filename, function () { throw new Error('Editing A Pad is not updating when it was last edited'); } }); - }); - describe('padUsers', function () { - it('gets User Count of a Pad', async function () { + it('gets User Count of a Pad again', async function () { const res = await agent.get(`${endPoint('padUsers')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.padUsers.length !== 0) throw new Error('Incorrect Pad Users'); }); - }); - describe('deletePad', function () { it('deletes a Pad', async function () { const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Deletion failed'); }); - }); - const newPadId = makeid(); - const copiedPadId = makeid(); - - describe('createPad', function () { - it('creates a new Pad with text', async function () { + it('creates the Pad again', async function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Creation failed'); }); - }); - describe('setText', function () { it('Sets text on a pad Id', async function () { const res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text}) @@ -396,9 +337,7 @@ describe(__filename, function () { .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Set Text failed'); }); - }); - describe('getText', function () { it('Gets text on a pad Id', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) @@ -406,9 +345,7 @@ describe(__filename, function () { if (res.body.code !== 0) throw new Error('Pad Get Text failed'); if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); }); - }); - describe('setText', function () { it('Sets text on a pad Id including an explicit newline', async function () { const res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) .field({text: `${text}\n`}) @@ -416,9 +353,7 @@ describe(__filename, function () { .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Set Text failed'); }); - }); - describe('getText', function () { it("Gets text on a pad Id and doesn't have an excess newline", async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) @@ -426,18 +361,14 @@ describe(__filename, function () { if (res.body.code !== 0) throw new Error('Pad Get Text failed'); if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); }); - }); - describe('getLastEdited', function () { it('Gets when pad was last edited', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); }); - }); - describe('movePad', function () { it('Move a Pad to a different Pad ID', async function () { const res = await agent.get( `${endPoint('movePad')}&sourceID=${testPadId}&destinationID=${newPadId}&force=true`) @@ -445,58 +376,43 @@ describe(__filename, function () { .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Moving Pad Failed'); }); - }); - describe('getText', function () { - it('Gets text on a pad Id', async function () { + it('Gets text from new pad', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${newPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); }); - }); - describe('movePad', function () { - it('Move a Pad to a different Pad ID', async function () { + it('Move pad back to original ID', async function () { const res = await agent.get( `${endPoint('movePad')}&sourceID=${newPadId}&destinationID=${testPadId}&force=false`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Moving Pad Failed'); }); - }); - describe('getText', function () { - it('Gets text on a pad Id', async function () { + it('Get text using original ID', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); }); - }); - describe('getLastEdited', function () { - it('Gets when pad was last edited', async function () { + it('Get last edit of original ID', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); }); - }); - describe('appendText', function () { it('Append text to a pad Id', async function () { - const res = await agent.get( + let res = await agent.get( `${endPoint('appendText', '1.2.13')}&padID=${testPadId}&text=hello`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Append Text failed'); - }); - }); - - describe('getText', function () { - it('Gets text on a pad Id', async function () { - const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('Pad Get Text failed'); @@ -504,9 +420,7 @@ describe(__filename, function () { throw new Error('Pad Text not set properly'); } }); - }); - describe('setHTML', function () { it('Sets the HTML of a Pad attempting to pass ugly HTML', async function () { const html = '
    Hello HTML
    '; const res = await agent.post(endPoint('setHTML')) @@ -520,11 +434,9 @@ describe(__filename, function () { throw new Error("Crappy HTML Can't be Imported[we weren't able to sanitize it']"); } }); - }); - describe('setHTML', function () { - it('Sets the HTML of a Pad with complex nested lists of different types', async function () { - const res = await agent.post(endPoint('setHTML')) + it('Pad with complex nested lists of different types', async function () { + let res = await agent.post(endPoint('setHTML')) .send({ padID: testPadId, html: ulHtml, @@ -532,12 +444,7 @@ describe(__filename, function () { .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('List HTML cant be imported'); - }); - }); - - describe('getHTML', function () { - it('Gets back the HTML of a Pad with complex nested lists of different types', async function () { - const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); @@ -553,20 +460,13 @@ describe(__filename, function () { ${ulHtml}`); } }); - }); - describe('setHTML', function () { - it('Sets the HTML of a Pad with white space between list items', async function () { - const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) + it('Pad with white space between list items', async function () { + let res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) .expect(200) .expect('Content-Type', /json/); if (res.body.code !== 0) throw new Error('List HTML cant be imported'); - }); - }); - - describe('getHTML', function () { - it('Gets back the HTML of a Pad with complex nested lists of different types', async function () { - const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); @@ -582,9 +482,7 @@ describe(__filename, function () { ${ulSpaceHtml}`); } }); - }); - describe('createPad', function () { it('errors if pad can be created', async function () { await Promise.all(['/', '%23', '%3F', '%26'].map(async (badUrlChar) => { const res = await agent.get(`${endPoint('createPad')}&padID=${badUrlChar}`) @@ -592,9 +490,7 @@ describe(__filename, function () { if (res.body.code !== 1) throw new Error('Pad with bad characters was created'); })); }); - }); - describe('copyPad', function () { it('copies the content of a existent pad', async function () { const res = await agent.get( `${endPoint('copyPad')}&sourceID=${testPadId}&destinationID=${copiedPadId}&force=true`) @@ -645,7 +541,7 @@ describe(__filename, function () { } }); - context('when try copy a pad with a group that does not exist', function () { + describe('when try copy a pad with a group that does not exist', function () { const padId = makeid(); const padWithNonExistentGroup = `notExistentGroup$${padId}`; it('throws an error', async function () { @@ -658,35 +554,29 @@ describe(__filename, function () { }); }); - context('when try copy a pad and destination pad already exist', function () { + describe('when try copy a pad and destination pad already exist', function () { const padIdExistent = makeid(); before(async function () { await createNewPadWithHtml(padIdExistent, ulHtml); }); - context('and force is false', function () { - it('throws an error', async function () { - const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + - `&sourceID=${sourcePadId}` + - `&destinationID=${padIdExistent}&force=false`) - .expect(200); - // code 1, it means an error has happened - if (res.body.code !== 1) throw new Error('It should report an error'); - }); + it('force=false throws an error', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padIdExistent}&force=false`) + .expect(200); + // code 1, it means an error has happened + if (res.body.code !== 1) throw new Error('It should report an error'); }); - context('and force is true', function () { - it('returns a successful response', async function () { - const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + - `&sourceID=${sourcePadId}` + - `&destinationID=${padIdExistent}&force=true`) - .expect(200); - // code 1, it means an error has happened - if (res.body.code !== 0) { - throw new Error('Copy pad without history with force true failed'); - } - }); + it('force=true returns a successful response', async function () { + const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` + + `&sourceID=${sourcePadId}` + + `&destinationID=${padIdExistent}&force=true`) + .expect(200); + // code 1, it means an error has happened + if (res.body.code !== 0) throw new Error('Copy pad without history with force true failed'); }); }); }); From e8514db36520e74610728a6f8ddbe066c4386a4b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 3 Oct 2021 20:23:30 -0400 Subject: [PATCH 157/322] tests: Replace manual checks with `assert` --- .../backend/specs/api/characterEncoding.js | 9 +- src/tests/backend/specs/api/pad.js | 168 ++++++------------ 2 files changed, 64 insertions(+), 113 deletions(-) diff --git a/src/tests/backend/specs/api/characterEncoding.js b/src/tests/backend/specs/api/characterEncoding.js index 028e4f5ea..2e579136b 100644 --- a/src/tests/backend/specs/api/characterEncoding.js +++ b/src/tests/backend/specs/api/characterEncoding.js @@ -6,6 +6,7 @@ * TODO: maybe unify those two files and merge in a single one. */ +const assert = require('assert').strict; const common = require('../../common'); const fs = require('fs'); const fsp = fs.promises; @@ -31,7 +32,7 @@ describe(__filename, function () { const res = await agent.get('/api/') .expect(200); apiVersion = res.body.currentVersion; - if (!res.body.currentVersion) throw new Error('No version set in API'); + assert(apiVersion); }); it('errors with invalid APIKey', async function () { @@ -47,7 +48,7 @@ describe(__filename, function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to create new Pad'); + assert.equal(res.body.code, 0); }); it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () { @@ -58,14 +59,14 @@ describe(__filename, function () { }) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error("Can't set HTML properly"); + assert.equal(res.body.code, 0); }); it('get the HTML of Pad with emojis', async function () { const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.html.indexOf('🇼') === -1) throw new Error('Unable to get the HTML'); + assert.match(res.body.data.html, /🇼/); }); }); }); diff --git a/src/tests/backend/specs/api/pad.js b/src/tests/backend/specs/api/pad.js index 15d591b1b..cf16f87f8 100644 --- a/src/tests/backend/specs/api/pad.js +++ b/src/tests/backend/specs/api/pad.js @@ -61,7 +61,7 @@ describe(__filename, function () { const res = await agent.get('/api/') .expect(200); apiVersion = res.body.currentVersion; - if (!apiVersion) throw new Error('No version set in API'); + assert(apiVersion); }); it('errors with invalid APIKey', async function () { @@ -125,30 +125,30 @@ describe(__filename, function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to create new Pad'); + assert.equal(res.body.code, 0); }); it('gets revision count of Pad', async function () { const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to get Revision Count'); - if (res.body.data.revisions !== 0) throw new Error('Incorrect Revision Count'); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.revisions, 0); }); it('gets saved revisions count of Pad', async function () { const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); - if (res.body.data.savedRevisions !== 0) throw new Error('Incorrect Saved Revisions Count'); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.savedRevisions, 0); }); it('gets saved revision list of Pad', async function () { const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); + assert.equal(res.body.code, 0); assert.deepEqual(res.body.data.savedRevisions, []); }); @@ -156,53 +156,49 @@ describe(__filename, function () { const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.html.length <= 1) throw new Error('Unable to get the HTML'); + assert(res.body.data.html.length > 1); }); it('list all pads', async function () { const res = await agent.get(endPoint('listAllPads')) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.padIDs.includes(testPadId) !== true) { - throw new Error('Unable to find pad in pad list'); - } + assert(res.body.data.padIDs.includes(testPadId)); }); it('deletes the Pad', async function () { const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Deletion failed'); + assert.equal(res.body.code, 0); }); it('list all pads again', async function () { const res = await agent.get(endPoint('listAllPads')) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.padIDs.includes(testPadId) !== false) { - throw new Error('Test pad should not be in pads list'); - } + assert(!res.body.data.padIDs.includes(testPadId)); }); it('get the HTML of a Pad -- Should return a failure', async function () { const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 1) throw new Error('Pad deletion failed'); + assert.equal(res.body.code, 1); }); it('creates a new Pad with text', async function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}&text=testText`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Creation failed'); + assert.equal(res.body.code, 0); }); it('gets the Pad text and expect it to be testText with trailing \\n', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.text !== 'testText\n') throw new Error('Pad Creation with text'); + assert.equal(res.body.data.text, 'testText\n'); }); it('set text', async function () { @@ -213,45 +209,43 @@ describe(__filename, function () { }) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad setting text failed'); + assert.equal(res.body.code, 0); }); it('gets the Pad text', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.text !== 'testTextTwo\n') throw new Error('Setting Text'); + assert.equal(res.body.data.text, 'testTextTwo\n'); }); it('gets Revision Count of a Pad', async function () { const res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.revisions !== 1) throw new Error('Unable to get text revision count'); + assert.equal(res.body.data.revisions, 1); }); it('saves Revision', async function () { const res = await agent.get(`${endPoint('saveRevision')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to save Revision'); + assert.equal(res.body.code, 0); }); it('gets saved revisions count of Pad again', async function () { const res = await agent.get(`${endPoint('getSavedRevisionsCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions Count'); - if (res.body.data.savedRevisions !== 1) { - throw new Error('Incorrect Saved Revisions Count'); - } + assert.equal(res.body.code, 0); + assert.equal(res.body.data.savedRevisions, 1); }); it('gets saved revision list of Pad again', async function () { const res = await agent.get(`${endPoint('listSavedRevisions')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Unable to get Saved Revisions List'); + assert.equal(res.body.code, 0); assert.deepEqual(res.body.data.savedRevisions, [1]); }); @@ -259,34 +253,29 @@ describe(__filename, function () { const res = await agent.get(`${endPoint('padUsersCount')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.padUsersCount !== 0) throw new Error('Incorrect Pad User count'); + assert.equal(res.body.data.padUsersCount, 0); }); it('Gets the Read Only ID of a Pad', async function () { const res = await agent.get(`${endPoint('getReadOnlyID')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (!res.body.data.readOnlyID) throw new Error('No Read Only ID for Pad'); + assert(res.body.data.readOnlyID); }); it('Get Authors of the Pad', async function () { const res = await agent.get(`${endPoint('listAuthorsOfPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.authorIDs.length !== 0) { - throw new Error('# of Authors of pad is not 0'); - } + assert.equal(res.body.data.authorIDs.length, 0); }); it('Get When Pad was left Edited', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (!res.body.data.lastEdited) { - throw new Error('# of Authors of pad is not 0'); - } else { - lastEdited = res.body.data.lastEdited; - } + assert(res.body.data.lastEdited); + lastEdited = res.body.data.lastEdited; }); it('set text again', async function () { @@ -297,37 +286,35 @@ describe(__filename, function () { }) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad setting text failed'); + assert.equal(res.body.code, 0); }); it('Get When Pad was left Edited again', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.lastEdited <= lastEdited) { - throw new Error('Editing A Pad is not updating when it was last edited'); - } + assert(res.body.data.lastEdited > lastEdited); }); it('gets User Count of a Pad again', async function () { const res = await agent.get(`${endPoint('padUsers')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.padUsers.length !== 0) throw new Error('Incorrect Pad Users'); + assert.equal(res.body.data.padUsers.length, 0); }); it('deletes a Pad', async function () { const res = await agent.get(`${endPoint('deletePad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Deletion failed'); + assert.equal(res.body.code, 0); }); it('creates the Pad again', async function () { const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Creation failed'); + assert.equal(res.body.code, 0); }); it('Sets text on a pad Id', async function () { @@ -335,15 +322,15 @@ describe(__filename, function () { .field({text}) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Set Text failed'); + assert.equal(res.body.code, 0); }); it('Gets text on a pad Id', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.text, `${text}\n`); }); it('Sets text on a pad Id including an explicit newline', async function () { @@ -351,22 +338,22 @@ describe(__filename, function () { .field({text: `${text}\n`}) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Set Text failed'); + assert.equal(res.body.code, 0); }); it("Gets text on a pad Id and doesn't have an excess newline", async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Text not set properly'); + assert.equal(res.body.code, 0); + assert.equal(res.body.data.text, `${text}\n`); }); it('Gets when pad was last edited', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); + assert.notEqual(res.body.lastEdited, 0); }); it('Move a Pad to a different Pad ID', async function () { @@ -374,14 +361,14 @@ describe(__filename, function () { `${endPoint('movePad')}&sourceID=${testPadId}&destinationID=${newPadId}&force=true`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Moving Pad Failed'); + assert.equal(res.body.code, 0); }); it('Gets text from new pad', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${newPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); + assert.equal(res.body.data.text, `${text}\n`); }); it('Move pad back to original ID', async function () { @@ -389,21 +376,21 @@ describe(__filename, function () { `${endPoint('movePad')}&sourceID=${newPadId}&destinationID=${testPadId}&force=false`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Moving Pad Failed'); + assert.equal(res.body.code, 0); }); it('Get text using original ID', async function () { const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.data.text !== `${text}\n`) throw new Error('Pad Get Text failed'); + assert.equal(res.body.data.text, `${text}\n`); }); it('Get last edit of original ID', async function () { const res = await agent.get(`${endPoint('getLastEdited')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.lastEdited === 0) throw new Error('Get Last Edited Failed'); + assert.notEqual(res.body.lastEdited, 0); }); it('Append text to a pad Id', async function () { @@ -411,14 +398,12 @@ describe(__filename, function () { `${endPoint('appendText', '1.2.13')}&padID=${testPadId}&text=hello`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Append Text failed'); + assert.equal(res.body.code, 0); res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Pad Get Text failed'); - if (res.body.data.text !== `${text}hello\n`) { - throw new Error('Pad Text not set properly'); - } + assert.equal(res.body.code, 0); + assert.equal(res.body.data.text, `${text}hello\n`); }); it('Sets the HTML of a Pad attempting to pass ugly HTML', async function () { @@ -430,9 +415,7 @@ describe(__filename, function () { }) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) { - throw new Error("Crappy HTML Can't be Imported[we weren't able to sanitize it']"); - } + assert.equal(res.body.code, 0); }); it('Pad with complex nested lists of different types', async function () { @@ -443,51 +426,31 @@ describe(__filename, function () { }) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('List HTML cant be imported'); + assert.equal(res.body.code, 0); res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. - Received: - ${receivedHtml} - - Expected: - ${expectedHtml} - - Which is a slightly modified version of the originally imported one: - ${ulHtml}`); - } + assert.equal(receivedHtml, expectedHtml); }); it('Pad with white space between list items', async function () { let res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}&html=${ulSpaceHtml}`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('List HTML cant be imported'); + assert.equal(res.body.code, 0); res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) .expect(200) .expect('Content-Type', /json/); const receivedHtml = res.body.data.html.replace('
    ', '').toLowerCase(); - if (receivedHtml !== expectedSpaceHtml) { - throw new Error(`HTML received from export is not the one we were expecting. - Received: - ${receivedHtml} - - Expected: - ${expectedSpaceHtml} - - Which is a slightly modified version of the originally imported one: - ${ulSpaceHtml}`); - } + assert.equal(receivedHtml, expectedSpaceHtml); }); it('errors if pad can be created', async function () { await Promise.all(['/', '%23', '%3F', '%26'].map(async (badUrlChar) => { const res = await agent.get(`${endPoint('createPad')}&padID=${badUrlChar}`) .expect('Content-Type', /json/); - if (res.body.code !== 1) throw new Error('Pad with bad characters was created'); + assert.equal(res.body.code, 1); })); }); @@ -496,7 +459,7 @@ describe(__filename, function () { `${endPoint('copyPad')}&sourceID=${testPadId}&destinationID=${copiedPadId}&force=true`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Copy Pad Failed'); + assert.equal(res.body.code, 0); }); }); @@ -517,28 +480,18 @@ describe(__filename, function () { `&destinationID=${newPad}&force=false`) .expect(200) .expect('Content-Type', /json/); - if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); + assert.equal(res.body.code, 0); }); // this test validates if the source pad's text and attributes are kept it('creates a new pad with the same content as the source pad', async function () { let res = await agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` + `&destinationID=${newPad}&force=false`); - if (res.body.code !== 0) throw new Error('Copy Pad Without History Failed'); + assert.equal(res.body.code, 0); res = await agent.get(`${endPoint('getHTML')}&padID=${newPad}`) .expect(200); const receivedHtml = res.body.data.html.replace('

    ', '').toLowerCase(); - if (receivedHtml !== expectedHtml) { - throw new Error(`HTML received from export is not the one we were expecting. - Received: - ${receivedHtml} - - Expected: - ${expectedHtml} - - Which is a slightly modified version of the originally imported one: - ${ulHtml}`); - } + assert.equal(receivedHtml, expectedHtml); }); describe('when try copy a pad with a group that does not exist', function () { @@ -549,8 +502,7 @@ describe(__filename, function () { `&sourceID=${sourcePadId}` + `&destinationID=${padWithNonExistentGroup}&force=true`) .expect(200); - // code 1, it means an error has happened - if (res.body.code !== 1) throw new Error('It should report an error'); + assert.equal(res.body.code, 1); }); }); @@ -566,8 +518,7 @@ describe(__filename, function () { `&sourceID=${sourcePadId}` + `&destinationID=${padIdExistent}&force=false`) .expect(200); - // code 1, it means an error has happened - if (res.body.code !== 1) throw new Error('It should report an error'); + assert.equal(res.body.code, 1); }); it('force=true returns a successful response', async function () { @@ -575,8 +526,7 @@ describe(__filename, function () { `&sourceID=${sourcePadId}` + `&destinationID=${padIdExistent}&force=true`) .expect(200); - // code 1, it means an error has happened - if (res.body.code !== 0) throw new Error('Copy pad without history with force true failed'); + assert.equal(res.body.code, 0); }); }); }); From ee610027c07f3b6163a06dda41bce47c622572f7 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 5 Oct 2021 06:23:36 +0000 Subject: [PATCH 158/322] fix: upgrade terser from 5.7.2 to 5.8.0 Snyk has created this PR to upgrade terser from 5.7.2 to 5.8.0. See this package in npm: https://www.npmjs.com/package/terser See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 14 +++++++------- src/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 80e21496c..a5988dbb8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8394,9 +8394,9 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -8877,13 +8877,13 @@ } }, "terser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.2.tgz", - "integrity": "sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.8.0.tgz", + "integrity": "sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==", "requires": { "commander": "^2.20.0", "source-map": "~0.7.2", - "source-map-support": "~0.5.19" + "source-map-support": "~0.5.20" }, "dependencies": { "source-map": { diff --git a/src/package.json b/src/package.json index 8e441058a..8deddff59 100644 --- a/src/package.json +++ b/src/package.json @@ -65,7 +65,7 @@ "security": "1.0.0", "semver": "^7.3.5", "socket.io": "^2.4.1", - "terser": "^5.7.2", + "terser": "^5.8.0", "threads": "^1.6.5", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", From a7734ddd94582e80ea587307e0414d65e17a5992 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 7 Oct 2021 03:58:33 -0400 Subject: [PATCH 159/322] deps: Update ueberdb2 to 1.4.18 This pulls in newer versions of some database drivers which silences some `npm audit` security warnings. This also adds support for PostgreSQL connection strings. --- src/package-lock.json | 563 ++++++++++++++++-------------------------- src/package.json | 2 +- 2 files changed, 207 insertions(+), 358 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index a5988dbb8..f765be744 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -38,9 +38,9 @@ } }, "@azure/core-client": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.3.0.tgz", - "integrity": "sha512-4ricu3aM1TQP2vglBcvFX8KgbWVe+7hl1jVAw6BzIGG4CTAvO3ygDS6th3O+zFwGN9xkgXFHa7Tp3u9za8ciIA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.3.1.tgz", + "integrity": "sha512-7IHm2DGg2u7dJYtCW84Ik7uENHfE8VsM/sWloZezPKYDoWZrg7JzwjvdGAfsaELKi2p0GE+JBaAbDYnNpr5V1w==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", @@ -62,9 +62,9 @@ } }, "@azure/core-http": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.1.0.tgz", - "integrity": "sha512-Pzj87F4b1RH4PFDUpxkZqCdDZ35c5AjDCt3lsTn3i7yCtrXasEm6PVJYhjwsvYYmtgM7aDZIXexcu/qLLf7kyA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.2.1.tgz", + "integrity": "sha512-7ATnV3OGzCO2K9kMrh3NKUM8b4v+xasmlUhkNZz6uMbm+8XH/AexLkhRGsoo0GyKNlEGvyGEfytqTk0nUY2I4A==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-asynciterator-polyfill": "^1.0.0", @@ -72,8 +72,8 @@ "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.1", - "form-data": "^3.0.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", "node-fetch": "^2.6.0", "process": "^0.11.10", "tough-cookie": "^4.0.0", @@ -92,16 +92,6 @@ "tslib": "^2.2.0" } }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -110,9 +100,9 @@ } }, "@azure/core-lro": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.0.tgz", - "integrity": "sha512-TJo95eNT1dwYOPCb0m1C2zyxVlHuRRkKGeg9TKu8XMF2qh4v6c1weD63r9RVIrLdHdnSqS0n6PTXBpWoB8NqMw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.1.tgz", + "integrity": "sha512-HE6PBl+mlKa0eBsLwusHqAqjLc5n9ByxeDo3Hz4kF3B1hqHvRkBr4oMgoT6tX7Hc3q97KfDctDUon7EhvoeHPA==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-tracing": "1.0.0-preview.13", @@ -141,15 +131,15 @@ } }, "@azure/core-rest-pipeline": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.2.0.tgz", - "integrity": "sha512-oOd8feRcuoSUwflPNLPO8x6v+m4TcJ9DmazlouuG9d64zJJEwaU757ovpRss9zaL8cggUAdm84C4EbtZ/ltMAw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.3.1.tgz", + "integrity": "sha512-xTQiv47O5cWzJFkwiDrUTT4K4IYbUIts0gaou5TZxAAuhQi9kAKWHEmFTjHVMOeAmyDhlMM5cb21M2n4WDto1A==", "requires": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-tracing": "1.0.0-preview.13", "@azure/logger": "^1.0.0", - "form-data": "^3.0.0", + "form-data": "^4.0.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "tslib": "^2.2.0", @@ -165,16 +155,6 @@ "tslib": "^2.2.0" } }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -192,9 +172,9 @@ } }, "@azure/identity": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.1.tgz", - "integrity": "sha512-ENYdcHT72PwEb+aiL2G6WIXxdm8mO0LNLZVPXaSRZYNsIshre72MF1H/rnJvcVGX9uVDVClSbNPxXwY5MJPLjw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-1.5.2.tgz", + "integrity": "sha512-vqyeRbd2i0h9F4mqW5JbkP1xfabqKQ21l/81osKhpOQ2LtwaJW6nw4+0PsVYnxcbPHFCIZt6EWAk74a3OGYZJA==", "requires": { "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.0.0", @@ -248,11 +228,11 @@ } }, "@azure/logger": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.2.tgz", - "integrity": "sha512-YZNjNV0vL3nN2nedmcjQBcpCTo3oqceXmgiQtEm6fLpucjRZyQKAQruhCmCpRlB1iykqKJJ/Y8CDmT5rIE6IJw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", + "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", "requires": { - "tslib": "^2.0.0" + "tslib": "^2.2.0" } }, "@azure/ms-rest-azure-env": { @@ -309,9 +289,9 @@ } }, "@azure/ms-rest-nodeauth": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.10.tgz", - "integrity": "sha512-oel7ibYlredh2wo7XwNYMx4jWlbMkIzCC8t8VpdhsAWDJVNSSce+DYj5jjZn1oED+QsCytVM2B7/QTuLN1/yDw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.1.0.tgz", + "integrity": "sha512-F4NKrbkZg0qD3+rUM8fvJHOFRkXFoEiptYTZtLBruN3VwBFIqbTFW0fmgRyBW9seZl+mX2OexQA5GzWenSA3Kw==", "requires": { "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-js": "^2.0.4", @@ -540,10 +520,27 @@ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "optional": true, + "requires": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + } + }, "@opentelemetry/api": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.2.tgz", - "integrity": "sha512-DCF9oC89ao8/EJUqrp/beBlDR8Bp2R43jqtzayqCoomIvkwTuPfLcHdVhIGRR69GFlkykFjcDW+V92t0AS7Tww==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.3.tgz", + "integrity": "sha512-puWxACExDe9nxbBB3lOymQFrLYml2dVOrd7USiVRnSbgXE+KwBu+HxFvxrzfqsiSda9IWsXJG1ef7C1O2/GmKQ==" }, "@sinonjs/commons": { "version": "1.8.3", @@ -609,9 +606,9 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "16.7.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.8.tgz", - "integrity": "sha512-8upnoQU0OPzbIkm+ZMM0zCeFCkw2s3mS0IWdx0+AAaWqm4fkBb0UJp8Edl7FVKRamYbpJC/aVsHpKWBIbiC7Zg==" + "version": "16.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", + "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==" }, "@types/node-fetch": { "version": "2.5.12", @@ -653,9 +650,9 @@ "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" }, "@types/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", "requires": { "@types/node": "*" } @@ -671,6 +668,11 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "@xmldom/xmldom": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz", + "integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==" + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -732,26 +734,20 @@ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" }, "adal-node": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.2.tgz", - "integrity": "sha512-luzQ9cXOjUlZoCiWeYbyR+nHwScSrPTDTbOInFphQs/PnwNz6wAIVkbsHEXtvYBnjLctByTTI8ccfpGX100oRQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.3.tgz", + "integrity": "sha512-gMKr8RuYEYvsj7jyfCv/4BfKToQThz20SP71N3AtFn3ia3yAR8Qt2T3aVQhuJzunWs2b38ZsQV0qsZPdwZr7VQ==", "requires": { - "@types/node": "^8.0.47", + "@xmldom/xmldom": "^0.7.0", "async": "^2.6.3", "axios": "^0.21.1", "date-utils": "*", "jws": "3.x.x", "underscore": ">= 1.3.1", "uuid": "^3.1.0", - "xmldom": ">= 0.1.x", "xpath.js": "~1.1.0" }, "dependencies": { - "@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" - }, "async": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", @@ -772,9 +768,9 @@ } }, "adm-zip": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.5.tgz", - "integrity": "sha512-IWwXKnCbirdbyXSfUDvCCrmYrOHANRZcc8NcRrvTlIApdl7PwE9oGcsYvNeJPAVY1M+70b4PxXGKIf8AEuiQ6w==" + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-QLEo3eoC2B0i3+g/G5nNzKbGoVOjW2ingZ4TXl7/YeDM+FAl3SiHSNnokTZLFEuVHBn5CbZ42KJcIIsRji1EgQ==" }, "after": { "version": "0.8.2", @@ -867,9 +863,9 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "optional": true, "requires": { "delegates": "^1.0.0", @@ -963,11 +959,11 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "requires": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.0" } }, "axios-cookiejar-support": { @@ -1081,15 +1077,6 @@ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "optional": true, - "requires": { - "inherits": "~2.0.0" - } - }, "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -1776,6 +1763,12 @@ "ansi-colors": "^4.1.1" } }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2491,9 +2484,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.2.tgz", - "integrity": "sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==" + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" }, "forever-agent": { "version": "0.6.1", @@ -2532,12 +2525,12 @@ "optional": true }, "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "optional": true, "requires": { - "minipass": "^2.6.0" + "minipass": "^3.0.0" } }, "fs.realpath": { @@ -2552,18 +2545,6 @@ "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -2942,15 +2923,6 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "ignore-walk": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", - "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -3161,9 +3133,9 @@ } }, "jsbi": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.1.tgz", - "integrity": "sha512-NzcT09wuJReIO829enrY3yRdHFz+ciVIq01PCGPkHlwIv5Dj9v2F4daQ4akwuPHf6xO/oii4Jrd3BsPepOxXrg==" + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.5.tgz", + "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==" }, "jsbn": { "version": "0.1.1", @@ -3572,6 +3544,23 @@ "yallist": "^4.0.0" } }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "optional": true + } + } + }, "measured-core": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-2.0.0.tgz", @@ -3641,40 +3630,29 @@ "optional": true }, "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", + "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", "optional": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true - } + "yallist": "^4.0.0" } }, "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "optional": true, "requires": { - "minipass": "^2.9.0" + "minipass": "^3.0.0", + "yallist": "^4.0.0" } }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "optional": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true }, "mkdirp-classic": { "version": "0.5.3", @@ -3782,14 +3760,14 @@ } }, "mongodb": { - "version": "3.6.12", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.12.tgz", - "integrity": "sha512-ErHpF4P4disEIQB8Nns2twIMVXcvmlwjpKqfVnyB/hhd/L5We48LfoBYjBjuUSiSqL6ffmcygPTgjvpy2LETRQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.2.tgz", + "integrity": "sha512-/Qi0LmOjzIoV66Y2JQkqmIIfFOy7ZKsXnQNlUXPFXChOw3FCdNqVD5zvci9ybm6pkMe/Nw+Rz9I0Zsk2a+05iQ==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", "denque": "^1.4.1", - "optional-require": "^1.0.3", + "optional-require": "^1.1.8", "safe-buffer": "^5.1.2", "saslprep": "^1.0.0" } @@ -3800,9 +3778,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "msal": { - "version": "1.4.12", - "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.12.tgz", - "integrity": "sha512-gjupwQ6nvNL6mZkl5NIXyUmZhTiEMRu5giNdgHMh8l5EPOnV2Xj6nukY1NIxFacSTkEYUSDB47Pej9GxDYf+1w==", + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.14.tgz", + "integrity": "sha512-k8M5+/jbfSQoCf7CyQzBP5HE5mY8TkBujykLGTEp2x0MvOK/FQsfUTNis28zlvvPVzhgrhb5GQiGM8rRpXyHdA==", "requires": { "tslib": "^1.9.3" }, @@ -3882,9 +3860,9 @@ } }, "nano": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/nano/-/nano-9.0.3.tgz", - "integrity": "sha512-NFI8+6q5ihnozH6qK+BJ+ilnPfZzBhlUswaFgqUvSp2EN5eJ2BMxbzkYiBsN+waa+N95FculCdbneDmzLWfXaQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/nano/-/nano-9.0.5.tgz", + "integrity": "sha512-fEAhwAdXh4hDDnC8cYJtW6D8ivOmpvFAqT90+zEuQREpRkzA/mJPcI4EKv15JUdajaqiLTXNoKK6PaRF+/06DQ==", "requires": { "@types/tough-cookie": "^4.0.0", "axios": "^0.21.1", @@ -3926,34 +3904,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "needle": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.0.tgz", - "integrity": "sha512-UBLC4P8w9to3rAhWOQYXIXzTUio9yVnDzIeKxfGbF+Hngy+2bXTqqFK+6nF42EAQKfJdezXK6vzMsefUa1Y3ag==", - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "optional": true - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -3984,9 +3934,9 @@ } }, "node-abi": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz", - "integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", + "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", "optional": true, "requires": { "semver": "^5.4.1" @@ -4012,108 +3962,50 @@ "optional": true }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "optional": true, + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", + "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" + "whatwg-url": "^5.0.0" }, "dependencies": { - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "optional": true + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "requires": { - "isexe": "^2.0.0" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } } } }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "node-gyp": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz", + "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", "optional": true, "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "optional": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "optional": true - }, - "tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "optional": true, - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true - } + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.3", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "request": "^2.88.2", + "rimraf": "^3.0.2", + "semver": "^7.3.2", + "tar": "^6.0.2", + "which": "^2.0.2" } }, "nodeify": { @@ -4126,9 +4018,9 @@ } }, "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "optional": true, "requires": { "abbrev": "1" @@ -7242,32 +7134,6 @@ } } }, - "npm-bundled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", - "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -7400,9 +7266,9 @@ "integrity": "sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw==" }, "optional-require": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.7.tgz", - "integrity": "sha512-cIeRZocXsZnZYn+SevbtSqNlLbeoS4mLzuNn4fvXRMDRNhTGg0sxuKXl0FnZCtnew85LorNxIbZp5OeliILhMw==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz", + "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==", "requires": { "require-at": "^1.0.6" } @@ -7420,28 +7286,6 @@ "word-wrap": "~1.2.3" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7969,9 +7813,9 @@ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "optional": true, "requires": { "glob": "^7.1.3" @@ -8156,9 +8000,9 @@ } }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "optional": true }, "simple-concat": { @@ -8179,9 +8023,9 @@ } }, "simple-git": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.45.0.tgz", - "integrity": "sha512-wu/Ujs9IXn0HuyYm4HyRvne+EKsjJSWKEMkB3wQa3gNHSMHt7y3oeNX9zRQ3UBPk7bRRMLLHAdIZCZfHT9ehPg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.46.0.tgz", + "integrity": "sha512-6eumII1vfP4NpRqxZcVWCcIT5xHH6dRyvBZSjkH4dJRDRpv+0f75hrN5ysp++y23Mfr3AbRC/dO2NDbfj1lJpQ==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", @@ -8461,14 +8305,13 @@ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" }, "sqlite3": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz", - "integrity": "sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==", + "version": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a", + "from": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a", "optional": true, "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", "node-addon-api": "^3.0.0", - "node-gyp": "3.x", - "node-pre-gyp": "^0.11.0" + "node-gyp": "7.x" } }, "sqlstring": { @@ -8707,14 +8550,25 @@ } }, "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "optional": true, "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true + } } }, "tar-fs": { @@ -9054,23 +8908,23 @@ } }, "ueberdb2": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.15.tgz", - "integrity": "sha512-Gk+aj/sVFrGe+f+dd4m8Z58FwUbHONsKPmrFyFdhB4GnUQaHRya31bCU3Ax1Kk2g14HSWlOy3Eub/xL9X5zbpA==", + "version": "1.4.18", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.18.tgz", + "integrity": "sha512-u0Joo4FpNPw4PeTJTPe6GIZBFscZ8DbIFuD0cd60mMbkBpAh7l039hhOxoAGHuF0eRM9QEEqPpOunlOOJ1TTeg==", "requires": { "async": "^3.2.1", "cassandra-driver": "^4.6.3", "dirty": "^1.1.3", - "elasticsearch": "^16.7.1", - "mongodb": "^3.6.11", + "elasticsearch": "^16.7.2", + "mongodb": "^3.7.1", "mssql": "^7.2.1", "mysql": "2.18.1", - "nano": "^9.0.3", + "nano": "^9.0.5", "pg": "^8.7.1", "redis": "^3.1.2", "rethinkdb": "^2.4.2", - "simple-git": "^2.45.0", - "sqlite3": "^5.0.2" + "simple-git": "^2.45.1", + "sqlite3": "github:mapbox/node-sqlite3#593c9d498be2510d286349134537e3bf89401c4a" } }, "uid-safe": { @@ -9379,11 +9233,6 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, - "xmldom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", - "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" - }, "xmlhttprequest-ssl": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", diff --git a/src/package.json b/src/package.json index 8deddff59..6746ab2c8 100644 --- a/src/package.json +++ b/src/package.json @@ -69,7 +69,7 @@ "threads": "^1.6.5", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", - "ueberdb2": "^1.4.15", + "ueberdb2": "^1.4.18", "underscore": "1.13.1", "unorm": "1.6.0", "wtfnode": "^0.9.1" From ac3a7191cf208f72e8ef0fc745fd6b6ee2afddde Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 7 Oct 2021 13:03:04 +0200 Subject: [PATCH 160/322] Localisation updates from https://translatewiki.net. --- src/locales/fi.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/locales/fi.json b/src/locales/fi.json index 9f569d44b..3c750b4a6 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -5,6 +5,7 @@ "Espeox", "Jl", "Lliehu", + "MITO", "Maantietäjä", "Macofe", "MrTapsa", @@ -18,7 +19,10 @@ "VezonThunder" ] }, + "admin.page-title": "Ylläpitäjän kojelauta - Etherpad", + "admin_plugins": "Lisäosien hallinta", "admin_plugins.available": "Saatavilla olevat liitännäiset", + "admin_plugins.available_not-found": "Lisäosia ei löytynyt.", "admin_plugins.available_install.value": "Lataa", "admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia", "admin_plugins.description": "Kuvaus", From 2155e216a6e0d95b5e357cb026c37073b784d694 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 5 Oct 2021 12:36:27 -0400 Subject: [PATCH 161/322] tests: Remove overly agressive timeouts --- src/tests/backend/specs/webaccess.js | 46 ---------------------------- 1 file changed, 46 deletions(-) diff --git a/src/tests/backend/specs/webaccess.js b/src/tests/backend/specs/webaccess.js index fe8c4c5c9..7594b57e3 100644 --- a/src/tests/backend/specs/webaccess.js +++ b/src/tests/backend/specs/webaccess.js @@ -43,67 +43,56 @@ describe(__filename, function () { describe('webaccess: without plugins', function () { it('!authn !authz anonymous / -> 200', async function () { - this.timeout(150); settings.requireAuthentication = false; settings.requireAuthorization = false; await agent.get('/').expect(200); }); it('!authn !authz anonymous /admin/ -> 401', async function () { - this.timeout(100); settings.requireAuthentication = false; settings.requireAuthorization = false; await agent.get('/admin/').expect(401); }); it('authn !authz anonymous / -> 401', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').expect(401); }); it('authn !authz user / -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('user', 'user-password').expect(200); }); it('authn !authz user /admin/ -> 403', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/admin/').auth('user', 'user-password').expect(403); }); it('authn !authz admin / -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/').auth('admin', 'admin-password').expect(200); }); it('authn !authz admin /admin/ -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = false; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); }); it('authn authz user / -> 403', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('user', 'user-password').expect(403); }); it('authn authz user /admin/ -> 403', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/admin/').auth('user', 'user-password').expect(403); }); it('authn authz admin / -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/').auth('admin', 'admin-password').expect(200); }); it('authn authz admin /admin/ -> 200', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); @@ -117,7 +106,6 @@ describe(__filename, function () { // parsing, resulting in successful comparisons against a null or undefined password. for (const creds of ['admin', 'admin:']) { it(`admin password: ${adminPassword} credentials: ${creds}`, async function () { - this.timeout(100); settings.users.admin.password = adminPassword; const encCreds = Buffer.from(creds).toString('base64'); await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401); @@ -173,13 +161,11 @@ describe(__filename, function () { }); it('defers if it returns []', async function () { - this.timeout(100); await agent.get('/').expect(200); // Note: The preAuthorize hook always runs even if requireAuthorization is false. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); it('bypasses authenticate and authorize hooks when true is returned', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; handlers.preAuthorize[0].innerHandle = () => [true]; @@ -187,7 +173,6 @@ describe(__filename, function () { assert.deepEqual(callOrder, ['preAuthorize_0']); }); it('bypasses authenticate and authorize hooks when false is returned', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; handlers.preAuthorize[0].innerHandle = () => [false]; @@ -195,14 +180,12 @@ describe(__filename, function () { assert.deepEqual(callOrder, ['preAuthorize_0']); }); it('bypasses authenticate and authorize hooks for static content, defers', async function () { - this.timeout(100); settings.requireAuthentication = true; settings.requireAuthorization = true; await agent.get('/static/robots.txt').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); it('cannot grant access to /admin', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => [true]; await agent.get('/admin/').expect(401); // Notes: @@ -216,13 +199,11 @@ describe(__filename, function () { 'authenticate_1']); }); it('can deny access to /admin', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => [false]; await agent.get('/admin/').auth('admin', 'admin-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0']); }); it('runs preAuthzFailure hook when access is denied', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => [false]; let called = false; plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => { @@ -238,7 +219,6 @@ describe(__filename, function () { assert(called); }); it('returns 500 if an exception is thrown', async function () { - this.timeout(100); handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); }); @@ -251,13 +231,11 @@ describe(__filename, function () { }); it('is not called if !requireAuthentication and not /admin/*', async function () { - this.timeout(100); settings.requireAuthentication = false; await agent.get('/').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); }); it('is called if !requireAuthentication and /admin/*', async function () { - this.timeout(100); settings.requireAuthentication = false; await agent.get('/admin/').expect(401); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -266,7 +244,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('defers if empty list returned', async function () { - this.timeout(100); await agent.get('/').expect(401); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', @@ -274,21 +251,18 @@ describe(__filename, function () { 'authenticate_1']); }); it('does not defer if return [true], 200', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = (req) => { req.session.user = {}; return [true]; }; await agent.get('/').expect(200); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); it('does not defer if return [false], 401', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = (req) => [false]; await agent.get('/').expect(401); // Note: authenticate_1 was not called because authenticate_0 handled it. assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); it('falls back to HTTP basic auth', async function () { - this.timeout(100); await agent.get('/').auth('user', 'user-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', @@ -296,7 +270,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('passes settings.users in context', async function () { - this.timeout(100); handlers.authenticate[0].checkContext = ({users}) => { assert.equal(users, settings.users); }; @@ -307,7 +280,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('passes user, password in context if provided', async function () { - this.timeout(100); handlers.authenticate[0].checkContext = ({username, password}) => { assert.equal(username, 'user'); assert.equal(password, 'user-password'); @@ -319,7 +291,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('does not pass user, password in context if not provided', async function () { - this.timeout(100); handlers.authenticate[0].checkContext = ({username, password}) => { assert(username == null); assert(password == null); @@ -331,13 +302,11 @@ describe(__filename, function () { 'authenticate_1']); }); it('errors if req.session.user is not created', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = () => [true]; await agent.get('/').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); }); it('returns 500 if an exception is thrown', async function () { - this.timeout(100); handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); @@ -351,7 +320,6 @@ describe(__filename, function () { }); it('is not called if !requireAuthorization (non-/admin)', async function () { - this.timeout(100); settings.requireAuthorization = false; await agent.get('/').auth('user', 'user-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -360,7 +328,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('is not called if !requireAuthorization (/admin)', async function () { - this.timeout(100); settings.requireAuthorization = false; await agent.get('/admin/').auth('admin', 'admin-password').expect(200); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -369,7 +336,6 @@ describe(__filename, function () { 'authenticate_1']); }); it('defers if empty list returned', async function () { - this.timeout(100); await agent.get('/').auth('user', 'user-password').expect(403); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', @@ -379,7 +345,6 @@ describe(__filename, function () { 'authorize_1']); }); it('does not defer if return [true], 200', async function () { - this.timeout(100); handlers.authorize[0].innerHandle = () => [true]; await agent.get('/').auth('user', 'user-password').expect(200); // Note: authorize_1 was not called because authorize_0 handled it. @@ -390,7 +355,6 @@ describe(__filename, function () { 'authorize_0']); }); it('does not defer if return [false], 403', async function () { - this.timeout(100); handlers.authorize[0].innerHandle = (req) => [false]; await agent.get('/').auth('user', 'user-password').expect(403); // Note: authorize_1 was not called because authorize_0 handled it. @@ -401,7 +365,6 @@ describe(__filename, function () { 'authorize_0']); }); it('passes req.path in context', async function () { - this.timeout(100); handlers.authorize[0].checkContext = ({resource}) => { assert.equal(resource, '/'); }; @@ -414,7 +377,6 @@ describe(__filename, function () { 'authorize_1']); }); it('returns 500 if an exception is thrown', async function () { - this.timeout(100); handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; await agent.get('/').auth('user', 'user-password').expect(500); assert.deepEqual(callOrder, ['preAuthorize_0', @@ -461,14 +423,12 @@ describe(__filename, function () { // authn failure tests it('authn fail, no hooks handle -> 401', async function () { - this.timeout(100); await agent.get('/').expect(401); assert(handlers.authnFailure.called); assert(!handlers.authzFailure.called); assert(handlers.authFailure.called); }); it('authn fail, authnFailure handles', async function () { - this.timeout(100); handlers.authnFailure.shouldHandle = true; await agent.get('/').expect(200, 'authnFailure'); assert(handlers.authnFailure.called); @@ -476,7 +436,6 @@ describe(__filename, function () { assert(!handlers.authFailure.called); }); it('authn fail, authFailure handles', async function () { - this.timeout(100); handlers.authFailure.shouldHandle = true; await agent.get('/').expect(200, 'authFailure'); assert(handlers.authnFailure.called); @@ -484,7 +443,6 @@ describe(__filename, function () { assert(handlers.authFailure.called); }); it('authnFailure trumps authFailure', async function () { - this.timeout(100); handlers.authnFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true; await agent.get('/').expect(200, 'authnFailure'); @@ -494,14 +452,12 @@ describe(__filename, function () { // authz failure tests it('authz fail, no hooks handle -> 403', async function () { - this.timeout(100); await agent.get('/').auth('user', 'user-password').expect(403); assert(!handlers.authnFailure.called); assert(handlers.authzFailure.called); assert(handlers.authFailure.called); }); it('authz fail, authzFailure handles', async function () { - this.timeout(100); handlers.authzFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); assert(!handlers.authnFailure.called); @@ -509,7 +465,6 @@ describe(__filename, function () { assert(!handlers.authFailure.called); }); it('authz fail, authFailure handles', async function () { - this.timeout(100); handlers.authFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); assert(!handlers.authnFailure.called); @@ -517,7 +472,6 @@ describe(__filename, function () { assert(handlers.authFailure.called); }); it('authzFailure trumps authFailure', async function () { - this.timeout(100); handlers.authzFailure.shouldHandle = true; handlers.authFailure.shouldHandle = true; await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); From eb495e9ea2001dade34c1cbe62ec5df781ec4951 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 4 Mar 2021 20:25:57 -0500 Subject: [PATCH 162/322] Changeset: Move out obsolete code --- src/node/easysync_tests.js | 36 +++++++++++++++++++++++++----------- src/static/js/Changeset.js | 19 ------------------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/node/easysync_tests.js b/src/node/easysync_tests.js index 25a70e028..5b2ec9fe1 100644 --- a/src/node/easysync_tests.js +++ b/src/node/easysync_tests.js @@ -31,6 +31,18 @@ function random() { this.nextDouble = (maxValue) => Math.random(); } +/** + * Converts stuff before $ to base 10 + * @param cs {string} the string + * @return integer + */ +const toBaseTen = (cs) => { + const dollarIndex = cs.indexOf('$'); + const beforeDollar = cs.substring(0, dollarIndex); + const fromDollar = cs.substring(dollarIndex); + return beforeDollar.replace(/[0-9a-z]+/g, (s) => String(Changeset.parseNum(s))) + fromDollar; +}; + const runTests = () => { const print = (str) => { console.log(str); @@ -602,9 +614,9 @@ const runTests = () => { const change3 = x3[0]; const text3 = x3[1]; - // print(literal(Changeset.toBaseTen(startText))); - // print(literal(Changeset.toBaseTen(change1))); - // print(literal(Changeset.toBaseTen(change2))); + // print(literal(toBaseTen(startText))); + // print(literal(toBaseTen(change1))); + // print(literal(toBaseTen(change2))); const change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); const change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); const change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); @@ -769,22 +781,24 @@ const runTests = () => { p.putAttrib(['name', 'david']); p.putAttrib(['color', 'green']); + const stringOp = (str) => Changeset.opIterator(str).next(); + assertEqualStrings('david', - Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p)); + Changeset.opAttributeValue(stringOp('*0*1+1'), 'name', p)); assertEqualStrings('david', - Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p)); + Changeset.opAttributeValue(stringOp('*0+1'), 'name', p)); assertEqualStrings('', - Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p)); + Changeset.opAttributeValue(stringOp('*1+1'), 'name', p)); assertEqualStrings('', - Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p)); + Changeset.opAttributeValue(stringOp('+1'), 'name', p)); assertEqualStrings('green', - Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p)); + Changeset.opAttributeValue(stringOp('*0*1+1'), 'color', p)); assertEqualStrings('green', - Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p)); + Changeset.opAttributeValue(stringOp('*1+1'), 'color', p)); assertEqualStrings('', - Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p)); + Changeset.opAttributeValue(stringOp('*0+1'), 'color', p)); assertEqualStrings('', - Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p)); + Changeset.opAttributeValue(stringOp('+1'), 'color', p)); })(); const testAppendATextToAssembler = (testId, atext, correctOps) => { diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 7cfb48ce2..d1fa90f35 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -70,20 +70,6 @@ exports.parseNum = (str) => parseInt(str, 36); */ exports.numToString = (num) => num.toString(36).toLowerCase(); -/** - * Converts stuff before $ to base 10 - * @obsolete not really used anywhere?? - * @param cs {string} the string - * @return integer - */ -exports.toBaseTen = (cs) => { - const dollarIndex = cs.indexOf('$'); - const beforeDollar = cs.substring(0, dollarIndex); - const fromDollar = cs.substring(dollarIndex); - return beforeDollar.replace(/[0-9a-z]+/g, (s) => String(exports.parseNum(s))) + fromDollar; -}; - - /** * ==================== Changeset Functions ======================= */ @@ -207,11 +193,6 @@ exports.opString = (op) => { return assem.toString(); }; -/** - * Used just for debugging - */ -exports.stringOp = (str) => exports.opIterator(str).next(); - /** * Used to check if a Changeset if valid * @param cs {Changeset} Changeset to be checked From 34cfff4e4c846df313df2d60e3b54045eba19860 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 4 Mar 2021 20:37:28 -0500 Subject: [PATCH 163/322] Changeset: Delete unused code --- src/static/js/Changeset.js | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index d1fa90f35..72d4eb00b 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -159,17 +159,6 @@ exports.newOp = (optOpcode) => ({ attribs: '', }); -/** - * Clones an Op - * @param op Op to be cloned - */ -exports.cloneOp = (op) => ({ - opcode: op.opcode, - chars: op.chars, - lines: op.lines, - attribs: op.attribs, -}); - /** * Copies op1 to op2 * @param op1 src Op @@ -182,17 +171,6 @@ exports.copyOp = (op1, op2) => { op2.attribs = op1.attribs; }; -/** - * Writes the Op in a string the way that changesets need it - */ -exports.opString = (op) => { - // just for debugging - if (!op.opcode) return 'null'; - const assem = exports.opAssembler(); - assem.append(op); - return assem.toString(); -}; - /** * Used to check if a Changeset if valid * @param cs {Changeset} Changeset to be checked @@ -556,8 +534,6 @@ exports.textLinesMutator = (lines) => { lines.splice.apply(lines, s); }; - const lines_toSource = () => lines.toSource(); - /** * Get a line from lines at given index * @param {Number} idx an index @@ -630,10 +606,6 @@ exports.textLinesMutator = (lines) => { */ const isCurLineInSplice = () => (curLine - curSplice[0] < (curSplice.length - 2)); - const debugPrint = (typ) => { /* eslint-disable-line no-unused-vars */ - print(`${typ}: ${curSplice.toSource()} / ${curLine},${curCol} / ${lines_toSource()}`); - }; - /** * Incorporates current line into the splice * and marks its old position to be deleted. @@ -1424,7 +1396,6 @@ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextA * @param cs Changeset */ exports.toSplices = (cs) => { - // const unpacked = exports.unpack(cs); const splices = []; From 044f6543a5e65c63f5914ded8225e34a5e06b90a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 4 Mar 2021 21:41:29 -0500 Subject: [PATCH 164/322] lint: Fix ESLint errors (mostly camelcase warnings) --- src/node/handler/PadMessageHandler.js | 10 +++------ src/static/js/Changeset.js | 32 +++++++++++++-------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index eb2bbc1e7..d34d0b95d 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -1233,12 +1233,10 @@ const handleChangesetRequest = async (socket, message) => { */ const getChangesetInfo = async (padId, startNum, endNum, granularity) => { const pad = await padManager.getPad(padId); - const head_revision = pad.getHeadRevisionNumber(); + const headRevision = pad.getHeadRevisionNumber(); // calculate the last full endnum - if (endNum > head_revision + 1) { - endNum = head_revision + 1; - } + if (endNum > headRevision + 1) endNum = headRevision + 1; endNum = Math.floor(endNum / granularity) * granularity; const compositesChangesetNeeded = []; @@ -1300,9 +1298,7 @@ const getChangesetInfo = async (padId, startNum, endNum, granularity) => { for (let compositeStart = startNum; compositeStart < endNum; compositeStart += granularity) { const compositeEnd = compositeStart + granularity; - if (compositeEnd > endNum || compositeEnd > head_revision + 1) { - break; - } + if (compositeEnd > endNum || compositeEnd > headRevision + 1) break; const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`]; const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 72d4eb00b..812a91154 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -530,15 +530,15 @@ exports.textLinesMutator = (lines) => { * It is called when leaving the splice * @param {Array} s curSplice */ - const lines_applySplice = (s) => { - lines.splice.apply(lines, s); + const linesApplySplice = (s) => { + lines.splice(...s); }; /** * Get a line from lines at given index * @param {Number} idx an index */ - const lines_get = (idx) => { + const linesGet = (idx) => { if (lines.get) { return lines.get(idx); } else { @@ -552,7 +552,7 @@ exports.textLinesMutator = (lines) => { * @param {Number} start the start index * @param {Number} end the end index */ - const lines_slice = (start, end) => { + const linesSlice = (start, end) => { if (lines.slice) { return lines.slice(start, end); } else { @@ -563,7 +563,7 @@ exports.textLinesMutator = (lines) => { /** * Return the length of lines array */ - const lines_length = () => { + const linesLength = () => { if ((typeof lines.length) === 'number') { return lines.length; } else { @@ -591,7 +591,7 @@ exports.textLinesMutator = (lines) => { * This is called via close or TODO(doc) */ const leaveSplice = () => { - lines_applySplice(curSplice); + linesApplySplice(curSplice); curSplice.length = 2; curSplice[0] = curSplice[1] = 0; inSplice = false; @@ -614,7 +614,7 @@ exports.textLinesMutator = (lines) => { */ const putCurLineInSplice = () => { if (!isCurLineInSplice()) { - curSplice.push(lines_get(curSplice[0] + curSplice[1])); + curSplice.push(linesGet(curSplice[0] + curSplice[1])); curSplice[1]++; } return 2 + curLine - curSplice[0]; // TODO should be the same as curSplice.length - 1 @@ -698,7 +698,7 @@ exports.textLinesMutator = (lines) => { */ const nextKLinesText = (k) => { const m = curSplice[0] + curSplice[1]; - return lines_slice(m, m + k).join(''); + return linesSlice(m, m + k).join(''); }; if (isCurLineInSplice()) { if (curCol === 0) { @@ -712,7 +712,7 @@ exports.textLinesMutator = (lines) => { const sline = curSplice.length - 1; removed = curSplice[sline].substring(curCol) + removed; curSplice[sline] = curSplice[sline].substring(0, curCol) + - lines_get(curSplice[0] + curSplice[1]); + linesGet(curSplice[0] + curSplice[1]); curSplice[1] += 1; } } else { @@ -803,7 +803,7 @@ exports.textLinesMutator = (lines) => { * @return {Boolean} indicates if there are lines left */ const hasMore = () => { - let docLines = lines_length(); + let docLines = linesLength(); if (inSplice) { docLines += curSplice.length - 2 - curSplice[1]; } @@ -1826,7 +1826,7 @@ exports.inverse = (cs, lines, alines, pool) => { // They may be arrays or objects with .get(i) and .length methods. // They include final newlines on lines. - const lines_get = (idx) => { + const linesGet = (idx) => { if (lines.get) { return lines.get(idx); } else { @@ -1834,7 +1834,7 @@ exports.inverse = (cs, lines, alines, pool) => { } }; - const alines_get = (idx) => { + const alinesGet = (idx) => { if (alines.get) { return alines.get(idx); } else { @@ -1855,7 +1855,7 @@ exports.inverse = (cs, lines, alines, pool) => { const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { // create curLineOpIter and advance it to curChar - curLineOpIter = exports.opIterator(alines_get(curLine)); + curLineOpIter = exports.opIterator(alinesGet(curLine)); curLineOpIterLine = curLine; let indexIntoLine = 0; let done = false; @@ -1876,7 +1876,7 @@ exports.inverse = (cs, lines, alines, pool) => { curChar = 0; curLineOpIterLine = curLine; curLineNextOp.chars = 0; - curLineOpIter = exports.opIterator(alines_get(curLine)); + curLineOpIter = exports.opIterator(alinesGet(curLine)); } if (!curLineNextOp.chars) { curLineOpIter.next(curLineNextOp); @@ -1909,13 +1909,13 @@ exports.inverse = (cs, lines, alines, pool) => { const nextText = (numChars) => { let len = 0; const assem = exports.stringAssembler(); - const firstString = lines_get(curLine).substring(curChar); + const firstString = linesGet(curLine).substring(curChar); len += firstString.length; assem.append(firstString); let lineNum = curLine + 1; while (len < numChars) { - const nextString = lines_get(lineNum); + const nextString = linesGet(lineNum); len += nextString.length; assem.append(nextString); lineNum++; From 019e296c4a9e32409c9c68eb801d36e8c4cf6d1f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 1 Oct 2021 01:22:54 -0400 Subject: [PATCH 165/322] lint: Fix awkward string formatting --- src/node/utils/padDiff.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 2da3532e2..67c5f68ff 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -9,11 +9,7 @@ function PadDiff(pad, fromRev, toRev) { } const range = pad.getValidRevisionRange(fromRev, toRev); - if (!range) { - throw new Error(`${'Invalid revision range.' + - ' startRev: '}${fromRev - } endRev: ${toRev}`); - } + if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`); this._pad = pad; this._fromRev = range.startRev; From d8cbd134d336215a7962ba65932eaa7cc66973d7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 21 Mar 2021 14:52:39 -0400 Subject: [PATCH 166/322] PadMessageHandler: Improve readability --- src/node/handler/PadMessageHandler.js | 45 +++++++++------------------ 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index d34d0b95d..63ef45bae 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -1256,39 +1256,22 @@ const getChangesetInfo = async (padId, startNum, endNum, granularity) => { revTimesNeeded.push(end - 1); } - // get all needed db values parallel - no await here since - // it would make all the lookups run in series - - // get all needed composite Changesets + // Get all needed db values in parallel. const composedChangesets = {}; - const p1 = Promise.all( - compositesChangesetNeeded.map( - (item) => composePadChangesets( - padId, item.start, item.end - ).then( - (changeset) => { - composedChangesets[`${item.start}/${item.end}`] = changeset; - } - ) - ) - ); - - // get all needed revision Dates const revisionDate = []; - const p2 = Promise.all(revTimesNeeded.map((revNum) => pad.getRevisionDate(revNum) - .then((revDate) => { - revisionDate[revNum] = Math.floor(revDate / 1000); - }) - )); - - // get the lines - let lines; - const p3 = getPadLines(padId, startNum - 1).then((_lines) => { - lines = _lines; - }); - - // wait for all of the above to complete - await Promise.all([p1, p2, p3]); + const [lines] = await Promise.all([ + getPadLines(padId, startNum - 1), + // Get all needed composite Changesets. + ...compositesChangesetNeeded.map(async (item) => { + const changeset = await composePadChangesets(padId, item.start, item.end); + composedChangesets[`${item.start}/${item.end}`] = changeset; + }), + // Get all needed revision Dates. + ...revTimesNeeded.map(async (revNum) => { + const revDate = await pad.getRevisionDate(revNum); + revisionDate[revNum] = Math.floor(revDate / 1000); + }), + ]); // doesn't know what happens here exactly :/ const timeDeltas = []; From a7c78768a197022f4fd1aba13fd0aa0a2a4d83d2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 30 Sep 2021 23:54:10 -0400 Subject: [PATCH 167/322] ExportHelper: Simplify `_analyzeLine()` a bit --- src/node/utils/ExportHelper.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index 0c593eca1..ba71269d1 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -53,7 +53,8 @@ exports._analyzeLine = (text, aline, apool) => { if (aline) { const opIter = Changeset.opIterator(aline); if (opIter.hasNext()) { - let listType = Changeset.opAttributeValue(opIter.next(), 'list', apool); + const op = opIter.next(); + let listType = Changeset.opAttributeValue(op, 'list', apool); if (listType) { lineMarker = 1; listType = /([a-z]+)([0-9]+)/.exec(listType); @@ -62,10 +63,7 @@ exports._analyzeLine = (text, aline, apool) => { line.listLevel = Number(listType[2]); } } - } - const opIter2 = Changeset.opIterator(aline); - if (opIter2.hasNext()) { - const start = Changeset.opAttributeValue(opIter2.next(), 'start', apool); + const start = Changeset.opAttributeValue(op, 'start', apool); if (start) { line.start = start; } From 4890cd8972a460f8c84dffafe0b3c436077624d5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 21 Mar 2021 17:56:21 -0400 Subject: [PATCH 168/322] Ace2Inner: Delete completed TODO comment --- src/static/js/ace2_inner.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 4b1d66317..73a4f281c 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1450,7 +1450,6 @@ function Ace2Inner(editorInfo, cssManagers) { } const linesMutatee = { - // TODO: Rhansen to check usage of args here. splice: (start, numRemoved, ...args) => { domAndRepSplice(start, numRemoved, args.map((s) => s.slice(0, -1))); }, From 788eb86d840609e4ee34dc57a43af795e872e5ea Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 21 Mar 2021 17:57:32 -0400 Subject: [PATCH 169/322] Ace2Inner: Inline code that is unnecessarily inside an IIFE --- src/static/js/ace2_inner.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 73a4f281c..4672751dc 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1475,27 +1475,25 @@ function Ace2Inner(editorInfo, cssManagers) { throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`); } - ((changes) => { - const editEvent = currentCallStack.editEvent; - if (editEvent.eventType === 'nonundoable') { - if (!editEvent.changeset) { - editEvent.changeset = changes; - } else { - editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool); - } + const editEvent = currentCallStack.editEvent; + if (editEvent.eventType === 'nonundoable') { + if (!editEvent.changeset) { + editEvent.changeset = changes; } else { - const inverseChangeset = Changeset.inverse(changes, { - get: (i) => `${rep.lines.atIndex(i).text}\n`, - length: () => rep.lines.length(), - }, rep.alines, rep.apool); - - if (!editEvent.backset) { - editEvent.backset = inverseChangeset; - } else { - editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool); - } + editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool); } - })(changes); + } else { + const inverseChangeset = Changeset.inverse(changes, { + get: (i) => `${rep.lines.atIndex(i).text}\n`, + length: () => rep.lines.length(), + }, rep.alines, rep.apool); + + if (!editEvent.backset) { + editEvent.backset = inverseChangeset; + } else { + editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool); + } + } Changeset.mutateAttributionLines(changes, rep.alines, rep.apool); From e9090727760b5f2027552acb9d92e010a41cef04 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 7 Oct 2021 19:46:07 -0400 Subject: [PATCH 170/322] /jserror: Refactor to handle errors better --- src/node/hooks/express/apicalls.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index b72ed11e5..45e38ce45 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -14,18 +14,19 @@ exports.expressCreateServer = (hookName, args, cb) => { }); }); + const parseJserrorForm = async (req) => await new Promise((resolve, reject) => { + const form = new formidable.IncomingForm(); + form.on('error', (err) => reject(err)); + form.parse(req, (err, fields) => err != null ? reject(err) : resolve(fields.errorInfo)); + }); + // The Etherpad client side sends information about client side javscript errors - args.app.post('/jserror', (req, res) => { - new formidable.IncomingForm().parse(req, (err, fields, files) => { - let data; - try { - data = JSON.parse(fields.errorInfo); - } catch (e) { - return res.end(); - } + args.app.post('/jserror', (req, res, next) => { + (async () => { + const data = JSON.parse(await parseJserrorForm(req)); clientLogger.warn(`${data.msg} --`, data); res.end('OK'); - }); + })().catch((err) => next(err || new Error(err))); }); // Provide a possibility to query the latest available API version From cb01ae8cbb0dc321abf0af68474692da0ef68a16 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 7 Oct 2021 19:46:38 -0400 Subject: [PATCH 171/322] /jserror: Reject files --- src/node/hooks/express/apicalls.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index 45e38ce45..a005de1a1 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -16,6 +16,7 @@ exports.expressCreateServer = (hookName, args, cb) => { const parseJserrorForm = async (req) => await new Promise((resolve, reject) => { const form = new formidable.IncomingForm(); + form.maxFileSize = 1; // Files are not expected. Not sure if 0 means unlimited, so 1 is used. form.on('error', (err) => reject(err)); form.parse(req, (err, fields) => err != null ? reject(err) : resolve(fields.errorInfo)); }); From 629e7d5072708624f9edcf56be79b03c6282b7fc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 7 Oct 2021 19:47:43 -0400 Subject: [PATCH 172/322] /jserror: Log all of the provided data --- src/node/hooks/express/apicalls.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index a005de1a1..5662398ac 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -4,6 +4,7 @@ const log4js = require('log4js'); const clientLogger = log4js.getLogger('client'); const formidable = require('formidable'); const apiHandler = require('../../handler/APIHandler'); +const util = require('util'); exports.expressCreateServer = (hookName, args, cb) => { // The Etherpad client side sends information about how a disconnect happened @@ -25,7 +26,13 @@ exports.expressCreateServer = (hookName, args, cb) => { args.app.post('/jserror', (req, res, next) => { (async () => { const data = JSON.parse(await parseJserrorForm(req)); - clientLogger.warn(`${data.msg} --`, data); + clientLogger.warn(`${data.msg} --`, { + [util.inspect.custom]: (depth, options) => { + // Depth is forced to infinity to ensure that all of the provided data is logged. + options = Object.assign({}, options, {depth: Infinity}); + return util.inspect(data, options); + }, + }); res.end('OK'); })().catch((err) => next(err || new Error(err))); }); From 3a5c44c8f7edce61d81e94384c17470239dada40 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 7 Oct 2021 19:47:59 -0400 Subject: [PATCH 173/322] /jserror: Enable colors to improve readability --- src/node/hooks/express/apicalls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js index 5662398ac..a0fbbc638 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.js @@ -29,7 +29,7 @@ exports.expressCreateServer = (hookName, args, cb) => { clientLogger.warn(`${data.msg} --`, { [util.inspect.custom]: (depth, options) => { // Depth is forced to infinity to ensure that all of the provided data is logged. - options = Object.assign({}, options, {depth: Infinity}); + options = Object.assign({}, options, {depth: Infinity, colors: true}); return util.inspect(data, options); }, }); From 0ea6f1518c9a05ee67f8ac85da20772326efb336 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 7 Oct 2021 20:21:24 -0400 Subject: [PATCH 174/322] tests: Remove overly agressive timeouts --- src/tests/backend/specs/hooks.js | 94 -------------------------------- 1 file changed, 94 deletions(-) diff --git a/src/tests/backend/specs/hooks.js b/src/tests/backend/specs/hooks.js index e601c9344..3120911ae 100644 --- a/src/tests/backend/specs/hooks.js +++ b/src/tests/backend/specs/hooks.js @@ -93,13 +93,11 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; callHookFnSync(hook); }); it('passes context', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; callHookFnSync(hook, val); @@ -107,7 +105,6 @@ describe(__filename, function () { }); it('returns the value provided to the callback', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; assert.equal(callHookFnSync(hook, val), val); @@ -115,7 +112,6 @@ describe(__filename, function () { }); it('returns the value returned by the hook function', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will error. hook.hook_fn = (hn, ctx) => ctx; @@ -124,19 +120,16 @@ describe(__filename, function () { }); it('does not catch exceptions', async function () { - this.timeout(30); hook.hook_fn = () => { throw new Error('test exception'); }; assert.throws(() => callHookFnSync(hook), {message: 'test exception'}); }); it('callback returns undefined', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; callHookFnSync(hook); }); it('checks for deprecation', async function () { - this.timeout(30); sinon.stub(console, 'warn'); hooks.deprecationNotices[hookName] = 'test deprecation'; callHookFnSync(hook); @@ -149,7 +142,6 @@ describe(__filename, function () { describe('supported hook function styles', function () { for (const tc of supportedSyncHookFunctions) { it(tc.name, async function () { - this.timeout(30); sinon.stub(console, 'warn'); sinon.stub(console, 'error'); hook.hook_fn = tc.fn; @@ -194,7 +186,6 @@ describe(__filename, function () { for (const tc of testCases) { it(tc.name, async function () { - this.timeout(30); sinon.stub(console, 'error'); hook.hook_fn = tc.fn; assert.equal(callHookFnSync(hook), tc.wantVal); @@ -246,7 +237,6 @@ describe(__filename, function () { if (step1.async && step2.async) continue; it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); @@ -310,7 +300,6 @@ describe(__filename, function () { if (step1.rejects !== step2.rejects) continue; it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { - this.timeout(30); const err = new Error('val'); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, err, 'val'); @@ -336,32 +325,27 @@ describe(__filename, function () { describe('hooks.callAll', function () { describe('basic behavior', function () { it('calls all in order', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]); }); it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hooks.callAll(hookName); }); it('undefined context -> {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName); }); it('null context -> {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callAll(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; hooks.callAll(hookName, wantContext); @@ -370,40 +354,34 @@ describe(__filename, function () { describe('result processing', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks.testHook; assert.deepEqual(hooks.callAll(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(hooks.callAll(hookName), []); }); it('flattens one level', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]); }); it('filters out undefined', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook([2]), makeHook([[3]])); assert.deepEqual(hooks.callAll(hookName), [2, [3]]); }); it('preserves null', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]])); assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]); }); it('all undefined -> []', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook()); assert.deepEqual(hooks.callAll(hookName), []); @@ -413,44 +391,37 @@ describe(__filename, function () { describe('hooks.callFirst', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks.testHook; assert.deepEqual(hooks.callFirst(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(hooks.callFirst(hookName), []); }); it('passes hook name => {}', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hooks.callFirst(hookName); }); it('undefined context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName); }); it('null context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hooks.callFirst(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; hooks.callFirst(hookName, wantContext); }); it('predicate never satisfied -> calls all in order', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -463,35 +434,30 @@ describe(__filename, function () { }); it('stops when predicate is satisfied', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1'), makeHook('val2')); assert.deepEqual(hooks.callFirst(hookName), ['val1']); }); it('skips values that do not satisfy predicate (undefined)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1')); assert.deepEqual(hooks.callFirst(hookName), ['val1']); }); it('skips values that do not satisfy predicate (empty list)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook([]), makeHook('val1')); assert.deepEqual(hooks.callFirst(hookName), ['val1']); }); it('null satisifes the predicate', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook('val1')); assert.deepEqual(hooks.callFirst(hookName), [null]); }); it('non-empty arrays are returned unmodified', async function () { - this.timeout(30); const want = ['val1']; testHooks.length = 0; testHooks.push(makeHook(want), makeHook(['val2'])); @@ -499,7 +465,6 @@ describe(__filename, function () { }); it('value can be passed via callback', async function () { - this.timeout(30); const want = {}; hook.hook_fn = (hn, ctx, cb) => { cb(want); }; const got = hooks.callFirst(hookName); @@ -513,13 +478,11 @@ describe(__filename, function () { describe('basic behavior', function () { it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; await callHookFnAsync(hook); }); it('passes context', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; await callHookFnAsync(hook, val); @@ -527,7 +490,6 @@ describe(__filename, function () { }); it('returns the value provided to the callback', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; assert.equal(await callHookFnAsync(hook, val), val); @@ -536,7 +498,6 @@ describe(__filename, function () { }); it('returns the value returned by the hook function', async function () { - this.timeout(30); for (const val of ['value', null, undefined]) { // Must not have the cb parameter otherwise returning undefined will never resolve. hook.hook_fn = (hn, ctx) => ctx; @@ -546,31 +507,26 @@ describe(__filename, function () { }); it('rejects if it throws an exception', async function () { - this.timeout(30); hook.hook_fn = () => { throw new Error('test exception'); }; await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('rejects if rejected Promise passed to callback', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception'))); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('rejects if rejected Promise returned', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception')); await assert.rejects(callHookFnAsync(hook), {message: 'test exception'}); }); it('callback returns undefined', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; await callHookFnAsync(hook); }); it('checks for deprecation', async function () { - this.timeout(30); sinon.stub(console, 'warn'); hooks.deprecationNotices[hookName] = 'test deprecation'; await callHookFnAsync(hook); @@ -663,7 +619,6 @@ describe(__filename, function () { for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) { it(tc.name, async function () { - this.timeout(30); sinon.stub(console, 'warn'); sinon.stub(console, 'error'); hook.hook_fn = tc.fn; @@ -811,7 +766,6 @@ describe(__filename, function () { if (step1.name.startsWith('return ') || step1.name === 'throw') continue; for (const step2 of behaviors) { it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - this.timeout(30); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, new Error(ctx.ret1), ctx.ret1); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); @@ -865,7 +819,6 @@ describe(__filename, function () { if (step1.rejects !== step2.rejects) continue; it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { - this.timeout(30); const err = new Error('val'); hook.hook_fn = (hn, ctx, cb) => { step1.fn(cb, err, 'val'); @@ -891,7 +844,6 @@ describe(__filename, function () { describe('hooks.aCallAll', function () { describe('basic behavior', function () { it('calls all asynchronously, returns values in order', async function () { - this.timeout(30); testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it. let nextIndex = 0; const hookPromises = []; @@ -926,25 +878,21 @@ describe(__filename, function () { }); it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; await hooks.aCallAll(hookName); }); it('undefined context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName); }); it('null context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallAll(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; await hooks.aCallAll(hookName, wantContext); @@ -953,13 +901,11 @@ describe(__filename, function () { describe('aCallAll callback', function () { it('exception in callback rejects', async function () { - this.timeout(30); const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); }); await assert.rejects(p, {message: 'test exception'}); }); it('propagates error on exception', async function () { - this.timeout(30); hook.hook_fn = () => { throw new Error('test exception'); }; await hooks.aCallAll(hookName, {}, (err) => { assert(err instanceof Error); @@ -968,14 +914,12 @@ describe(__filename, function () { }); it('propagages null error on success', async function () { - this.timeout(30); await hooks.aCallAll(hookName, {}, (err) => { assert(err == null, `got non-null error: ${err}`); }); }); it('propagages results on success', async function () { - this.timeout(30); hook.hook_fn = () => 'val'; await hooks.aCallAll(hookName, {}, (err, results) => { assert.deepEqual(results, ['val']); @@ -983,47 +927,40 @@ describe(__filename, function () { }); it('returns callback return value', async function () { - this.timeout(30); assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val'); }); }); describe('result processing', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks[hookName]; assert.deepEqual(await hooks.aCallAll(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(await hooks.aCallAll(hookName), []); }); it('flattens one level', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]); }); it('filters out undefined', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve())); assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]); }); it('preserves null', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null))); assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]); }); it('all undefined -> []', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook(Promise.resolve())); assert.deepEqual(await hooks.aCallAll(hookName), []); @@ -1034,7 +971,6 @@ describe(__filename, function () { describe('hooks.callAllSerial', function () { describe('basic behavior', function () { it('calls all asynchronously, serially, in order', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -1057,25 +993,21 @@ describe(__filename, function () { }); it('passes hook name', async function () { - this.timeout(30); hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; await hooks.callAllSerial(hookName); }); it('undefined context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName); }); it('null context -> {}', async function () { - this.timeout(30); hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.callAllSerial(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; await hooks.callAllSerial(hookName, wantContext); @@ -1084,40 +1016,34 @@ describe(__filename, function () { describe('result processing', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks[hookName]; assert.deepEqual(await hooks.callAllSerial(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(await hooks.callAllSerial(hookName), []); }); it('flattens one level', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]); }); it('filters out undefined', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve())); assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]); }); it('preserves null', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null))); assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]); }); it('all undefined -> []', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook(Promise.resolve())); assert.deepEqual(await hooks.callAllSerial(hookName), []); @@ -1127,44 +1053,37 @@ describe(__filename, function () { describe('hooks.aCallFirst', function () { it('no registered hooks (undefined) -> []', async function () { - this.timeout(30); delete plugins.hooks.testHook; assert.deepEqual(await hooks.aCallFirst(hookName), []); }); it('no registered hooks (empty list) -> []', async function () { - this.timeout(30); testHooks.length = 0; assert.deepEqual(await hooks.aCallFirst(hookName), []); }); it('passes hook name => {}', async function () { - this.timeout(30); hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; await hooks.aCallFirst(hookName); }); it('undefined context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName); }); it('null context => {}', async function () { - this.timeout(30); hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; await hooks.aCallFirst(hookName, null); }); it('context unmodified', async function () { - this.timeout(30); const wantContext = {}; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; await hooks.aCallFirst(hookName, wantContext); }); it('default predicate: predicate never satisfied -> calls all in order', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -1177,7 +1096,6 @@ describe(__filename, function () { }); it('calls hook functions serially', async function () { - this.timeout(30); const gotCalls = []; testHooks.length = 0; for (let i = 0; i < 3; i++) { @@ -1200,35 +1118,30 @@ describe(__filename, function () { }); it('default predicate: stops when satisfied', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1'), makeHook('val2')); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); }); it('default predicate: skips values that do not satisfy (undefined)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(), makeHook('val1')); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); }); it('default predicate: skips values that do not satisfy (empty list)', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook([]), makeHook('val1')); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); }); it('default predicate: null satisifes', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(null), makeHook('val1')); assert.deepEqual(await hooks.aCallFirst(hookName), [null]); }); it('custom predicate: called for each hook function', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(0), makeHook(1), makeHook(2)); let got = 0; @@ -1237,7 +1150,6 @@ describe(__filename, function () { }); it('custom predicate: boolean false/true continues/stops iteration', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; @@ -1250,7 +1162,6 @@ describe(__filename, function () { }); it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () { - this.timeout(30); testHooks.length = 0; testHooks.push(makeHook(1), makeHook(2), makeHook(3)); let nCall = 0; @@ -1263,7 +1174,6 @@ describe(__filename, function () { }); it('custom predicate: array value passed unmodified to predicate', async function () { - this.timeout(30); const want = [0]; hook.hook_fn = () => want; const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! @@ -1271,20 +1181,17 @@ describe(__filename, function () { }); it('custom predicate: normalized value passed to predicate (undefined)', async function () { - this.timeout(30); const predicate = (got) => { assert.deepEqual(got, []); }; await hooks.aCallFirst(hookName, null, null, predicate); }); it('custom predicate: normalized value passed to predicate (null)', async function () { - this.timeout(30); hook.hook_fn = () => null; const predicate = (got) => { assert.deepEqual(got, [null]); }; await hooks.aCallFirst(hookName, null, null, predicate); }); it('non-empty arrays are returned unmodified', async function () { - this.timeout(30); const want = ['val1']; testHooks.length = 0; testHooks.push(makeHook(want), makeHook(['val2'])); @@ -1292,7 +1199,6 @@ describe(__filename, function () { }); it('value can be passed via callback', async function () { - this.timeout(30); const want = {}; hook.hook_fn = (hn, ctx, cb) => { cb(want); }; const got = await hooks.aCallFirst(hookName); From c47134b3ab3f105e9d46e52e62ee74852f17d579 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sat, 9 Oct 2021 14:44:48 +0100 Subject: [PATCH 175/322] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d003a4787..4adcdb102 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,5 @@ +IMPORTANT: Please disable plugins prior to posting a bug report. If you have a problem with a plugin please post on the plugin repository. Thanks! + --- name: Bug report about: Create a report to help us improve @@ -28,6 +30,7 @@ If applicable, add screenshots to help explain your problem. - OS: [e.g., Ubuntu 20.04] - Node.js version (`node --version`): - npm version (`npm --version`): + - Is the server free of plugins: **Desktop (please complete the following information):** - OS: [e.g. iOS] From 3722d943c7999d62736051e4139726112a24eaa7 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 13 Oct 2021 07:15:16 +0000 Subject: [PATCH 176/322] fix: upgrade terser from 5.8.0 to 5.9.0 Snyk has created this PR to upgrade terser from 5.8.0 to 5.9.0. See this package in npm: https://www.npmjs.com/package/terser See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index f765be744..2b199c6f8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8731,9 +8731,9 @@ } }, "terser": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.8.0.tgz", - "integrity": "sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", + "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", "requires": { "commander": "^2.20.0", "source-map": "~0.7.2", diff --git a/src/package.json b/src/package.json index 6746ab2c8..5b8bff69d 100644 --- a/src/package.json +++ b/src/package.json @@ -65,7 +65,7 @@ "security": "1.0.0", "semver": "^7.3.5", "socket.io": "^2.4.1", - "terser": "^5.8.0", + "terser": "^5.9.0", "threads": "^1.6.5", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", From 428736cdc36213c6c508b9e5338e07d55f47ba41 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 15 Oct 2021 20:53:41 -0400 Subject: [PATCH 177/322] tests: easysync: Run with the frontend tests --- .../frontend/specs/easysync.js} | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) rename src/{node/easysync_tests.js => tests/frontend/specs/easysync.js} (99%) diff --git a/src/node/easysync_tests.js b/src/tests/frontend/specs/easysync.js similarity index 99% rename from src/node/easysync_tests.js rename to src/tests/frontend/specs/easysync.js index 5b2ec9fe1..76ce19b1f 100644 --- a/src/node/easysync_tests.js +++ b/src/tests/frontend/specs/easysync.js @@ -23,8 +23,8 @@ */ -const Changeset = require('../static/js/Changeset'); -const AttributePool = require('../static/js/AttributePool'); +const Changeset = require('../../../static/js/Changeset'); +const AttributePool = require('../../../static/js/AttributePool'); function random() { this.nextInt = (maxValue) => Math.floor(Math.random() * maxValue); @@ -992,4 +992,8 @@ const runTests = () => { for (let i = 0; i < 30; i++) testInverseRandom(i); }; -runTests(); +describe('easysync', function () { + it('tests', async function () { + runTests(); + }); +}); From 59a6a9f6a0c3ab12bdb234f90bfd524991615497 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 15 Oct 2021 20:20:48 -0400 Subject: [PATCH 178/322] tests: easysync: Delete commented-out and unused code --- src/tests/frontend/specs/easysync.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index 76ce19b1f..515e6d416 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -31,18 +31,6 @@ function random() { this.nextDouble = (maxValue) => Math.random(); } -/** - * Converts stuff before $ to base 10 - * @param cs {string} the string - * @return integer - */ -const toBaseTen = (cs) => { - const dollarIndex = cs.indexOf('$'); - const beforeDollar = cs.substring(0, dollarIndex); - const fromDollar = cs.substring(dollarIndex); - return beforeDollar.replace(/[0-9a-z]+/g, (s) => String(Changeset.parseNum(s))) + fromDollar; -}; - const runTests = () => { const print = (str) => { console.log(str); @@ -155,7 +143,6 @@ const runTests = () => { assertEqualArrays(correct, lines); const correctText = correct.join(''); - // print(literal(cs)); const outText = Changeset.applyToText(cs, inText); assertEqualStrings(correctText, outText); }; @@ -614,9 +601,6 @@ const runTests = () => { const change3 = x3[0]; const text3 = x3[1]; - // print(literal(toBaseTen(startText))); - // print(literal(toBaseTen(change1))); - // print(literal(toBaseTen(change2))); const change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); const change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); const change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); @@ -962,10 +946,7 @@ const runTests = () => { const stylifier = randomTestChangeset(startText, rand, true)[0]; - // print(alines.join('\n')); Changeset.mutateAttributionLines(stylifier, alines, p); - // print(stylifier); - // print(alines.join('\n')); Changeset.mutateTextLines(stylifier, lines); const changeset = randomTestChangeset(lines.join(''), rand, true)[0]; @@ -976,15 +957,8 @@ const runTests = () => { Changeset.mutateTextLines(changeset, lines); Changeset.mutateAttributionLines(changeset, alines, p); - // print(origALines.join('\n')); - // print(changeset); - // print(inverseChangeset); - // print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n')); - // print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n')); - // print(alines.join('\n')); Changeset.mutateTextLines(inverseChangeset, lines); Changeset.mutateAttributionLines(inverseChangeset, alines, p); - // print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n')); assertEqualArrays(origLines, lines); assertEqualArrays(origALines, alines); }; From 8dd61f847ead675d2dc3f021110e17a65d5b408d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 15 Oct 2021 19:13:58 -0400 Subject: [PATCH 179/322] tests: easysync: Fix some ESLint errors --- src/tests/frontend/specs/easysync.js | 47 ++++++++++++++++++---------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index 515e6d416..b73092a3e 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -26,7 +26,7 @@ const Changeset = require('../../../static/js/Changeset'); const AttributePool = require('../../../static/js/AttributePool'); -function random() { +function Random() { this.nextInt = (maxValue) => Math.floor(Math.random() * maxValue); this.nextDouble = (maxValue) => Math.random(); } @@ -87,7 +87,7 @@ const runTests = () => { const applyMutations = (mu, arrayOfArrays) => { arrayOfArrays.forEach((a) => { - const result = mu[a[0]].apply(mu, a.slice(1)); + const result = mu[a[0]](...a.slice(1)); if (a[0] === 'remove' && a[3]) { assertEqualStrings(a[3], result); } @@ -290,8 +290,7 @@ const runTests = () => { print(`> runMutateAttributionTest#${testId}`); const p = poolOrArray(attribs); const alines2 = Array.prototype.slice.call(alines); - const result = Changeset.mutateAttributionLines( - Changeset.checkRep(cs), alines2, p); + Changeset.mutateAttributionLines(Changeset.checkRep(cs), alines2, p); assertEqualArrays(outCorrect, alines2); print(`> runMutateAttributionTest#${testId}.applyToAttribution`); @@ -336,7 +335,23 @@ const runTests = () => { // based on runMutationTest#1 runMutateAttributionTest(5, testPoolWithChars, - 'Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$' + 'tucream\npie\nbot\nbu', ['*a+1*p+2*l+1*e+1*0|1+1', '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', '*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1', '*d+1*u+1*f+2*l+1*e+1*0|1+1', '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1'], ['*t+1*u+1*p+1*l+1*e+1*0|1+1', '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', '|1+6', '|1+4', '*c+1*a+1*b+1*o+1*t+1*0|1+1', '*b+1*u+1*b+2*a+1*0|1+1', '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1']); + 'Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$tucream\npie\nbot\nbu', + [ + '*a+1*p+2*l+1*e+1*0|1+1', + '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', + '*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1', + '*d+1*u+1*f+2*l+1*e+1*0|1+1', + '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1', + ], + [ + '*t+1*u+1*p+1*l+1*e+1*0|1+1', + '*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1', + '|1+6', + '|1+4', + '*c+1*a+1*b+1*o+1*t+1*0|1+1', + '*b+1*u+1*b+2*a+1*0|1+1', + '*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1', + ]); // based on runMutationTest#3 runMutateAttributionTest(6, testPoolWithChars, @@ -512,7 +527,7 @@ const runTests = () => { // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] if (opcode === '-' || rand.nextInt(3)) { return ''; - } else if (rand.nextInt(3)) { + } else if (rand.nextInt(3)) { // eslint-disable-line no-dupe-else-if if (opcode === '+' || rand.nextInt(2)) { return `*${Changeset.numToString(rand.nextInt(2) * 2 + 1)}`; } else { @@ -582,7 +597,7 @@ const runTests = () => { }; const testCompose = (randomSeed) => { - const rand = new random(); + const rand = new Random(); print(`> testCompose#${randomSeed}`); const p = new AttributePool(); @@ -614,7 +629,7 @@ const runTests = () => { for (let i = 0; i < 30; i++) testCompose(i); - (function simpleComposeAttributesTest() { + (() => { print('> simpleComposeAttributesTest'); const p = new AttributePool(); p.putAttrib(['bold', '']); @@ -625,7 +640,7 @@ const runTests = () => { assertEqualStrings('Z:2>1+1*0|1=2$x', cs12); })(); - (function followAttributesTest() { + (() => { const p = new AttributePool(); p.putAttrib(['x', '']); p.putAttrib(['x', 'abc']); @@ -652,7 +667,7 @@ const runTests = () => { })(); const testFollow = (randomSeed) => { - const rand = new random(); + const rand = new Random(); print(`> testFollow#${randomSeed}`); const p = new AttributePool(); @@ -674,7 +689,7 @@ const runTests = () => { for (let i = 0; i < 30; i++) testFollow(i); const testSplitJoinAttributionLines = (randomSeed) => { - const rand = new random(); + const rand = new Random(); print(`> testSplitJoinAttributionLines#${randomSeed}`); const doc = `${randomMultiline(10, 20, rand)}\n`; @@ -701,7 +716,7 @@ const runTests = () => { for (let i = 0; i < 10; i++) testSplitJoinAttributionLines(i); - (function testMoveOpsToNewPool() { + (() => { print('> testMoveOpsToNewPool'); const pool1 = new AttributePool(); @@ -719,7 +734,7 @@ const runTests = () => { })(); - (function testMakeSplice() { + (() => { print('> testMakeSplice'); const t = 'a\nb\nc\n'; @@ -727,7 +742,7 @@ const runTests = () => { assertEqualStrings('a\nb\ncdef\n', t2); })(); - (function testToSplices() { + (() => { print('> testToSplices'); const cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk'); @@ -758,7 +773,7 @@ const runTests = () => { testCharacterRangeFollow(10, 'Z:2>1+1$a', [0, 0], false, [1, 1]); testCharacterRangeFollow(11, 'Z:2>1+1$a', [0, 0], true, [0, 0]); - (function testOpAttributeValue() { + (() => { print('> testOpAttributeValue'); const p = new AttributePool(); @@ -935,7 +950,7 @@ const runTests = () => { testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']); const testInverseRandom = (randomSeed) => { - const rand = new random(); + const rand = new Random(); print(`> testInverseRandom#${randomSeed}`); const p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); From 2c7d0604c321ae7c4ce59bf69d988c9ae339f60a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 15 Oct 2021 19:34:11 -0400 Subject: [PATCH 180/322] tests: easysync: Remove unnecessary `Random` class --- src/tests/frontend/specs/easysync.js | 85 +++++++++++++--------------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index b73092a3e..85ed5dad2 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -26,10 +26,7 @@ const Changeset = require('../../../static/js/Changeset'); const AttributePool = require('../../../static/js/AttributePool'); -function Random() { - this.nextInt = (maxValue) => Math.floor(Math.random() * maxValue); - this.nextDouble = (maxValue) => Math.random(); -} +const randInt = (maxValue) => Math.floor(Math.random() * maxValue); const runTests = () => { const print = (str) => { @@ -396,22 +393,22 @@ const runTests = () => { '|1+1', ]); - const randomInlineString = (len, rand) => { + const randomInlineString = (len) => { const assem = Changeset.stringAssembler(); for (let i = 0; i < len; i++) { - assem.append(String.fromCharCode(rand.nextInt(26) + 97)); + assem.append(String.fromCharCode(randInt(26) + 97)); } return assem.toString(); }; - const randomMultiline = (approxMaxLines, approxMaxCols, rand) => { - const numParts = rand.nextInt(approxMaxLines * 2) + 1; + const randomMultiline = (approxMaxLines, approxMaxCols) => { + const numParts = randInt(approxMaxLines * 2) + 1; const txt = Changeset.stringAssembler(); - txt.append(rand.nextInt(2) ? '\n' : ''); + txt.append(randInt(2) ? '\n' : ''); for (let i = 0; i < numParts; i++) { if ((i % 2) === 0) { - if (rand.nextInt(10)) { - txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand)); + if (randInt(10)) { + txt.append(randomInlineString(randInt(approxMaxCols) + 1)); } else { txt.append('\n'); } @@ -422,14 +419,14 @@ const runTests = () => { return txt.toString(); }; - const randomStringOperation = (numCharsLeft, rand) => { + const randomStringOperation = (numCharsLeft) => { let result; - switch (rand.nextInt(9)) { + switch (randInt(9)) { case 0: { // insert char result = { - insert: randomInlineString(1, rand), + insert: randomInlineString(1), }; break; } @@ -453,7 +450,7 @@ const runTests = () => { { // insert small result = { - insert: randomInlineString(rand.nextInt(4) + 1, rand), + insert: randomInlineString(randInt(4) + 1), }; break; } @@ -461,7 +458,7 @@ const runTests = () => { { // delete small result = { - remove: rand.nextInt(4) + 1, + remove: randInt(4) + 1, }; break; } @@ -469,7 +466,7 @@ const runTests = () => { { // skip small result = { - skip: rand.nextInt(4) + 1, + skip: randInt(4) + 1, }; break; } @@ -477,7 +474,7 @@ const runTests = () => { { // insert multiline; result = { - insert: randomMultiline(5, 20, rand), + insert: randomMultiline(5, 20), }; break; } @@ -485,7 +482,7 @@ const runTests = () => { { // delete multiline result = { - remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()), + remove: Math.round(numCharsLeft * Math.random() * Math.random()), }; break; } @@ -493,7 +490,7 @@ const runTests = () => { { // skip multiline result = { - skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble()), + skip: Math.round(numCharsLeft * Math.random() * Math.random()), }; break; } @@ -523,24 +520,24 @@ const runTests = () => { return result; }; - const randomTwoPropAttribs = (opcode, rand) => { + const randomTwoPropAttribs = (opcode) => { // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] - if (opcode === '-' || rand.nextInt(3)) { + if (opcode === '-' || randInt(3)) { return ''; - } else if (rand.nextInt(3)) { // eslint-disable-line no-dupe-else-if - if (opcode === '+' || rand.nextInt(2)) { - return `*${Changeset.numToString(rand.nextInt(2) * 2 + 1)}`; + } else if (randInt(3)) { // eslint-disable-line no-dupe-else-if + if (opcode === '+' || randInt(2)) { + return `*${Changeset.numToString(randInt(2) * 2 + 1)}`; } else { - return `*${Changeset.numToString(rand.nextInt(2) * 2)}`; + return `*${Changeset.numToString(randInt(2) * 2)}`; } - } else if (opcode === '+' || rand.nextInt(4) === 0) { + } else if (opcode === '+' || randInt(4) === 0) { return '*1*3'; } else { - return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)]; + return ['*0*2', '*0*3', '*1*2'][randInt(3)]; } }; - const randomTestChangeset = (origText, rand, withAttribs) => { + const randomTestChangeset = (origText, withAttribs) => { const charBank = Changeset.stringAssembler(); let textLeft = origText; // always keep final newline const outTextAssem = Changeset.stringAssembler(); @@ -552,7 +549,7 @@ const runTests = () => { const appendMultilineOp = (opcode, txt) => { nextOp.opcode = opcode; if (withAttribs) { - nextOp.attribs = randomTwoPropAttribs(opcode, rand); + nextOp.attribs = randomTwoPropAttribs(opcode); } txt.replace(/\n|[^\n]+/g, (t) => { if (t === '\n') { @@ -569,7 +566,7 @@ const runTests = () => { }; const doOp = () => { - const o = randomStringOperation(textLeft.length, rand); + const o = randomStringOperation(textLeft.length); if (o.insert) { const txt = o.insert; charBank.append(txt); @@ -597,22 +594,21 @@ const runTests = () => { }; const testCompose = (randomSeed) => { - const rand = new Random(); print(`> testCompose#${randomSeed}`); const p = new AttributePool(); - const startText = `${randomMultiline(10, 20, rand)}\n`; + const startText = `${randomMultiline(10, 20)}\n`; - const x1 = randomTestChangeset(startText, rand); + const x1 = randomTestChangeset(startText); const change1 = x1[0]; const text1 = x1[1]; - const x2 = randomTestChangeset(text1, rand); + const x2 = randomTestChangeset(text1); const change2 = x2[0]; const text2 = x2[1]; - const x3 = randomTestChangeset(text2, rand); + const x3 = randomTestChangeset(text2); const change3 = x3[0]; const text3 = x3[1]; @@ -667,15 +663,14 @@ const runTests = () => { })(); const testFollow = (randomSeed) => { - const rand = new Random(); print(`> testFollow#${randomSeed}`); const p = new AttributePool(); - const startText = `${randomMultiline(10, 20, rand)}\n`; + const startText = `${randomMultiline(10, 20)}\n`; - const cs1 = randomTestChangeset(startText, rand)[0]; - const cs2 = randomTestChangeset(startText, rand)[0]; + const cs1 = randomTestChangeset(startText)[0]; + const cs2 = randomTestChangeset(startText)[0]; const afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); const bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); @@ -689,10 +684,9 @@ const runTests = () => { for (let i = 0; i < 30; i++) testFollow(i); const testSplitJoinAttributionLines = (randomSeed) => { - const rand = new Random(); print(`> testSplitJoinAttributionLines#${randomSeed}`); - const doc = `${randomMultiline(10, 20, rand)}\n`; + const doc = `${randomMultiline(10, 20)}\n`; const stringToOps = (str) => { const assem = Changeset.mergingOpAssembler(); @@ -950,21 +944,20 @@ const runTests = () => { testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']); const testInverseRandom = (randomSeed) => { - const rand = new Random(); print(`> testInverseRandom#${randomSeed}`); const p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); - const startText = `${randomMultiline(10, 20, rand)}\n`; + const startText = `${randomMultiline(10, 20)}\n`; const alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); const lines = startText.slice(0, -1).split('\n').map((s) => `${s}\n`); - const stylifier = randomTestChangeset(startText, rand, true)[0]; + const stylifier = randomTestChangeset(startText, true)[0]; Changeset.mutateAttributionLines(stylifier, alines, p); Changeset.mutateTextLines(stylifier, lines); - const changeset = randomTestChangeset(lines.join(''), rand, true)[0]; + const changeset = randomTestChangeset(lines.join(''), true)[0]; const inverseChangeset = Changeset.inverse(changeset, lines, alines, p); const origLines = lines.slice(); From ebb7dfabd72012d3f695bcaed02a3c6c34cffb09 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 16 Oct 2021 14:58:57 -0400 Subject: [PATCH 181/322] tests: easysync: Use mocha `describe()` and `it()` --- src/tests/frontend/specs/easysync.js | 314 +++++++++++++-------------- 1 file changed, 148 insertions(+), 166 deletions(-) diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index 85ed5dad2..d179afb4b 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -28,11 +28,7 @@ const AttributePool = require('../../../static/js/AttributePool'); const randInt = (maxValue) => Math.floor(Math.random() * maxValue); -const runTests = () => { - const print = (str) => { - console.log(str); - }; - +describe('easysync', function () { const assert = (code, optMsg) => { if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); /* eslint-disable-line no-eval */ }; @@ -70,17 +66,15 @@ const runTests = () => { return assem.toString(); }; - (() => { - print('> throughIterator'); + it('throughIterator', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; assert(`throughIterator(${literal(x)}) == ${literal(x)}`); - })(); + }); - (() => { - print('> throughSmartAssembler'); + it('throughSmartAssembler', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; assert(`throughSmartAssembler(${literal(x)}) == ${literal(x)}`); - })(); + }); const applyMutations = (mu, arrayOfArrays) => { arrayOfArrays.forEach((a) => { @@ -126,22 +120,23 @@ const runTests = () => { }; const runMutationTest = (testId, origLines, muts, correct) => { - print(`> runMutationTest#${testId}`); - let lines = origLines.slice(); - const mu = Changeset.textLinesMutator(lines); - applyMutations(mu, muts); - mu.close(); - assertEqualArrays(correct, lines); + it(`runMutationTest#${testId}`, async function () { + let lines = origLines.slice(); + const mu = Changeset.textLinesMutator(lines); + applyMutations(mu, muts); + mu.close(); + assertEqualArrays(correct, lines); - const inText = origLines.join(''); - const cs = mutationsToChangeset(inText.length, muts); - lines = origLines.slice(); - Changeset.mutateTextLines(cs, lines); - assertEqualArrays(correct, lines); + const inText = origLines.join(''); + const cs = mutationsToChangeset(inText.length, muts); + lines = origLines.slice(); + Changeset.mutateTextLines(cs, lines); + assertEqualArrays(correct, lines); - const correctText = correct.join(''); - const outText = Changeset.applyToText(cs, inText); - assertEqualStrings(correctText, outText); + const correctText = correct.join(''); + const outText = Changeset.applyToText(cs, inText); + assertEqualStrings(correctText, outText); + }); }; runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [ @@ -227,11 +222,11 @@ const runTests = () => { }; const runApplyToAttributionTest = (testId, attribs, cs, inAttr, outCorrect) => { - print(`> applyToAttribution#${testId}`); - const p = poolOrArray(attribs); - const result = Changeset.applyToAttribution( - Changeset.checkRep(cs), inAttr, p); - assertEqualStrings(outCorrect, result); + it(`applyToAttribution#${testId}`, async function () { + const p = poolOrArray(attribs); + const result = Changeset.applyToAttribution(Changeset.checkRep(cs), inAttr, p); + assertEqualStrings(outCorrect, result); + }); }; // turn cactus\n into actusabcd\n @@ -242,8 +237,7 @@ const runTests = () => { runApplyToAttributionTest(2, ['bold,', 'bold,true'], 'Z:g<4*1|1=6*1=5-4$', '|2+g', '*1|1+6*1+5|1+1'); - (() => { - print('> mutatorHasMore'); + it('mutatorHasMore', async function () { const lines = ['1\n', '2\n', '3\n', '4\n']; let mu; @@ -281,22 +275,21 @@ const runTests = () => { assert(`${mu.hasMore()} == false`); mu.close(); assert(`${mu.hasMore()} == false`); - })(); + }); const runMutateAttributionTest = (testId, attribs, cs, alines, outCorrect) => { - print(`> runMutateAttributionTest#${testId}`); - const p = poolOrArray(attribs); - const alines2 = Array.prototype.slice.call(alines); - Changeset.mutateAttributionLines(Changeset.checkRep(cs), alines2, p); - assertEqualArrays(outCorrect, alines2); + it(`runMutateAttributionTest#${testId}`, async function () { + const p = poolOrArray(attribs); + const alines2 = Array.prototype.slice.call(alines); + Changeset.mutateAttributionLines(Changeset.checkRep(cs), alines2, p); + assertEqualArrays(outCorrect, alines2); - print(`> runMutateAttributionTest#${testId}.applyToAttribution`); - - const removeQuestionMarks = (a) => a.replace(/\?/g, ''); - const inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); - const correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); - const mergedResult = Changeset.applyToAttribution(cs, inMerged, p); - assertEqualStrings(correctMerged, mergedResult); + const removeQuestionMarks = (a) => a.replace(/\?/g, ''); + const inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); + const correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); + const mergedResult = Changeset.applyToAttribution(cs, inMerged, p); + assertEqualStrings(correctMerged, mergedResult); + }); }; // turn 123\n 456\n 789\n into 123\n 456\n 789\n @@ -594,39 +587,38 @@ const runTests = () => { }; const testCompose = (randomSeed) => { - print(`> testCompose#${randomSeed}`); + it(`testCompose#${randomSeed}`, async function () { + const p = new AttributePool(); - const p = new AttributePool(); + const startText = `${randomMultiline(10, 20)}\n`; - const startText = `${randomMultiline(10, 20)}\n`; + const x1 = randomTestChangeset(startText); + const change1 = x1[0]; + const text1 = x1[1]; - const x1 = randomTestChangeset(startText); - const change1 = x1[0]; - const text1 = x1[1]; + const x2 = randomTestChangeset(text1); + const change2 = x2[0]; + const text2 = x2[1]; - const x2 = randomTestChangeset(text1); - const change2 = x2[0]; - const text2 = x2[1]; + const x3 = randomTestChangeset(text2); + const change3 = x3[0]; + const text3 = x3[1]; - const x3 = randomTestChangeset(text2); - const change3 = x3[0]; - const text3 = x3[1]; + const change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); + const change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); + const change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); + const change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); + assertEqualStrings(change123, change123a); - const change12 = Changeset.checkRep(Changeset.compose(change1, change2, p)); - const change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); - const change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); - const change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); - assertEqualStrings(change123, change123a); - - assertEqualStrings(text2, Changeset.applyToText(change12, startText)); - assertEqualStrings(text3, Changeset.applyToText(change23, text1)); - assertEqualStrings(text3, Changeset.applyToText(change123, startText)); + assertEqualStrings(text2, Changeset.applyToText(change12, startText)); + assertEqualStrings(text3, Changeset.applyToText(change23, text1)); + assertEqualStrings(text3, Changeset.applyToText(change123, startText)); + }); }; for (let i = 0; i < 30; i++) testCompose(i); - (() => { - print('> simpleComposeAttributesTest'); + it('simpleComposeAttributesTest', async function () { const p = new AttributePool(); p.putAttrib(['bold', '']); p.putAttrib(['bold', 'true']); @@ -634,7 +626,7 @@ const runTests = () => { const cs2 = Changeset.checkRep('Z:3>0*0|1=3$'); const cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p)); assertEqualStrings('Z:2>1+1*0|1=2$x', cs12); - })(); + }); (() => { const p = new AttributePool(); @@ -644,12 +636,15 @@ const runTests = () => { p.putAttrib(['y', '']); p.putAttrib(['y', 'abc']); p.putAttrib(['y', 'def']); + let n = 0; const testFollow = (a, b, afb, bfa, merge) => { - assertEqualStrings(afb, Changeset.followAttributes(a, b, p)); - assertEqualStrings(bfa, Changeset.followAttributes(b, a, p)); - assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p)); - assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p)); + it(`testFollow manual #${++n}`, async function () { + assertEqualStrings(afb, Changeset.followAttributes(a, b, p)); + assertEqualStrings(bfa, Changeset.followAttributes(b, a, p)); + assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p)); + assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p)); + }); }; testFollow('', '', '', '', ''); @@ -663,31 +658,27 @@ const runTests = () => { })(); const testFollow = (randomSeed) => { - print(`> testFollow#${randomSeed}`); + it(`testFollow#${randomSeed}`, async function () { + const p = new AttributePool(); - const p = new AttributePool(); + const startText = `${randomMultiline(10, 20)}\n`; - const startText = `${randomMultiline(10, 20)}\n`; + const cs1 = randomTestChangeset(startText)[0]; + const cs2 = randomTestChangeset(startText)[0]; - const cs1 = randomTestChangeset(startText)[0]; - const cs2 = randomTestChangeset(startText)[0]; + const afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); + const bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); - const afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p)); - const bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p)); + const merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); + const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); - const merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); - const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); - - assertEqualStrings(merge1, merge2); + assertEqualStrings(merge1, merge2); + }); }; for (let i = 0; i < 30; i++) testFollow(i); const testSplitJoinAttributionLines = (randomSeed) => { - print(`> testSplitJoinAttributionLines#${randomSeed}`); - - const doc = `${randomMultiline(10, 20)}\n`; - const stringToOps = (str) => { const assem = Changeset.mergingOpAssembler(); const o = Changeset.newOp('+'); @@ -701,18 +692,20 @@ const runTests = () => { return assem.toString(); }; - const theJoined = stringToOps(doc); - const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); + it(`testSplitJoinAttributionLines#${randomSeed}`, async function () { + const doc = `${randomMultiline(10, 20)}\n`; - assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc)); - assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit)); + const theJoined = stringToOps(doc); + const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); + + assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc)); + assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit)); + }); }; for (let i = 0; i < 10; i++) testSplitJoinAttributionLines(i); - (() => { - print('> testMoveOpsToNewPool'); - + it('testMoveOpsToNewPool', async function () { const pool1 = new AttributePool(); const pool2 = new AttributePool(); @@ -725,33 +718,29 @@ const runTests = () => { Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab'); assertEqualStrings( Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1'); - })(); - - - (() => { - print('> testMakeSplice'); + }); + it('testMakeSplice', async function () { const t = 'a\nb\nc\n'; const t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, 'def'), t); assertEqualStrings('a\nb\ncdef\n', t2); - })(); - - (() => { - print('> testToSplices'); + }); + it('testToSplices', async function () { const cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk'); const correctSplices = [ [5, 8, '123456789'], [9, 17, 'abcdefghijk'], ]; assertEqualArrays(correctSplices, Changeset.toSplices(cs)); - })(); + }); const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => { - print(`> testCharacterRangeFollow#${testId}`); - cs = Changeset.checkRep(cs); - assertEqualArrays(correctNewRange, Changeset.characterRangeFollow( - cs, oldRange[0], oldRange[1], insertionsAfter)); + it(`testCharacterRangeFollow#${testId}`, async function () { + cs = Changeset.checkRep(cs); + assertEqualArrays(correctNewRange, Changeset.characterRangeFollow( + cs, oldRange[0], oldRange[1], insertionsAfter)); + }); }; testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', @@ -767,9 +756,7 @@ const runTests = () => { testCharacterRangeFollow(10, 'Z:2>1+1$a', [0, 0], false, [1, 1]); testCharacterRangeFollow(11, 'Z:2>1+1$a', [0, 0], true, [0, 0]); - (() => { - print('> testOpAttributeValue'); - + it('testOpAttributeValue', async function () { const p = new AttributePool(); p.putAttrib(['name', 'david']); p.putAttrib(['color', 'green']); @@ -792,14 +779,14 @@ const runTests = () => { Changeset.opAttributeValue(stringOp('*0+1'), 'color', p)); assertEqualStrings('', Changeset.opAttributeValue(stringOp('+1'), 'color', p)); - })(); + }); const testAppendATextToAssembler = (testId, atext, correctOps) => { - print(`> testAppendATextToAssembler#${testId}`); - - const assem = Changeset.smartOpAssembler(); - Changeset.appendATextToAssembler(atext, assem); - assertEqualStrings(correctOps, assem.toString()); + it(`testAppendATextToAssembler#${testId}`, async function () { + const assem = Changeset.smartOpAssembler(); + Changeset.appendATextToAssembler(atext, assem); + assertEqualStrings(correctOps, assem.toString()); + }); }; testAppendATextToAssembler(1, { @@ -836,11 +823,11 @@ const runTests = () => { }, '|2+2*x|1+1*x+3'); const testMakeAttribsString = (testId, pool, opcode, attribs, correctString) => { - print(`> testMakeAttribsString#${testId}`); - - const p = poolOrArray(pool); - const str = Changeset.makeAttribsString(opcode, attribs, p); - assertEqualStrings(correctString, str); + it(`testMakeAttribsString#${testId}`, async function () { + const p = poolOrArray(pool); + const str = Changeset.makeAttribsString(opcode, attribs, p); + assertEqualStrings(correctString, str); + }); }; testMakeAttribsString(1, ['bold,'], '+', [ @@ -859,10 +846,10 @@ const runTests = () => { ], '*0*1'); const testSubattribution = (testId, astr, start, end, correctOutput) => { - print(`> testSubattribution#${testId}`); - - const str = Changeset.subattribution(astr, start, end); - assertEqualStrings(correctOutput, str); + it(`testSubattribution#${testId}`, async function () { + const str = Changeset.subattribution(astr, start, end); + assertEqualStrings(correctOutput, str); + }); }; testSubattribution(1, '+1', 0, 0, ''); @@ -909,10 +896,10 @@ const runTests = () => { testSubattribution(42, '*0+2+1*1+3', 2, 6, '+1*1+3'); const testFilterAttribNumbers = (testId, cs, filter, correctOutput) => { - print(`> testFilterAttribNumbers#${testId}`); - - const str = Changeset.filterAttribNumbers(cs, filter); - assertEqualStrings(correctOutput, str); + it(`testFilterAttribNumbers#${testId}`, async function () { + const str = Changeset.filterAttribNumbers(cs, filter); + assertEqualStrings(correctOutput, str); + }); }; testFilterAttribNumbers(1, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6', @@ -921,11 +908,11 @@ const runTests = () => { (n) => (n % 2) === 1, '*1+1+2+3*1+4+5*1*b+6'); const testInverse = (testId, cs, lines, alines, pool, correctOutput) => { - print(`> testInverse#${testId}`); - - pool = poolOrArray(pool); - const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); - assertEqualStrings(correctOutput, str); + it(`testInverse#${testId}`, async function () { + pool = poolOrArray(pool); + const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); + assertEqualStrings(correctOutput, str); + }); }; // take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--" @@ -933,49 +920,44 @@ const runTests = () => { ['+4*1+5'], ['bold,', 'bold,true'], 'Z:9>0=2*0=1=2*1=2$'); const testMutateTextLines = (testId, cs, lines, correctLines) => { - print(`> testMutateTextLines#${testId}`); - - const a = lines.slice(); - Changeset.mutateTextLines(cs, a); - assertEqualArrays(correctLines, a); + it(`testMutateTextLines#${testId}`, async function () { + const a = lines.slice(); + Changeset.mutateTextLines(cs, a); + assertEqualArrays(correctLines, a); + }); }; testMutateTextLines(1, 'Z:4<1|1-2-1|1+1+1$\nc', ['a\n', 'b\n'], ['\n', 'c\n']); testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']); const testInverseRandom = (randomSeed) => { - print(`> testInverseRandom#${randomSeed}`); + it(`testInverseRandom#${randomSeed}`, async function () { + const p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); - const p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']); + const startText = `${randomMultiline(10, 20)}\n`; + const alines = + Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); + const lines = startText.slice(0, -1).split('\n').map((s) => `${s}\n`); - const startText = `${randomMultiline(10, 20)}\n`; - const alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText); - const lines = startText.slice(0, -1).split('\n').map((s) => `${s}\n`); + const stylifier = randomTestChangeset(startText, true)[0]; - const stylifier = randomTestChangeset(startText, true)[0]; + Changeset.mutateAttributionLines(stylifier, alines, p); + Changeset.mutateTextLines(stylifier, lines); - Changeset.mutateAttributionLines(stylifier, alines, p); - Changeset.mutateTextLines(stylifier, lines); + const changeset = randomTestChangeset(lines.join(''), true)[0]; + const inverseChangeset = Changeset.inverse(changeset, lines, alines, p); - const changeset = randomTestChangeset(lines.join(''), true)[0]; - const inverseChangeset = Changeset.inverse(changeset, lines, alines, p); + const origLines = lines.slice(); + const origALines = alines.slice(); - const origLines = lines.slice(); - const origALines = alines.slice(); - - Changeset.mutateTextLines(changeset, lines); - Changeset.mutateAttributionLines(changeset, alines, p); - Changeset.mutateTextLines(inverseChangeset, lines); - Changeset.mutateAttributionLines(inverseChangeset, alines, p); - assertEqualArrays(origLines, lines); - assertEqualArrays(origALines, alines); + Changeset.mutateTextLines(changeset, lines); + Changeset.mutateAttributionLines(changeset, alines, p); + Changeset.mutateTextLines(inverseChangeset, lines); + Changeset.mutateAttributionLines(inverseChangeset, alines, p); + assertEqualArrays(origLines, lines); + assertEqualArrays(origALines, alines); + }); }; for (let i = 0; i < 30; i++) testInverseRandom(i); -}; - -describe('easysync', function () { - it('tests', async function () { - runTests(); - }); }); From e535129f3c533d8826ecfdbaff0cc9129e02aaa1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 16 Oct 2021 15:45:15 -0400 Subject: [PATCH 182/322] tests: easysync: Use `expect.js` for checks --- src/tests/frontend/specs/easysync.js | 145 +++++++++++---------------- 1 file changed, 59 insertions(+), 86 deletions(-) diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index d179afb4b..e6665aee3 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -29,24 +29,6 @@ const AttributePool = require('../../../static/js/AttributePool'); const randInt = (maxValue) => Math.floor(Math.random() * maxValue); describe('easysync', function () { - const assert = (code, optMsg) => { - if (!eval(code)) throw new Error(`FALSE: ${optMsg || code}`); /* eslint-disable-line no-eval */ - }; - - const literal = (v) => { - if ((typeof v) === 'string') { - return `"${v.replace(/[\\"]/g, '\\$1').replace(/\n/g, '\\n')}"`; - } else { return JSON.stringify(v); } - }; - - const assertEqualArrays = (a, b) => { - assert(`JSON.stringify(${literal(a)}) == JSON.stringify(${literal(b)})`); - }; - - const assertEqualStrings = (a, b) => { - assert(`${literal(a)} == ${literal(b)}`); - }; - const throughIterator = (opsStr) => { const iter = Changeset.opIterator(opsStr); const assem = Changeset.opAssembler(); @@ -68,19 +50,19 @@ describe('easysync', function () { it('throughIterator', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - assert(`throughIterator(${literal(x)}) == ${literal(x)}`); + expect(throughIterator(x)).to.equal(x); }); it('throughSmartAssembler', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - assert(`throughSmartAssembler(${literal(x)}) == ${literal(x)}`); + expect(throughSmartAssembler(x)).to.equal(x); }); const applyMutations = (mu, arrayOfArrays) => { arrayOfArrays.forEach((a) => { const result = mu[a[0]](...a.slice(1)); if (a[0] === 'remove' && a[3]) { - assertEqualStrings(a[3], result); + expect(result).to.equal(a[3]); } }); }; @@ -125,17 +107,17 @@ describe('easysync', function () { const mu = Changeset.textLinesMutator(lines); applyMutations(mu, muts); mu.close(); - assertEqualArrays(correct, lines); + expect(lines).to.eql(correct); const inText = origLines.join(''); const cs = mutationsToChangeset(inText.length, muts); lines = origLines.slice(); Changeset.mutateTextLines(cs, lines); - assertEqualArrays(correct, lines); + expect(lines).to.eql(correct); const correctText = correct.join(''); const outText = Changeset.applyToText(cs, inText); - assertEqualStrings(correctText, outText); + expect(outText).to.equal(correctText); }); }; @@ -225,7 +207,7 @@ describe('easysync', function () { it(`applyToAttribution#${testId}`, async function () { const p = poolOrArray(attribs); const result = Changeset.applyToAttribution(Changeset.checkRep(cs), inAttr, p); - assertEqualStrings(outCorrect, result); + expect(result).to.equal(outCorrect); }); }; @@ -242,39 +224,39 @@ describe('easysync', function () { let mu; mu = Changeset.textLinesMutator(lines); - assert(`${mu.hasMore()} == true`); + expect(mu.hasMore()).to.be(true); mu.skip(8, 4); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); mu.close(); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); // still 1,2,3,4 mu = Changeset.textLinesMutator(lines); - assert(`${mu.hasMore()} == true`); + expect(mu.hasMore()).to.be(true); mu.remove(2, 1); - assert(`${mu.hasMore()} == true`); + expect(mu.hasMore()).to.be(true); mu.skip(2, 1); - assert(`${mu.hasMore()} == true`); + expect(mu.hasMore()).to.be(true); mu.skip(2, 1); - assert(`${mu.hasMore()} == true`); + expect(mu.hasMore()).to.be(true); mu.skip(2, 1); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); mu.insert('5\n', 1); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); mu.close(); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); // 2,3,4,5 now mu = Changeset.textLinesMutator(lines); - assert(`${mu.hasMore()} == true`); + expect(mu.hasMore()).to.be(true); mu.remove(6, 3); - assert(`${mu.hasMore()} == true`); + expect(mu.hasMore()).to.be(true); mu.remove(2, 1); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); mu.insert('hello\n', 1); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); mu.close(); - assert(`${mu.hasMore()} == false`); + expect(mu.hasMore()).to.be(false); }); const runMutateAttributionTest = (testId, attribs, cs, alines, outCorrect) => { @@ -282,13 +264,13 @@ describe('easysync', function () { const p = poolOrArray(attribs); const alines2 = Array.prototype.slice.call(alines); Changeset.mutateAttributionLines(Changeset.checkRep(cs), alines2, p); - assertEqualArrays(outCorrect, alines2); + expect(alines2).to.eql(outCorrect); const removeQuestionMarks = (a) => a.replace(/\?/g, ''); const inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks)); const correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks)); const mergedResult = Changeset.applyToAttribution(cs, inMerged, p); - assertEqualStrings(correctMerged, mergedResult); + expect(mergedResult).to.equal(correctMerged); }); }; @@ -608,11 +590,11 @@ describe('easysync', function () { const change23 = Changeset.checkRep(Changeset.compose(change2, change3, p)); const change123 = Changeset.checkRep(Changeset.compose(change12, change3, p)); const change123a = Changeset.checkRep(Changeset.compose(change1, change23, p)); - assertEqualStrings(change123, change123a); + expect(change123a).to.equal(change123); - assertEqualStrings(text2, Changeset.applyToText(change12, startText)); - assertEqualStrings(text3, Changeset.applyToText(change23, text1)); - assertEqualStrings(text3, Changeset.applyToText(change123, startText)); + expect(Changeset.applyToText(change12, startText)).to.equal(text2); + expect(Changeset.applyToText(change23, text1)).to.equal(text3); + expect(Changeset.applyToText(change123, startText)).to.equal(text3); }); }; @@ -625,7 +607,7 @@ describe('easysync', function () { const cs1 = Changeset.checkRep('Z:2>1*1+1*1=1$x'); const cs2 = Changeset.checkRep('Z:3>0*0|1=3$'); const cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p)); - assertEqualStrings('Z:2>1+1*0|1=2$x', cs12); + expect(cs12).to.equal('Z:2>1+1*0|1=2$x'); }); (() => { @@ -640,10 +622,10 @@ describe('easysync', function () { const testFollow = (a, b, afb, bfa, merge) => { it(`testFollow manual #${++n}`, async function () { - assertEqualStrings(afb, Changeset.followAttributes(a, b, p)); - assertEqualStrings(bfa, Changeset.followAttributes(b, a, p)); - assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p)); - assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p)); + expect(Changeset.followAttributes(a, b, p)).to.equal(afb); + expect(Changeset.followAttributes(b, a, p)).to.equal(bfa); + expect(Changeset.composeAttributes(a, afb, true, p)).to.equal(merge); + expect(Changeset.composeAttributes(b, bfa, true, p)).to.equal(merge); }); }; @@ -672,7 +654,7 @@ describe('easysync', function () { const merge1 = Changeset.checkRep(Changeset.compose(cs1, afb)); const merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa)); - assertEqualStrings(merge1, merge2); + expect(merge2).to.equal(merge1); }); }; @@ -698,8 +680,8 @@ describe('easysync', function () { const theJoined = stringToOps(doc); const theSplit = doc.match(/[^\n]*\n/g).map(stringToOps); - assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc)); - assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit)); + expect(Changeset.splitAttributionLines(theJoined, doc)).to.eql(theSplit); + expect(Changeset.joinAttributionLines(theSplit)).to.equal(theJoined); }); }; @@ -714,16 +696,15 @@ describe('easysync', function () { pool2.putAttrib(['foo', 'bar']); - assertEqualStrings( - Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab'); - assertEqualStrings( - Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1'); + expect(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2)) + .to.equal('Z:1>2*0+1*1+1$ab'); + expect(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2)).to.equal('*0+1*1+1'); }); it('testMakeSplice', async function () { const t = 'a\nb\nc\n'; const t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, 'def'), t); - assertEqualStrings('a\nb\ncdef\n', t2); + expect(t2).to.equal('a\nb\ncdef\n'); }); it('testToSplices', async function () { @@ -732,14 +713,14 @@ describe('easysync', function () { [5, 8, '123456789'], [9, 17, 'abcdefghijk'], ]; - assertEqualArrays(correctSplices, Changeset.toSplices(cs)); + expect(Changeset.toSplices(cs)).to.eql(correctSplices); }); const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => { it(`testCharacterRangeFollow#${testId}`, async function () { cs = Changeset.checkRep(cs); - assertEqualArrays(correctNewRange, Changeset.characterRangeFollow( - cs, oldRange[0], oldRange[1], insertionsAfter)); + expect(Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter)) + .to.eql(correctNewRange); }); }; @@ -763,29 +744,21 @@ describe('easysync', function () { const stringOp = (str) => Changeset.opIterator(str).next(); - assertEqualStrings('david', - Changeset.opAttributeValue(stringOp('*0*1+1'), 'name', p)); - assertEqualStrings('david', - Changeset.opAttributeValue(stringOp('*0+1'), 'name', p)); - assertEqualStrings('', - Changeset.opAttributeValue(stringOp('*1+1'), 'name', p)); - assertEqualStrings('', - Changeset.opAttributeValue(stringOp('+1'), 'name', p)); - assertEqualStrings('green', - Changeset.opAttributeValue(stringOp('*0*1+1'), 'color', p)); - assertEqualStrings('green', - Changeset.opAttributeValue(stringOp('*1+1'), 'color', p)); - assertEqualStrings('', - Changeset.opAttributeValue(stringOp('*0+1'), 'color', p)); - assertEqualStrings('', - Changeset.opAttributeValue(stringOp('+1'), 'color', p)); + expect(Changeset.opAttributeValue(stringOp('*0*1+1'), 'name', p)).to.equal('david'); + expect(Changeset.opAttributeValue(stringOp('*0+1'), 'name', p)).to.equal('david'); + expect(Changeset.opAttributeValue(stringOp('*1+1'), 'name', p)).to.equal(''); + expect(Changeset.opAttributeValue(stringOp('+1'), 'name', p)).to.equal(''); + expect(Changeset.opAttributeValue(stringOp('*0*1+1'), 'color', p)).to.equal('green'); + expect(Changeset.opAttributeValue(stringOp('*1+1'), 'color', p)).to.equal('green'); + expect(Changeset.opAttributeValue(stringOp('*0+1'), 'color', p)).to.equal(''); + expect(Changeset.opAttributeValue(stringOp('+1'), 'color', p)).to.equal(''); }); const testAppendATextToAssembler = (testId, atext, correctOps) => { it(`testAppendATextToAssembler#${testId}`, async function () { const assem = Changeset.smartOpAssembler(); Changeset.appendATextToAssembler(atext, assem); - assertEqualStrings(correctOps, assem.toString()); + expect(assem.toString()).to.equal(correctOps); }); }; @@ -826,7 +799,7 @@ describe('easysync', function () { it(`testMakeAttribsString#${testId}`, async function () { const p = poolOrArray(pool); const str = Changeset.makeAttribsString(opcode, attribs, p); - assertEqualStrings(correctString, str); + expect(str).to.equal(correctString); }); }; @@ -848,7 +821,7 @@ describe('easysync', function () { const testSubattribution = (testId, astr, start, end, correctOutput) => { it(`testSubattribution#${testId}`, async function () { const str = Changeset.subattribution(astr, start, end); - assertEqualStrings(correctOutput, str); + expect(str).to.equal(correctOutput); }); }; @@ -898,7 +871,7 @@ describe('easysync', function () { const testFilterAttribNumbers = (testId, cs, filter, correctOutput) => { it(`testFilterAttribNumbers#${testId}`, async function () { const str = Changeset.filterAttribNumbers(cs, filter); - assertEqualStrings(correctOutput, str); + expect(str).to.equal(correctOutput); }); }; @@ -911,7 +884,7 @@ describe('easysync', function () { it(`testInverse#${testId}`, async function () { pool = poolOrArray(pool); const str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool); - assertEqualStrings(correctOutput, str); + expect(str).to.equal(correctOutput); }); }; @@ -923,7 +896,7 @@ describe('easysync', function () { it(`testMutateTextLines#${testId}`, async function () { const a = lines.slice(); Changeset.mutateTextLines(cs, a); - assertEqualArrays(correctLines, a); + expect(a).to.eql(correctLines); }); }; @@ -954,8 +927,8 @@ describe('easysync', function () { Changeset.mutateAttributionLines(changeset, alines, p); Changeset.mutateTextLines(inverseChangeset, lines); Changeset.mutateAttributionLines(inverseChangeset, alines, p); - assertEqualArrays(origLines, lines); - assertEqualArrays(origALines, alines); + expect(lines).to.eql(origLines); + expect(alines).to.eql(origALines); }); }; From 4f283b64cf849c1baca6335b8e32b2b836da9f36 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 15 Oct 2021 20:53:00 -0400 Subject: [PATCH 183/322] tests: easysync: Inline some functions that are only used once --- src/tests/frontend/specs/easysync.js | 30 +++++++++------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index e6665aee3..5c4c47ae4 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -29,33 +29,21 @@ const AttributePool = require('../../../static/js/AttributePool'); const randInt = (maxValue) => Math.floor(Math.random() * maxValue); describe('easysync', function () { - const throughIterator = (opsStr) => { - const iter = Changeset.opIterator(opsStr); - const assem = Changeset.opAssembler(); - while (iter.hasNext()) { - assem.append(iter.next()); - } - return assem.toString(); - }; - - const throughSmartAssembler = (opsStr) => { - const iter = Changeset.opIterator(opsStr); - const assem = Changeset.smartOpAssembler(); - while (iter.hasNext()) { - assem.append(iter.next()); - } - assem.endDocument(); - return assem.toString(); - }; - it('throughIterator', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - expect(throughIterator(x)).to.equal(x); + const iter = Changeset.opIterator(x); + const assem = Changeset.opAssembler(); + while (iter.hasNext()) assem.append(iter.next()); + expect(assem.toString()).to.equal(x); }); it('throughSmartAssembler', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - expect(throughSmartAssembler(x)).to.equal(x); + const iter = Changeset.opIterator(x); + const assem = Changeset.smartOpAssembler(); + while (iter.hasNext()) assem.append(iter.next()); + assem.endDocument(); + expect(assem.toString()).to.equal(x); }); const applyMutations = (mu, arrayOfArrays) => { From 1a008ed6fabf915d018fca05f2b20bc3ecb0c59b Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sun, 17 Oct 2021 06:17:34 +0000 Subject: [PATCH 184/322] fix: upgrade clean-css from 5.1.5 to 5.2.0 Snyk has created this PR to upgrade clean-css from 5.1.5 to 5.2.0. See this package in npm: https://www.npmjs.com/package/clean-css See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 2b199c6f8..f028c4a8e 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1271,9 +1271,9 @@ "optional": true }, "clean-css": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.1.5.tgz", - "integrity": "sha512-9dr/cU/LjMpU57PXlSvDkVRh0rPxJBXiBtD0+SgYt8ahTCsXtfKjCkNYgIoTC6mBg8CFr5EKhW3DKCaGMUbUfQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.0.tgz", + "integrity": "sha512-2639sWGa43EMmG7fn8mdVuBSs6HuWaSor+ZPoFWzenBc6oN+td8YhTfghWXZ25G1NiiSvz8bOFBS7PdSbTiqEA==", "requires": { "source-map": "~0.6.0" } diff --git a/src/package.json b/src/package.json index 5b8bff69d..309d7f622 100644 --- a/src/package.json +++ b/src/package.json @@ -32,7 +32,7 @@ "dependencies": { "async": "^3.2.1", "channels": "0.0.4", - "clean-css": "^5.1.5", + "clean-css": "^5.2.0", "cookie-parser": "1.4.5", "cross-spawn": "^7.0.3", "ejs": "^3.1.6", From ccd7a8d5ff8ee1702aad6a53a1e40a1169fd9514 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sun, 17 Oct 2021 06:17:38 +0000 Subject: [PATCH 185/322] fix: upgrade threads from 1.6.5 to 1.7.0 Snyk has created this PR to upgrade threads from 1.6.5 to 1.7.0. See this package in npm: https://www.npmjs.com/package/threads See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index f028c4a8e..092c48c4a 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -8754,9 +8754,9 @@ "dev": true }, "threads": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.5.tgz", - "integrity": "sha512-yL1NN4qZ25crW8wDoGn7TqbENJ69w3zCEjIGXpbqmQ4I+QHrG8+DLaZVKoX74OQUXWCI2lbbrUxDxAbr1xjDGQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz", + "integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==", "requires": { "callsites": "^3.1.0", "debug": "^4.2.0", diff --git a/src/package.json b/src/package.json index 309d7f622..203e6a13b 100644 --- a/src/package.json +++ b/src/package.json @@ -66,7 +66,7 @@ "semver": "^7.3.5", "socket.io": "^2.4.1", "terser": "^5.9.0", - "threads": "^1.6.5", + "threads": "^1.7.0", "tiny-worker": "^2.3.0", "tinycon": "0.6.8", "ueberdb2": "^1.4.18", From 41e2ee484821b4ef0e3d7d4d5d07ed09657a2654 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 20 Oct 2021 08:24:39 +0000 Subject: [PATCH 186/322] fix: upgrade rate-limiter-flexible from 2.2.4 to 2.3.0 Snyk has created this PR to upgrade rate-limiter-flexible from 2.2.4 to 2.3.0. See this package in npm: https://www.npmjs.com/package/rate-limiter-flexible See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 092c48c4a..98dc3d3cf 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -7581,9 +7581,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.4.tgz", - "integrity": "sha512-8u4k5b1afuBcfydX0L0l3J2PNjgcuo3zua8plhvIisyDqOBldrCwfSFut/Fj00LAB1nxJYVM9jeszr2rZyDhQw==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.0.tgz", + "integrity": "sha512-78jZpRGOQ1LC7ZR7CzOjgjrqSxEd74qsn3SkgdY7nPXGD6rzS+3lrKP8lkzW75jzXA1XY5fmxVlJSKpYTAt39Q==" }, "raw-body": { "version": "2.4.0", diff --git a/src/package.json b/src/package.json index 203e6a13b..d4e20b1a8 100644 --- a/src/package.json +++ b/src/package.json @@ -57,7 +57,7 @@ "npm": "^6.14.15", "openapi-backend": "^4.2.0", "proxy-addr": "^2.0.7", - "rate-limiter-flexible": "^2.2.4", + "rate-limiter-flexible": "^2.3.0", "rehype": "^11.0.0", "rehype-minify-whitespace": "^4.0.5", "request": "2.88.2", From 2c15e68e4acb393d84cb0ed9458c90c215396090 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Fri, 22 Oct 2021 07:26:47 +0000 Subject: [PATCH 187/322] fix: upgrade clean-css from 5.2.0 to 5.2.1 Snyk has created this PR to upgrade clean-css from 5.2.0 to 5.2.1. See this package in npm: https://www.npmjs.com/package/clean-css See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 98dc3d3cf..5446aca9d 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1271,9 +1271,9 @@ "optional": true }, "clean-css": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.0.tgz", - "integrity": "sha512-2639sWGa43EMmG7fn8mdVuBSs6HuWaSor+ZPoFWzenBc6oN+td8YhTfghWXZ25G1NiiSvz8bOFBS7PdSbTiqEA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.1.tgz", + "integrity": "sha512-ooQCa1/70oRfVdUUGjKpbHuxgMgm8BsDT5EBqBGvPxMoRoGXf4PNx5mMnkjzJ9Ptx4vvmDdha0QVh86QtYIk1g==", "requires": { "source-map": "~0.6.0" } diff --git a/src/package.json b/src/package.json index d4e20b1a8..b896cc050 100644 --- a/src/package.json +++ b/src/package.json @@ -32,7 +32,7 @@ "dependencies": { "async": "^3.2.1", "channels": "0.0.4", - "clean-css": "^5.2.0", + "clean-css": "^5.2.1", "cookie-parser": "1.4.5", "cross-spawn": "^7.0.3", "ejs": "^3.1.6", From bbc8b29ffdb165cce68a134cfca42ee53a51aa9c Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 17 Oct 2021 20:14:34 +0200 Subject: [PATCH 188/322] Changeset: Improve JSDoc comments Co-authored-by: Richard Hansen --- src/static/js/Changeset.js | 703 ++++++++++++++++++++++++------------- 1 file changed, 459 insertions(+), 244 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 812a91154..8465a5d7f 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1,17 +1,5 @@ 'use strict'; -/* - * This is the Changeset library copied from the old Etherpad with some modifications - * to use it in node.js - * Can be found in https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js - */ - -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - /* * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * @@ -28,15 +16,18 @@ * limitations under the License. */ +/* + * This is the Changeset library copied from the old Etherpad with some modifications + * to use it in node.js. The original can be found at: + * https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js + */ + const AttributePool = require('./AttributePool'); /** - * ==================== General Util Functions ======================= - */ - -/** - * This method is called whenever there is an error in the sync process - * @param msg {string} Just some message + * This method is called whenever there is an error in the sync process. + * + * @param {string} msg - Just some message */ exports.error = (msg) => { const e = new Error(msg); @@ -45,10 +36,11 @@ exports.error = (msg) => { }; /** - * This method is used for assertions with Messages - * if assert fails, the error function is called. - * @param b {boolean} assertion condition - * @param msgParts {string} error to be passed if it fails + * Assert that a condition is truthy. If the condition is falsy, the `error` function is called to + * throw an exception. + * + * @param {boolean} b - assertion condition + * @param {...string} msgParts - error message to include in the exception */ exports.assert = (b, ...msgParts) => { if (!b) { @@ -57,41 +49,94 @@ exports.assert = (b, ...msgParts) => { }; /** - * Parses a number from string base 36 - * @param str {string} string of the number in base 36 - * @returns {int} number + * Parses a number from string base 36. + * + * @param {string} str - string of the number in base 36 + * @returns {number} number */ exports.parseNum = (str) => parseInt(str, 36); /** - * Writes a number in base 36 and puts it in a string - * @param num {int} number + * Writes a number in base 36 and puts it in a string. + * + * @param {number} num - number * @returns {string} string */ exports.numToString = (num) => num.toString(36).toLowerCase(); /** - * ==================== Changeset Functions ======================= + * An operation to apply to a shared document. + * + * @typedef {object} Op + * @property {('+'|'-'|'='|'')} opcode - The operation's operator: + * - '=': Keep the next `chars` characters (containing `lines` newlines) from the base + * document. + * - '-': Remove the next `chars` characters (containing `lines` newlines) from the base + * document. + * - '+': Insert `chars` characters (containing `lines` newlines) at the current position in + * the document. The inserted characters come from the changeset's character bank. + * - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an + * operation. + * @property {number} chars - The number of characters to keep, insert, or delete. + * @property {number} lines - The number of characters among the `chars` characters that are + * newlines. If non-zero, the last character must be a newline. + * @property {string} attribs - Identifiers of attributes to apply to the text, represented as a + * repeated (zero or more) sequence of asterisk followed by a non-negative base-36 (lower-case) + * integer. For example, '*2*1o' indicates that attributes 2 and 60 apply to the text affected + * by the operation. The identifiers come from the document's attribution pool. This is the + * empty string for remove ('-') operations. For keep ('=') operations, the attributes are + * merged with the base text's existing attributes: + * - A keep op attribute with a non-empty value replaces an existing base text attribute that + * has the same key. + * - A keep op attribute with an empty value is interpreted as an instruction to remove an + * existing base text attribute that has the same key, if one exists. */ /** - * returns the required length of the text before changeset - * can be applied - * @param cs {string} String representation of the Changeset + * Describes changes to apply to a document. Does not include the attribute pool or the original + * document. + * + * @typedef {object} Changeset + * @property {number} oldLen - The length of the base document. + * @property {number} newLen - The length of the document after applying the changeset. + * @property {string} ops - Serialized sequence of operations. Use `deserializeOps` to parse this + * string. + * @property {string} charBank - Characters inserted by insert operations. + */ + +/** + * Returns the required length of the text before changeset can be applied. + * + * @param {string} cs - String representation of the Changeset + * @returns {number} oldLen property */ exports.oldLen = (cs) => exports.unpack(cs).oldLen; /** - * returns the length of the text after changeset is applied - * @param cs {string} String representation of the Changeset + * Returns the length of the text after changeset is applied. + * + * @param {string} cs - String representation of the Changeset + * @returns {number} newLen property */ exports.newLen = (cs) => exports.unpack(cs).newLen; /** - * this function creates an iterator which decodes string changeset operations - * @param opsStr {string} String encoding of the change operations to be performed - * @param optStartIndex {int} from where in the string should the iterator start - * @return {Op} type object iterator + * Iterator over a changeset's operations. + * + * Note: This class does NOT implement the ECMAScript iterable or iterator protocols. + * + * @typedef {object} OpIter + * @property {Function} hasNext - + * @property {Function} lastIndex - + * @property {Function} next - + */ + +/** + * Creates an iterator which decodes string changeset operations. + * + * @param {string} opsStr - String encoding of the change operations to perform. + * @param {number} [optStartIndex=0] - From where in the string should the iterator start. + * @returns {OpIter} Operator iterator object. */ exports.opIterator = (opsStr, optStartIndex) => { const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; @@ -138,8 +183,9 @@ exports.opIterator = (opsStr, optStartIndex) => { }; /** - * Cleans an Op object - * @param {Op} object to be cleared + * Cleans an Op object. + * + * @param {Op} op - object to clear */ exports.clearOp = (op) => { op.opcode = ''; @@ -150,7 +196,9 @@ exports.clearOp = (op) => { /** * Creates a new Op object - * @param optOpcode the type operation of the Op object + * + * @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator. + * @returns {Op} */ exports.newOp = (optOpcode) => ({ opcode: (optOpcode || ''), @@ -161,8 +209,9 @@ exports.newOp = (optOpcode) => ({ /** * Copies op1 to op2 - * @param op1 src Op - * @param op2 dest Op + * + * @param {Op} op1 - src Op + * @param {Op} op2 - dest Op */ exports.copyOp = (op1, op2) => { op2.opcode = op1.opcode; @@ -172,12 +221,51 @@ exports.copyOp = (op1, op2) => { }; /** - * Used to check if a Changeset if valid - * @param cs {Changeset} Changeset to be checked + * Serializes a sequence of Ops. + * + * @typedef {object} OpAssembler + * @property {Function} append - + * @property {Function} clear - + * @property {Function} toString - + */ + +/** + * Efficiently merges consecutive operations that are mergeable, ignores no-ops, and drops final + * pure "keeps". It does not re-order operations. + * + * @typedef {object} MergingOpAssembler + * @property {Function} append - + * @property {Function} clear - + * @property {Function} endDocument - + * @property {Function} toString - + */ + +/** + * Creates an object that allows you to append operations (type Op) and also compresses them if + * possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser + * input, at the cost of speed. Specifically: + * - merges consecutive operations that can be merged + * - strips final "=" + * - ignores 0-length changes + * - reorders consecutive + and - (which MergingOpAssembler doesn't do) + * + * @typedef {object} SmartOpAssembler + * @property {Function} append - + * @property {Function} appendOpWithText - + * @property {Function} clear - + * @property {Function} endDocument - + * @property {Function} getLengthChange - + * @property {Function} toString - + */ + +/** + * Used to check if a Changeset is valid. This function does not check things that require access to + * the attribute pool (e.g., attribute order) or original text (e.g., newline positions). + * + * @param {string} cs - Changeset to check + * @returns {string} the checked Changeset */ exports.checkRep = (cs) => { - // doesn't check things that require access to attrib pool (e.g. attribute order) - // or original string (e.g. newline positions) const unpacked = exports.unpack(cs); const oldLen = unpacked.oldLen; const newLen = unpacked.newLen; @@ -224,23 +312,10 @@ exports.checkRep = (cs) => { return cs; }; - /** - * ==================== Util Functions ======================= - */ - -/** - * creates an object that allows you to append operations (type Op) and also - * compresses them if possible + * @returns {SmartOpAssembler} */ exports.smartOpAssembler = () => { - // Like opAssembler but able to produce conforming exportss - // from slightly looser input, at the cost of speed. - // Specifically: - // - merges consecutive operations that can be merged - // - strips final "=" - // - ignores 0-length changes - // - reorders consecutive + and - (which margingOpAssembler doesn't do) const minusAssem = exports.mergingOpAssembler(); const plusAssem = exports.mergingOpAssembler(); const keepAssem = exports.mergingOpAssembler(); @@ -333,12 +408,10 @@ exports.smartOpAssembler = () => { }; }; - +/** + * @returns {MergingOpAssembler} + */ exports.mergingOpAssembler = () => { - // This assembler can be used in production; it efficiently - // merges consecutive operations that are mergeable, ignores - // no-ops, and drops final pure "keeps". It does not re-order - // operations. const assem = exports.opAssembler(); const bufOp = exports.newOp(); @@ -408,11 +481,15 @@ exports.mergingOpAssembler = () => { }; }; - +/** + * @returns {OpAssembler} + */ exports.opAssembler = () => { const pieces = []; - // this function allows op to be mutated later (doesn't keep a ref) + /** + * @param {Op} op - Operation to add. Ownership remains with the caller. + */ const append = (op) => { pieces.push(op.attribs); if (op.lines) { @@ -436,7 +513,18 @@ exports.opAssembler = () => { /** * A custom made String Iterator - * @param str {string} String to be iterated over + * + * @typedef {object} StringIterator + * @property {Function} newlines - + * @property {Function} peek - + * @property {Function} remaining - + * @property {Function} skip - + * @property {Function} take - + */ + +/** + * @param {string} str - String to iterate over + * @returns {StringIterator} */ exports.stringIterator = (str) => { let curIndex = 0; @@ -479,10 +567,21 @@ exports.stringIterator = (str) => { /** * A custom made StringBuffer + * + * @typedef {object} StringAssembler + * @property {Function} append - + * @property {Function} toString - + */ + +/** + * @returns {StringAssembler} */ exports.stringAssembler = () => { const pieces = []; + /** + * @param {string} x - + */ const append = (x) => { pieces.push(String(x)); }; @@ -495,25 +594,48 @@ exports.stringAssembler = () => { }; /** - * This class allows to iterate and modify texts which have several lines - * It is used for applying Changesets on arrays of lines - * Note from prev docs: "lines" need not be an array as long as it supports - * certain calls (lines_foo inside). + * Class to iterate and modify texts which have several lines. It is used for applying Changesets on + * arrays of lines. + * + * Mutation operations have the same constraints as exports operations with respect to newlines, but + * not the other additional constraints (i.e. ins/del ordering, forbidden no-ops, non-mergeability, + * final newline). Can be used to mutate lists of strings where the last char of each string is not + * actually a newline, but for the purposes of N and L values, the caller should pretend it is, and + * for things to work right in that case, the input to the `insert` method should be a single line + * with no newlines. + * + * @typedef {object} TextLinesMutator + * @property {Function} close - + * @property {Function} hasMore - + * @property {Function} insert - + * @property {Function} remove - + * @property {Function} removeLines - + * @property {Function} skip - + * @property {Function} skipLines - + */ + +/** + * @param {string[]} lines - Lines to mutate (in place). This does not need to be an array as long + * as it supports certain methods/properties: + * - `get(i)`: Returns the line at index `i`. + * - `length`: Number like `Array.prototype.length`, or a method that returns the length. + * - `slice(...)`: Like `Array.prototype.slice(...)`. Optional if the return value of the + * `removeLines` method is not needed. + * - `splice(...)`: Like `Array.prototype.splice(...)`. + * @returns {TextLinesMutator} */ exports.textLinesMutator = (lines) => { - // Mutates lines, an array of strings, in place. - // Mutation operations have the same constraints as exports operations - // with respect to newlines, but not the other additional constraints - // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). - // Can be used to mutate lists of strings where the last char of each string - // is not actually a newline, but for the purposes of N and L values, - // the caller should pretend it is, and for things to work right in that case, the input - // to insert() should be a single line with no newlines. - - // The splice holds information which lines are to be deleted or changed. - // curSplice[0] is an index into the lines array - // curSplice[1] is the number of lines that will be removed from lines - // the other elements represent mutated (changed by ops) lines or new lines (added by ops) + /** + * curSplice holds values that will be passed as arguments to lines.splice() to insert, delete, or + * change lines: + * - curSplice[0] is an index into the lines array. + * - curSplice[1] is the number of lines that will be removed from the lines array starting at + * the index. + * - The other elements represent mutated (changed by ops) lines or new lines (added by ops) to + * insert at the index. + * + * @type {[number, number?, ...string[]?]} + */ const curSplice = [0, 0]; let inSplice = false; @@ -526,17 +648,19 @@ exports.textLinesMutator = (lines) => { // curCol == 0 /** - * Adds and/or removes entries at a specific offset in lines array - * It is called when leaving the splice - * @param {Array} s curSplice + * Adds and/or removes entries at a specific offset in `lines`. Called when leaving the splice. + * + * @param {[number, number?, ...string[]?]} s - curSplice */ const linesApplySplice = (s) => { lines.splice(...s); }; /** - * Get a line from lines at given index - * @param {Number} idx an index + * Get a line from `lines` at given index. + * + * @param {number} idx - an index + * @returns {string} */ const linesGet = (idx) => { if (lines.get) { @@ -545,12 +669,13 @@ exports.textLinesMutator = (lines) => { return lines[idx]; } }; - // can be unimplemented if removeLines's return value not needed /** - * Return a slice from lines array - * @param {Number} start the start index - * @param {Number} end the end index + * Return a slice from `lines`. + * + * @param {number} start - the start index + * @param {number} end - the end index + * @returns {string[]} */ const linesSlice = (start, end) => { if (lines.slice) { @@ -561,7 +686,9 @@ exports.textLinesMutator = (lines) => { }; /** - * Return the length of lines array + * Return the length of `lines`. + * + * @returns {number} */ const linesLength = () => { if ((typeof lines.length) === 'number') { @@ -586,9 +713,8 @@ exports.textLinesMutator = (lines) => { }; /** - * Changes the lines array according to the values in curSplice - * and resets curSplice. - * This is called via close or TODO(doc) + * Changes the lines array according to the values in curSplice and resets curSplice. Called via + * close or TODO(doc). */ const leaveSplice = () => { linesApplySplice(curSplice); @@ -599,18 +725,18 @@ exports.textLinesMutator = (lines) => { /** * Indicates if curLine is already in the splice. This is necessary because the last element in - * curSplice is curLine when this line is currently worked on (e.g. when skipping are inserting) + * curSplice is curLine when this line is currently worked on (e.g. when skipping are inserting). * * TODO(doc) why aren't removals considered? - * @returns {Boolean} true if curLine is in splice + * + * @returns {boolean} true if curLine is in splice */ const isCurLineInSplice = () => (curLine - curSplice[0] < (curSplice.length - 2)); /** - * Incorporates current line into the splice - * and marks its old position to be deleted. + * Incorporates current line into the splice and marks its old position to be deleted. * - * @returns {Number} the index of the added line in curSplice + * @returns {number} the index of the added line in curSplice */ const putCurLineInSplice = () => { if (!isCurLineInSplice()) { @@ -623,7 +749,8 @@ exports.textLinesMutator = (lines) => { /** * It will skip some newlines by putting them into the splice. * - * @param {Boolean} includeInSplice indicates if attributes are present + * @param {number} L - + * @param {boolean} includeInSplice - indicates if attributes are present */ const skipLines = (L, includeInSplice) => { if (L) { @@ -656,9 +783,9 @@ exports.textLinesMutator = (lines) => { /** * Skip some characters. Can contain newlines. * - * @param {Number} N number of characters to skip - * @param {Number} L number of newlines to skip - * @param {Boolean} includeInSplice indicates if attributes are present + * @param {number} N - number of characters to skip + * @param {number} L - number of newlines to skip + * @param {boolean} includeInSplice - indicates if attributes are present */ const skip = (N, L, includeInSplice) => { if (N) { @@ -679,9 +806,10 @@ exports.textLinesMutator = (lines) => { }; /** - * Remove whole lines from lines array + * Remove whole lines from lines array. * - * @param {Number} L number of lines to be removed + * @param {number} L - number of lines to remove + * @returns {string} */ const removeLines = (L) => { let removed = ''; @@ -691,10 +819,10 @@ exports.textLinesMutator = (lines) => { } /** - * Gets a string of joined lines after the end of the splice + * Gets a string of joined lines after the end of the splice. * - * @param k {Number} number of lines - * @returns {String} joined lines + * @param {number} k - number of lines + * @returns {string} joined lines */ const nextKLinesText = (k) => { const m = curSplice[0] + curSplice[1]; @@ -724,10 +852,11 @@ exports.textLinesMutator = (lines) => { }; /** - * Remove text from lines array + * Remove text from lines array. * - * @param N {Number} characters to delete - * @param L {Number} lines to delete + * @param {number} N - characters to delete + * @param {number} L - lines to delete + * @returns {string} */ const remove = (N, L) => { let removed = ''; @@ -752,8 +881,8 @@ exports.textLinesMutator = (lines) => { /** * Inserts text into lines array. * - * @param text {String} the text to insert - * @param L {Number} number of newlines in text + * @param {string} text - the text to insert + * @param {number} L - number of newlines in text */ const insert = (text, L) => { if (text) { @@ -764,6 +893,7 @@ exports.textLinesMutator = (lines) => { const newLines = exports.splitTextLines(text); if (isCurLineInSplice()) { const sline = curSplice.length - 1; + /** @type {string} */ const theLine = curSplice[sline]; const lineCol = curCol; // insert the first new line @@ -797,10 +927,9 @@ exports.textLinesMutator = (lines) => { }; /** - * Checks if curLine (the line we are in when curSplice is applied) is the last line - * in lines. + * Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`. * - * @return {Boolean} indicates if there are lines left + * @returns {boolean} indicates if there are lines left */ const hasMore = () => { let docLines = linesLength(); @@ -832,19 +961,27 @@ exports.textLinesMutator = (lines) => { }; /** - * Function allowing iterating over two Op strings. - * @params in1 {string} first Op string - * @params idx1 {int} integer where 1st iterator should start - * @params in2 {string} second Op string - * @params idx2 {int} integer where 2nd iterator should start - * @params func {function} which decides how 1st or 2nd iterator - * advances. When opX.opcode = 0, iterator X advances to - * next element - * func has signature f(op1, op2, opOut) - * op1 - current operation of the first iterator - * op2 - current operation of the second iterator - * opOut - result operator to be put into Changeset - * @return {string} the integrated changeset + * Apply operations to other operations. + * + * @param {string} in1 - first Op string + * @param {number} idx1 - integer where 1st iterator should start + * @param {string} in2 - second Op string + * @param {number} idx2 - integer where 2nd iterator should start + * @param {Function} func - Callback that applies an operation to another operation. Will be called + * multiple times depending on the number of operations in `in1` and `in2`. `func` has signature + * `f(op1, op2, opOut)`: + * - `op1` is the current operation from `in1`. `func` is expected to mutate `op1` to + * partially or fully consume it, and MUST set `op1.opcode` to the empty string once `op1` + * is fully consumed. If `op1` is not fully consumed, `func` will be called again with the + * same `op1` value. If `op1` is fully consumed, the next call to `func` will be given the + * next operation from `in1`. If there are no more operations in `in1`, `op1.opcode` will be + * the empty string. + * - `op2` is the current operation from `in2`, to apply to `op1`. Has the same consumption + * and advancement semantics as `op1`. + * - `opOut` MUST be mutated to reflect the result of applying `op2` (before consumption) to + * `op1` (before consumption). If there is no result (perhaps `op1` and `op2` cancelled each + * other out), `opOut.opcode` MUST be set to the empty string. + * @returns {string} the integrated changeset */ exports.applyZip = (in1, idx1, in2, idx2, func) => { const iter1 = exports.opIterator(in1, idx1); @@ -867,9 +1004,10 @@ exports.applyZip = (in1, idx1, in2, idx2, func) => { }; /** - * Unpacks a string encoded Changeset into a proper Changeset object - * @params cs {string} String encoded Changeset - * @returns {Changeset} a Changeset class + * Parses an encoded changeset. + * + * @param {string} cs - The encoded changeset. + * @returns {Changeset} */ exports.unpack = (cs) => { const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; @@ -893,12 +1031,13 @@ exports.unpack = (cs) => { }; /** - * Packs Changeset object into a string - * @params oldLen {int} Old length of the Changeset - * @params newLen {int] New length of the Changeset - * @params opsStr {string} String encoding of the changes to be made - * @params bank {string} Charbank of the Changeset - * @returns {Changeset} a Changeset class + * Creates an encoded changeset. + * + * @param {number} oldLen - The length of the document before applying the changeset. + * @param {number} newLen - The length of the document after applying the changeset. + * @param {string} opsStr - Encoded operations to apply to the document. + * @param {string} bank - Characters for insert operations. + * @returns {string} The encoded changeset. */ exports.pack = (oldLen, newLen, opsStr, bank) => { const lenDiff = newLen - oldLen; @@ -910,9 +1049,11 @@ exports.pack = (oldLen, newLen, opsStr, bank) => { }; /** - * Applies a Changeset to a string - * @params cs {string} String encoded Changeset - * @params str {string} String to which a Changeset should be applied + * Applies a Changeset to a string. + * + * @param {string} cs - String encoded Changeset + * @param {string} str - String to which a Changeset should be applied + * @returns {string} */ exports.applyToText = (cs, str) => { const unpacked = exports.unpack(cs); @@ -956,9 +1097,10 @@ exports.applyToText = (cs, str) => { }; /** - * applies a changeset on an array of lines - * @param CS {Changeset} the changeset to be applied - * @param lines The lines to which the changeset needs to be applied + * Applies a changeset on an array of lines. + * + * @param {string} cs - the changeset to apply + * @param {string[]} lines - The lines to which the changeset needs to be applied */ exports.mutateTextLines = (cs, lines) => { const unpacked = exports.unpack(cs); @@ -984,10 +1126,12 @@ exports.mutateTextLines = (cs, lines) => { /** * Composes two attribute strings (see below) into one. - * @param att1 {string} first attribute string - * @param att2 {string} second attribue string - * @param resultIsMutaton {boolean} - * @param pool {AttribPool} attribute pool + * + * @param {string} att1 - first attribute string + * @param {string} att2 - second attribue string + * @param {boolean} resultIsMutation - + * @param {AttributePool} pool - attribute pool + * @returns {string} */ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. @@ -1046,13 +1190,15 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { }; /** - * Function used as parameter for applyZip to apply a Changeset to an - * attribute + * Function used as parameter for applyZip to apply a Changeset to an attribute. + * + * @param {Op} attOp - The op from the sequence that is being operated on, either an attribution + * string or the earlier of two exportss being composed. + * @param {Op} csOp - + * @param {Op} opOut - Mutated to hold the result of applying `csOp` to `attOp`. + * @param {AttributePool} pool - Can be null if definitely not needed. */ exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { - // attOp is the op from the sequence that is being operated on, either an - // attribution string or the earlier of two exportss being composed. - // pool can be null if definitely not needed. if (attOp.opcode === '-') { exports.copyOp(attOp, opOut); attOp.opcode = ''; @@ -1138,9 +1284,11 @@ exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { /** * Applies a Changeset to the attribs string of a AText. - * @param cs {string} Changeset - * @param astr {string} the attribs string of a AText - * @param pool {AttribsPool} the attibutes pool + * + * @param {string} cs - Changeset + * @param {string} astr - the attribs string of a AText + * @param {AttributePool} pool - the attibutes pool + * @returns {string} */ exports.applyToAttribution = (cs, astr, pool) => { const unpacked = exports.unpack(cs); @@ -1157,6 +1305,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { // treat the attribution lines as text lines, mutating a line at a time const mut = exports.textLinesMutator(lines); + /** @type {?OpIter} */ let lineIter = null; const isNextMutOp = () => (lineIter && lineIter.hasNext()) || mut.hasMore(); @@ -1233,8 +1382,9 @@ exports.mutateAttributionLines = (cs, lines, pool) => { }; /** - * joins several Attribution lines - * @param theAlines collection of Attribution lines + * Joins several Attribution lines. + * + * @param {string[]} theAlines - collection of Attribution lines * @returns {string} joined Attribution lines */ exports.joinAttributionLines = (theAlines) => { @@ -1288,16 +1438,20 @@ exports.splitAttributionLines = (attrOps, text) => { }; /** - * splits text into lines - * @param {string} text to be splitted + * Splits text into lines. + * + * @param {string} text - text to split + * @returns {string[]} */ exports.splitTextLines = (text) => text.match(/[^\n]*(?:\n|[^\n]$)/g); /** - * compose two Changesets - * @param cs1 {Changeset} first Changeset - * @param cs2 {Changeset} second Changeset - * @param pool {AtribsPool} Attribs pool + * Compose two Changesets. + * + * @param {string} cs1 - first Changeset + * @param {string} cs2 - second Changeset + * @param {AttributePool} pool - Attribs pool + * @returns {string} */ exports.compose = (cs1, cs2, pool) => { const unpacked1 = exports.unpack(cs1); @@ -1330,11 +1484,12 @@ exports.compose = (cs1, cs2, pool) => { }; /** - * returns a function that tests if a string of attributes - * (e.g. *3*4) contains a given attribute key,value that - * is already present in the pool. - * @param attribPair array [key,value] of the attribute - * @param pool {AttribPool} Attribute pool + * Returns a function that tests if a string of attributes (e.g. '*3*4') contains a given attribute + * key,value that is already present in the pool. + * + * @param {[string, string]} attribPair - Array of attribute pairs. + * @param {AttributePool} pool - Attribute pool + * @returns {Function} */ exports.attributeTester = (attribPair, pool) => { const never = (attribs) => false; @@ -1351,23 +1506,25 @@ exports.attributeTester = (attribPair, pool) => { }; /** - * creates the identity Changeset of length N - * @param N {int} length of the identity changeset + * Creates the identity Changeset of length N. + * + * @param {number} N - length of the identity changeset + * @returns {string} */ exports.identity = (N) => exports.pack(N, N, '', ''); - /** - * creates a Changeset which works on oldFullText and removes text - * from spliceStart to spliceStart+numRemoved and inserts newText - * instead. Also gives possibility to add attributes optNewTextAPairs - * for the new text. - * @param oldFullText {string} old text - * @param spliecStart {int} where splicing starts - * @param numRemoved {int} number of characters to be removed - * @param newText {string} string to be inserted - * @param optNewTextAPairs {string} new pairs to be inserted - * @param pool {AttribPool} Attribution Pool + * Creates a Changeset which works on oldFullText and removes text from spliceStart to + * spliceStart+numRemoved and inserts newText instead. Also gives possibility to add attributes + * optNewTextAPairs for the new text. + * + * @param {string} oldFullText - old text + * @param {number} spliceStart - where splicing starts + * @param {number} numRemoved - number of characters to remove + * @param {string} newText - string to insert + * @param {string} optNewTextAPairs - new pairs to insert + * @param {AttributePool} pool - Attribute pool + * @returns {string} */ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) => { const oldLen = oldFullText.length; @@ -1390,13 +1547,15 @@ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextA }; /** - * Transforms a changeset into a list of splices in the form - * [startChar, endChar, newText] meaning replace text from - * startChar to endChar with newText - * @param cs Changeset + * Transforms a changeset into a list of splices in the form [startChar, endChar, newText] meaning + * replace text from startChar to endChar with newText. + * + * @param {string} cs - Changeset + * @returns {[number, number, string][]} */ exports.toSplices = (cs) => { const unpacked = exports.unpack(cs); + /** @type {[number, number, string][]} */ const splices = []; let oldPos = 0; @@ -1426,7 +1585,11 @@ exports.toSplices = (cs) => { }; /** - * + * @param {string} cs - + * @param {number} startChar - + * @param {number} endChar - + * @param {number} insertionsAfter - + * @returns {[number, number]} */ exports.characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { let newStartChar = startChar; @@ -1473,12 +1636,12 @@ exports.characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { }; /** - * Iterate over attributes in a changeset and move them from - * oldPool to newPool - * @param cs {Changeset} Chageset/attribution string to be iterated over - * @param oldPool {AttribPool} old attributes pool - * @param newPool {AttribPool} new attributes pool - * @return {string} the new Changeset + * Iterate over attributes in a changeset and move them from oldPool to newPool. + * + * @param {string} cs - Chageset/attribution string to iterate over + * @param {AttributePool} oldPool - old attributes pool + * @param {AttributePool} newPool - new attributes pool + * @returns {string} the new Changeset */ exports.moveOpsToNewPool = (cs, oldPool, newPool) => { // works on exports or attribution string @@ -1509,8 +1672,10 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => { }; /** - * create an attribution inserting a text - * @param text {string} text to be inserted + * Create an attribution inserting a text. + * + * @param {string} text - text to insert + * @returns {string} */ exports.makeAttribution = (text) => { const assem = exports.smartOpAssembler(); @@ -1519,10 +1684,11 @@ exports.makeAttribution = (text) => { }; /** - * Iterates over attributes in exports, attribution string, or attribs property of an op - * and runs function func on them - * @param cs {Changeset} changeset - * @param func {function} function to be called + * Iterates over attributes in exports, attribution string, or attribs property of an op and runs + * function func on them. + * + * @param {string} cs - changeset + * @param {Function} func - function to call */ exports.eachAttribNumber = (cs, func) => { let dollarPos = cs.indexOf('$'); @@ -1538,17 +1704,22 @@ exports.eachAttribNumber = (cs, func) => { }; /** - * Filter attributes which should remain in a Changeset - * callable on a exports, attribution string, or attribs property of an op, - * though it may easily create adjacent ops that can be merged. - * @param cs {Changeset} changeset to be filtered - * @param filter {function} fnc which returns true if an - * attribute X (int) should be kept in the Changeset + * Filter attributes which should remain in a Changeset. Callable on a exports, attribution string, + * or attribs property of an op, though it may easily create adjacent ops that can be merged. + * + * @param {string} cs - changeset to filter + * @param {Function} filter - fnc which returns true if an attribute X (int) should be kept in the + * Changeset + * @returns {string} */ exports.filterAttribNumbers = (cs, filter) => exports.mapAttribNumbers(cs, filter); /** - * does exactly the same as exports.filterAttribNumbers + * Does exactly the same as exports.filterAttribNumbers. + * + * @param {string} cs - + * @param {Function} func - + * @returns {string} */ exports.mapAttribNumbers = (cs, func) => { let dollarPos = cs.indexOf('$'); @@ -1572,10 +1743,21 @@ exports.mapAttribNumbers = (cs, func) => { }; /** - * Create a Changeset going from Identity to a certain state - * @params text {string} text of the final change - * @attribs attribs {string} optional, operations which insert - * the text and also puts the right attributes + * Represents text with attributes. + * + * @typedef {object} AText + * @property {string} attribs - Serialized sequence of insert operations that cover the text in + * `text`. These operations describe which parts of the text have what attributes. + * @property {string} text - The text. + */ + +/** + * Create a Changeset going from Identity to a certain state. + * + * @param {string} text - text of the final change + * @param {string} attribs - optional, operations which insert the text and also puts the right + * attributes + * @returns {AText} */ exports.makeAText = (text, attribs) => ({ text, @@ -1583,10 +1765,12 @@ exports.makeAText = (text, attribs) => ({ }); /** - * Apply a Changeset to a AText - * @param cs {Changeset} Changeset to be applied - * @param atext {AText} - * @param pool {AttribPool} Attribute Pool to add to + * Apply a Changeset to a AText. + * + * @param {string} cs - Changeset to apply + * @param {AText} atext - + * @param {AttributePool} pool - Attribute Pool to add to + * @returns {AText} */ exports.applyToAText = (cs, atext, pool) => ({ text: exports.applyToText(cs, atext.text), @@ -1594,8 +1778,10 @@ exports.applyToAText = (cs, atext, pool) => ({ }); /** - * Clones a AText structure - * @param atext {AText} + * Clones a AText structure. + * + * @param {AText} atext - + * @returns {AText} */ exports.cloneAText = (atext) => { if (atext) { @@ -1607,8 +1793,10 @@ exports.cloneAText = (atext) => { }; /** - * Copies a AText structure from atext1 to atext2 - * @param atext {AText} + * Copies a AText structure from atext1 to atext2. + * + * @param {AText} atext1 - + * @param {AText} atext2 - */ exports.copyAText = (atext1, atext2) => { atext2.text = atext1.text; @@ -1616,9 +1804,10 @@ exports.copyAText = (atext1, atext2) => { }; /** - * Append the set of operations from atext to an assembler - * @param atext {AText} - * @param assem Assembler like smartOpAssembler + * Append the set of operations from atext to an assembler. + * + * @param {AText} atext - + * @param assem - Assembler like SmartOpAssembler TODO add desc */ exports.appendATextToAssembler = (atext, assem) => { // intentionally skips last newline char of atext @@ -1654,9 +1843,11 @@ exports.appendATextToAssembler = (atext, assem) => { }; /** - * Creates a clone of a Changeset and it's APool - * @param cs {Changeset} - * @param pool {AtributePool} + * Creates a clone of a Changeset and it's APool. + * + * @param {string} cs - + * @param {AttributePool} pool - + * @returns {{translated: string, pool: AttributePool}} */ exports.prepareForWire = (cs, pool) => { const newPool = new AttributePool(); @@ -1668,7 +1859,10 @@ exports.prepareForWire = (cs, pool) => { }; /** - * Checks if a changeset s the identity changeset + * Checks if a changeset s the identity changeset. + * + * @param {string} cs - + * @returns {boolean} */ exports.isIdentity = (cs) => { const unpacked = exports.unpack(cs); @@ -1676,20 +1870,22 @@ exports.isIdentity = (cs) => { }; /** - * returns all the values of attributes with a certain key - * in an Op attribs string - * @param attribs {string} Attribute string of a Op - * @param key {string} string to be seached for - * @param pool {AttribPool} attribute pool + * Returns all the values of attributes with a certain key in an Op attribs string. + * + * @param {Op} op - Op + * @param {string} key - string to search for + * @param {AttributePool} pool - attribute pool + * @returns {string} */ exports.opAttributeValue = (op, key, pool) => exports.attribsAttributeValue(op.attribs, key, pool); /** - * returns all the values of attributes with a certain key - * in an attribs string - * @param attribs {string} Attribute string - * @param key {string} string to be seached for - * @param pool {AttribPool} attribute pool + * Returns all the values of attributes with a certain key in an attribs string. + * + * @param {string} attribs - Attribute string + * @param {string} key - string to search for + * @param {AttributePool} pool - attribute pool + * @returns {string} */ exports.attribsAttributeValue = (attribs, key, pool) => { let value = ''; @@ -1704,9 +1900,19 @@ exports.attribsAttributeValue = (attribs, key, pool) => { }; /** - * Creates a Changeset builder for a string with initial - * length oldLen. Allows to add/remove parts of it - * @param oldLen {int} Old length + * Incrementally builds a Changeset. + * + * @typedef {object} Builder + * @property {Function} insert - + * @property {Function} keep - + * @property {Function} keepText - + * @property {Function} remove - + * @property {Function} toString - + */ + +/** + * @param {number} oldLen - Old length + * @returns {Builder} */ exports.builder = (oldLen) => { const assem = exports.smartOpAssembler(); @@ -1714,7 +1920,10 @@ exports.builder = (oldLen) => { const charBank = exports.stringAssembler(); const self = { - // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) + /** + * @param attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in + * latter case). + */ keep: (N, L, attribs, pool) => { o.opcode = '='; o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || ''; @@ -1772,7 +1981,9 @@ exports.makeAttribsString = (opcode, attribs, pool) => { } }; -// like "substring" but on a single-line attribution string +/** + * Like "substring" but on a single-line attribution string. + */ exports.subattribution = (astr, start, optEnd) => { const iter = exports.opIterator(astr, 0); const assem = exports.smartOpAssembler(); @@ -1834,6 +2045,10 @@ exports.inverse = (cs, lines, alines, pool) => { } }; + /** + * @param {number} idx - + * @returns {string} + */ const alinesGet = (idx) => { if (alines.get) { return alines.get(idx); From c98b521539e84dc71f9915907fa2e27b9d2b6fa0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 22 Oct 2021 21:59:03 -0400 Subject: [PATCH 189/322] AttributePool: Use ES6 class syntax --- src/static/js/AttributePool.js | 111 +++++++++++++++++---------------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index 79849f4d3..750c13937 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -29,69 +29,70 @@ used to reference Attributes in Changesets. */ -const AttributePool = function () { - this.numToAttrib = {}; // e.g. {0: ['foo','bar']} - this.attribToNum = {}; // e.g. {'foo,bar': 0} - this.nextNum = 0; -}; - -AttributePool.prototype.putAttrib = function (attrib, dontAddIfAbsent) { - const str = String(attrib); - if (str in this.attribToNum) { - return this.attribToNum[str]; +class AttributePool { + constructor() { + this.numToAttrib = {}; // e.g. {0: ['foo','bar']} + this.attribToNum = {}; // e.g. {'foo,bar': 0} + this.nextNum = 0; } - if (dontAddIfAbsent) { - return -1; + + putAttrib(attrib, dontAddIfAbsent = false) { + const str = String(attrib); + if (str in this.attribToNum) { + return this.attribToNum[str]; + } + if (dontAddIfAbsent) { + return -1; + } + const num = this.nextNum++; + this.attribToNum[str] = num; + this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; + return num; } - const num = this.nextNum++; - this.attribToNum[str] = num; - this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; - return num; -}; -AttributePool.prototype.getAttrib = function (num) { - const pair = this.numToAttrib[num]; - if (!pair) { - return pair; + getAttrib(num) { + const pair = this.numToAttrib[num]; + if (!pair) { + return pair; + } + return [pair[0], pair[1]]; // return a mutable copy } - return [pair[0], pair[1]]; // return a mutable copy -}; -AttributePool.prototype.getAttribKey = function (num) { - const pair = this.numToAttrib[num]; - if (!pair) return ''; - return pair[0]; -}; - -AttributePool.prototype.getAttribValue = function (num) { - const pair = this.numToAttrib[num]; - if (!pair) return ''; - return pair[1]; -}; - -AttributePool.prototype.eachAttrib = function (func) { - for (const n of Object.keys(this.numToAttrib)) { - const pair = this.numToAttrib[n]; - func(pair[0], pair[1]); + getAttribKey(num) { + const pair = this.numToAttrib[num]; + if (!pair) return ''; + return pair[0]; } -}; -AttributePool.prototype.toJsonable = function () { - return { - numToAttrib: this.numToAttrib, - nextNum: this.nextNum, - }; -}; - -AttributePool.prototype.fromJsonable = function (obj) { - this.numToAttrib = obj.numToAttrib; - this.nextNum = obj.nextNum; - this.attribToNum = {}; - for (const n of Object.keys(this.numToAttrib)) { - this.attribToNum[String(this.numToAttrib[n])] = Number(n); + getAttribValue(num) { + const pair = this.numToAttrib[num]; + if (!pair) return ''; + return pair[1]; } - return this; -}; + eachAttrib(func) { + for (const n of Object.keys(this.numToAttrib)) { + const pair = this.numToAttrib[n]; + func(pair[0], pair[1]); + } + } + + toJsonable() { + return { + numToAttrib: this.numToAttrib, + nextNum: this.nextNum, + }; + } + + fromJsonable(obj) { + this.numToAttrib = obj.numToAttrib; + this.nextNum = obj.nextNum; + this.attribToNum = {}; + for (const n of Object.keys(this.numToAttrib)) { + this.attribToNum[String(this.numToAttrib[n])] = Number(n); + } + return this; + } +} module.exports = AttributePool; From de3dfb5ce2fd7533285c3c060a32ef5e3f992b6a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 24 Oct 2021 17:04:47 -0400 Subject: [PATCH 190/322] AttributePool: Add JSDoc comments --- src/static/js/AttributePool.js | 105 +++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.js index 750c13937..d8e587654 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.js @@ -23,19 +23,83 @@ * limitations under the License. */ -/* - An AttributePool maintains a mapping from [key,value] Pairs called - Attributes to Numbers (unsigened integers) and vice versa. These numbers are - used to reference Attributes in Changesets. -*/ +/** + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute + */ +/** + * Maps an attribute's identifier to the attribute. + * + * @typedef {Object.} NumToAttrib + */ + +/** + * An intermediate representation of the contents of an attribute pool, suitable for serialization + * via `JSON.stringify` and transmission to another user. + * + * @typedef {Object} Jsonable + * @property {NumToAttrib} numToAttrib - The pool's attributes and their identifiers. + * @property {number} nextNum - The attribute ID to assign to the next new attribute. + */ + +/** + * Represents an attribute pool, which is a collection of attributes (pairs of key and value + * strings) along with their identifiers (non-negative integers). + * + * The attribute pool enables attribute interning: rather than including the key and value strings + * in changesets, changesets reference attributes by their identifiers. + * + * There is one attribute pool per pad, and it includes every current and historical attribute used + * in the pad. + */ class AttributePool { constructor() { + /** + * Maps an attribute identifier to the attribute's `[key, value]` string pair. + * + * TODO: Rename to `_numToAttrib` once all users have been migrated to call `getAttrib` instead + * of accessing this directly. + * @private + * TODO: Convert to an array. + * @type {NumToAttrib} + */ this.numToAttrib = {}; // e.g. {0: ['foo','bar']} + + /** + * Maps the string representation of an attribute (`String([key, value])`) to its non-negative + * identifier. + * + * TODO: Rename to `_attribToNum` once all users have been migrated to use `putAttrib` instead + * of accessing this directly. + * @private + * TODO: Convert to a `Map` object. + * @type {Object.} + */ this.attribToNum = {}; // e.g. {'foo,bar': 0} + + /** + * The attribute ID to assign to the next new attribute. + * + * TODO: This property will not be necessary once `numToAttrib` is converted to an array (just + * push onto the array). + * + * @private + * @type {number} + */ this.nextNum = 0; } + /** + * Add an attribute to the attribute set, or query for an existing attribute identifier. + * + * @param {Attribute} attrib - The attribute's `[key, value]` pair of strings. + * @param {boolean} [dontAddIfAbsent=false] - If true, do not insert the attribute into the pool + * if the attribute does not already exist in the pool. This can be used to test for + * membership in the pool without mutating the pool. + * @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool. + */ putAttrib(attrib, dontAddIfAbsent = false) { const str = String(attrib); if (str in this.attribToNum) { @@ -50,6 +114,11 @@ class AttributePool { return num; } + /** + * @param {number} num - The identifier of the attribute to fetch. + * @returns {Attribute} The attribute with the given identifier, or nullish if there is no such + * attribute. + */ getAttrib(num) { const pair = this.numToAttrib[num]; if (!pair) { @@ -58,18 +127,34 @@ class AttributePool { return [pair[0], pair[1]]; // return a mutable copy } + /** + * @param {number} num - The identifier of the attribute to fetch. + * @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty + * string. + */ getAttribKey(num) { const pair = this.numToAttrib[num]; if (!pair) return ''; return pair[0]; } + /** + * @param {number} num - The identifier of the attribute to fetch. + * @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty + * string. + */ getAttribValue(num) { const pair = this.numToAttrib[num]; if (!pair) return ''; return pair[1]; } + /** + * Executes a callback for each attribute in the pool. + * + * @param {Function} func - Callback to call with two arguments: key and value. Its return value + * is ignored. + */ eachAttrib(func) { for (const n of Object.keys(this.numToAttrib)) { const pair = this.numToAttrib[n]; @@ -77,6 +162,10 @@ class AttributePool { } } + /** + * @returns {Jsonable} An object that can be passed to `fromJsonable` to reconstruct this + * attribute pool. The returned object can be converted to JSON. + */ toJsonable() { return { numToAttrib: this.numToAttrib, @@ -84,6 +173,12 @@ class AttributePool { }; } + /** + * Replace the contents of this attribute pool with values from a previous call to `toJsonable`. + * + * @param {Jsonable} obj - Object returned by `toJsonable` containing the attributes and their + * identifiers. + */ fromJsonable(obj) { this.numToAttrib = obj.numToAttrib; this.nextNum = obj.nextNum; From 4a8c72a38c36cd5eb3e31cf014a33f3e08ae20b5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 24 Oct 2021 19:42:59 -0400 Subject: [PATCH 191/322] Changeset: Minimize changeset docs, add links to code This reduces the amount of duplicate documentation, and helps keep the documentation and code in sync. --- doc/api/changeset_library.md | 166 ++++++----------------------------- 1 file changed, 27 insertions(+), 139 deletions(-) diff --git a/doc/api/changeset_library.md b/doc/api/changeset_library.md index c776c17c5..7929aa48b 100644 --- a/doc/api/changeset_library.md +++ b/doc/api/changeset_library.md @@ -1,156 +1,44 @@ # Changeset Library -``` -"Z:z>1|2=m=b*0|1+1$\n" +The [changeset +library](https://github.com/ether/etherpad-lite/blob/develop/src/static/js/Changeset.js) +provides tools to create, read, and apply changesets. + +## Changeset + +```javascript +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); ``` -This is a Changeset. It's just a string and it's very difficult to read in this form. But the Changeset Library gives us some tools to read it. +A changeset describes the difference between two revisions of a document. When a +user edits a pad, the browser generates and sends a changeset to the server, +which relays it to the other users and saves a copy (so that every past revision +is accessible). -A changeset describes the diff between two revisions of the document. The Browser sends changesets to the server and the server sends them to the clients to update them. These Changesets also get saved into the history of a pad. This allows us to go back to every revision from the past. - -## Changeset.unpack(changeset) - - * `changeset` {String} - -This function returns an object representation of the changeset, similar to this: +A transmitted changeset looks like this: ``` -{ oldLen: 35, newLen: 36, ops: '|2=m=b*0|1+1', charBank: '\n' } +'Z:z>1|2=m=b*0|1+1$\n' ``` - * `oldLen` {Number} the original length of the document. - * `newLen` {Number} the length of the document after the changeset is applied. - * `ops` {String} the actual changes, introduced by this changeset. - * `charBank` {String} All characters that are added by this changeset. +## Attribute Pool -## Changeset.opIterator(ops) - - * `ops` {String} The operators, returned by `Changeset.unpack()` - -Returns an operator iterator. This iterator allows us to iterate over all operators that are in the changeset. - -You can iterate with an opIterator using its `next()` and `hasNext()` methods. Next returns the `next()` operator object and `hasNext()` indicates, whether there are any operators left. - -## The Operator object -There are 3 types of operators: `+`,`-` and `=`. These operators describe different changes to the document, beginning with the first character of the document. A `=` operator doesn't change the text, but it may add or remove text attributes. A `-` operator removes text. And a `+` Operator adds text and optionally adds some attributes to it. - - * `opcode` {String} the operator type - * `chars` {Number} the length of the text changed by this operator. - * `lines` {Number} the number of lines changed by this operator. - * `attribs` {attribs} attributes set on this text. - -### Example -``` -{ opcode: '+', - chars: 1, - lines: 1, - attribs: '*0' } +```javascript +const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); ``` -## APool +Changesets do not include any attribute key–value pairs. Instead, they use +numeric identifiers that reference attributes kept in an [attribute +pool](https://github.com/ether/etherpad-lite/blob/develop/src/static/js/AttributePool.js). +This attribute interning reduces the transmission overhead of attributes that +are used many times. -``` -> var AttributePoolFactory = require("./utils/AttributePoolFactory"); -> var apool = AttributePoolFactory.createAttributePool(); -> console.log(apool) -{ numToAttrib: {}, - attribToNum: {}, - nextNum: 0, - putAttrib: [Function], - getAttrib: [Function], - getAttribKey: [Function], - getAttribValue: [Function], - eachAttrib: [Function], - toJsonable: [Function], - fromJsonable: [Function] } -``` +There is one attribute pool per pad, and it includes every current and +historical attribute used in the pad. -This creates an empty apool. An apool saves which attributes were used during the history of a pad. There is one apool for each pad. It only saves the attributes that were really used, it doesn't save unused attributes. Let's fill this apool with some values - -``` -> apool.fromJsonable({"numToAttrib":{"0":["author","a.kVnWeomPADAT2pn9"],"1":["bold","true"],"2":["italic","true"]},"nextNum":3}); -> console.log(apool) -{ numToAttrib: - { '0': [ 'author', 'a.kVnWeomPADAT2pn9' ], - '1': [ 'bold', 'true' ], - '2': [ 'italic', 'true' ] }, - attribToNum: - { 'author,a.kVnWeomPADAT2pn9': 0, - 'bold,true': 1, - 'italic,true': 2 }, - nextNum: 3, - putAttrib: [Function], - getAttrib: [Function], - getAttribKey: [Function], - getAttribValue: [Function], - eachAttrib: [Function], - toJsonable: [Function], - fromJsonable: [Function] } -``` - -We used the fromJsonable function to fill the empty apool with values. the fromJsonable and toJsonable functions are used to serialize and deserialize an apool. You can see that it stores the relation between numbers and attributes. So for example the attribute 1 is the attribute bold and vise versa. An attribute is always a key value pair. For stuff like bold and italic it's just 'italic':'true'. For authors it's author:$AUTHORID. So a character can be bold and italic. But it can't belong to multiple authors - -``` -> apool.getAttrib(1) -[ 'bold', 'true' ] -``` - -Simple example of how to get the key value pair for the attribute 1 - -## AText - -``` -> var atext = {"text":"bold text\nitalic text\nnormal text\n\n","attribs":"*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2"}; -> console.log(atext) -{ text: 'bold text\nitalic text\nnormal text\n\n', - attribs: '*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2' } -``` - -This is an atext. An atext has two parts: text and attribs. The text is just the text of the pad as a string. We will look closer at the attribs at the next steps - -``` -> var opiterator = Changeset.opIterator(atext.attribs) -> console.log(opiterator) -{ next: [Function: next], - hasNext: [Function: hasNext], - lastIndex: [Function: lastIndex] } -> opiterator.next() -{ opcode: '+', - chars: 9, - lines: 0, - attribs: '*0*1' } -> opiterator.next() -{ opcode: '+', - chars: 1, - lines: 1, - attribs: '*0' } -> opiterator.next() -{ opcode: '+', - chars: 11, - lines: 0, - attribs: '*0*1*2' } -> opiterator.next() -{ opcode: '+', - chars: 1, - lines: 1, - attribs: '' } -> opiterator.next() -{ opcode: '+', - chars: 11, - lines: 0, - attribs: '*0' } -> opiterator.next() -{ opcode: '+', - chars: 2, - lines: 2, - attribs: '' } -``` - -The attribs are again a bunch of operators like .ops in the changeset was. But these operators are only + operators. They describe which part of the text has which attributes - -## Resources / further reading +## Further Reading Detailed information about the changesets & Easysync protocol: -* Easysync Protocol - [/doc/easysync/easysync-notes.pdf](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.pdf) -* Etherpad and EasySync Technical Manual - [/doc/easysync/easysync-full-description.pdf](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-full-description.pdf) +* [Easysync Protocol](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.pdf) +* [Etherpad and EasySync Technical Manual](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-full-description.pdf) From 2c15ae6ac99f920dbed8d1d92e088f8e2ca13505 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 25 Oct 2021 13:04:22 +0200 Subject: [PATCH 192/322] Localisation updates from https://translatewiki.net. --- src/locales/uk.json | 2 +- src/locales/zh-hans.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/locales/uk.json b/src/locales/uk.json index e13cd0e7f..994c32ba3 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -122,7 +122,7 @@ "pad.modals.rateLimited": "Швидкість обмежено.", "pad.modals.rateLimited.explanation": "Ви надіслали надто багато повідомлень у цей документ, тому він вас від'єднав.", "pad.modals.rejected.explanation": "Сервер відхилив повідомлення, надіслане вашим браузером.", - "pad.modals.rejected.cause": "Сервер міг оновитися, поки ви переглядали документ, а може це баг в Etherpad'і. Спробуйте перезавантажити сторінку.", + "pad.modals.rejected.cause": "Сервер міг оновитися, поки ви переглядали документ, а може це помилка у Etherpad'і. Спробуйте перезавантажити сторінку.", "pad.modals.disconnected": "Вас було від'єднано.", "pad.modals.disconnected.explanation": "З'єднання з сервером втрачено", "pad.modals.disconnected.cause": "Сервер, можливо, недоступний. Будь ласка, повідомте адміністратора служби, якщо це повторюватиметься.", diff --git a/src/locales/zh-hans.json b/src/locales/zh-hans.json index 7a5d46e39..e2dca09a8 100644 --- a/src/locales/zh-hans.json +++ b/src/locales/zh-hans.json @@ -3,6 +3,7 @@ "authors": [ "94rain", "Dimension", + "GuoPC", "Hydra", "Hzy980512", "JuneAugust", @@ -67,7 +68,7 @@ "pad.importExport.exportopen": "ODF(开放文档格式)", "pad.importExport.abiword.innerHTML": "您只可以导入纯文本或HTML格式。要获取更高级的导入功能,请安装 AbiWord 或是 LibreOffice。", "pad.modals.connected": "已连接。", - "pad.modals.reconnecting": "重新连接到您的记事本...", + "pad.modals.reconnecting": "重新连接到您的记事本…", "pad.modals.forcereconnect": "强制重新连接", "pad.modals.reconnecttimer": "尝试重新连入", "pad.modals.cancel": "取消", From 0d67d05b783a922962ad2b2c6bfceadff55abb14 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Mon, 25 Oct 2021 09:06:25 +0000 Subject: [PATCH 193/322] fix: upgrade express-rate-limit from 5.3.0 to 5.4.0 Snyk has created this PR to upgrade express-rate-limit from 5.3.0 to 5.4.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 5446aca9d..a7dee1ee7 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2328,9 +2328,9 @@ } }, "express-rate-limit": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz", - "integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew==" + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.4.0.tgz", + "integrity": "sha512-sT+rk1wvj06+0MpEiij7y3kGdB4hoMyQ+a5zcESUpDMLhbLXoYIQI6JfsvLBz1wOhmfF//ALG/Q59FKMI0x2Eg==" }, "express-session": { "version": "1.17.2", diff --git a/src/package.json b/src/package.json index b896cc050..8ac1eeec7 100644 --- a/src/package.json +++ b/src/package.json @@ -39,7 +39,7 @@ "etherpad-require-kernel": "^1.0.15", "etherpad-yajsml": "0.0.12", "express": "4.17.1", - "express-rate-limit": "5.3.0", + "express-rate-limit": "5.4.0", "express-session": "1.17.2", "fast-deep-equal": "^3.1.3", "find-root": "1.1.0", From cea7eb8ba6b4f8dfd560498904348773c701b2b1 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 26 Oct 2021 10:12:27 +0000 Subject: [PATCH 194/322] fix: upgrade mime-types from 2.1.32 to 2.1.33 Snyk has created this PR to upgrade mime-types from 2.1.32 to 2.1.33. See this package in npm: https://www.npmjs.com/package/mime-types See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 14 +++++++------- src/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index a7dee1ee7..ddfe26798 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3597,16 +3597,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" }, "mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", "requires": { - "mime-db": "1.49.0" + "mime-db": "1.50.0" } }, "mimic-response": { diff --git a/src/package.json b/src/package.json index 8ac1eeec7..8087da881 100644 --- a/src/package.json +++ b/src/package.json @@ -52,7 +52,7 @@ "lodash.clonedeep": "4.5.0", "log4js": "0.6.38", "measured-core": "^2.0.0", - "mime-types": "^2.1.32", + "mime-types": "^2.1.33", "nodeify": "1.0.1", "npm": "^6.14.15", "openapi-backend": "^4.2.0", From dd8608fe6e7d93d4cf624670cabfcac2b3f3376f Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Tue, 26 Oct 2021 10:12:32 +0000 Subject: [PATCH 195/322] fix: upgrade rate-limiter-flexible from 2.3.0 to 2.3.1 Snyk has created this PR to upgrade rate-limiter-flexible from 2.3.0 to 2.3.1. See this package in npm: https://www.npmjs.com/package/rate-limiter-flexible See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index ddfe26798..552c74c31 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -7581,9 +7581,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.0.tgz", - "integrity": "sha512-78jZpRGOQ1LC7ZR7CzOjgjrqSxEd74qsn3SkgdY7nPXGD6rzS+3lrKP8lkzW75jzXA1XY5fmxVlJSKpYTAt39Q==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.1.tgz", + "integrity": "sha512-u4Ual0ssf/RHHxK3rqKo9W2S7ulVoNdCAOrsk1gR9JLtzqg7fGw+yaCeyBAEncsL2n6XqHh/0qJk3BPDn49BjA==" }, "raw-body": { "version": "2.4.0", diff --git a/src/package.json b/src/package.json index 8087da881..1aac25f65 100644 --- a/src/package.json +++ b/src/package.json @@ -57,7 +57,7 @@ "npm": "^6.14.15", "openapi-backend": "^4.2.0", "proxy-addr": "^2.0.7", - "rate-limiter-flexible": "^2.3.0", + "rate-limiter-flexible": "^2.3.1", "rehype": "^11.0.0", "rehype-minify-whitespace": "^4.0.5", "request": "2.88.2", From 8eb5640cb74a5d7ddf43b208230f755deb533a6f Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 27 Oct 2021 09:57:33 +0000 Subject: [PATCH 196/322] fix: upgrade express-rate-limit from 5.4.0 to 5.4.1 Snyk has created this PR to upgrade express-rate-limit from 5.4.0 to 5.4.1. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 552c74c31..d8ea3b440 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2328,9 +2328,9 @@ } }, "express-rate-limit": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.4.0.tgz", - "integrity": "sha512-sT+rk1wvj06+0MpEiij7y3kGdB4hoMyQ+a5zcESUpDMLhbLXoYIQI6JfsvLBz1wOhmfF//ALG/Q59FKMI0x2Eg==" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.4.1.tgz", + "integrity": "sha512-ZQh2h3qiu7wWdvWNYHznBhaOp2ZIXNnT4hl2Ff608STeWtCuJ251NzqQlk7mo5wnO2HmrydBYHuVA9Z3S3ZtXg==" }, "express-session": { "version": "1.17.2", diff --git a/src/package.json b/src/package.json index 1aac25f65..dc0f0c69e 100644 --- a/src/package.json +++ b/src/package.json @@ -39,7 +39,7 @@ "etherpad-require-kernel": "^1.0.15", "etherpad-yajsml": "0.0.12", "express": "4.17.1", - "express-rate-limit": "5.4.0", + "express-rate-limit": "5.4.1", "express-session": "1.17.2", "fast-deep-equal": "^3.1.3", "find-root": "1.1.0", From 668d62fa3f9273561d25ff924c9ef5c4f3755698 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 29 Oct 2021 00:23:19 +0200 Subject: [PATCH 197/322] importHtml: avoid race when applying empty document and the import changeset at the same time --- src/node/utils/ImportHtml.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 13fb74795..13befe721 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -94,8 +94,6 @@ exports.setPadHTML = async (pad, html) => { const theChangeset = builder.toString(); apiLogger.debug(`The changeset: ${theChangeset}`); - await Promise.all([ - pad.setText('\n'), - pad.appendRevision(theChangeset), - ]); + await pad.setText('\n'); + await pad.appendRevision(theChangeset); }; From 63de249236e8088b0cd04b7bc4fd24c7e59c6366 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 29 Oct 2021 00:53:36 +0200 Subject: [PATCH 198/322] tests: do not re-add identical text with setText --- src/tests/backend/specs/api/pad.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/tests/backend/specs/api/pad.js b/src/tests/backend/specs/api/pad.js index cf16f87f8..e91689851 100644 --- a/src/tests/backend/specs/api/pad.js +++ b/src/tests/backend/specs/api/pad.js @@ -461,6 +461,35 @@ describe(__filename, function () { .expect('Content-Type', /json/); assert.equal(res.body.code, 0); }); + + it('does not add an useless revision', async function () { + let res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) + .field({text: 'identical text\n'}) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + + res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.text, 'identical text\n'); + + res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + const revCount = res.body.data.revisions; + + res = await agent.post(`${endPoint('setText')}&padID=${testPadId}`) + .field({text: 'identical text\n'}) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + + res = await agent.get(`${endPoint('getRevisionsCount')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.revisions, revCount); + }); }); describe('copyPadWithoutHistory', function () { From f7f5e3dad85c606e9b5a880a598a5ea9acdc3675 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 29 Oct 2021 00:54:18 +0200 Subject: [PATCH 199/322] setText: prevent adding useless revision in case the pad text did not change --- src/node/db/Pad.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index db35daf19..fe4ea78d0 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -258,7 +258,7 @@ Pad.prototype.setText = async function (newText) { } // append the changeset - await this.appendRevision(changeset); + if (newText !== oldText) await this.appendRevision(changeset); }; Pad.prototype.appendText = async function (newText) { From 9bad1d03d302afde1c2f1fcc3fa592bb9801c07c Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 29 Oct 2021 00:59:06 +0200 Subject: [PATCH 200/322] importHtml: do not add an useless identity changeset --- src/node/utils/ImportHtml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 13befe721..2749977d6 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -95,5 +95,5 @@ exports.setPadHTML = async (pad, html) => { apiLogger.debug(`The changeset: ${theChangeset}`); await pad.setText('\n'); - await pad.appendRevision(theChangeset); + if (!Changeset.isIdentity(theChangeset)) await pad.appendRevision(theChangeset); }; From d8ca3a693d08f68044cdd08f1bab7f706f75cfdc Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Fri, 29 Oct 2021 00:12:47 +0200 Subject: [PATCH 201/322] timeslider scrollTo: fixes wrong line number calculation in case there are no attribute changes and no length changes --- src/static/js/broadcast.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 909b6a085..6ba2ef0ab 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -164,10 +164,16 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro return true; // break } }); - // deal with someone is the author of a line and changes one character, - // so the alines won't change + // some chars are replaced (no attributes change and no length change) + // test if there are keep ops at the start of the cs if (lineChanged === undefined) { - lineChanged = Changeset.opIterator(Changeset.unpack(changeset).ops).next().lines; + lineChanged = 0; + const opIter = Changeset.opIterator(Changeset.unpack(changeset).ops); + + if (opIter.hasNext()) { + const op = opIter.next(); + if (op.opcode === '=') lineChanged += op.lines; + } } const goToLineNumber = (lineNumber) => { From e974622561f6bf4e3b30ad044a1679a31f91e01a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 15:15:05 -0400 Subject: [PATCH 202/322] pad: Use `window.` to avoid ESLint error --- src/static/js/pad.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index ccf29cc57..35f783db6 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -434,8 +434,7 @@ const pad = { padutils.setupGlobalExceptionHandler(); $(document).ready(() => { - // start the custom js - if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef + if (window.customStart != null) window.customStart(); handshake(); // To use etherpad you have to allow cookies. From 5cbbcbcee6899828f64e8333ee791dbfd3c44774 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 15:55:47 -0400 Subject: [PATCH 203/322] pad: Simplify reload after `.etherpad` import The old "switch to pad" logic looked buggy, and it complicates pad initialization. Forcing a refresh after importing an `.etherpad` file isn't much of a UX downgrade. --- CHANGELOG.md | 1 + doc/api/hooks_server-side.md | 6 ++-- src/node/handler/ImportHandler.js | 4 +-- src/node/handler/PadMessageHandler.js | 40 --------------------------- src/static/js/pad.js | 40 ++------------------------- src/static/js/pad_impexp.js | 2 +- 6 files changed, 10 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14114580a..a0f111d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ ### Notable enhancements +* Simplified pad reload after importing an `.etherpad` file. * For plugin authors: * `clientVars` was added to the context for the `postAceInit` client-side hook. Plugins should use this instead of the `clientVars` global variable. diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index c46da350c..2f948a790 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -579,9 +579,9 @@ Things in context: This hook allows plugins to grant temporary write access to a pad. It is called for each incoming message from a client. If write access is granted, it applies to the current message and all future messages from the same socket.io -connection until the next `CLIENT_READY` or `SWITCH_TO_PAD` message. Read-only -access is reset **after** each `CLIENT_READY` or `SWITCH_TO_PAD` message, so -granting write access has no effect for those message types. +connection until the next `CLIENT_READY` message. Read-only access is reset +**after** each `CLIENT_READY` message, so granting write access has no effect +for those message types. The handleMessageSecurity function must return a Promise. If the Promise resolves to `true`, write access is granted as described above. Returning diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index acaaf0927..7cc62e113 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -235,8 +235,8 @@ const doImport = async (req, res, padId) => { pad = await padManager.getPad(padId); padManager.unloadPad(padId); - // direct Database Access means a pad user should perform a switchToPad - // and not attempt to receive updated pad data + // Direct database access means a pad user should reload the pad and not attempt to receive + // updated pad data. if (directDatabaseAccess) return true; // tell clients to update diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 63ef45bae..ead00a3b5 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -283,8 +283,6 @@ exports.handleMessage = async (socket, message) => { } else { messageLogger.warn(`Dropped message, unknown COLLABROOM Data Type ${message.data.type}`); } - } else if (message.type === 'SWITCH_TO_PAD') { - await handleSwitchToPad(socket, message, authorID); } else { messageLogger.warn(`Dropped message, unknown Message Type ${message.type}`); } @@ -806,44 +804,6 @@ const _correctMarkersInPad = (atext, apool) => { return builder.toString(); }; -const handleSwitchToPad = async (socket, message, _authorID) => { - const currentSessionInfo = sessioninfos[socket.id]; - const padId = currentSessionInfo.padId; - - // Check permissions for the new pad. - const newPadIds = await readOnlyManager.getIds(message.padId); - const {session: {user} = {}} = socket.client.request; - const {accessStatus, authorID} = await securityManager.checkAccess( - newPadIds.padId, message.sessionID, message.token, user); - if (accessStatus !== 'grant') { - // Access denied. Send the reason to the user. - socket.json.send({accessStatus}); - return; - } - // The same token and session ID were passed to checkAccess in handleMessage, so this second call - // to checkAccess should return the same author ID. - assert(authorID === _authorID); - assert(authorID === currentSessionInfo.author); - - // Check if the connection dropped during the access check. - if (sessioninfos[socket.id] !== currentSessionInfo) return; - - // clear the session and leave the room - _getRoomSockets(padId).forEach((socket) => { - const sinfo = sessioninfos[socket.id]; - if (sinfo && sinfo.author === currentSessionInfo.author) { - // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[socket.id] = {}; - socket.leave(padId); - } - }); - - // start up the new pad - const newSessionInfo = sessioninfos[socket.id]; - createSessionInfoAuth(newSessionInfo, message); - await handleClientReady(socket, message, authorID); -}; - // Creates/replaces the auth object in the given session info. const createSessionInfoAuth = (sessionInfo, message) => { // Remember this information since we won't diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 35f783db6..20c79af55 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -48,8 +48,6 @@ const socketio = require('./socketio'); const hooks = require('./pluginfw/hooks'); -let receivedClientVars = false; - // This array represents all GET-parameters which can be used to change a setting. // name: the parameter-name, eg `?noColors=true` => `noColors` // checkVal: the callback is only executed when @@ -181,8 +179,7 @@ const getUrlVars = () => { return vars; }; -const sendClientReady = (isReconnect, messageType) => { - messageType = typeof messageType !== 'undefined' ? messageType : 'CLIENT_READY'; +const sendClientReady = (isReconnect) => { let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); // unescape neccesary due to Safari and Opera interpretation of spaces padId = decodeURIComponent(padId); @@ -201,7 +198,7 @@ const sendClientReady = (isReconnect, messageType) => { const msg = { component: 'pad', - type: messageType, + type: 'CLIENT_READY', padId, sessionID: Cookies.get('sessionID'), token, @@ -218,6 +215,7 @@ const sendClientReady = (isReconnect, messageType) => { }; const handshake = () => { + let receivedClientVars = false; let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); // unescape neccesary due to Safari and Opera interpretation of spaces padId = decodeURIComponent(padId); @@ -394,38 +392,6 @@ const pad = { getUserId: () => pad.myUserInfo.userId, getUserName: () => pad.myUserInfo.name, userList: () => paduserlist.users(), - switchToPad: (padId) => { - let newHref = new RegExp(/.*\/p\/[^/]+/).exec(document.location.pathname) || clientVars.padId; - newHref = newHref[0]; - - const options = clientVars.padOptions; - if (typeof options !== 'undefined' && options != null) { - const optionArr = []; - $.each(options, (k, v) => { - const str = `${k}=${v}`; - optionArr.push(str); - }); - const optionStr = optionArr.join('&'); - - newHref = `${newHref}?${optionStr}`; - } - - // destroy old pad from DOM - // See https://github.com/ether/etherpad-lite/pull/3915 - // TODO: Check if Destroying is enough and doesn't leave negative stuff - // See ace.js "editor.destroy" for a reference of how it was done before - $('#editorcontainer').find('iframe')[0].remove(); - - if (window.history && window.history.pushState) { - $('#chattext p').remove(); // clear the chat messages - window.history.pushState('', '', newHref); - receivedClientVars = false; - sendClientReady(false, 'SWITCH_TO_PAD'); - } else { - // fallback - window.location.href = newHref; - } - }, sendClientMessage: (msg) => { pad.collabClient.sendClientMessage(msg); }, diff --git a/src/static/js/pad_impexp.js b/src/static/js/pad_impexp.js index 8689b5b35..c85a791eb 100644 --- a/src/static/js/pad_impexp.js +++ b/src/static/js/pad_impexp.js @@ -67,7 +67,7 @@ const padimpexp = (() => { importErrorMessage(message); } else { $('#import_export').removeClass('popup-show'); - if (directDatabaseAccess) pad.switchToPad(clientVars.padId); + if (directDatabaseAccess) window.location.reload(); } $('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton')); window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0); From bd44a87388e0669503ce31d1f205a6f6bf966b99 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 17:21:04 -0400 Subject: [PATCH 204/322] pad: Unexport unnecessarily exported `handshake()` function --- src/static/js/pad.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 20c79af55..1aabc18a3 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -715,6 +715,5 @@ exports.settings = settings; exports.randomString = randomString; exports.getParams = getParams; exports.getUrlVars = getUrlVars; -exports.handshake = handshake; exports.pad = pad; exports.init = init; From f6c5ce606e4972e2dc8c8546dccb8e64b648fdf3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 16:04:23 -0400 Subject: [PATCH 205/322] pad: Move UI setup from `handshake()` to `init()` --- src/static/js/pad.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 1aabc18a3..92b38963e 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -366,12 +366,6 @@ const handshake = () => { pad.collabClient.handleMessageFromServer(obj); } }); - // Bind the colorpicker - $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); - // Bind the read only button - $('#readonlyinput').on('click', () => { - padeditbar.setEmbedLinks(); - }); }; const pad = { @@ -401,6 +395,9 @@ const pad = { $(document).ready(() => { if (window.customStart != null) window.customStart(); + $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); + $('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); }); + handshake(); // To use etherpad you have to allow cookies. From 9fb754ce3a6c26aa77ad6fd033cfe74d22964cd6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 16:09:31 -0400 Subject: [PATCH 206/322] pad: Initialize pad cookie before starting the handshake --- src/static/js/pad.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 92b38963e..0ac246a33 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -397,13 +397,8 @@ const pad = { if (window.customStart != null) window.customStart(); $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); $('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); }); - - handshake(); - - // To use etherpad you have to allow cookies. - // This will check if the prefs-cookie is set. - // Otherwise it shows up a message to the user. padcookie.init(); + handshake(); }); }, _afterHandshake() { From be0298290d753e3345ff4b8b6fa9f1f95490c637 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 16:32:11 -0400 Subject: [PATCH 207/322] pad: Move post-handshake code to `_afterHandshake()` --- src/static/js/pad.js | 99 ++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 0ac246a33..0dc4a4a54 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -303,55 +303,6 @@ const handshake = () => { // initialize the pad pad._afterHandshake(); - - if (clientVars.readonly) { - chat.hide(); - $('#myusernameedit').attr('disabled', true); - $('#chatinput').attr('disabled', true); - $('#chaticon').hide(); - $('#options-chatandusers').parent().hide(); - $('#options-stickychat').parent().hide(); - } else if (!settings.hideChat) { $('#chaticon').show(); } - - $('body').addClass(clientVars.readonly ? 'readonly' : 'readwrite'); - - padeditor.ace.callWithAce((ace) => { - ace.ace_setEditable(!clientVars.readonly); - }); - - // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers - if (settings.LineNumbersDisabled === true) { - pad.changeViewOption('showLineNumbers', false); - } - - // If the noColors value is set to true then we need to - // hide the background colors on the ace spans - if (settings.noColors === true) { - pad.changeViewOption('noColors', true); - } - - if (settings.rtlIsTrue === true) { - pad.changeViewOption('rtlIsTrue', true); - } - - // If the Monospacefont value is set to true then change it to monospace. - if (settings.useMonospaceFontGlobal === true) { - pad.changeViewOption('padFontFamily', 'RobotoMono'); - } - // if the globalUserName value is set we need to tell the server and - // the client about the new authorname - if (settings.globalUserName !== false) { - pad.notifyChangeName(settings.globalUserName); // Notifies the server - pad.myUserInfo.name = settings.globalUserName; - $('#myusernameedit').val(settings.globalUserName); // Updates the current users UI - } - if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) { - // Add a 'globalUserColor' property to myUserInfo, - // so collabClient knows we have a query parameter. - pad.myUserInfo.globalUserColor = settings.globalUserColor; - pad.notifyChangeColor(settings.globalUserColor); // Updates pad.myUserInfo.colorId - paduserlist.setMyUserInfo(pad.myUserInfo); - } } else if (obj.disconnect) { padconnectionstatus.disconnected(obj.disconnect); socket.disconnect(); @@ -488,7 +439,57 @@ const pad = { // there are no messages $('#chatloadmessagesbutton').css('display', 'none'); } + + if (window.clientVars.readonly) { + chat.hide(); + $('#myusernameedit').attr('disabled', true); + $('#chatinput').attr('disabled', true); + $('#chaticon').hide(); + $('#options-chatandusers').parent().hide(); + $('#options-stickychat').parent().hide(); + } else if (!settings.hideChat) { $('#chaticon').show(); } + + $('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite'); + + padeditor.ace.callWithAce((ace) => { + ace.ace_setEditable(!window.clientVars.readonly); + }); + + // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers + if (settings.LineNumbersDisabled === true) { + this.changeViewOption('showLineNumbers', false); + } + + // If the noColors value is set to true then we need to + // hide the background colors on the ace spans + if (settings.noColors === true) { + this.changeViewOption('noColors', true); + } + + if (settings.rtlIsTrue === true) { + this.changeViewOption('rtlIsTrue', true); + } + + // If the Monospacefont value is set to true then change it to monospace. + if (settings.useMonospaceFontGlobal === true) { + this.changeViewOption('padFontFamily', 'RobotoMono'); + } + // if the globalUserName value is set we need to tell the server and + // the client about the new authorname + if (settings.globalUserName !== false) { + this.notifyChangeName(settings.globalUserName); // Notifies the server + this.myUserInfo.name = settings.globalUserName; + $('#myusernameedit').val(settings.globalUserName); // Updates the current users UI + } + if (settings.globalUserColor !== false && colorutils.isCssHex(settings.globalUserColor)) { + // Add a 'globalUserColor' property to myUserInfo, + // so collabClient knows we have a query parameter. + this.myUserInfo.globalUserColor = settings.globalUserColor; + this.notifyChangeColor(settings.globalUserColor); // Updates this.myUserInfo.colorId + paduserlist.setMyUserInfo(this.myUserInfo); + } }, + dispose: () => { padeditor.dispose(); }, From cd4f5ff281539f339fc31e1373d44696567a8bb5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 29 Oct 2021 18:54:52 -0400 Subject: [PATCH 208/322] pad: Defer message handling until handshake completes --- src/static/js/pad.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 0dc4a4a54..d91929bb6 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -314,11 +314,33 @@ const handshake = () => { return; } else { - pad.collabClient.handleMessageFromServer(obj); + pad._messageQ.enqueue(obj); } }); }; +/** Defers message handling until setCollabClient() is called with a non-null value. */ +class MessageQueue { + constructor() { + this._q = []; + this._cc = null; + } + + setCollabClient(cc) { + this._cc = cc; + this.enqueue(); // Flush. + } + + enqueue(...msgs) { + if (this._cc == null) { + this._q.push(...msgs); + } else { + while (this._q.length > 0) this._cc.handleMessageFromServer(this._q.shift()); + for (const msg of msgs) this._cc.handleMessageFromServer(msg); + } + } +} + const pad = { // don't access these directly from outside this file, except // for debugging @@ -328,6 +350,7 @@ const pad = { initTime: 0, clientTimeOffset: null, padOptions: {}, + _messageQ: new MessageQueue(), // these don't require init; clientVars should all go through here getPadId: () => clientVars.padId, @@ -423,6 +446,7 @@ const pad = { pad.collabClient = getCollabClient( padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, {colorPalette: pad.getColorPalette()}, pad); + this._messageQ.setCollabClient(this.collabClient); pad.collabClient.setOnUserJoin(pad.handleUserJoin); pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate); pad.collabClient.setOnUserLeave(pad.handleUserLeave); From 80e84636d7a87d2ea87100847040fbb2e6af758e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 17:15:42 -0400 Subject: [PATCH 209/322] pad: Promisify `handshake()` --- src/static/js/pad.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index d91929bb6..e1c3186fb 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -214,7 +214,7 @@ const sendClientReady = (isReconnect) => { socket.json.send(msg); }; -const handshake = () => { +const handshake = async () => { let receivedClientVars = false; let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); // unescape neccesary due to Safari and Opera interpretation of spaces @@ -295,14 +295,8 @@ const handshake = () => { } } } else if (!receivedClientVars && obj.type === 'CLIENT_VARS') { - // if we haven't recieved the clientVars yet, then this message should it be receivedClientVars = true; - - // set some client vars window.clientVars = obj.data; - - // initialize the pad - pad._afterHandshake(); } else if (obj.disconnect) { padconnectionstatus.disconnected(obj.disconnect); socket.disconnect(); @@ -317,6 +311,15 @@ const handshake = () => { pad._messageQ.enqueue(obj); } }); + + await new Promise((resolve) => { + const h = (obj) => { + if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return; + socket.off('message', h); + resolve(); + }; + socket.on('message', h); + }); }; /** Defers message handling until setCollabClient() is called with a non-null value. */ @@ -364,16 +367,20 @@ const pad = { pad.collabClient.sendClientMessage(msg); }, - init: () => { + init() { padutils.setupGlobalExceptionHandler(); - $(document).ready(() => { + // $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is + // an async function for some bizarre reason, so the async function is wrapped in a non-async + // function. + $(() => (async () => { if (window.customStart != null) window.customStart(); $('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220}); $('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); }); padcookie.init(); - handshake(); - }); + await handshake(); + this._afterHandshake(); + })()); }, _afterHandshake() { pad.clientTimeOffset = Date.now() - clientVars.serverTimestamp; From f4f3aa6aae29c573c77227346c8a89c2e301b1d2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 29 Oct 2021 23:09:27 -0400 Subject: [PATCH 210/322] docs: Improve `userLeave` and `clientReady` hook documentation --- doc/api/hooks_server-side.md | 47 ++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 2f948a790..1a3f80833 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -807,36 +807,51 @@ Example: exports.exportEtherpadAdditionalContent = () => ['comments']; ``` -## userLeave -Called from src/node/handler/PadMessageHandler.js +## `userLeave` -This in context: +Called from: `src/node/handler/PadMessageHandler.js` -1. session (including the pad id and author id) +Called when a user disconnects from a pad. This is useful if you want to perform +certain actions after a pad has been edited. -This hook gets called when an author leaves a pad. This is useful if you want to perform certain actions after a pad has been edited +Context properties: + +* `auth`: Object containing information used for authentication, provided by the + user. Properties: + * `padID`: Pad identifier requested by the user. Unlike the `padId` property + described below, this might be a read-only pad ID. + * `sessionID`: Copied from the client's `sessionID` cookie, which should be + the value returned from the `createSession()` HTTP API. This will be nullish + if `createSession()` isn't used or the portal doesn't set the `sessionID` + cookie. + * `token`: User-supplied token. +* `author`: The user's author ID. +* `padId`: The pad's real (not read-only) identifier. +* `readOnlyPadId`: The pad's read-only identifier. +* `readonly`: If truthy, the user only has read-only access. +* `rev`: The last revision that was sent to the client. Example: -``` -exports.userLeave = function(hook, session, callback) { - console.log('%s left pad %s', session.author, session.padId); +```javascript +exports.userLeave = (hookName, {author, padId}) => { + console.log(`${author} left pad ${padId}`); }; ``` -### clientReady -Called from src/node/handler/PadMessageHandler.js +## `clientReady` -This in context: +Called from: `src/node/handler/PadMessageHandler.js` -1. message +Called when a `CLIENT_READY` message is received, which is the first message a +newly connected client sends. -This hook gets called when handling a CLIENT_READY which is the first message from the client to the server. +The context is the raw message received from the user. Example: -``` -exports.clientReady = function(hook, message) { - console.log('Client has entered the pad' + message.padId); +```javascript +exports.clientReady = (hookName, {padId}) => { + console.log(`Client has joined pad ${padId}); }; ``` From a3b4d985ac46a84fcdfaf545022983516cda60c8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 29 Oct 2021 22:47:56 -0400 Subject: [PATCH 211/322] lint: Fix awkward string formatting --- src/node/handler/PadMessageHandler.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index ead00a3b5..e4631359b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -134,12 +134,14 @@ exports.handleDisconnect = async (socket) => { // send a disconnect message to the others if (session && session.author) { const {session: {user} = {}} = socket.client.request; - accessLogger.info(`${'[LEAVE]' + + /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ + accessLogger.info('[LEAVE]' + ` pad:${session.padId}` + ` socket:${socket.id}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + - ` authorID:${session.author}`}${ - (user && user.username) ? ` username:${user.username}` : ''}`); + ` authorID:${session.author}` + + (user && user.username ? ` username:${user.username}` : '')); + /* eslint-enable prefer-template */ // get the author color out of the db const color = await authorManager.getAuthorColorId(session.author); @@ -906,12 +908,14 @@ const handleClientReady = async (socket, message, authorID) => { padIds.readonly || !webaccess.userCanModify(message.padId, socket.client.request); const {session: {user} = {}} = socket.client.request; - accessLogger.info(`${`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + + /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ + accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + ` pad:${padIds.padId}` + ` socket:${socket.id}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + - ` authorID:${authorID}`}${ - (user && user.username) ? ` username:${user.username}` : ''}`); + ` authorID:${authorID}` + + (user && user.username ? ` username:${user.username}` : '')); + /* eslint-enable prefer-template */ if (message.reconnect) { // If this is a reconnect, we don't have to send the client the ClientVars again From 7522d76c40eeb473178d99c6dfa521f2dfec968f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 29 Oct 2021 22:49:11 -0400 Subject: [PATCH 212/322] PadMessageHandler: Invert condition to improve readability --- src/node/handler/PadMessageHandler.js | 70 ++++++++++++--------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index e4631359b..6f921d0b6 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -126,47 +126,39 @@ exports.kickSessionsFromPad = (padID) => { */ exports.handleDisconnect = async (socket) => { stats.meter('disconnects').mark(); - - // save the padname of this session const session = sessioninfos[socket.id]; - - // if this connection was already etablished with a handshake, - // send a disconnect message to the others - if (session && session.author) { - const {session: {user} = {}} = socket.client.request; - /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ - accessLogger.info('[LEAVE]' + - ` pad:${session.padId}` + - ` socket:${socket.id}` + - ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + - ` authorID:${session.author}` + - (user && user.username ? ` username:${user.username}` : '')); - /* eslint-enable prefer-template */ - - // get the author color out of the db - const color = await authorManager.getAuthorColorId(session.author); - - // prepare the notification for the other users on the pad, that this user left - const messageToTheOtherUsers = { - type: 'COLLABROOM', - data: { - type: 'USER_LEAVE', - userInfo: { - colorId: color, - userId: session.author, - }, - }, - }; - - // Go through all user that are still on the pad, and send them the USER_LEAVE message - socket.broadcast.to(session.padId).json.send(messageToTheOtherUsers); - - // Allow plugins to hook into users leaving the pad - hooks.callAll('userLeave', session); - } - - // Delete the sessioninfos entrys of this session delete sessioninfos[socket.id]; + if (!session || !session.author) return; + const {session: {user} = {}} = socket.client.request; + /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ + accessLogger.info('[LEAVE]' + + ` pad:${session.padId}` + + ` socket:${socket.id}` + + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + + ` authorID:${session.author}` + + (user && user.username ? ` username:${user.username}` : '')); + /* eslint-enable prefer-template */ + + // get the author color out of the db + const color = await authorManager.getAuthorColorId(session.author); + + // prepare the notification for the other users on the pad, that this user left + const messageToTheOtherUsers = { + type: 'COLLABROOM', + data: { + type: 'USER_LEAVE', + userInfo: { + colorId: color, + userId: session.author, + }, + }, + }; + + // Go through all user that are still on the pad, and send them the USER_LEAVE message + socket.broadcast.to(session.padId).json.send(messageToTheOtherUsers); + + // Allow plugins to hook into users leaving the pad + hooks.callAll('userLeave', session); }; /** From f2a118b311dd84312f0ba0ebea2c994e4c21d1cc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 29 Oct 2021 22:50:42 -0400 Subject: [PATCH 213/322] PadMessageHandler: Inline unnecessary variables Also delete some unneccessary comments. --- src/node/handler/PadMessageHandler.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 6f921d0b6..2385a825e 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -138,26 +138,16 @@ exports.handleDisconnect = async (socket) => { ` authorID:${session.author}` + (user && user.username ? ` username:${user.username}` : '')); /* eslint-enable prefer-template */ - - // get the author color out of the db - const color = await authorManager.getAuthorColorId(session.author); - - // prepare the notification for the other users on the pad, that this user left - const messageToTheOtherUsers = { + socket.broadcast.to(session.padId).json.send({ type: 'COLLABROOM', data: { type: 'USER_LEAVE', userInfo: { - colorId: color, + colorId: await authorManager.getAuthorColorId(session.author), userId: session.author, }, }, - }; - - // Go through all user that are still on the pad, and send them the USER_LEAVE message - socket.broadcast.to(session.padId).json.send(messageToTheOtherUsers); - - // Allow plugins to hook into users leaving the pad + }); hooks.callAll('userLeave', session); }; @@ -849,9 +839,7 @@ const handleClientReady = async (socket, message, authorID) => { // get all authordata of this new user assert(authorID); - const value = await authorManager.getAuthor(authorID); - const authorColorId = value.colorId; - const authorName = value.name; + const {colorId: authorColorId, name: authorName} = await authorManager.getAuthor(authorID); // load the pad-object from the database const pad = await padManager.getPad(padIds.padId); From 5d30e0b1b2a3af8df1d6ee0296f0462de1af3dd1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 29 Oct 2021 22:52:17 -0400 Subject: [PATCH 214/322] PadMessageHandler: Run the `userLeave` hook asynchronously --- doc/api/hooks_server-side.md | 2 +- src/node/handler/PadMessageHandler.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 1a3f80833..25c02aae5 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -834,7 +834,7 @@ Context properties: Example: ```javascript -exports.userLeave = (hookName, {author, padId}) => { +exports.userLeave = async (hookName, {author, padId}) => { console.log(`${author} left pad ${padId}`); }; ``` diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 2385a825e..d7c4157b2 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -148,7 +148,7 @@ exports.handleDisconnect = async (socket) => { }, }, }); - hooks.callAll('userLeave', session); + await hooks.aCallAll('userLeave', session); }; /** From fa54dc10538e6d62a2973c62ae9327f9cb4d275e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 00:18:56 -0400 Subject: [PATCH 215/322] PadMessageHandler: Run the `clientReady` hook asynchronously --- doc/api/hooks_server-side.md | 2 +- src/node/handler/PadMessageHandler.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 25c02aae5..14b7d574a 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -851,7 +851,7 @@ The context is the raw message received from the user. Example: ```javascript -exports.clientReady = (hookName, {padId}) => { +exports.clientReady = async (hookName, {padId}) => { console.log(`Client has joined pad ${padId}); }; ``` diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index d7c4157b2..8460cd545 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -832,7 +832,7 @@ const handleClientReady = async (socket, message, authorID) => { return; } - hooks.callAll('clientReady', message); + await hooks.aCallAll('clientReady', message); // Get ro/rw id:s const padIds = await readOnlyManager.getIds(message.padId); From ce730b04936d61306a15f400c9fef63907beee3f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 00:27:09 -0400 Subject: [PATCH 216/322] PadMessageHandler: Inline `createSessionInfoAuth()` This function is only used once so it doesn't need to be separate. --- src/node/handler/PadMessageHandler.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 8460cd545..5f7279c86 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -187,8 +187,14 @@ exports.handleMessage = async (socket, message) => { } if (message.type === 'CLIENT_READY') { - // client tried to auth for the first time (first msg from the client) - createSessionInfoAuth(thisSession, message); + // Remember this information since we won't have the cookie in further socket.io messages. This + // information will be used to check if the sessionId of this connection is still valid since it + // could have been deleted by the API. + thisSession.auth = { + sessionID: message.sessionID, + padID: message.padId, + token: message.token, + }; } const auth = thisSession.auth; @@ -788,20 +794,6 @@ const _correctMarkersInPad = (atext, apool) => { return builder.toString(); }; -// Creates/replaces the auth object in the given session info. -const createSessionInfoAuth = (sessionInfo, message) => { - // Remember this information since we won't - // have the cookie in further socket.io messages. - // This information will be used to check if - // the sessionId of this connection is still valid - // since it could have been deleted by the API. - sessionInfo.auth = { - sessionID: message.sessionID, - padID: message.padId, - token: message.token, - }; -}; - /** * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client * to the server. The Client sends his token From d36a37d6666bb99562fa2f6b812ee6ab12ffdca0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 00:34:15 -0400 Subject: [PATCH 217/322] PadMessageHandler: Delete unnecessary `protocolVersion` We can assume that the client code is always in sync with what the server expects. --- src/node/handler/PadMessageHandler.js | 11 ----------- src/node/handler/SocketIORouter.js | 4 ---- src/static/js/pad.js | 1 - src/static/js/timeslider.js | 1 - src/tests/backend/specs/socketio.js | 3 --- 5 files changed, 20 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 5f7279c86..61b9733b1 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -813,17 +813,6 @@ const handleClientReady = async (socket, message, authorID) => { return; } - if (!message.protocolVersion) { - messageLogger.warn('Dropped message, CLIENT_READY Message has no protocolVersion!'); - return; - } - - if (message.protocolVersion !== 2) { - messageLogger.warn('Dropped message, CLIENT_READY Message has a unknown protocolVersion ' + - `'${message.protocolVersion}'!`); - return; - } - await hooks.aCallAll('clientReady', message); // Get ro/rw id:s diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 0fa18755e..53bb6d241 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -69,10 +69,6 @@ exports.setSocketIO = (_io) => { } socket.on('message', (message, ack = () => {}) => { - if (message.protocolVersion && message.protocolVersion !== 2) { - logger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`); - return; - } if (!message.component || !components[message.component]) { logger.error(`Can't route the message: ${JSON.stringify(message)}`); return; diff --git a/src/static/js/pad.js b/src/static/js/pad.js index e1c3186fb..66e46391e 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -202,7 +202,6 @@ const sendClientReady = (isReconnect) => { padId, sessionID: Cookies.get('sessionID'), token, - protocolVersion: 2, }; // this is a reconnect, lets tell the server our revisionnumber diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 2b226aef0..7268f95f0 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -102,7 +102,6 @@ const sendSocketMsg = (type, data) => { padId, token, sessionID: Cookies.get('sessionID'), - protocolVersion: 2, }); }; diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 30ef82748..f10f63004 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -86,7 +86,6 @@ const handshake = async (socket, padID) => { padId: padID, sessionID: null, token: 't.12345', - protocolVersion: 2, }); logger.debug('waiting for CLIENT_VARS response...'); const msg = await getSocketEvent(socket, 'message'); @@ -464,7 +463,6 @@ describe(__filename, function () { let serverSocket; const want = { component: this.test.fullTitle(), - protocolVersion: 2, foo: {bar: 'asdf'}, }; let rx; @@ -482,7 +480,6 @@ describe(__filename, function () { }); const tx = async (socket, message = {}) => await new Promise((resolve, reject) => { - message = Object.assign({protocolVersion: 2}, message); const AckErr = class extends Error { constructor(name, ...args) { super(...args); this.name = name; } }; From 0992f195707b710906becf90003ce315448a8f34 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 00:46:05 -0400 Subject: [PATCH 218/322] PadMessageHandler: Improve readability of historical author fetch --- src/node/handler/PadMessageHandler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 61b9733b1..05b9e4464 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -833,7 +833,8 @@ const handleClientReady = async (socket, message, authorID) => { // get all author data out of the database (in parallel) const historicalAuthorData = {}; - await Promise.all(authors.map((authorId) => authorManager.getAuthor(authorId).then((author) => { + await Promise.all(authors.map(async (authorId) => { + const author = await authorManager.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'); @@ -841,7 +842,7 @@ const handleClientReady = async (socket, message, authorID) => { // Filter author attribs (e.g. don't send author's pads to all clients) historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; } - }))); + })); // glue the clientVars together, send them and tell the other clients that a new one is there From 10e930408c7c20495ce7d2ce21c683eb8445c307 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 00:48:52 -0400 Subject: [PATCH 219/322] PadMessageHandler: Delete unnecessary CLIENT_READY checks The checks are already performed by the security manager. --- src/node/handler/PadMessageHandler.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 05b9e4464..0acfa7cbb 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -802,17 +802,6 @@ const _correctMarkersInPad = (atext, apool) => { * @param message the message from the client */ const handleClientReady = async (socket, message, authorID) => { - // check if all ok - if (!message.token) { - messageLogger.warn('Dropped message, CLIENT_READY Message has no token!'); - return; - } - - if (!message.padId) { - messageLogger.warn('Dropped message, CLIENT_READY Message has no padId!'); - return; - } - await hooks.aCallAll('clientReady', message); // Get ro/rw id:s From 50b9e0df1f4cc27137cd448a5c31ec7f106f8f63 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 01:04:43 -0400 Subject: [PATCH 220/322] PadMessageHandler: Use values from session info object This is more consistent with the rest of the code, and it provides a single source of truth. --- src/node/handler/PadMessageHandler.js | 48 +++++++++++++++------------ 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 0acfa7cbb..c69403670 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -64,7 +64,7 @@ exports.socketio = () => { * - token: User-supplied token. * - author: The user's author ID. * - padId: The real (not read-only) ID of the pad. - * - readonlyPadId: The read-only ID of the pad. + * - readOnlyPadId: The read-only ID of the pad. * - readonly: Whether the client has read-only access (true) or read/write access (false). * - rev: The last revision that was sent to the client. */ @@ -249,7 +249,7 @@ exports.handleMessage = async (socket, message) => { // Check what type of message we get and delegate to the other methods if (message.type === 'CLIENT_READY') { - await handleClientReady(socket, message, authorID); + await handleClientReady(socket, message); } else if (message.type === 'CHANGESET_REQ') { await handleChangesetRequest(socket, message); } else if (message.type === 'COLLABROOM') { @@ -801,15 +801,20 @@ const _correctMarkersInPad = (atext, apool) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleClientReady = async (socket, message, authorID) => { +const handleClientReady = async (socket, message) => { + const sessionInfo = sessioninfos[socket.id]; + // Check if the user has already disconnected. + if (sessionInfo == null) return; + await hooks.aCallAll('clientReady', message); // Get ro/rw id:s - const padIds = await readOnlyManager.getIds(message.padId); + const padIds = await readOnlyManager.getIds(sessionInfo.auth.padID); // get all authordata of this new user - assert(authorID); - const {colorId: authorColorId, name: authorName} = await authorManager.getAuthor(authorID); + assert(sessionInfo.author); + const {colorId: authorColorId, name: authorName} = + await authorManager.getAuthor(sessionInfo.author); // load the pad-object from the database const pad = await padManager.getPad(padIds.padId); @@ -844,7 +849,7 @@ const handleClientReady = async (socket, message, authorID) => { for (const socket of roomSockets) { const sinfo = sessioninfos[socket.id]; - if (sinfo && sinfo.author === authorID) { + if (sinfo && sinfo.author === sessionInfo.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins sessioninfos[socket.id] = {}; socket.leave(padIds.padId); @@ -856,22 +861,22 @@ const handleClientReady = async (socket, message, authorID) => { sessionInfo.padId = padIds.padId; sessionInfo.readOnlyPadId = padIds.readOnlyPadId; sessionInfo.readonly = - padIds.readonly || !webaccess.userCanModify(message.padId, socket.client.request); + padIds.readonly || !webaccess.userCanModify(sessionInfo.auth.padID, socket.client.request); const {session: {user} = {}} = socket.client.request; /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + - ` pad:${padIds.padId}` + + ` pad:${sessionInfo.padId}` + ` socket:${socket.id}` + ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + - ` authorID:${authorID}` + + ` authorID:${sessionInfo.author}` + (user && user.username ? ` username:${user.username}` : '')); /* eslint-enable prefer-template */ if (message.reconnect) { // If this is a reconnect, we don't have to send the client the ClientVars again // Join the pad and start receiving updates - socket.join(padIds.padId); + socket.join(sessionInfo.padId); // Save the revision in sessioninfos, we take the revision from the info the client send to us sessionInfo.rev = message.client_rev; @@ -948,9 +953,8 @@ const handleClientReady = async (socket, message, authorID) => { return; } - // Warning: never ever send padIds.padId to the client. If the - // client is read only you would open a security hole 1 swedish - // mile wide... + // 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 = { skinName: settings.skinName, skinVariants: settings.skinVariants, @@ -965,7 +969,7 @@ const handleClientReady = async (socket, message, authorID) => { collab_client_vars: { initialAttributedText: atext, clientIp: '127.0.0.1', - padId: message.padId, + padId: sessionInfo.auth.padID, historicalAuthorData, apool, rev: pad.getHeadRevisionNumber(), @@ -974,19 +978,19 @@ const handleClientReady = async (socket, message, authorID) => { colorPalette: authorManager.getColorPalette(), clientIp: '127.0.0.1', userColor: authorColorId, - padId: message.padId, + padId: sessionInfo.auth.padID, padOptions: settings.padOptions, padShortcutEnabled: settings.padShortcutEnabled, - initialTitle: `Pad: ${message.padId}`, + initialTitle: `Pad: ${sessionInfo.auth.padID}`, opts: {}, // tell the client the number of the latest chat-message, which will be // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) chatHead: pad.chatHead, numConnectedUsers: roomSockets.length, - readOnlyId: padIds.readOnlyPadId, + readOnlyId: sessionInfo.readOnlyPadId, readonly: sessionInfo.readonly, serverTimestamp: Date.now(), - userId: authorID, + userId: sessionInfo.author, abiwordAvailable: settings.abiwordAvailable(), sofficeAvailable: settings.sofficeAvailable(), exportAvailable: settings.exportAvailable(), @@ -1025,7 +1029,7 @@ const handleClientReady = async (socket, message, authorID) => { } // Join the pad and start receiving updates - socket.join(padIds.padId); + socket.join(sessionInfo.padId); // Send the clientVars to the Client socket.json.send({type: 'CLIENT_VARS', data: clientVars}); @@ -1035,14 +1039,14 @@ const handleClientReady = async (socket, message, authorID) => { } // Notify other users about this new user. - socket.broadcast.to(padIds.padId).json.send({ + socket.broadcast.to(sessionInfo.padId).json.send({ type: 'COLLABROOM', data: { type: 'USER_NEWINFO', userInfo: { colorId: authorColorId, name: authorName, - userId: authorID, + userId: sessionInfo.author, }, }, }); From 00e7b04518aba95aac918c0419c8fb31fd745b29 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 01:16:17 -0400 Subject: [PATCH 221/322] PadMessageHandler: Improve readability of changeset loading --- src/node/handler/PadMessageHandler.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index c69403670..a005eb922 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -905,15 +905,14 @@ const handleClientReady = async (socket, message) => { changesets[r] = {}; } - // get changesets, author and timestamp needed for pending revisions (in parallel) - const promises = []; - for (const revNum of revisionsNeeded) { + await Promise.all(revisionsNeeded.map(async (revNum) => { const cs = changesets[revNum]; - promises.push(pad.getRevisionChangeset(revNum).then((result) => cs.changeset = result)); - promises.push(pad.getRevisionAuthor(revNum).then((result) => cs.author = result)); - promises.push(pad.getRevisionDate(revNum).then((result) => cs.timestamp = result)); - } - await Promise.all(promises); + [cs.changeset, cs.author, cs.timestamp] = await Promise.all([ + pad.getRevisionChangeset(revNum), + pad.getRevisionAuthor(revNum), + pad.getRevisionDate(revNum), + ]); + })); // return pending changesets for (const r of revisionsNeeded) { From b7de24c85f161e6c01c55fe7e0735a1548291740 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 01:31:17 -0400 Subject: [PATCH 222/322] PadMessageHandler: Fix readability of duplicate user check --- src/node/handler/PadMessageHandler.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index a005eb922..38c359a39 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -847,13 +847,15 @@ const handleClientReady = async (socket, message) => { // Check if this author is already on the pad, if yes, kick the other sessions! const roomSockets = _getRoomSockets(pad.id); - for (const socket of roomSockets) { - const sinfo = sessioninfos[socket.id]; + for (const otherSocket of roomSockets) { + // The user shouldn't have joined the room yet, but check anyway just in case. + if (otherSocket.id === socket.id) continue; + const sinfo = sessioninfos[otherSocket.id]; if (sinfo && sinfo.author === sessionInfo.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[socket.id] = {}; - socket.leave(padIds.padId); - socket.json.send({disconnect: 'userdup'}); + sessioninfos[otherSocket.id] = {}; + otherSocket.leave(padIds.padId); + otherSocket.json.send({disconnect: 'userdup'}); } } From c98910e1c5e9f221bc08b6facc2ec66124d3809d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 01:40:20 -0400 Subject: [PATCH 223/322] PadMessageHandler: Populate session info as early as possible --- src/node/handler/PadMessageHandler.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 38c359a39..da8e97177 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -806,10 +806,13 @@ const handleClientReady = async (socket, message) => { // Check if the user has already disconnected. if (sessionInfo == null) return; - await hooks.aCallAll('clientReady', message); - - // Get ro/rw id:s const padIds = await readOnlyManager.getIds(sessionInfo.auth.padID); + sessionInfo.padId = padIds.padId; + sessionInfo.readOnlyPadId = padIds.readOnlyPadId; + sessionInfo.readonly = + padIds.readonly || !webaccess.userCanModify(sessionInfo.auth.padID, socket.client.request); + + await hooks.aCallAll('clientReady', message); // get all authordata of this new user assert(sessionInfo.author); @@ -817,7 +820,7 @@ const handleClientReady = async (socket, message) => { await authorManager.getAuthor(sessionInfo.author); // load the pad-object from the database - const pad = await padManager.getPad(padIds.padId); + const pad = await padManager.getPad(sessionInfo.padId); // these db requests all need the pad object (timestamp of latest revision, author data) const authors = pad.getAllAuthors(); @@ -840,9 +843,8 @@ const handleClientReady = async (socket, message) => { // glue the clientVars together, send them and tell the other clients that a new one is there - // Check that the client is still here. It might have disconnected between callbacks. - const sessionInfo = sessioninfos[socket.id]; - if (sessionInfo == null) return; + // Check if the user has disconnected during any of the above awaits. + if (sessionInfo !== sessioninfos[socket.id]) return; // Check if this author is already on the pad, if yes, kick the other sessions! const roomSockets = _getRoomSockets(pad.id); @@ -854,17 +856,11 @@ const handleClientReady = async (socket, message) => { if (sinfo && sinfo.author === sessionInfo.author) { // fix user's counter, works on page refresh or if user closes browser window and then rejoins sessioninfos[otherSocket.id] = {}; - otherSocket.leave(padIds.padId); + otherSocket.leave(sessionInfo.padId); otherSocket.json.send({disconnect: 'userdup'}); } } - // Save in sessioninfos that this session belonges to this pad - sessionInfo.padId = padIds.padId; - sessionInfo.readOnlyPadId = padIds.readOnlyPadId; - sessionInfo.readonly = - padIds.readonly || !webaccess.userCanModify(sessionInfo.auth.padID, socket.client.request); - const {session: {user} = {}} = socket.client.request; /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + From a6d060d67b1e0863a53c0600d55c78738529588a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 02:05:09 -0400 Subject: [PATCH 224/322] PadMessageHandler: Replace `clientReady` hook with new `userJoin` hook --- CHANGELOG.md | 3 ++ doc/api/hooks_server-side.md | 41 ++++++++++++++++----------- src/node/handler/PadMessageHandler.js | 13 ++++++++- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f111d4e..9d328bc0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ different. See [citizenos/ep_image_upload#49](https://github.com/citizenos/ep_image_upload/pull/49) for an example fix. + * The `clientReady` server-side hook is deprecated; use the new `userJoin` + hook instead. ### Notable enhancements @@ -38,6 +40,7 @@ * For plugin authors: * `clientVars` was added to the context for the `postAceInit` client-side hook. Plugins should use this instead of the `clientVars` global variable. + * New `userJoin` server-side hook. # 1.8.14 diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 14b7d574a..8f22478e4 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -807,6 +807,30 @@ Example: exports.exportEtherpadAdditionalContent = () => ['comments']; ``` +## `userJoin` + +Called from: `src/node/handler/PadMessageHandler.js` + +Called after users have been notified that a new user has joined the pad. + +Context properties: + +* `authorId`: The user's author identifier. +* `displayName`: The user's display name. +* `padId`: The real (not read-only) identifier of the pad the user joined. This + MUST NOT be shared with any users that are connected with read-only access. +* `readOnly`: Whether the user only has read-only access. +* `readOnlyPadId`: The read-only identifier of the pad the user joined. +* `socket`: The socket.io Socket object. + +Example: + +```javascript +exports.userJoin = async (hookName, {authorId, displayName, padId}) => { + console.log(`${authorId} (${displayName}) joined pad ${padId}); +}; +``` + ## `userLeave` Called from: `src/node/handler/PadMessageHandler.js` @@ -838,20 +862,3 @@ exports.userLeave = async (hookName, {author, padId}) => { console.log(`${author} left pad ${padId}`); }; ``` - -## `clientReady` - -Called from: `src/node/handler/PadMessageHandler.js` - -Called when a `CLIENT_READY` message is received, which is the first message a -newly connected client sends. - -The context is the raw message received from the user. - -Example: - -```javascript -exports.clientReady = async (hookName, {padId}) => { - console.log(`Client has joined pad ${padId}); -}; -``` diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index da8e97177..65ba11803 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -43,6 +43,8 @@ const webaccess = require('../hooks/express/webaccess'); let rateLimiter; let socketio = null; +hooks.deprecationNotices.clientReady = 'use the userJoin hook instead'; + exports.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 @@ -812,7 +814,7 @@ const handleClientReady = async (socket, message) => { sessionInfo.readonly = padIds.readonly || !webaccess.userCanModify(sessionInfo.auth.padID, socket.client.request); - await hooks.aCallAll('clientReady', message); + await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context. // get all authordata of this new user assert(sessionInfo.author); @@ -1088,6 +1090,15 @@ const handleClientReady = async (socket, message) => { socket.json.send(msg); })); + + await hooks.aCallAll('userJoin', { + authorId: sessionInfo.author, + displayName: authorName, + padId: sessionInfo.padId, + readOnly: sessionInfo.readonly, + readOnlyPadId: sessionInfo.readOnlyPadId, + socket, + }); }; /** From 9aaf78154818e93af5ade905202048dc8b6bdb0a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 02:22:15 -0400 Subject: [PATCH 225/322] PadMessageHandler: Modernize `userLeave` hook context properties --- CHANGELOG.md | 6 ++++++ doc/api/hooks_server-side.md | 15 +++------------ src/node/handler/PadMessageHandler.js | 7 ++++++- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d328bc0d..ca7f4662f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ for an example fix. * The `clientReady` server-side hook is deprecated; use the new `userJoin` hook instead. + * The `userLeave` server-side hook's context properties have changed: + * `auth`: Deprecated. + * `author`: Deprecated; use the new `authorId` property instead. + * `readonly`: Deprecated; use the new `readOnly` property instead. + * `rev`: Deprecated. ### Notable enhancements @@ -41,6 +46,7 @@ * `clientVars` was added to the context for the `postAceInit` client-side hook. Plugins should use this instead of the `clientVars` global variable. * New `userJoin` server-side hook. + * The `userLeave` server-side hook has a new `socket` context property. # 1.8.14 diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 8f22478e4..4836c4b73 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -840,20 +840,11 @@ certain actions after a pad has been edited. Context properties: -* `auth`: Object containing information used for authentication, provided by the - user. Properties: - * `padID`: Pad identifier requested by the user. Unlike the `padId` property - described below, this might be a read-only pad ID. - * `sessionID`: Copied from the client's `sessionID` cookie, which should be - the value returned from the `createSession()` HTTP API. This will be nullish - if `createSession()` isn't used or the portal doesn't set the `sessionID` - cookie. - * `token`: User-supplied token. -* `author`: The user's author ID. +* `authorId`: The user's author ID. * `padId`: The pad's real (not read-only) identifier. +* `readOnly`: If truthy, the user only has read-only access. * `readOnlyPadId`: The pad's read-only identifier. -* `readonly`: If truthy, the user only has read-only access. -* `rev`: The last revision that was sent to the client. +* `socket`: The socket.io Socket object. Example: diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 65ba11803..e38d77160 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -150,7 +150,12 @@ exports.handleDisconnect = async (socket) => { }, }, }); - await hooks.aCallAll('userLeave', session); + await hooks.aCallAll('userLeave', { + ...session, // For backwards compatibility. + authorId: session.author, + readOnly: session.readonly, + socket, + }); }; /** From 9987834b15332badc4c6d7b547bcf8e9cc535bb8 Mon Sep 17 00:00:00 2001 From: Peter VandeHaar Date: Wed, 20 Oct 2021 17:34:16 -0400 Subject: [PATCH 226/322] Clarify instructions in PULL_REQUEST_TEMPLATE.md This change - removes instructions about commit headers that nobody follows, - links to useful resources for first-time contributors, - simplifies some text, and - hides all text inside . --- .github/PULL_REQUEST_TEMPLATE.md | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d745034b4..d9f660907 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,29 +1,13 @@ - -> Please provide enough information so that others can review your pull request: - - - -> Explain the **details** for making this change. What existing problem does the pull request solve? - - - -> Screenshots/GIFs - - From c8e544ec8dccafc564c6b5de22be38fa38e97643 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 27 Oct 2021 16:12:35 -0400 Subject: [PATCH 227/322] tests: Fix handling of nullish module definitions --- src/tests/frontend/runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/frontend/runner.js b/src/tests/frontend/runner.js index 48a473aa7..3e512d4d1 100644 --- a/src/tests/frontend/runner.js +++ b/src/tests/frontend/runner.js @@ -214,7 +214,7 @@ $(() => (async () => { const [origDefs] = args; const defs = {}; for (const [path, origDef] of Object.entries(origDefs)) { - defs[path] = function (require, exports, module) { + defs[path] = origDef == null ? origDef : function (require, exports, module) { const calls = []; mochaCalls.set(module.id.replace(/\.js$/, ''), calls); // Backup Mocha functions. Note that because modules can require other modules, these From e28c9ffc9763e8715a318322959cf6f6e9cd01a5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 17:36:41 -0400 Subject: [PATCH 228/322] tests: Support injecting hook functions during pad load --- CHANGELOG.md | 3 +++ src/templates/pad.html | 4 ++++ src/tests/frontend/helper.js | 22 +++++++++++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7f4662f..65822cd14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,9 @@ hook. Plugins should use this instead of the `clientVars` global variable. * New `userJoin` server-side hook. * The `userLeave` server-side hook has a new `socket` context property. + * The `helper.aNewPad()` function (accessible to client-side tests) now + accepts hook functions to inject when opening a pad. This can be used to + test any new client-side hooks your plugin provides. # 1.8.14 diff --git a/src/templates/pad.html b/src/templates/pad.html index bcede8fd3..7ff447dc9 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -478,6 +478,10 @@ plugins.baseURL = baseURL; plugins.update(function () { + // Mechanism for tests to register hook functions (install fake plugins). + window._postPluginUpdateForTestingDone = false; + if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting(); + window._postPluginUpdateForTestingDone = true; // Call documentReady hook $(function() { hooks.aCallAll('documentReady'); diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 7da7fff04..8f8f61627 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -96,6 +96,7 @@ const helper = {}; _retry: 0, clearCookies: true, id: `FRONTEND_TEST_${helper.randomString(20)}`, + hookFns: {}, }, opts); // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah. @@ -122,7 +123,26 @@ const helper = {}; $('#iframe-container iframe').remove(); // set new iframe $('#iframe-container').append($iframe); - await new Promise((resolve) => $iframe.one('load', resolve)); + await Promise.all([ + new Promise((resolve) => $iframe.one('load', resolve)), + // Install the hook functions as early as possible because some of them fire right away. + new Promise((resolve, reject) => { + if ($iframe[0].contentWindow._postPluginUpdateForTestingDone) { + return reject(new Error( + 'failed to set _postPluginUpdateForTesting before it would have been called')); + } + $iframe[0].contentWindow._postPluginUpdateForTesting = () => { + const {hooks} = + $iframe[0].contentWindow.require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); + for (const [hookName, hookFns] of Object.entries(opts.hookFns)) { + if (hooks[hookName] == null) hooks[hookName] = []; + hooks[hookName].push( + ...hookFns.map((hookFn) => ({hook_name: hookName, hook_fn: hookFn}))); + } + resolve(); + }; + }), + ]); helper.padChrome$ = await helper.getFrameJQuery($('#iframe-container iframe'), true); helper.padChrome$.padeditor = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor; From c8e0916e1ac997ff0268f98368e7fc87cf60b947 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 17:44:10 -0400 Subject: [PATCH 229/322] tests: Spy on socket.io messages as early as possible --- src/static/js/pad.js | 23 +++++++++++++++-------- src/tests/frontend/helper.js | 31 ++++++++++--------------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 66e46391e..1d33a3c99 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -311,14 +311,21 @@ const handshake = async () => { } }); - await new Promise((resolve) => { - const h = (obj) => { - if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return; - socket.off('message', h); - resolve(); - }; - socket.on('message', h); - }); + await Promise.all([ + new Promise((resolve) => { + const h = (obj) => { + if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return; + socket.off('message', h); + resolve(); + }; + socket.on('message', h); + }), + // This hook is only intended to be used by test code. If a plugin would like to use this hook, + // the hook must first be promoted to officially supported by deleting the leading underscore + // from the name, adding documentation to `doc/api/hooks_client-side.md`, and deleting this + // comment. + hooks.aCallAll('_socketCreated', {socket}), + ]); }; /** Defers message handling until setCollabClient() is called with a non-null value. */ diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js index 8f8f61627..dbb5d4b2d 100644 --- a/src/tests/frontend/helper.js +++ b/src/tests/frontend/helper.js @@ -99,6 +99,16 @@ const helper = {}; hookFns: {}, }, opts); + // Set up socket.io spying as early as possible. + /** chat messages received */ + helper.chatMessages = []; + /** changeset commits from the server */ + helper.commits = []; + /** userInfo messages from the server */ + helper.userInfos = []; + if (opts.hookFns._socketCreated == null) opts.hookFns._socketCreated = []; + opts.hookFns._socketCreated.unshift(() => helper.spyOnSocketIO()); + // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah. let encodedParams; if (opts.params) { @@ -171,27 +181,6 @@ const helper = {}; helper.padOuter$.fx.off = true; helper.padInner$.fx.off = true; - /* - * chat messages received - * @type {Array} - */ - helper.chatMessages = []; - - /* - * changeset commits from the server - * @type {Array} - */ - helper.commits = []; - - /* - * userInfo messages from the server - * @type {Array} - */ - helper.userInfos = []; - - // listen for server messages - helper.spyOnSocketIO(); - return opts.id; }; From 66a8c48fac190c412be58309738a55458cb2be6f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 17:38:25 -0400 Subject: [PATCH 230/322] tests: Save the CHAT_MESSAGE payload, not the wrapper --- src/tests/frontend/helper/methods.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.js index 253bfbc0d..4f4917d18 100644 --- a/src/tests/frontend/helper/methods.js +++ b/src/tests/frontend/helper/methods.js @@ -12,7 +12,7 @@ helper.spyOnSocketIO = () => { } else if (msg.data.type === 'USER_NEWINFO') { helper.userInfos.push(msg); } else if (msg.data.type === 'CHAT_MESSAGE') { - helper.chatMessages.push(msg); + helper.chatMessages.push(msg.data); } }); }; From e471cb12e6b8f37bdf1a3466cc8eabf36ac5e56d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 28 Oct 2021 17:40:02 -0400 Subject: [PATCH 231/322] tests: Also spy on initially loaded chat messages --- src/tests/frontend/helper/methods.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.js index 4f4917d18..c6b9c6a15 100644 --- a/src/tests/frontend/helper/methods.js +++ b/src/tests/frontend/helper/methods.js @@ -13,6 +13,8 @@ helper.spyOnSocketIO = () => { helper.userInfos.push(msg); } else if (msg.data.type === 'CHAT_MESSAGE') { helper.chatMessages.push(msg.data); + } else if (msg.data.type === 'CHAT_MESSAGES') { + helper.chatMessages.push(...msg.data.messages); } }); }; From 23037280a8de8cb22366d6f41d37442210bd8ace Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 19:33:36 -0400 Subject: [PATCH 232/322] Pad: Simplify `getChatMessages()` --- src/node/db/Pad.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index fe4ea78d0..adf982724 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -295,32 +295,27 @@ Pad.prototype.getChatMessage = async function (entryNum) { return entry; }; +/** + * @param {number} start - ID of the first desired chat message. + * @param {number} end - ID of the last desired chat message. + * @returns {object[]} Any existing messages with IDs between `start` (inclusive) and `end` + * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open + * interval as is typical in code. + */ Pad.prototype.getChatMessages = async function (start, end) { - // collect the numbers of chat entries and in which order we need them - const neededEntries = []; - for (let order = 0, entryNum = start; entryNum <= end; ++order, ++entryNum) { - neededEntries.push({entryNum, order}); - } - - // get all entries out of the database - const entries = []; - await Promise.all( - neededEntries.map((entryObject) => this.getChatMessage(entryObject.entryNum).then((entry) => { - entries[entryObject.order] = entry; - }))); + const entries = await Promise.all( + [...Array(end + 1 - start).keys()].map((i) => this.getChatMessage(start + i))); // sort out broken chat entries // it looks like in happened in the past that the chat head was // incremented, but the chat message wasn't added - const cleanedEntries = entries.filter((entry) => { + return entries.filter((entry) => { const pass = (entry != null); if (!pass) { console.warn(`WARNING: Found broken chat entry in pad ${this.id}`); } return pass; }); - - return cleanedEntries; }; Pad.prototype.init = async function (text) { From c8dbf35fe4533513cb2e760888904d4af52ae5f2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 26 Oct 2021 21:56:53 -0400 Subject: [PATCH 233/322] docs: Improve `chatNewMessage` client-side hook documentation --- doc/api/hooks_client-side.md | 42 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 6f5f6e065..36c5d67ce 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -279,30 +279,32 @@ Things in context: This hook is called on the client side whenever a user joins or changes. This can be used to create notifications or an alternate user list. -## chatNewMessage +## `chatNewMessage` -Called from: src/static/js/chat.js +Called from: `src/static/js/chat.js` -Things in context: - -1. authorName - The user that wrote this message -2. author - The authorID of the user that wrote the message -3. text - the message text -4. sticky (boolean) - if you want the gritter notification bubble to fade out on - its own or just sit there -5. timestamp - the timestamp of the chat message -6. timeStr - the timestamp as a formatted string -7. duration - for how long in milliseconds should the gritter notification - appear (0 to disable) - -This hook is called on the client side whenever a chat message is received from -the server. It can be used to create different notifications for chat messages. -Hoook functions can modify the `author`, `authorName`, `duration`, `sticky`, -`text`, and `timeStr` context properties to change how the message is processed. -The `text` and `timeStr` properties may contain HTML, but plugins should be -careful to sanitize any added user input to avoid introducing an XSS +This hook runs on the client side whenever a chat message is received from the +server. It can be used to create different notifications for chat messages. Hook +functions can modify the `author`, `authorName`, `duration`, `sticky`, `text`, +and `timeStr` context properties to change how the message is processed. The +`text` and `timeStr` properties may contain HTML and come pre-sanitized; plugins +should be careful to sanitize any added user input to avoid introducing an XSS vulnerability. +Context properties: + +* `authorName`: The display name of the user that wrote the message. +* `author`: The author ID of the user that wrote the message. +* `text`: Sanitized message HTML, with URLs wrapped like `url`. +* `sticky` (boolean): Whether the gritter notification should fade out on its + own or just sit there until manually closed. +* `timestamp`: When the chat message was sent (milliseconds since epoch), + corrected using the difference between the local clock and the server's clock. +* `timeStr`: The message timestamp as a formatted string. +* `duration`: How long (in milliseconds) to display the gritter notification (0 + to disable). + ## collectContentPre Called from: src/static/js/contentcollector.js From 195a6bd81b2e020326d037ea0e04e94188d44037 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 19:48:54 -0400 Subject: [PATCH 234/322] chat: Move click handler setup to `init()` --- src/static/js/chat.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index b62dc13dc..f91623a31 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -203,12 +203,6 @@ exports.chat = (() => { } } }); - - // Clear the chat mentions when the user clicks on the chat input box - $('#chatinput').click(() => { - chatMentions = 0; - Tinycon.setBubble(0); - }); if (!isHistoryAdd) this.scrollDown(); }, init(pad) { @@ -224,6 +218,11 @@ exports.chat = (() => { return false; } }); + // Clear the chat mentions when the user clicks on the chat input box + $('#chatinput').click(() => { + chatMentions = 0; + Tinycon.setBubble(0); + }); const self = this; $('body:not(#chatinput)').on('keypress', function (evt) { From 3f7f629eeb3ea5c62db2e44a7a919d3bcfaf55ee Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 19:50:49 -0400 Subject: [PATCH 235/322] chat: Scroll down after the `chatNewMessage` hook finishes --- src/static/js/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index f91623a31..2df78b614 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -202,8 +202,8 @@ exports.chat = (() => { }); } } + if (!isHistoryAdd) this.scrollDown(); }); - if (!isHistoryAdd) this.scrollDown(); }, init(pad) { this._pad = pad; From caac4bf71142857cdcc1f1511dc6882e15ceba7c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 19:51:37 -0400 Subject: [PATCH 236/322] chat: Promisify `addMessage()` --- src/static/js/chat.js | 80 +++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 2df78b614..7ec3abdf0 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -105,7 +105,7 @@ exports.chat = (() => { this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', text}); $('#chatinput').val(''); }, - addMessage(msg, increment, isHistoryAdd) { + async addMessage(msg, increment, isHistoryAdd) { // correct the time msg.time += this._pad.clientTimeOffset; @@ -161,49 +161,47 @@ exports.chat = (() => { ctx.sticky = true; } - // Call chat message hook - hooks.aCallAll('chatNewMessage', ctx, () => { - const cls = authorClass(ctx.author); - const chatMsg = $('

    ') - .attr('data-authorId', ctx.author) - .addClass(cls) - .append($('').text(`${ctx.authorName}:`)) - .append($('') - .addClass('time') - .addClass(cls) - // Hook functions are trusted to not introduce an XSS vulnerability by adding - // unescaped user input to ctx.timeStr. - .html(ctx.timeStr)) - .append(' ') - // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not - // introduce an XSS vulnerability by adding unescaped user input. - .append($('

    ').html(ctx.text).contents()); - if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton'); - else $('#chattext').append(chatMsg); + await hooks.aCallAll('chatNewMessage', ctx); + const cls = authorClass(ctx.author); + const chatMsg = $('

    ') + .attr('data-authorId', ctx.author) + .addClass(cls) + .append($('').text(`${ctx.authorName}:`)) + .append($('') + .addClass('time') + .addClass(cls) + // Hook functions are trusted to not introduce an XSS vulnerability by adding + // unescaped user input to ctx.timeStr. + .html(ctx.timeStr)) + .append(' ') + // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not + // introduce an XSS vulnerability by adding unescaped user input. + .append($('

    ').html(ctx.text).contents()); + if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton'); + else $('#chattext').append(chatMsg); - // should we increment the counter?? - if (increment && !isHistoryAdd) { - // Update the counter of unread messages - let count = Number($('#chatcounter').text()); - count++; - $('#chatcounter').text(count); + // should we increment the counter?? + if (increment && !isHistoryAdd) { + // Update the counter of unread messages + let count = Number($('#chatcounter').text()); + count++; + $('#chatcounter').text(count); - if (!chatOpen && ctx.duration > 0) { - $.gritter.add({ - text: $('

    ') - .append($('').addClass('author-name').text(ctx.authorName)) - // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted - // to not introduce an XSS vulnerability by adding unescaped user input. - .append($('

    ').html(ctx.text).contents()), - sticky: ctx.sticky, - time: 5000, - position: 'bottom', - class_name: 'chat-gritter-msg', - }); - } + if (!chatOpen && ctx.duration > 0) { + $.gritter.add({ + text: $('

    ') + .append($('').addClass('author-name').text(ctx.authorName)) + // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted + // to not introduce an XSS vulnerability by adding unescaped user input. + .append($('

    ').html(ctx.text).contents()), + sticky: ctx.sticky, + time: 5000, + position: 'bottom', + class_name: 'chat-gritter-msg', + }); } - if (!isHistoryAdd) this.scrollDown(); - }); + } + if (!isHistoryAdd) this.scrollDown(); }, init(pad) { this._pad = pad; From fc5a3f553d5514257d90388c5b3b1b64c71b207c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 27 Oct 2021 19:52:24 -0400 Subject: [PATCH 237/322] chat: Test processing in `chatNewMessage` hook --- src/tests/frontend/specs/chat_hooks.js | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/tests/frontend/specs/chat_hooks.js diff --git a/src/tests/frontend/specs/chat_hooks.js b/src/tests/frontend/specs/chat_hooks.js new file mode 100644 index 000000000..2a0307a80 --- /dev/null +++ b/src/tests/frontend/specs/chat_hooks.js @@ -0,0 +1,64 @@ +'use strict'; + +describe('chat hooks', function () { + let hooks; + const hooksBackup = {}; + + const loadPad = async (opts = {}) => { + await helper.aNewPad(opts); + ({hooks} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pluginfw/plugin_defs')); + for (const [name, defs] of Object.entries(hooks)) { + hooksBackup[name] = defs; + hooks[name] = [...defs]; + } + await helper.showChat(); + }; + + before(async function () { + await loadPad(); + }); + + afterEach(async function () { + for (const [name, defs] of Object.entries(hooksBackup)) hooks[name] = [...defs]; + for (const name of Object.keys(hooks)) { + if (hooksBackup[name] == null) delete hooks[name]; + } + }); + + const checkHook = async (hookName, checkFn) => { + if (hooks[hookName] == null) hooks[hookName] = []; + await new Promise((resolve, reject) => { + hooks[hookName].push({ + hook_fn: async (hookName, context) => { + if (checkFn == null) return; + try { + // Make sure checkFn is called only once. + const _checkFn = checkFn; + checkFn = null; + await _checkFn(context); + } catch (err) { + reject(err); + return; + } + resolve(); + }, + }); + }); + }; + + describe('chatNewMessage', function () { + for (const [desc, msg, wantRegEx] of [ + ['HTML is escaped', '', /^[^<]*$/], + ['URL becomes a link', 'https://etherpad.org', /]*href/], + ]) { + it(`text processing: ${desc}`, async function () { + await Promise.all([ + checkHook('chatNewMessage', ({text}) => { + expect(text).to.match(wantRegEx); + }), + helper.sendChatMessage(`${msg}{enter}`), + ]); + }); + } + }); +}); From 2597b940f473c6dcc7018cf1a2ce342ad57ce9d1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 26 Oct 2021 21:58:34 -0400 Subject: [PATCH 238/322] chat: Give `chatNewMessage` hook access to the raw message object --- CHANGELOG.md | 4 ++++ doc/api/hooks_client-side.md | 7 ++++++- src/static/js/chat.js | 1 + src/tests/frontend/specs/chat_hooks.js | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65822cd14..80dc6b6f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,10 @@ * The `helper.aNewPad()` function (accessible to client-side tests) now accepts hook functions to inject when opening a pad. This can be used to test any new client-side hooks your plugin provides. + * Chat improvements: + * The `chatNewMessage` client-side hook context has new properties: + * `message`: Provides access to the raw message object so that plugins can + see the original unprocessed message text and any added metadata. # 1.8.14 diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 36c5d67ce..031d490e1 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -296,7 +296,12 @@ Context properties: * `authorName`: The display name of the user that wrote the message. * `author`: The author ID of the user that wrote the message. * `text`: Sanitized message HTML, with URLs wrapped like `url`. + href="url">url`. (Note that `message.text` is not sanitized or processed + in any way.) +* `message`: The raw message object as received from the server, except with + time correction and a default `userId` property if missing. Plugins must not + modify this object. Warning: Unlike `text`, `message.text` is not + pre-sanitized or processed in any way. * `sticky` (boolean): Whether the gritter notification should fade out on its own or just sit there until manually closed. * `timestamp`: When the chat message was sent (milliseconds since epoch), diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 7ec3abdf0..c9e8046ba 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -131,6 +131,7 @@ exports.chat = (() => { authorName: msg.userName != null ? msg.userName : html10n.get('pad.userlist.unnamed'), author: msg.userId, text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'), + message: msg, sticky: false, timestamp: msg.time, timeStr: (() => { diff --git a/src/tests/frontend/specs/chat_hooks.js b/src/tests/frontend/specs/chat_hooks.js index 2a0307a80..314ddfc6b 100644 --- a/src/tests/frontend/specs/chat_hooks.js +++ b/src/tests/frontend/specs/chat_hooks.js @@ -60,5 +60,24 @@ describe('chat hooks', function () { ]); }); } + + it('message is an object', async function () { + await Promise.all([ + checkHook('chatNewMessage', ({message}) => { + expect(message).to.be.an('object'); + }), + helper.sendChatMessage(`${this.test.title}{enter}`), + ]); + }); + + it('message.text is not processed', async function () { + const msg = ' https://etherpad.org'; + await Promise.all([ + checkHook('chatNewMessage', ({message: {text}}) => { + expect(text).to.equal(`${msg}\n`); + }), + helper.sendChatMessage(`${msg}{enter}`), + ]); + }); }); }); From f1f4ed7c582d9c2372ce39f6a328e793e2aff396 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 20:44:35 -0400 Subject: [PATCH 239/322] chat: Allow `chatNewMessage` hook to control rendering --- CHANGELOG.md | 2 ++ doc/api/hooks_client-side.md | 15 ++++++++++----- src/static/js/chat.js | 3 ++- src/tests/frontend/specs/chat_hooks.js | 13 +++++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80dc6b6f8..82b936620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ * The `chatNewMessage` client-side hook context has new properties: * `message`: Provides access to the raw message object so that plugins can see the original unprocessed message text and any added metadata. + * `rendered`: Allows plugins to completely override how the message is + rendered in the UI. # 1.8.14 diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 031d490e1..d507459c2 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -285,11 +285,11 @@ Called from: `src/static/js/chat.js` This hook runs on the client side whenever a chat message is received from the server. It can be used to create different notifications for chat messages. Hook -functions can modify the `author`, `authorName`, `duration`, `sticky`, `text`, -and `timeStr` context properties to change how the message is processed. The -`text` and `timeStr` properties may contain HTML and come pre-sanitized; plugins -should be careful to sanitize any added user input to avoid introducing an XSS -vulnerability. +functions can modify the `author`, `authorName`, `duration`, `rendered`, +`sticky`, `text`, and `timeStr` context properties to change how the message is +processed. The `text` and `timeStr` properties may contain HTML and come +pre-sanitized; plugins should be careful to sanitize any added user input to +avoid introducing an XSS vulnerability. Context properties: @@ -302,6 +302,11 @@ Context properties: time correction and a default `userId` property if missing. Plugins must not modify this object. Warning: Unlike `text`, `message.text` is not pre-sanitized or processed in any way. +* `rendered` - Used to override the default message rendering. Initially set to + `null`. If the hook function sets this to a DOM element object or a jQuery + object, then that object will be used as the rendered message UI. Otherwise, + if this is set to `null`, then Etherpad will render a default UI for the + message using the other context properties. * `sticky` (boolean): Whether the gritter notification should fade out on its own or just sit there until manually closed. * `timestamp`: When the chat message was sent (milliseconds since epoch), diff --git a/src/static/js/chat.js b/src/static/js/chat.js index c9e8046ba..ef150065e 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -132,6 +132,7 @@ exports.chat = (() => { author: msg.userId, text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'), message: msg, + rendered: null, sticky: false, timestamp: msg.time, timeStr: (() => { @@ -164,7 +165,7 @@ exports.chat = (() => { await hooks.aCallAll('chatNewMessage', ctx); const cls = authorClass(ctx.author); - const chatMsg = $('

    ') + const chatMsg = ctx.rendered != null ? $(ctx.rendered) : $('

    ') .attr('data-authorId', ctx.author) .addClass(cls) .append($('').text(`${ctx.authorName}:`)) diff --git a/src/tests/frontend/specs/chat_hooks.js b/src/tests/frontend/specs/chat_hooks.js index 314ddfc6b..eb305e40a 100644 --- a/src/tests/frontend/specs/chat_hooks.js +++ b/src/tests/frontend/specs/chat_hooks.js @@ -79,5 +79,18 @@ describe('chat hooks', function () { helper.sendChatMessage(`${msg}{enter}`), ]); }); + + it('`rendered` overrides default rendering', async function () { + let rendered; + await Promise.all([ + checkHook('chatNewMessage', (context) => { + expect(context.rendered == null).to.be.ok(); + rendered = context.rendered = helper.padChrome$.document.createElement('p'); + rendered.append('message rendering overridden'); + }), + helper.sendChatMessage(`${this.test.title}{enter}`), + ]); + expect(helper.chatTextParagraphs().last()[0]).to.be(rendered); + }); }); }); From 0f47ca9046c3ab860f113cf1959feef2481bf7a3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 26 Oct 2021 00:56:27 -0400 Subject: [PATCH 240/322] chat: Plumb message object end to end This will make it possible for future commits to add hooks that allow plugins to augment chat messages with arbitrary metadata. --- src/node/db/API.js | 3 +- src/node/db/Pad.js | 38 +++++++++++++------- src/node/handler/PadMessageHandler.js | 49 ++++++++++++------------- src/node/utils/tar.json | 1 + src/static/js/ChatMessage.js | 50 ++++++++++++++++++++++++++ src/static/js/chat.js | 4 ++- src/static/js/collab_client.js | 2 +- src/tests/frontend/helper/methods.js | 2 +- src/tests/frontend/specs/chat_hooks.js | 6 ++-- 9 files changed, 113 insertions(+), 42 deletions(-) create mode 100644 src/static/js/ChatMessage.js diff --git a/src/node/db/API.js b/src/node/db/API.js index f119f5847..d48867558 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -20,6 +20,7 @@ */ 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'); @@ -364,7 +365,7 @@ exports.appendChatMessage = async (padID, text, authorID, time) => { // @TODO - missing getPadSafe() call ? // save chat message to database and send message to all connected clients - await padMessageHandler.sendChatMessageToPadClients(time, authorID, text, padID); + await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID); }; /* *************** diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index adf982724..36585426b 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -5,6 +5,7 @@ const Changeset = require('../../static/js/Changeset'); +const ChatMessage = require('../../static/js/ChatMessage'); const AttributePool = require('../../static/js/AttributePool'); const db = require('./DB'); const settings = require('../utils/Settings'); @@ -274,31 +275,44 @@ Pad.prototype.appendText = async function (newText) { await this.appendRevision(changeset); }; -Pad.prototype.appendChatMessage = async function (text, userId, time) { +/** + * Adds a chat message to the pad, including saving it to the database. + * + * @param {(ChatMessage|string)} msgOrText - Either a chat message object (recommended) or a string + * containing the raw text of the user's chat message (deprecated). + * @param {?string} [userId] - The user's author ID. Deprecated; use `msgOrText.userId` instead. + * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use + * `msgOrText.time` instead. + */ +Pad.prototype.appendChatMessage = async function (msgOrText, userId = null, time = null) { + const msg = + msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, userId, time); this.chatHead++; - // save the chat entry in the database await Promise.all([ - db.set(`pad:${this.id}:chat:${this.chatHead}`, {text, userId, time}), + // Don't save the display name in the database because the user can change it at any time. The + // `userName` property will be populated with the current value when the message is read from + // the database. + db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, userName: undefined}), this.saveToDatabase(), ]); }; +/** + * @param {number} entryNum - ID of the desired chat message. + * @returns {?ChatMessage} + */ Pad.prototype.getChatMessage = async function (entryNum) { - // get the chat entry const entry = await db.get(`pad:${this.id}:chat:${entryNum}`); - - // get the authorName if the entry exists - if (entry != null) { - entry.userName = await authorManager.getAuthorName(entry.userId); - } - - return entry; + if (entry == null) return null; + const message = ChatMessage.fromObject(entry); + message.userName = await authorManager.getAuthorName(message.userId); + return message; }; /** * @param {number} start - ID of the first desired chat message. * @param {number} end - ID of the last desired chat message. - * @returns {object[]} Any existing messages with IDs between `start` (inclusive) and `end` + * @returns {ChatMessage[]} Any existing messages with IDs between `start` (inclusive) and `end` * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open * interval as is typical in code. */ diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index e38d77160..36682abef 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -21,6 +21,7 @@ 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'); @@ -340,37 +341,37 @@ exports.handleCustomMessage = (padID, msgString) => { * @param message the message from the client */ const handleChatMessage = async (socket, message) => { - const time = Date.now(); - const text = message.data.text; + const chatMessage = ChatMessage.fromObject(message.data.message); const {padId, author: authorId} = sessioninfos[socket.id]; - await exports.sendChatMessageToPadClients(time, authorId, text, padId); + // Don't trust the user-supplied values. + chatMessage.time = Date.now(); + chatMessage.userId = authorId; + await exports.sendChatMessageToPadClients(chatMessage, padId); }; /** - * Sends a chat message to all clients of this pad - * @param time the timestamp of the chat message - * @param userId the author id of the chat message - * @param text the text of the chat message - * @param padId the padId to send the chat message to + * Adds a new chat message to a pad and sends it to connected clients. + * + * @param {(ChatMessage|number)} mt - Either a chat message object (recommended) or the timestamp of + * the chat message in ms since epoch (deprecated). + * @param {string} puId - If `mt` is a chat message object, this is the destination pad ID. + * Otherwise, this is the user's author ID (deprecated). + * @param {string} [text] - The text of the chat message. Deprecated; use `mt.text` instead. + * @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 (time, userId, text, padId) => { - // get the pad +exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => { + const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt); + padId = mt instanceof ChatMessage ? puId : padId; const pad = await padManager.getPad(padId); - - // get the author - const userName = await authorManager.getAuthorName(userId); - - // save the chat message - const promise = pad.appendChatMessage(text, userId, time); - - const msg = { + // pad.appendChatMessage() ignores the userName 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.userName = await authorManager.getAuthorName(message.userId); + socketio.sockets.in(padId).json.send({ type: 'COLLABROOM', - data: {type: 'CHAT_MESSAGE', userId, userName, time, text}, - }; - - // broadcast the chat message to everyone on the pad - socketio.sockets.in(padId).json.send(msg); - + data: {type: 'CHAT_MESSAGE', message}, + }); await promise; }; diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json index e6caa3e67..896913ffe 100644 --- a/src/node/utils/tar.json +++ b/src/node/utils/tar.json @@ -19,6 +19,7 @@ , "pad_impexp.js" , "pad_savedrevs.js" , "pad_connectionstatus.js" + , "ChatMessage.js" , "chat.js" , "vendors/gritter.js" , "$js-cookie/dist/js.cookie.js" diff --git a/src/static/js/ChatMessage.js b/src/static/js/ChatMessage.js new file mode 100644 index 000000000..f6e10c2b5 --- /dev/null +++ b/src/static/js/ChatMessage.js @@ -0,0 +1,50 @@ +'use strict'; + +/** + * Represents a chat message stored in the database and transmitted among users. Plugins can extend + * the object with additional properties. + * + * Supports serialization to JSON. + */ +class ChatMessage { + static fromObject(obj) { + return Object.assign(new ChatMessage(), obj); + } + + /** + * @param {?string} [text] - Initial value of the `text` property. + * @param {?string} [userId] - Initial value of the `userId` property. + * @param {?number} [time] - Initial value of the `time` property. + */ + constructor(text = null, userId = null, time = null) { + /** + * The raw text of the user's chat message (before any rendering or processing). + * + * @type {?string} + */ + this.text = text; + + /** + * The user's author ID. + * + * @type {?string} + */ + this.userId = userId; + + /** + * The message's timestamp, as milliseconds since epoch. + * + * @type {?number} + */ + this.time = time; + + /** + * The user's display name. + * + * @type {?string} + */ + this.userName = null; + } +} + +module.exports = ChatMessage; diff --git a/src/static/js/chat.js b/src/static/js/chat.js index ef150065e..62f355499 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -15,6 +15,7 @@ * limitations under the License. */ +const ChatMessage = require('./ChatMessage'); const padutils = require('./pad_utils').padutils; const padcookie = require('./pad_cookie').padcookie; const Tinycon = require('tinycon/tinycon'); @@ -102,10 +103,11 @@ exports.chat = (() => { send() { const text = $('#chatinput').val(); if (text.replace(/\s+/, '').length === 0) return; - this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', text}); + this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', message: new ChatMessage(text)}); $('#chatinput').val(''); }, async addMessage(msg, increment, isHistoryAdd) { + msg = ChatMessage.fromObject(msg); // correct the time msg.time += this._pad.clientTimeOffset; diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index e507d6b06..837fc3ba0 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -272,7 +272,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } else if (msg.type === 'CLIENT_MESSAGE') { callbacks.onClientMessage(msg.payload); } else if (msg.type === 'CHAT_MESSAGE') { - chat.addMessage(msg, true, false); + chat.addMessage(msg.message, true, false); } else if (msg.type === 'CHAT_MESSAGES') { for (let i = msg.messages.length - 1; i >= 0; i--) { chat.addMessage(msg.messages[i], true, true); diff --git a/src/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.js index c6b9c6a15..b828cd601 100644 --- a/src/tests/frontend/helper/methods.js +++ b/src/tests/frontend/helper/methods.js @@ -12,7 +12,7 @@ helper.spyOnSocketIO = () => { } else if (msg.data.type === 'USER_NEWINFO') { helper.userInfos.push(msg); } else if (msg.data.type === 'CHAT_MESSAGE') { - helper.chatMessages.push(msg.data); + helper.chatMessages.push(msg.data.message); } else if (msg.data.type === 'CHAT_MESSAGES') { helper.chatMessages.push(...msg.data.messages); } diff --git a/src/tests/frontend/specs/chat_hooks.js b/src/tests/frontend/specs/chat_hooks.js index eb305e40a..a307ad5b6 100644 --- a/src/tests/frontend/specs/chat_hooks.js +++ b/src/tests/frontend/specs/chat_hooks.js @@ -1,11 +1,13 @@ 'use strict'; describe('chat hooks', function () { + let ChatMessage; let hooks; const hooksBackup = {}; const loadPad = async (opts = {}) => { await helper.aNewPad(opts); + ChatMessage = helper.padChrome$.window.require('ep_etherpad-lite/static/js/ChatMessage'); ({hooks} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pluginfw/plugin_defs')); for (const [name, defs] of Object.entries(hooks)) { hooksBackup[name] = defs; @@ -61,10 +63,10 @@ describe('chat hooks', function () { }); } - it('message is an object', async function () { + it('message is a ChatMessage object', async function () { await Promise.all([ checkHook('chatNewMessage', ({message}) => { - expect(message).to.be.an('object'); + expect(message).to.be.a(ChatMessage); }), helper.sendChatMessage(`${this.test.title}{enter}`), ]); From 4c2f7f9a1131a9af2f914ad381bebe5893152ce3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 26 Oct 2021 01:05:24 -0400 Subject: [PATCH 241/322] chat: Rename `userId` to `authorId`, `userName` to `displayName` --- doc/api/hooks_client-side.md | 2 +- src/node/db/Pad.js | 12 ++++----- src/node/handler/PadMessageHandler.js | 6 ++--- src/static/js/ChatMessage.js | 38 ++++++++++++++++++++++++--- src/static/js/chat.js | 12 ++++----- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index d507459c2..dc5608931 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -299,7 +299,7 @@ Context properties: href="url">url`. (Note that `message.text` is not sanitized or processed in any way.) * `message`: The raw message object as received from the server, except with - time correction and a default `userId` property if missing. Plugins must not + time correction and a default `authorId` property if missing. Plugins must not modify this object. Warning: Unlike `text`, `message.text` is not pre-sanitized or processed in any way. * `rendered` - Used to override the default message rendering. Initially set to diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 36585426b..2ec573744 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -280,19 +280,19 @@ Pad.prototype.appendText = async function (newText) { * * @param {(ChatMessage|string)} msgOrText - Either a chat message object (recommended) or a string * containing the raw text of the user's chat message (deprecated). - * @param {?string} [userId] - The user's author ID. Deprecated; use `msgOrText.userId` instead. + * @param {?string} [authorId] - The user's author ID. Deprecated; use `msgOrText.authorId` instead. * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use * `msgOrText.time` instead. */ -Pad.prototype.appendChatMessage = async function (msgOrText, userId = null, time = null) { +Pad.prototype.appendChatMessage = async function (msgOrText, authorId = null, time = null) { const msg = - msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, userId, time); + msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); this.chatHead++; await Promise.all([ // Don't save the display name in the database because the user can change it at any time. The - // `userName` property will be populated with the current value when the message is read from + // `displayName` property will be populated with the current value when the message is read from // the database. - db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, userName: undefined}), + db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}), this.saveToDatabase(), ]); }; @@ -305,7 +305,7 @@ Pad.prototype.getChatMessage = async function (entryNum) { const entry = await db.get(`pad:${this.id}:chat:${entryNum}`); if (entry == null) return null; const message = ChatMessage.fromObject(entry); - message.userName = await authorManager.getAuthorName(message.userId); + message.displayName = await authorManager.getAuthorName(message.authorId); return message; }; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 36682abef..64027f6e4 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -345,7 +345,7 @@ const handleChatMessage = async (socket, message) => { const {padId, author: authorId} = sessioninfos[socket.id]; // Don't trust the user-supplied values. chatMessage.time = Date.now(); - chatMessage.userId = authorId; + chatMessage.authorId = authorId; await exports.sendChatMessageToPadClients(chatMessage, padId); }; @@ -364,10 +364,10 @@ exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt); padId = mt instanceof ChatMessage ? puId : padId; const pad = await padManager.getPad(padId); - // pad.appendChatMessage() ignores the userName property so we don't need to wait for + // pad.appendChatMessage() ignores the displayName property so we don't need to wait for // authorManager.getAuthorName() to resolve before saving the message to the database. const promise = pad.appendChatMessage(message); - message.userName = await authorManager.getAuthorName(message.userId); + message.displayName = await authorManager.getAuthorName(message.userId); socketio.sockets.in(padId).json.send({ type: 'COLLABROOM', data: {type: 'CHAT_MESSAGE', message}, diff --git a/src/static/js/ChatMessage.js b/src/static/js/ChatMessage.js index f6e10c2b5..b4658575e 100644 --- a/src/static/js/ChatMessage.js +++ b/src/static/js/ChatMessage.js @@ -13,10 +13,10 @@ class ChatMessage { /** * @param {?string} [text] - Initial value of the `text` property. - * @param {?string} [userId] - Initial value of the `userId` property. + * @param {?string} [authorId] - Initial value of the `authorId` property. * @param {?number} [time] - Initial value of the `time` property. */ - constructor(text = null, userId = null, time = null) { + constructor(text = null, authorId = null, time = null) { /** * The raw text of the user's chat message (before any rendering or processing). * @@ -29,7 +29,7 @@ class ChatMessage { * * @type {?string} */ - this.userId = userId; + this.authorId = authorId; /** * The message's timestamp, as milliseconds since epoch. @@ -43,7 +43,37 @@ class ChatMessage { * * @type {?string} */ - this.userName = null; + this.displayName = null; + } + + /** + * Alias of `authorId`, for compatibility with old plugins. + * + * @deprecated Use `authorId` instead. + * @type {string} + */ + get userId() { return this.authorId; } + set userId(val) { this.authorId = val; } + + /** + * Alias of `displayName`, for compatibility with old plugins. + * + * @deprecated Use `displayName` instead. + * @type {string} + */ + get userName() { return this.displayName; } + set userName(val) { this.displayName = val; } + + // TODO: Delete this method once users are unlikely to roll back to a version of Etherpad that + // doesn't support authorId and displayName. + toJSON() { + return { + ...this, + authorId: undefined, + displayName: undefined, + userId: this.authorId, + userName: this.displayName, + }; } } diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 62f355499..51da91c6d 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -111,15 +111,15 @@ exports.chat = (() => { // correct the time msg.time += this._pad.clientTimeOffset; - if (!msg.userId) { + if (!msg.authorId) { /* * If, for a bug or a database corruption, the message coming from the - * server does not contain the userId field (see for example #3731), + * server does not contain the authorId field (see for example #3731), * let's be defensive and replace it with "unknown". */ - msg.userId = 'unknown'; + msg.authorId = 'unknown'; console.warn( - 'The "userId" field of a chat message coming from the server was not present. ' + + 'The "authorId" field of a chat message coming from the server was not present. ' + 'Replacing with "unknown". This may be a bug or a database corruption.'); } @@ -130,8 +130,8 @@ exports.chat = (() => { // the hook args const ctx = { - authorName: msg.userName != null ? msg.userName : html10n.get('pad.userlist.unnamed'), - author: msg.userId, + authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'), + author: msg.authorId, text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'), message: msg, rendered: null, From 9fbd2e5c3d771ae57078ca6c9d5c6b5ec118ee60 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 26 Oct 2021 02:08:14 -0400 Subject: [PATCH 242/322] chat: New `chatSendMessage` client-side hook --- CHANGELOG.md | 3 ++ doc/api/hooks_client-side.md | 12 ++++++ src/static/js/chat.js | 6 ++- src/tests/frontend/specs/chat_hooks.js | 53 +++++++++++++++++++++++++- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b936620..a2f74e1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ see the original unprocessed message text and any added metadata. * `rendered`: Allows plugins to completely override how the message is rendered in the UI. + * New `chatSendMessage` client-side hook that enables plugins to process the + text before sending it to the server or augment the message object with + custom metadata. # 1.8.14 diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index dc5608931..45ef18a01 100755 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -315,6 +315,18 @@ Context properties: * `duration`: How long (in milliseconds) to display the gritter notification (0 to disable). +## `chatSendMessage` + +Called from: `src/static/js/chat.js` + +This hook runs on the client side whenever the user sends a new chat message. +Plugins can mutate the message object to change the message text or add metadata +to control how the message will be rendered by the `chatNewMessage` hook. + +Context properties: + +* `message`: The message object that will be sent to the Etherpad server. + ## collectContentPre Called from: src/static/js/contentcollector.js diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 51da91c6d..088d16111 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -100,10 +100,12 @@ exports.chat = (() => { } } }, - send() { + async send() { const text = $('#chatinput').val(); if (text.replace(/\s+/, '').length === 0) return; - this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', message: new ChatMessage(text)}); + const message = new ChatMessage(text); + await hooks.aCallAll('chatSendMessage', Object.freeze({message})); + this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', message}); $('#chatinput').val(''); }, async addMessage(msg, increment, isHistoryAdd) { diff --git a/src/tests/frontend/specs/chat_hooks.js b/src/tests/frontend/specs/chat_hooks.js index a307ad5b6..eb6aeb6ea 100644 --- a/src/tests/frontend/specs/chat_hooks.js +++ b/src/tests/frontend/specs/chat_hooks.js @@ -4,9 +4,10 @@ describe('chat hooks', function () { let ChatMessage; let hooks; const hooksBackup = {}; + let padId; const loadPad = async (opts = {}) => { - await helper.aNewPad(opts); + padId = await helper.aNewPad(opts); ChatMessage = helper.padChrome$.window.require('ep_etherpad-lite/static/js/ChatMessage'); ({hooks} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pluginfw/plugin_defs')); for (const [name, defs] of Object.entries(hooks)) { @@ -95,4 +96,54 @@ describe('chat hooks', function () { expect(helper.chatTextParagraphs().last()[0]).to.be(rendered); }); }); + + describe('chatSendMessage', function () { + it('message is a ChatMessage object', async function () { + await Promise.all([ + checkHook('chatSendMessage', ({message}) => { + expect(message).to.be.a(ChatMessage); + }), + helper.sendChatMessage(`${this.test.title}{enter}`), + ]); + }); + + it('message metadata propagates end-to-end', async function () { + const metadata = {foo: this.test.title}; + await Promise.all([ + checkHook('chatSendMessage', ({message}) => { + message.customMetadata = metadata; + }), + checkHook('chatNewMessage', ({message: {customMetadata}}) => { + expect(JSON.stringify(customMetadata)).to.equal(JSON.stringify(metadata)); + }), + helper.sendChatMessage(`${this.test.title}{enter}`), + ]); + }); + + it('message metadata is saved in the database', async function () { + const msg = this.test.title; + const metadata = {foo: this.test.title}; + await Promise.all([ + checkHook('chatSendMessage', ({message}) => { + message.customMetadata = metadata; + }), + helper.sendChatMessage(`${msg}{enter}`), + ]); + let gotMessage; + const messageP = new Promise((resolve) => gotMessage = resolve); + await loadPad({ + id: padId, + hookFns: { + chatNewMessage: [ + (hookName, {message}) => { + if (message.text === `${msg}\n`) gotMessage(message); + }, + ], + }, + }); + const message = await messageP; + expect(message).to.be.a(ChatMessage); + expect(JSON.stringify(message.customMetadata)).to.equal(JSON.stringify(metadata)); + }); + }); }); From bea57ff249be2eb33b7881bed6e96a4d44c0d203 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 22:18:42 -0400 Subject: [PATCH 243/322] tests: Use `logger` variable for consistency --- src/tests/backend/common.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tests/backend/common.js b/src/tests/backend/common.js index aabc48a54..e2dd541e2 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.js @@ -17,7 +17,8 @@ exports.baseUrl = null; exports.httpServer = null; exports.logger = log4js.getLogger('test'); -const logLevel = exports.logger.level; +const logger = exports.logger; +const logLevel = logger.level; // Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions. // https://github.com/mochajs/mocha/issues/2640 @@ -34,10 +35,10 @@ exports.init = async function () { agentPromise = new Promise((resolve) => { agentResolve = resolve; }); if (!logLevel.isLessThanOrEqualTo(log4js.levels.DEBUG)) { - exports.logger.warn('Disabling non-test logging for the duration of the test. ' + - 'To enable non-test logging, change the loglevel setting to DEBUG.'); + logger.warn('Disabling non-test logging for the duration of the test. ' + + 'To enable non-test logging, change the loglevel setting to DEBUG.'); log4js.setGlobalLogLevel(log4js.levels.OFF); - exports.logger.setLevel(logLevel); + logger.setLevel(logLevel); } // Note: This is only a shallow backup. @@ -49,7 +50,7 @@ exports.init = async function () { settings.commitRateLimiting = {duration: 0.001, points: 1e6}; exports.httpServer = await server.start(); exports.baseUrl = `http://localhost:${exports.httpServer.address().port}`; - exports.logger.debug(`HTTP server at ${exports.baseUrl}`); + logger.debug(`HTTP server at ${exports.baseUrl}`); // Create a supertest user agent for the HTTP server. exports.agent = supertest(exports.baseUrl); // Speed up authn tests. From 65bd5970530b38fc2bf17a7c3409a6aaf7188ed5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 22:23:13 -0400 Subject: [PATCH 244/322] tests: Move socket.io connection helpers to `common.js` --- src/tests/backend/common.js | 100 ++++++++++++++ src/tests/backend/specs/socketio.js | 207 ++++++++-------------------- 2 files changed, 161 insertions(+), 146 deletions(-) diff --git a/src/tests/backend/common.js b/src/tests/backend/common.js index e2dd541e2..12b5eb456 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.js @@ -1,9 +1,11 @@ 'use strict'; const apiHandler = require('../../node/handler/APIHandler'); +const io = require('socket.io-client'); const log4js = require('log4js'); const process = require('process'); const server = require('../../node/server'); +const setCookieParser = require('set-cookie-parser'); const settings = require('../../node/utils/Settings'); const supertest = require('supertest'); const webaccess = require('../../node/hooks/express/webaccess'); @@ -68,3 +70,101 @@ exports.init = async function () { agentResolve(exports.agent); return exports.agent; }; + +/** + * Waits for the next named socket.io event. Rejects if there is an error event while waiting + * (unless waiting for that error event). + * + * @param {io.Socket} socket - The socket.io Socket object to listen on. + * @param {string} event - The socket.io Socket event to listen for. + * @returns The argument(s) passed to the event handler. + */ +exports.getSocketEvent = async (socket, event) => { + const errorEvents = [ + 'error', + 'connect_error', + 'connect_timeout', + 'reconnect_error', + 'reconnect_failed', + ]; + const handlers = {}; + let timeoutId; + return new Promise((resolve, reject) => { + timeoutId = setTimeout(() => reject(new Error(`timed out waiting for ${event} event`)), 1000); + for (const event of errorEvents) { + handlers[event] = (errorString) => { + logger.debug(`socket.io ${event} event: ${errorString}`); + reject(new Error(errorString)); + }; + } + // This will overwrite one of the above handlers if the user is waiting for an error event. + handlers[event] = (...args) => { + logger.debug(`socket.io ${event} event`); + if (args.length > 1) return resolve(args); + resolve(args[0]); + }; + Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler)); + }).finally(() => { + clearTimeout(timeoutId); + Object.entries(handlers).forEach(([event, handler]) => socket.off(event, handler)); + }); +}; + +/** + * Establishes a new socket.io connection. + * + * @param {object} [res] - Optional HTTP response object. The cookies from this response's + * `set-cookie` header(s) are passed to the server when opening the socket.io connection. If + * nullish, no cookies are passed to the server. + * @returns {io.Socket} A socket.io client Socket object. + */ +exports.connect = async (res = null) => { + // Convert the `set-cookie` header(s) into a `cookie` header. + const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true}); + const reqCookieHdr = Object.entries(resCookies).map( + ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); + + logger.debug('socket.io connecting...'); + let padId = null; + if (res) { + padId = res.req.path.split('/p/')[1]; + } + const socket = io(`${exports.baseUrl}/`, { + forceNew: true, // Different tests will have different query parameters. + path: '/socket.io', + // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the + // express_sid cookie must be passed as a query parameter. + query: {cookie: reqCookieHdr, padId}, + }); + try { + await exports.getSocketEvent(socket, 'connect'); + } catch (e) { + socket.close(); + throw e; + } + logger.debug('socket.io connected'); + + return socket; +}; + +/** + * Helper function to exchange CLIENT_READY+CLIENT_VARS messages for the named pad. + * + * @param {io.Socket} socket - Connected socket.io Socket object. + * @param {string} padId - Which pad to join. + * @returns The CLIENT_VARS message from the server. + */ +exports.handshake = async (socket, padId) => { + logger.debug('sending CLIENT_READY...'); + socket.send({ + component: 'pad', + type: 'CLIENT_READY', + padId, + sessionID: null, + token: 't.12345', + }); + logger.debug('waiting for CLIENT_VARS response...'); + const msg = await exports.getSocketEvent(socket, 'message'); + logger.debug('received CLIENT_VARS message'); + return msg; +}; diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index f10f63004..15f561774 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -2,97 +2,12 @@ const assert = require('assert').strict; const common = require('../common'); -const io = require('socket.io-client'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); const readOnlyManager = require('../../../node/db/ReadOnlyManager'); -const setCookieParser = require('set-cookie-parser'); const settings = require('../../../node/utils/Settings'); const socketIoRouter = require('../../../node/handler/SocketIORouter'); -const logger = common.logger; - -// Waits for and returns the next named socket.io event. Rejects if there is any error while waiting -// (unless waiting for that error event). -const getSocketEvent = async (socket, event) => { - const errorEvents = [ - 'error', - 'connect_error', - 'connect_timeout', - 'reconnect_error', - 'reconnect_failed', - ]; - const handlers = {}; - let timeoutId; - return new Promise((resolve, reject) => { - timeoutId = setTimeout(() => reject(new Error(`timed out waiting for ${event} event`)), 1000); - for (const event of errorEvents) { - handlers[event] = (errorString) => { - logger.debug(`socket.io ${event} event: ${errorString}`); - reject(new Error(errorString)); - }; - } - // This will overwrite one of the above handlers if the user is waiting for an error event. - handlers[event] = (...args) => { - logger.debug(`socket.io ${event} event`); - if (args.length > 1) return resolve(args); - resolve(args[0]); - }; - Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler)); - }).finally(() => { - clearTimeout(timeoutId); - Object.entries(handlers).forEach(([event, handler]) => socket.off(event, handler)); - }); -}; - -// Establishes a new socket.io connection. Passes the cookies from the `set-cookie` header(s) in -// `res` (which may be nullish) to the server. Returns a socket.io Socket object. -const connect = async (res) => { - // Convert the `set-cookie` header(s) into a `cookie` header. - const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true}); - const reqCookieHdr = Object.entries(resCookies).map( - ([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; '); - - logger.debug('socket.io connecting...'); - let padId = null; - if (res) { - padId = res.req.path.split('/p/')[1]; - } - const socket = io(`${common.baseUrl}/`, { - forceNew: true, // Different tests will have different query parameters. - path: '/socket.io', - // socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the - // express_sid cookie must be passed as a query parameter. - query: {cookie: reqCookieHdr, padId}, - }); - try { - await getSocketEvent(socket, 'connect'); - } catch (e) { - socket.close(); - throw e; - } - logger.debug('socket.io connected'); - - return socket; -}; - -// Helper function to exchange CLIENT_READY+CLIENT_VARS messages for the named pad. -// Returns the CLIENT_VARS message from the server. -const handshake = async (socket, padID) => { - logger.debug('sending CLIENT_READY...'); - socket.send({ - component: 'pad', - type: 'CLIENT_READY', - padId: padID, - sessionID: null, - token: 't.12345', - }); - logger.debug('waiting for CLIENT_VARS response...'); - const msg = await getSocketEvent(socket, 'message'); - logger.debug('received CLIENT_VARS message'); - return msg; -}; - describe(__filename, function () { this.timeout(30000); let agent; @@ -143,26 +58,26 @@ describe(__filename, function () { describe('Normal accesses', function () { it('!authn anonymous cookie /p/pad -> 200, ok', async function () { const res = await agent.get('/p/pad').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('!authn !cookie -> ok', async function () { - socket = await connect(null); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(null); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('!authn user /p/pad -> 200, ok', async function () { const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('authn user /p/pad -> 200, ok', async function () { settings.requireAuthentication = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); @@ -176,16 +91,16 @@ describe(__filename, function () { }; settings.requireAuthentication = authn; let res = await get('/p/pad'); - socket = await connect(res); - let clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + let clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); const readOnlyId = clientVars.data.readOnlyId; assert(readOnlyManager.isReadOnlyId(readOnlyId)); socket.close(); res = await get(`/p/${readOnlyId}`); - socket = await connect(res); - clientVars = await handshake(socket, readOnlyId); + socket = await common.connect(res); + clientVars = await common.handshake(socket, readOnlyId); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, true); }); @@ -195,8 +110,8 @@ describe(__filename, function () { settings.requireAuthentication = true; settings.requireAuthorization = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); it('supports pad names with characters that must be percent-encoded', async function () { @@ -208,8 +123,8 @@ describe(__filename, function () { settings.requireAuthorization = true; const encodedPadId = encodeURIComponent('päd'); const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'päd'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'päd'); assert.equal(clientVars.type, 'CLIENT_VARS'); }); }); @@ -219,31 +134,31 @@ describe(__filename, function () { settings.requireAuthentication = true; const res = await agent.get('/p/pad').expect(401); // Despite the 401, try to create the pad via a socket.io connection anyway. - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('authn anonymous read-only /p/pad -> 401, error', async function () { settings.requireAuthentication = true; let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); const readOnlyId = clientVars.data.readOnlyId; assert(readOnlyManager.isReadOnlyId(readOnlyId)); socket.close(); res = await agent.get(`/p/${readOnlyId}`).expect(401); // Despite the 401, try to read the pad via a socket.io connection anyway. - socket = await connect(res); - const message = await handshake(socket, readOnlyId); + socket = await common.connect(res); + const message = await common.handshake(socket, readOnlyId); assert.equal(message.accessStatus, 'deny'); }); it('authn !cookie -> error', async function () { settings.requireAuthentication = true; - socket = await connect(null); - const message = await handshake(socket, 'pad'); + socket = await common.connect(null); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('authorization bypass attempt -> error', async function () { @@ -253,9 +168,9 @@ describe(__filename, function () { settings.requireAuthorization = true; // First authenticate and establish a session. const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); + socket = await common.connect(res); // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. - const message = await handshake(socket, 'other-pad'); + const message = await common.handshake(socket, 'other-pad'); assert.equal(message.accessStatus, 'deny'); }); }); @@ -269,16 +184,16 @@ describe(__filename, function () { it("level='create' -> can create", async function () { authorize = () => 'create'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); it('level=true -> can create', async function () { authorize = () => true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); @@ -286,8 +201,8 @@ describe(__filename, function () { await padManager.getPad('pad'); // Create the pad. authorize = () => 'modify'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); @@ -295,31 +210,31 @@ describe(__filename, function () { authorize = () => 'create'; settings.editOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it("level='modify' settings.editOnly=false -> unable to create", async function () { authorize = () => 'modify'; settings.editOnly = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it("level='readOnly' -> unable to create", async function () { authorize = () => 'readOnly'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it("level='readOnly' -> unable to modify", async function () { await padManager.getPad('pad'); // Create the pad. authorize = () => 'readOnly'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, true); }); @@ -333,39 +248,39 @@ describe(__filename, function () { it('user.canCreate = true -> can create and modify', async function () { settings.users.user.canCreate = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); it('user.canCreate = false -> unable to create', async function () { settings.users.user.canCreate = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('user.readOnly = true -> unable to create', async function () { settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('user.readOnly = true -> unable to modify', async function () { await padManager.getPad('pad'); // Create the pad. settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, true); }); it('user.readOnly = false -> can create and modify', async function () { settings.users.user.readOnly = false; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const clientVars = await handshake(socket, 'pad'); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.data.readonly, false); }); @@ -373,8 +288,8 @@ describe(__filename, function () { settings.users.user.canCreate = true; settings.users.user.readOnly = true; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); }); @@ -389,8 +304,8 @@ describe(__filename, function () { settings.users.user.readOnly = true; authorize = () => 'create'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); it('user settings does not elevate level from authorize hook', async function () { @@ -398,8 +313,8 @@ describe(__filename, function () { settings.users.user.canCreate = true; authorize = () => 'readOnly'; const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await connect(res); - const message = await handshake(socket, 'pad'); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); assert.equal(message.accessStatus, 'deny'); }); }); @@ -430,7 +345,7 @@ describe(__filename, function () { socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { handleConnect(socket) { serverSocket = socket; } }()); - socket = await connect(); + socket = await common.connect(); assert(serverSocket != null); }); @@ -452,7 +367,7 @@ describe(__filename, function () { resolveDisconnected(); } }()); - socket = await connect(); + socket = await common.connect(); await connected; socket.close(); socket = null; @@ -474,7 +389,7 @@ describe(__filename, function () { socketIoRouter.addComponent(`${this.test.fullTitle()} #2`, new class extends Module { handleMessage(socket, message) { assert.fail('wrong handler called'); } }()); - socket = await connect(); + socket = await common.connect(); socket.send(want); assert.deepEqual(await got, want); }); @@ -492,7 +407,7 @@ describe(__filename, function () { socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { handleMessage(socket, msg) { return want; } }()); - socket = await connect(); + socket = await common.connect(); const got = await tx(socket, {component: this.test.fullTitle()}); assert.equal(got, want); }); @@ -504,7 +419,7 @@ describe(__filename, function () { socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { handleMessage(socket, msg) { throw new InjectedError(); } }()); - socket = await connect(); + socket = await common.connect(); await assert.rejects(tx(socket, {component: this.test.fullTitle()}), new InjectedError()); }); }); From 3132235f2c4d78f5800dfe55bf66a0c0ff3240a2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 22:29:37 -0400 Subject: [PATCH 245/322] tests: Rename `getSocketEvent()` to `waitForSocketEvent()` --- src/tests/backend/common.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/backend/common.js b/src/tests/backend/common.js index 12b5eb456..9c6711d3a 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.js @@ -79,7 +79,7 @@ exports.init = async function () { * @param {string} event - The socket.io Socket event to listen for. * @returns The argument(s) passed to the event handler. */ -exports.getSocketEvent = async (socket, event) => { +exports.waitForSocketEvent = async (socket, event) => { const errorEvents = [ 'error', 'connect_error', @@ -137,7 +137,7 @@ exports.connect = async (res = null) => { query: {cookie: reqCookieHdr, padId}, }); try { - await exports.getSocketEvent(socket, 'connect'); + await exports.waitForSocketEvent(socket, 'connect'); } catch (e) { socket.close(); throw e; @@ -164,7 +164,7 @@ exports.handshake = async (socket, padId) => { token: 't.12345', }); logger.debug('waiting for CLIENT_VARS response...'); - const msg = await exports.getSocketEvent(socket, 'message'); + const msg = await exports.waitForSocketEvent(socket, 'message'); logger.debug('received CLIENT_VARS message'); return msg; }; From 23a98e59463d9c26f78f82988d0e96708a3c0c6a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 22:28:12 -0400 Subject: [PATCH 246/322] tests: Refactor `waitForSocketEvent()` to improve readability --- src/tests/backend/common.js | 54 ++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/tests/backend/common.js b/src/tests/backend/common.js index 9c6711d3a..793828ac7 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.js @@ -87,27 +87,43 @@ exports.waitForSocketEvent = async (socket, event) => { 'reconnect_error', 'reconnect_failed', ]; - const handlers = {}; - let timeoutId; - return new Promise((resolve, reject) => { - timeoutId = setTimeout(() => reject(new Error(`timed out waiting for ${event} event`)), 1000); - for (const event of errorEvents) { - handlers[event] = (errorString) => { + const handlers = new Map(); + let cancelTimeout; + try { + const timeoutP = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error(`timed out waiting for ${event} event`)); + cancelTimeout = () => {}; + }, 1000); + cancelTimeout = () => { + clearTimeout(timeout); + resolve(); + cancelTimeout = () => {}; + }; + }); + const errorEventP = Promise.race(errorEvents.map((event) => new Promise((resolve, reject) => { + handlers.set(event, (errorString) => { logger.debug(`socket.io ${event} event: ${errorString}`); reject(new Error(errorString)); - }; - } - // This will overwrite one of the above handlers if the user is waiting for an error event. - handlers[event] = (...args) => { - logger.debug(`socket.io ${event} event`); - if (args.length > 1) return resolve(args); - resolve(args[0]); - }; - Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler)); - }).finally(() => { - clearTimeout(timeoutId); - Object.entries(handlers).forEach(([event, handler]) => socket.off(event, handler)); - }); + }); + }))); + const eventP = new Promise((resolve) => { + // This will overwrite one of the above handlers if the user is waiting for an error event. + handlers.set(event, (...args) => { + logger.debug(`socket.io ${event} event`); + if (args.length > 1) return resolve(args); + resolve(args[0]); + }); + }); + for (const [event, handler] of handlers) socket.on(event, handler); + // timeoutP and errorEventP are guaranteed to never resolve here (they can only reject), so the + // Promise returned by Promise.race() is guaranteed to resolve to the eventP value (if + // the event arrives). + return await Promise.race([timeoutP, errorEventP, eventP]); + } finally { + cancelTimeout(); + for (const [event, handler] of handlers) socket.off(event, handler); + } }; /** From 26675c5019d80001e477e21926e54488c2b85d2d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Oct 2021 16:58:28 -0400 Subject: [PATCH 247/322] chat: New `chatNewMessage` server-side hook --- CHANGELOG.md | 2 + doc/api/hooks_server-side.md | 17 +++ src/node/handler/PadMessageHandler.js | 1 + src/tests/backend/specs/chat.js | 160 ++++++++++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 src/tests/backend/specs/chat.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f74e1b8..b76219548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ * New `chatSendMessage` client-side hook that enables plugins to process the text before sending it to the server or augment the message object with custom metadata. + * New `chatNewMessage` server-side hook to process new chat messages before + they are saved to the database and relayed to users. # 1.8.14 diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 4836c4b73..0cf033220 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -853,3 +853,20 @@ exports.userLeave = async (hookName, {author, padId}) => { console.log(`${author} left pad ${padId}`); }; ``` + +## `chatNewMessage` + +Called from: `src/node/handler/PadMessageHandler.js` + +Called when a user (or plugin) generates a new chat message, just before it is +saved to the pad and relayed to all connected users. + +Context properties: + +* `message`: The chat message object. Plugins can mutate this object to change + the message text or add custom metadata to control how the message will be + rendered by the `chatNewMessage` client-side hook. The message's `authorId` + property can be trusted (the server overwrites any client-provided author ID + value with the user's actual author ID before this hook runs). +* `padId`: The pad's real (not read-only) identifier. +* `pad`: The pad's Pad object. diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 64027f6e4..71cafd0b0 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -364,6 +364,7 @@ exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt); padId = mt instanceof ChatMessage ? puId : padId; const pad = await padManager.getPad(padId); + await hooks.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); diff --git a/src/tests/backend/specs/chat.js b/src/tests/backend/specs/chat.js new file mode 100644 index 000000000..aefa64183 --- /dev/null +++ b/src/tests/backend/specs/chat.js @@ -0,0 +1,160 @@ +'use strict'; + +const ChatMessage = require('../../../static/js/ChatMessage'); +const {Pad} = require('../../../node/db/Pad'); +const assert = require('assert').strict; +const common = require('../common'); +const padManager = require('../../../node/db/PadManager'); +const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); + +const logger = common.logger; + +const checkHook = async (hookName, checkFn) => { + if (pluginDefs.hooks[hookName] == null) pluginDefs.hooks[hookName] = []; + await new Promise((resolve, reject) => { + pluginDefs.hooks[hookName].push({ + hook_fn: async (hookName, context) => { + if (checkFn == null) return; + logger.debug(`hook ${hookName} invoked`); + try { + // Make sure checkFn is called only once. + const _checkFn = checkFn; + checkFn = null; + await _checkFn(context); + } catch (err) { + reject(err); + return; + } + resolve(); + }, + }); + }); +}; + +const sendMessage = (socket, data) => { + socket.send({ + type: 'COLLABROOM', + component: 'pad', + data, + }); +}; + +const sendChat = (socket, message) => sendMessage(socket, {type: 'CHAT_MESSAGE', message}); + +describe(__filename, function () { + const padId = 'testChatPad'; + const hooksBackup = {}; + + before(async function () { + for (const [name, defs] of Object.entries(pluginDefs.hooks)) { + if (defs == null) continue; + hooksBackup[name] = defs; + } + }); + + beforeEach(async function () { + for (const [name, defs] of Object.entries(hooksBackup)) pluginDefs.hooks[name] = [...defs]; + for (const name of Object.keys(pluginDefs.hooks)) { + if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; + } + if (await padManager.doesPadExist(padId)) { + const pad = await padManager.getPad(padId); + await pad.remove(); + } + }); + + after(async function () { + Object.assign(pluginDefs.hooks, hooksBackup); + for (const name of Object.keys(pluginDefs.hooks)) { + if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; + } + }); + + describe('chatNewMessage hook', function () { + let authorId; + let socket; + + beforeEach(async function () { + socket = await common.connect(); + const {data: clientVars} = await common.handshake(socket, padId); + authorId = clientVars.userId; + }); + + afterEach(async function () { + socket.close(); + }); + + it('message', async function () { + const start = Date.now(); + await Promise.all([ + checkHook('chatNewMessage', ({message}) => { + assert(message != null); + assert(message instanceof ChatMessage); + assert.equal(message.authorId, authorId); + assert.equal(message.text, this.test.title); + assert(message.time >= start); + assert(message.time <= Date.now()); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('pad', async function () { + await Promise.all([ + checkHook('chatNewMessage', ({pad}) => { + assert(pad != null); + assert(pad instanceof Pad); + assert.equal(pad.id, padId); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('padId', async function () { + await Promise.all([ + checkHook('chatNewMessage', (context) => { + assert.equal(context.padId, padId); + }), + sendChat(socket, {text: this.test.title}), + ]); + }); + + it('mutations propagate', async function () { + const listen = async (type) => await new Promise((resolve) => { + const handler = (msg) => { + if (msg.type !== 'COLLABROOM') return; + if (msg.data == null || msg.data.type !== type) return; + resolve(msg.data); + socket.off('message', handler); + }; + socket.on('message', handler); + }); + + const modifiedText = `${this.test.title} `; + const customMetadata = {foo: this.test.title}; + await Promise.all([ + checkHook('chatNewMessage', ({message}) => { + message.text = modifiedText; + message.customMetadata = customMetadata; + }), + (async () => { + const {message} = await listen('CHAT_MESSAGE'); + assert(message != null); + assert.equal(message.text, modifiedText); + assert.deepEqual(message.customMetadata, customMetadata); + })(), + sendChat(socket, {text: this.test.title}), + ]); + // Simulate fetch of historical chat messages when a pad is first loaded. + await Promise.all([ + (async () => { + const {messages: [message]} = await listen('CHAT_MESSAGES'); + assert(message != null); + assert.equal(message.text, modifiedText); + assert.deepEqual(message.customMetadata, customMetadata); + })(), + sendMessage(socket, {type: 'GET_CHAT_MESSAGES', start: 0, end: 0}), + ]); + }); + }); +}); From 23f963c9fe085a119579fb572cbe18da8526fdd5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Nov 2021 01:24:32 -0400 Subject: [PATCH 248/322] chat: Improve name mention detection --- src/static/js/chat.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 088d16111..f16be57f8 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -22,6 +22,9 @@ const Tinycon = require('tinycon/tinycon'); const hooks = require('./pluginfw/hooks'); const padeditor = require('./pad_editor').padeditor; +// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463 +const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); + exports.chat = (() => { let isStuck = false; let userAndChat = false; @@ -155,10 +158,11 @@ exports.chat = (() => { // does the user already have the chatbox open? const chatOpen = $('#chatbox').hasClass('visible'); - // does this message contain this user's name? (is the curretn user mentioned?) - const myName = $('#myusernameedit').val(); + // does this message contain this user's name? (is the current user mentioned?) const wasMentioned = - ctx.text.toLowerCase().indexOf(myName.toLowerCase()) !== -1 && myName !== 'undefined'; + msg.authorId !== window.clientVars.userId && + ctx.authorName !== html10n.get('pad.userlist.unnamed') && + normalize(ctx.text).includes(normalize(ctx.authorName)); // If the user was mentioned, make the message sticky if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) { From 51907015ede3de57e90a0801d248d7d0067c1fe6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Nov 2021 01:27:06 -0400 Subject: [PATCH 249/322] chat: Fix gritter duration --- src/static/js/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index f16be57f8..86b025097 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -205,7 +205,7 @@ exports.chat = (() => { // to not introduce an XSS vulnerability by adding unescaped user input. .append($('

    ').html(ctx.text).contents()), sticky: ctx.sticky, - time: 5000, + time: ctx.duration, position: 'bottom', class_name: 'chat-gritter-msg', }); From 99b7bdd176fa684d45a6033cef856ee0a12f8e34 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Nov 2021 01:38:40 -0400 Subject: [PATCH 250/322] chat: Set timestamp CSS `font-size: smaller;` --- src/static/css/pad/chat.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/static/css/pad/chat.css b/src/static/css/pad/chat.css index 9e0a17dac..bfda0109a 100644 --- a/src/static/css/pad/chat.css +++ b/src/static/css/pad/chat.css @@ -98,7 +98,12 @@ #chattext .time { float: right; font-style: italic; - font-size: .85rem; + /* + * 'smaller' is relative to the parent element, so if the parent has its own + * 'font-size: smaller' rule then the timestamp will become even smaller (as + * desired). + */ + font-size: smaller; opacity: .8; margin-left: 3px; margin-right: 2px; From 6bf8142221e64679ea4ec33361a11ee5705243e0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Nov 2021 04:56:46 -0400 Subject: [PATCH 251/322] chat: Translate newly created message entries --- src/static/js/chat.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 86b025097..63c17c153 100755 --- a/src/static/js/chat.js +++ b/src/static/js/chat.js @@ -189,6 +189,7 @@ exports.chat = (() => { .append($('
    ').html(ctx.text).contents()); if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton'); else $('#chattext').append(chatMsg); + chatMsg.each((i, e) => html10n.translateElement(html10n.translations, e)); // should we increment the counter?? if (increment && !isHistoryAdd) { @@ -198,12 +199,14 @@ exports.chat = (() => { $('#chatcounter').text(count); if (!chatOpen && ctx.duration > 0) { + const text = $('

    ') + .append($('').addClass('author-name').text(ctx.authorName)) + // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted + // to not introduce an XSS vulnerability by adding unescaped user input. + .append($('

    ').html(ctx.text).contents()); + text.each((i, e) => html10n.translateElement(html10n.translations, e)); $.gritter.add({ - text: $('

    ') - .append($('').addClass('author-name').text(ctx.authorName)) - // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted - // to not introduce an XSS vulnerability by adding unescaped user input. - .append($('

    ').html(ctx.text).contents()), + text, sticky: ctx.sticky, time: ctx.duration, position: 'bottom', From cc4772a99e6c5af10f21790b3f1c58124bd4df8a Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 4 Nov 2021 13:03:08 +0100 Subject: [PATCH 252/322] Localisation updates from https://translatewiki.net. --- src/locales/fi.json | 7 ++++--- src/locales/ko.json | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/locales/fi.json b/src/locales/fi.json index 3c750b4a6..c0c827e78 100644 --- a/src/locales/fi.json +++ b/src/locales/fi.json @@ -8,6 +8,7 @@ "MITO", "Maantietäjä", "Macofe", + "Markus Mikkonen", "MrTapsa", "Nedergard", "Nike", @@ -23,7 +24,7 @@ "admin_plugins": "Lisäosien hallinta", "admin_plugins.available": "Saatavilla olevat liitännäiset", "admin_plugins.available_not-found": "Lisäosia ei löytynyt.", - "admin_plugins.available_install.value": "Lataa", + "admin_plugins.available_install.value": "Asenna", "admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia", "admin_plugins.description": "Kuvaus", "admin_plugins.installed": "Asennetut laajennukset", @@ -47,7 +48,7 @@ "admin_settings": "Asetukset", "admin_settings.current": "Nykyinen kokoonpano", "admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista", - "admin_settings.current_save.value": "Tallenna Asetukset", + "admin_settings.current_save.value": "Tallenna asetukset", "admin_settings.page-title": "asetukset - Etherpad", "index.newPad": "Uusi muistio", "index.createOpenPad": "tai luo tai avaa muistio nimellä:", @@ -71,7 +72,7 @@ "pad.colorpicker.save": "Tallenna", "pad.colorpicker.cancel": "Peru", "pad.loading": "Ladataan…", - "pad.noCookie": "Evästettä ei löytynyt. Ole hyvä, ja salli evästeet selaimessasi!", + "pad.noCookie": "Evästettä ei löytynyt. Ole hyvä, ja salli evästeet selaimessasi! Istuntoasi ja asetuksiasi ei tulla tallentamaan vierailujen välillä. Tämä voi johtua siitä, että Etherpad on sisällytetty iFrameen joissain selaimissa. Varmistathan että Etherpad on samalla subdomainilla/domainilla kuin ylätason iFrame", "pad.permissionDenied": "Käyttöoikeutesi eivät riitä tämän muistion käyttämiseen.", "pad.settings.padSettings": "Muistion asetukset", "pad.settings.myView": "Oma näkymä", diff --git a/src/locales/ko.json b/src/locales/ko.json index 04c279a05..3fae86e93 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -9,6 +9,7 @@ "Revi", "SeoJeongHo", "Ykhwong", + "그냥기여자", "아라" ] }, @@ -132,6 +133,7 @@ "pad.chat.loadmessages": "더 많은 메시지 불러오기", "pad.chat.stick.title": "채팅을 화면에 고정", "pad.chat.writeMessage.placeholder": "여기에 메시지를 적으십시오", + "timeslider.followContents": "패드 콘텐츠의 갱신 주시하기", "timeslider.pageTitle": "{{appTitle}} 시간슬라이더", "timeslider.toolbar.returnbutton": "패드로 돌아가기", "timeslider.toolbar.authors": "저자:", From edf99dc811eb29412af13e66ece5d788c2c8c1d6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 5 Nov 2021 17:08:17 -0400 Subject: [PATCH 253/322] docs: Document the `import` server-side hook --- doc/api/hooks_server-side.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 0cf033220..c63a81a08 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -807,6 +807,20 @@ Example: exports.exportEtherpadAdditionalContent = () => ['comments']; ``` +## `import` + +Called from: `src/node/handler/ImportHandler.js` + +Called when a user submits a document for import, before the document is +converted to HTML. The hook function should return `undefined` (or an empty +list) if it did NOT convert convert the document to HTML. + +Context properties: + +* `destFile`: The destination HTML filename. +* `fileEnding`: The lower-cased filename extension from `srcFile`. +* `srcFile`: The document to convert. + ## `userJoin` Called from: `src/node/handler/PadMessageHandler.js` From caf4e9f28c0c8c8d4eea3c5b230c74bf5c3a98a4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 5 Nov 2021 17:15:52 -0400 Subject: [PATCH 254/322] ImportHandler: Use truthiness to signal conversion handled --- doc/api/hooks_server-side.md | 4 ++-- src/node/handler/ImportHandler.js | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index c63a81a08..23428eb4e 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -812,8 +812,8 @@ exports.exportEtherpadAdditionalContent = () => ['comments']; Called from: `src/node/handler/ImportHandler.js` Called when a user submits a document for import, before the document is -converted to HTML. The hook function should return `undefined` (or an empty -list) if it did NOT convert convert the document to HTML. +converted to HTML. The hook function should return a truthy value if the hook +function elected to convert the document to HTML. Context properties: diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index 7cc62e113..7e3313524 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -142,11 +142,8 @@ const doImport = async (req, res, padId) => { } const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); - - // Logic for allowing external Import Plugins - const result = await hooks.aCallAll('import', {srcFile, destFile, fileEnding}); - const importHandledByPlugin = (result.length > 0); // This feels hacky and wrong.. - + const importHandledByPlugin = + (await hooks.aCallAll('import', {srcFile, destFile, fileEnding})).some((x) => x); const fileIsEtherpad = (fileEnding === '.etherpad'); const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsTXT = (fileEnding === '.txt'); From fe87e2df874379bc9f29ebc14d4765afb4fb7150 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 5 Nov 2021 17:16:40 -0400 Subject: [PATCH 255/322] ImportHandler: Pass the pad ID to the `import` hook This enables plugins to log the pad ID if desired. --- doc/api/hooks_server-side.md | 1 + src/node/handler/ImportHandler.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 23428eb4e..02c7e2fe6 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -819,6 +819,7 @@ Context properties: * `destFile`: The destination HTML filename. * `fileEnding`: The lower-cased filename extension from `srcFile`. +* `padId`: The identifier of the destination pad. * `srcFile`: The document to convert. ## `userJoin` diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index 7e3313524..c865dcf98 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -143,7 +143,7 @@ const doImport = async (req, res, padId) => { const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); const importHandledByPlugin = - (await hooks.aCallAll('import', {srcFile, destFile, fileEnding})).some((x) => x); + (await hooks.aCallAll('import', {srcFile, destFile, fileEnding, padId})).some((x) => x); const fileIsEtherpad = (fileEnding === '.etherpad'); const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsTXT = (fileEnding === '.txt'); From 7a0d8cb52e57de14a0b33aec25c5d8020106a116 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 2 Nov 2021 22:58:31 -0400 Subject: [PATCH 256/322] pad: Use URLSearchParams to process search params Also unexport `getUrlVars` because it isn't used outside the file. --- src/static/js/pad.js | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 1d33a3c99..d2d75f7c7 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -157,27 +157,15 @@ const getParams = () => { // Then URL applied stuff const params = getUrlVars(); - for (const setting of getParameters) { - const value = params[setting.name]; - + const value = params.get(setting.name); if (value && (value === setting.checkVal || setting.checkVal == null)) { setting.callback(value); } } }; -const getUrlVars = () => { - const vars = []; - let hash; - const hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); - for (let i = 0; i < hashes.length; i++) { - hash = hashes[i].split('='); - vars.push(hash[0]); - vars[hash[0]] = hash[1]; - } - return vars; -}; +const getUrlVars = () => new URL(window.location.href).searchParams; const sendClientReady = (isReconnect) => { let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); @@ -744,6 +732,5 @@ exports.baseURL = ''; exports.settings = settings; exports.randomString = randomString; exports.getParams = getParams; -exports.getUrlVars = getUrlVars; exports.pad = pad; exports.init = init; From 56cb08f4c515e89b09b1dc98bd514926d946c833 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 3 Nov 2021 00:50:51 -0400 Subject: [PATCH 257/322] pad: Pass color and display name in `CLIENT_READY` --- src/node/handler/PadMessageHandler.js | 15 +++++++++++---- src/static/js/collab_client.js | 8 -------- src/static/js/pad.js | 11 +++++++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 71cafd0b0..fa8e45124 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -814,6 +814,7 @@ const handleClientReady = async (socket, message) => { const sessionInfo = sessioninfos[socket.id]; // Check if the user has already disconnected. if (sessionInfo == null) return; + assert(sessionInfo.author); const padIds = await readOnlyManager.getIds(sessionInfo.auth.padID); sessionInfo.padId = padIds.padId; @@ -823,10 +824,16 @@ const handleClientReady = async (socket, message) => { await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context. - // get all authordata of this new user - assert(sessionInfo.author); - const {colorId: authorColorId, name: authorName} = - await authorManager.getAuthor(sessionInfo.author); + let {colorId: authorColorId, name: authorName} = message.userInfo || {}; + if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) { + messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`); + authorColorId = null; + } + await Promise.all([ + authorName && authorManager.setAuthorName(sessionInfo.author, authorName), + authorColorId && authorManager.setAuthorColorId(sessionInfo.author, authorColorId), + ]); + ({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author)); // load the pad-object from the database const pad = await padManager.getPad(sessionInfo.padId); diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.js index 837fc3ba0..849fff5fc 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.js @@ -245,14 +245,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) } else if (msg.type === 'USER_NEWINFO') { const userInfo = msg.userInfo; const id = userInfo.userId; - - // Avoid a race condition when setting colors. If our color was set by a - // query param, ignore our own "new user" message's color value. - if (id === initialUserInfo.userId && initialUserInfo.globalUserColor) { - msg.userInfo.colorId = initialUserInfo.globalUserColor; - } - - if (userSet[id]) { userSet[id] = userInfo; callbacks.onUpdateUserInfo(userInfo); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index d2d75f7c7..306c2b191 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -184,12 +184,23 @@ const sendClientReady = (isReconnect) => { Cookies.set('token', token, {expires: 60}); } + // If known, propagate the display name and color to the server in the CLIENT_READY message. This + // allows the server to include the values in its reply CLIENT_VARS message (which avoids + // initialization race conditions) and in the USER_NEWINFO messages sent to the other users on the + // pad (which enables them to display a user join notification with the correct name). + const params = getUrlVars(); + const userInfo = { + colorId: params.get('userColor'), + name: params.get('userName'), + }; + const msg = { component: 'pad', type: 'CLIENT_READY', padId, sessionID: Cookies.get('sessionID'), token, + userInfo, }; // this is a reconnect, lets tell the server our revisionnumber From 088fb14784abb23cabe0a9247156d05e720df6ff Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 3 Nov 2021 11:32:35 +0000 Subject: [PATCH 258/322] fix: upgrade express-rate-limit from 5.4.1 to 5.5.0 Snyk has created this PR to upgrade express-rate-limit from 5.4.1 to 5.5.0. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=referral&page=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index d8ea3b440..5f602c433 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2328,9 +2328,9 @@ } }, "express-rate-limit": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.4.1.tgz", - "integrity": "sha512-ZQh2h3qiu7wWdvWNYHznBhaOp2ZIXNnT4hl2Ff608STeWtCuJ251NzqQlk7mo5wnO2HmrydBYHuVA9Z3S3ZtXg==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.0.tgz", + "integrity": "sha512-/1mrKggjXMxd1/ghPub5N3d36u5VlK8KjbQFQLxYub09BWSSgSXMQbXgFiIW0BYxjM49YCj8bkihONZR2U4+mQ==" }, "express-session": { "version": "1.17.2", diff --git a/src/package.json b/src/package.json index dc0f0c69e..f09968be5 100644 --- a/src/package.json +++ b/src/package.json @@ -39,7 +39,7 @@ "etherpad-require-kernel": "^1.0.15", "etherpad-yajsml": "0.0.12", "express": "4.17.1", - "express-rate-limit": "5.4.1", + "express-rate-limit": "5.5.0", "express-session": "1.17.2", "fast-deep-equal": "^3.1.3", "find-root": "1.1.0", From 87a490d4564ec6afa33a999b9c21741c9a877687 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 6 Nov 2021 01:05:55 -0400 Subject: [PATCH 259/322] docs: `import` hook `fileEnding` has leading period --- doc/api/hooks_server-side.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 02c7e2fe6..c99ea58aa 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -818,7 +818,8 @@ function elected to convert the document to HTML. Context properties: * `destFile`: The destination HTML filename. -* `fileEnding`: The lower-cased filename extension from `srcFile`. +* `fileEnding`: The lower-cased filename extension from `srcFile` **with leading + period** (examples: `'.docx'`, `'.html'`, `'.etherpad'`). * `padId`: The identifier of the destination pad. * `srcFile`: The document to convert. From caefd2184a2b750415944b5c3508a11198319391 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 7 Nov 2021 01:30:39 -0400 Subject: [PATCH 260/322] PadMessageHandler: Don't fire `userLeave` if user hasn't joined yet --- src/node/handler/PadMessageHandler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index fa8e45124..c9f8856b4 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -131,7 +131,8 @@ exports.handleDisconnect = async (socket) => { stats.meter('disconnects').mark(); const session = sessioninfos[socket.id]; delete sessioninfos[socket.id]; - if (!session || !session.author) return; + // session.padId can be nullish if the user disconnects before sending CLIENT_READY. + if (!session || !session.author || !session.padId) return; const {session: {user} = {}} = socket.client.request; /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ accessLogger.info('[LEAVE]' + From ad6de52eb251d031e81c77af77e8bef260e919b5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 02:40:01 -0400 Subject: [PATCH 261/322] Changeset: More JSDoc comment improvements --- src/static/js/Changeset.js | 82 +++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 8465a5d7f..dea6f6b96 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -24,6 +24,12 @@ const AttributePool = require('./AttributePool'); +/** + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute + */ + /** * This method is called whenever there is an error in the sync process. * @@ -40,7 +46,8 @@ exports.error = (msg) => { * throw an exception. * * @param {boolean} b - assertion condition - * @param {...string} msgParts - error message to include in the exception + * @param {...any} msgParts - error message to include in the exception + * @type {(b: boolean, ...msgParts: any[]) => asserts b} */ exports.assert = (b, ...msgParts) => { if (!b) { @@ -83,9 +90,9 @@ exports.numToString = (num) => num.toString(36).toLowerCase(); * @property {string} attribs - Identifiers of attributes to apply to the text, represented as a * repeated (zero or more) sequence of asterisk followed by a non-negative base-36 (lower-case) * integer. For example, '*2*1o' indicates that attributes 2 and 60 apply to the text affected - * by the operation. The identifiers come from the document's attribution pool. This is the - * empty string for remove ('-') operations. For keep ('=') operations, the attributes are - * merged with the base text's existing attributes: + * by the operation. The identifiers come from the document's attribute pool. This is the empty + * string for remove ('-') operations. For keep ('=') operations, the attributes are merged with + * the base text's existing attributes: * - A keep op attribute with a non-empty value replaces an existing base text attribute that * has the same key. * - A keep op attribute with an empty value is interpreted as an instruction to remove an @@ -360,6 +367,15 @@ exports.smartOpAssembler = () => { lastOpcode = op.opcode; }; + /** + * Generates operations from the given text and attributes. + * + * @param {('-'|'+'|'=')} opcode - The operator to use. + * @param {string} text - The text to remove/add/keep. + * @param {(string|Attribute[])} attribs - The attributes to apply to the operations. See + * `makeAttribsString`. + * @param {?AttributePool} pool - See `makeAttribsString`. + */ const appendOpWithText = (opcode, text, attribs, pool) => { const op = exports.newOp(opcode); op.attribs = exports.makeAttribsString(opcode, attribs, pool); @@ -593,6 +609,18 @@ exports.stringAssembler = () => { }; }; +/** + * @typedef {object} StringArrayLike + * @property {(i: number) => string} get - Returns the line at index `i`. + * @property {(number|(() => number))} length - The number of lines, or a method that returns the + * number of lines. + * @property {(((start?: number, end?: number) => string[])|undefined)} slice - Like + * `Array.prototype.slice()`. Optional if the return value of the `removeLines` method is not + * needed. + * @property {(i: number, d?: number, ...l: string[]) => any} splice - Like + * `Array.prototype.splice()`. + */ + /** * Class to iterate and modify texts which have several lines. It is used for applying Changesets on * arrays of lines. @@ -615,13 +643,7 @@ exports.stringAssembler = () => { */ /** - * @param {string[]} lines - Lines to mutate (in place). This does not need to be an array as long - * as it supports certain methods/properties: - * - `get(i)`: Returns the line at index `i`. - * - `length`: Number like `Array.prototype.length`, or a method that returns the length. - * - `slice(...)`: Like `Array.prototype.slice(...)`. Optional if the return value of the - * `removeLines` method is not needed. - * - `splice(...)`: Like `Array.prototype.splice(...)`. + * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). * @returns {TextLinesMutator} */ exports.textLinesMutator = (lines) => { @@ -1487,7 +1509,7 @@ exports.compose = (cs1, cs2, pool) => { * Returns a function that tests if a string of attributes (e.g. '*3*4') contains a given attribute * key,value that is already present in the pool. * - * @param {[string, string]} attribPair - Array of attribute pairs. + * @param {Attribute} attribPair - `[key, value]` pair of strings. * @param {AttributePool} pool - Attribute pool * @returns {Function} */ @@ -1921,8 +1943,14 @@ exports.builder = (oldLen) => { const self = { /** - * @param attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in - * latter case). + * @param {number} N - Number of characters to keep. + * @param {number} L - Number of newlines among the `N` characters. If positive, the last + * character must be a newline. + * @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' + * (no pool needed in latter case). + * @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of + * attribute key, value pairs. + * @returns {Builder} this */ keep: (N, L, attribs, pool) => { o.opcode = '='; @@ -1932,15 +1960,40 @@ exports.builder = (oldLen) => { assem.append(o); return self; }, + + /** + * @param {string} text - Text to keep. + * @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' + * (no pool needed in latter case). + * @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of + * attribute key, value pairs. + * @returns {Builder} this + */ keepText: (text, attribs, pool) => { assem.appendOpWithText('=', text, attribs, pool); return self; }, + + /** + * @param {string} text - Text to insert. + * @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' + * (no pool needed in latter case). + * @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of + * attribute key, value pairs. + * @returns {Builder} this + */ insert: (text, attribs, pool) => { assem.appendOpWithText('+', text, attribs, pool); charBank.append(text); return self; }, + + /** + * @param {number} N - Number of characters to remove. + * @param {number} L - Number of newlines among the `N` characters. If positive, the last + * character must be a newline. + * @returns {Builder} this + */ remove: (N, L) => { o.opcode = '-'; o.attribs = ''; @@ -1949,6 +2002,7 @@ exports.builder = (oldLen) => { assem.append(o); return self; }, + toString: () => { assem.endDocument(); const newLen = oldLen + assem.getLengthChange(); From 9f66ffbc3b1d61f9dc9113ca71a23d5d7d3107f3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 10 Oct 2021 19:39:58 -0400 Subject: [PATCH 262/322] Pad: Delete no-op call to `SmartOpAssembler.appendOpWithText()` --- src/node/db/Pad.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 2ec573744..677e9c014 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -499,7 +499,6 @@ Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) { // based on Changeset.makeSplice const assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('=', ''); Changeset.appendATextToAssembler(oldAText, assem); assem.endDocument(); From b3215992c9cb807ce3b62a540e4307a20fb06f73 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 21 Oct 2021 00:49:35 -0400 Subject: [PATCH 263/322] changesettracker: Delete IE8 compatibility code --- src/static/js/changesettracker.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.js index 6a132247c..94bc5071b 100644 --- a/src/static/js/changesettracker.js +++ b/src/static/js/changesettracker.js @@ -136,15 +136,6 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => { // that includes old submittedChangeset toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool); } else { - // add forEach function to Array.prototype for IE8 - if (!('forEach' in Array.prototype)) { - Array.prototype.forEach = function (action, that /* opt*/) { - for (let i = 0, n = this.length; i < n; i++) { - if (i in this) action.call(that, this[i], i, this); - } - }; - } - // Get my authorID const authorId = parent.parent.pad.myUserInfo.userId; From 43dae4cb1d121817a8bd2f749961af5fe3a82c57 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 27 Sep 2021 16:36:42 -0400 Subject: [PATCH 264/322] Changeset: Delete unused index parameters in `applyZip()` --- src/static/js/Changeset.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index dea6f6b96..e483afbac 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -986,9 +986,7 @@ exports.textLinesMutator = (lines) => { * Apply operations to other operations. * * @param {string} in1 - first Op string - * @param {number} idx1 - integer where 1st iterator should start * @param {string} in2 - second Op string - * @param {number} idx2 - integer where 2nd iterator should start * @param {Function} func - Callback that applies an operation to another operation. Will be called * multiple times depending on the number of operations in `in1` and `in2`. `func` has signature * `f(op1, op2, opOut)`: @@ -1005,9 +1003,9 @@ exports.textLinesMutator = (lines) => { * other out), `opOut.opcode` MUST be set to the empty string. * @returns {string} the integrated changeset */ -exports.applyZip = (in1, idx1, in2, idx2, func) => { - const iter1 = exports.opIterator(in1, idx1); - const iter2 = exports.opIterator(in2, idx2); +exports.applyZip = (in1, in2, func) => { + const iter1 = exports.opIterator(in1); + const iter2 = exports.opIterator(in2); const assem = exports.smartOpAssembler(); const op1 = exports.newOp(); const op2 = exports.newOp(); @@ -1315,7 +1313,7 @@ exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { exports.applyToAttribution = (cs, astr, pool) => { const unpacked = exports.unpack(cs); - return exports.applyZip(astr, 0, unpacked.ops, 0, + return exports.applyZip(astr, unpacked.ops, (op1, op2, opOut) => exports._slicerZipperFunc(op1, op2, opOut, pool)); }; @@ -1486,7 +1484,7 @@ exports.compose = (cs1, cs2, pool) => { const bankIter2 = exports.stringIterator(unpacked2.charBank); const bankAssem = exports.stringAssembler(); - const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { + const newOps = exports.applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { const op1code = op1.opcode; const op2code = op2.opcode; if (op1code === '+' && op2code === '-') { @@ -2265,7 +2263,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); - const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { + const newOps = exports.applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { if (op1.opcode === '+' || op2.opcode === '+') { let whichToDo; if (op2.opcode !== '+') { @@ -2437,7 +2435,7 @@ exports.composeWithDeletions = (cs1, cs2, pool) => { const bankIter2 = exports.stringIterator(unpacked2.charBank); const bankAssem = exports.stringAssembler(); - const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => { + const newOps = exports.applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { const op1code = op1.opcode; const op2code = op2.opcode; if (op1code === '+' && op2code === '-') { From 0fd2a4678316fdc06d5d1a52320d92847a7d275a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 20 Oct 2021 19:15:30 -0400 Subject: [PATCH 265/322] Changeset: Remove unused start index parameter for `opIterator()` --- CHANGELOG.md | 2 ++ src/static/js/Changeset.js | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b76219548..da1fb2068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ * `author`: Deprecated; use the new `authorId` property instead. * `readonly`: Deprecated; use the new `readOnly` property instead. * `rev`: Deprecated. + * Changes to the `src/static/js/Changeset.js` library: + * `opIterator()`: The unused start index parameter has been removed. ### Notable enhancements diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index e483afbac..796ae9130 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -142,13 +142,11 @@ exports.newLen = (cs) => exports.unpack(cs).newLen; * Creates an iterator which decodes string changeset operations. * * @param {string} opsStr - String encoding of the change operations to perform. - * @param {number} [optStartIndex=0] - From where in the string should the iterator start. * @returns {OpIter} Operator iterator object. */ -exports.opIterator = (opsStr, optStartIndex) => { +exports.opIterator = (opsStr) => { const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - const startIndex = (optStartIndex || 0); - let curIndex = startIndex; + let curIndex = 0; let prevIndex = curIndex; const nextRegexMatch = () => { @@ -2037,7 +2035,7 @@ exports.makeAttribsString = (opcode, attribs, pool) => { * Like "substring" but on a single-line attribution string. */ exports.subattribution = (astr, start, optEnd) => { - const iter = exports.opIterator(astr, 0); + const iter = exports.opIterator(astr); const assem = exports.smartOpAssembler(); const attOp = exports.newOp(); const csOp = exports.newOp(); From dd8ec4e291188c48448021d651508d3fe3ee3466 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 20 Oct 2021 19:15:30 -0400 Subject: [PATCH 266/322] Changeset: Remove unused `lastIndex()` method from op iterator --- CHANGELOG.md | 3 ++- src/static/js/Changeset.js | 9 --------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da1fb2068..450a2d8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,8 @@ * `readonly`: Deprecated; use the new `readOnly` property instead. * `rev`: Deprecated. * Changes to the `src/static/js/Changeset.js` library: - * `opIterator()`: The unused start index parameter has been removed. + * `opIterator()`: The unused start index parameter has been removed, as has + the unused `lastIndex()` method on the returned object. ### Notable enhancements diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 796ae9130..0d3d9991c 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -134,7 +134,6 @@ exports.newLen = (cs) => exports.unpack(cs).newLen; * * @typedef {object} OpIter * @property {Function} hasNext - - * @property {Function} lastIndex - * @property {Function} next - */ @@ -146,14 +145,9 @@ exports.newLen = (cs) => exports.unpack(cs).newLen; */ exports.opIterator = (opsStr) => { const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - let curIndex = 0; - let prevIndex = curIndex; const nextRegexMatch = () => { - prevIndex = curIndex; - regex.lastIndex = curIndex; const result = regex.exec(opsStr); - curIndex = regex.lastIndex; if (result[0] === '?') { exports.error('Hit error opcode in op stream'); } @@ -178,12 +172,9 @@ exports.opIterator = (opsStr) => { const hasNext = () => !!(regexResult[0]); - const lastIndex = () => prevIndex; - return { next, hasNext, - lastIndex, }; }; From 5283bfc14a4a18796a346797cc90a97ecb2c5ffc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Nov 2021 18:30:54 -0400 Subject: [PATCH 267/322] linestylefilter: Delete unused arguments --- src/static/js/linestylefilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 254168990..ac8df82f4 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -131,7 +131,7 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool linestylefilter, text: txt, class: cls, - }, ' ', ' ', ''); + }); const disableAuthors = (disableAuthColorForThisLine == null || disableAuthColorForThisLine.length === 0) ? false : disableAuthColorForThisLine[0]; while (txt.length > 0) { From cc688f7367d15935a442415c67e5a56a1c346356 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 3 Nov 2021 03:08:08 -0400 Subject: [PATCH 268/322] ace: Delete dead code --- doc/api/editorInfo.md | 3 --- src/static/js/ace.js | 6 ------ src/static/js/ace2_inner.js | 28 ---------------------------- 3 files changed, 37 deletions(-) diff --git a/doc/api/editorInfo.md b/doc/api/editorInfo.md index 5a8551eba..834f5ac3c 100644 --- a/doc/api/editorInfo.md +++ b/doc/api/editorInfo.md @@ -29,9 +29,6 @@ Returns the `rep` object. ## editorInfo.ace_applyPreparedChangesetToBase() ## editorInfo.ace_setUserChangeNotificationCallback(f) ## editorInfo.ace_setAuthorInfo(author, info) -## editorInfo.ace_setAuthorSelectionRange(author, start, end) -## editorInfo.ace_getUnhandledErrors() -## editorInfo.ace_getDebugProperty(prop) ## editorInfo.ace_fastIncorp(?) ## editorInfo.ace_isCaret(?) ## editorInfo.ace_getLineAndCharForPoint(?) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index efb38077f..b0a042570 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -120,7 +120,6 @@ const Ace2Editor = function () { 'applyPreparedChangesetToBase', 'setUserChangeNotificationCallback', 'setAuthorInfo', - 'setAuthorSelectionRange', 'callWithAce', 'execCommand', 'replaceRange', @@ -137,8 +136,6 @@ const Ace2Editor = function () { this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n'; - this.getDebugProperty = (prop) => info.ace_getDebugProperty(prop); - this.getInInternationalComposition = () => loaded ? info.ace_getInInternationalComposition() : null; @@ -152,9 +149,6 @@ const Ace2Editor = function () { // changes, and modify the changeset to be applied by applyPreparedChangesetToBase accordingly. this.prepareUserChangeset = () => loaded ? info.ace_prepareUserChangeset() : null; - // returns array of {error: , time: +new Date()} - this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : []; - const addStyleTagsFor = (doc, files) => { for (const file of files) { const link = doc.createElement('link'); diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 4672751dc..675427019 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -50,8 +50,6 @@ function Ace2Inner(editorInfo, cssManagers) { const FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; const SELECT_BUTTON_CLASS = 'selected'; - const caughtErrors = []; - let thisAuthor = ''; let disposed = false; @@ -375,13 +373,6 @@ function Ace2Inner(editorInfo, cssManagers) { }); cleanExit = true; - } catch (e) { - caughtErrors.push( - { - error: e, - time: +new Date(), - }); - throw e; } finally { const cs = currentCallStack; if (cleanExit) { @@ -695,28 +686,9 @@ function Ace2Inner(editorInfo, cssManagers) { editorInfo.ace_setAuthorInfo = (author, info) => { setAuthorInfo(author, info); }; - editorInfo.ace_setAuthorSelectionRange = (author, start, end) => { - changesetTracker.setAuthorSelectionRange(author, start, end); - }; - - editorInfo.ace_getUnhandledErrors = () => caughtErrors.slice(); editorInfo.ace_getDocument = () => document; - editorInfo.ace_getDebugProperty = (prop) => { - if (prop === 'debugger') { - // obfuscate "eval" so as not to scare yuicompressor - window['ev' + 'al']('debugger'); - } else if (prop === 'rep') { - return rep; - } else if (prop === 'window') { - return window; - } else if (prop === 'document') { - return document; - } - return undefined; - }; - const now = () => Date.now(); const newTimeLimit = (ms) => { From cb95e5907cae28a18055d3b0bcaf99c5b3627b1e Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 8 Nov 2021 13:04:14 +0100 Subject: [PATCH 269/322] Localisation updates from https://translatewiki.net. --- src/locales/sd.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/sd.json b/src/locales/sd.json index 2d2d4bcd3..a167196f9 100644 --- a/src/locales/sd.json +++ b/src/locales/sd.json @@ -49,7 +49,7 @@ "pad.modals.cancel": "رد", "pad.modals.userdup": "هڪ ٻي دري ۾ کليل", "pad.modals.unauth": "اختيار نه آهي", - "pad.modals.initsocketfail": "سَروَرَ کي پڄي نٿو سگھجي.", + "pad.modals.initsocketfail": "سَروَرَ تائين پُڄي نٿو سگهجي.", "pad.modals.slowcommit.explanation": "سَروَر جواب نٿو ڏي.", "pad.modals.corruptPad.explanation": "جيڪا پٽي توهان حاصل ڪرڻ چاهيو ٿا اها بدعنوان آهي.", "pad.modals.deleted": "ختم ڪيل.", From 8b73b911c943fe7162d6047388d551b200520be6 Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 17 Oct 2021 20:33:53 +0200 Subject: [PATCH 270/322] Changeset: Use string templates Co-authored-by: Richard Hansen --- src/static/js/Changeset.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 0d3d9991c..f43a236cf 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -46,13 +46,11 @@ exports.error = (msg) => { * throw an exception. * * @param {boolean} b - assertion condition - * @param {...any} msgParts - error message to include in the exception - * @type {(b: boolean, ...msgParts: any[]) => asserts b} + * @param {string} msg - error message to include in the exception + * @type {(b: boolean, msg: string) => asserts b} */ -exports.assert = (b, ...msgParts) => { - if (!b) { - exports.error(`Failed assertion: ${msgParts.join('')}`); - } +exports.assert = (b, msg) => { + if (!b) exports.error(`Failed assertion: ${msg}`); }; /** @@ -282,13 +280,13 @@ exports.checkRep = (cs) => { break; case '-': oldPos += o.chars; - exports.assert(oldPos <= oldLen, oldPos, ' > ', oldLen, ' in ', cs); + exports.assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`); break; case '+': { calcNewLen += o.chars; numInserted += o.chars; - exports.assert(calcNewLen <= newLen, calcNewLen, ' > ', newLen, ' in ', cs); + exports.assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`); break; } } @@ -538,7 +536,7 @@ exports.stringIterator = (str) => { const getnewLines = () => newLines; const assertRemaining = (n) => { - exports.assert(n <= remaining(), '!(', n, ' <= ', remaining(), ')'); + exports.assert(n <= remaining(), `!(${n} <= ${remaining()})`); }; const take = (n) => { @@ -1066,8 +1064,8 @@ exports.pack = (oldLen, newLen, opsStr, bank) => { */ exports.applyToText = (cs, str) => { const unpacked = exports.unpack(cs); - exports.assert(str.length === unpacked.oldLen, 'mismatched apply: ', str.length, - ' / ', unpacked.oldLen); + exports.assert( + str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); const csIter = exports.opIterator(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); const strIter = exports.stringIterator(str); @@ -1338,7 +1336,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { } lineAssem.append(op); if (op.lines > 0) { - exports.assert(op.lines === 1, "Can't have op.lines of ", op.lines, ' in attribution lines'); + exports.assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`); // ship it to the mut mut.insert(lineAssem.toString(), 1); lineAssem = null; From 6d773f7d562d188568d8f8746cf3aee0fd40526c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 9 Nov 2021 05:15:24 -0500 Subject: [PATCH 271/322] Put regression tests with the bugfix commit, mention bug in PR --- CONTRIBUTING.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 724e02ac0..15dfae99d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,9 +15,17 @@ number of the issue that is being fixed, in the form: Fixes #someIssueNumber ``` * if the PR is a **bug fix**: - * the first commit in the series must be a test that shows the failure - * subsequent commits will fix the bug and make the test pass - * the final commit message should include the text `Fixes: #xxx` to link it to its bug report + * The commit that fixes the bug should **include a regression test** that + would fail if the bug fix was reverted. Adding the regression test in the + same commit as the bug fix makes it easier for a reviewer to verify that the + test is appropriate for the bug fix. + * If there is a bug report, **the pull request description should include the + text "`Fixes #xxx`"** so that the bug report is auto-closed when the PR is + merged. It is less useful to say the same thing in a commit message because + GitHub will spam the bug report every time the commit is rebased, and + because a bug number alone becomes meaningless in forks. (A full URL would + be better, but ideally each commit is readable on its own without the need + to examine an external reference to understand motivation or context.) * think about stability: code has to be backwards compatible as much as possible. Always **assume your code will be run with an older version of the DB/config file** * if you want to remove a feature, **deprecate it instead**: * write an issue with your deprecation plan From b0a7237443c3f8c15a64160e34d7c88a9df95507 Mon Sep 17 00:00:00 2001 From: Timon Engelke Date: Fri, 12 Nov 2021 22:58:49 +0100 Subject: [PATCH 272/322] Fix comparison between CustomError and string --- src/node/db/SessionManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js index 9a222e2fb..b5f93094d 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.js @@ -255,7 +255,7 @@ const listSessionsWithDBKey = async (dbkey) => { const sessionInfo = await exports.getSessionInfo(sessionID); sessions[sessionID] = sessionInfo; } catch (err) { - if (err === 'apierror: sessionID does not exist') { + if (err.name === 'apierror') { console.warn(`Found bad session ${sessionID} in ${dbkey}`); sessions[sessionID] = null; } else { From 4e2f816665c05cf07029096b3f40f0f515b6491a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 10 Nov 2021 18:23:30 -0500 Subject: [PATCH 273/322] adminsettings: Use object destructuring --- src/node/hooks/express/adminsettings.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 8cbf3762a..e77595d11 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -6,8 +6,8 @@ const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugins'); const settings = require('../../utils/Settings'); -exports.expressCreateServer = (hookName, args, cb) => { - args.app.get('/admin/settings', (req, res) => { +exports.expressCreateServer = (hookName, {app}, cb) => { + app.get('/admin/settings', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { req, settings: '', @@ -17,9 +17,8 @@ exports.expressCreateServer = (hookName, args, cb) => { return cb(); }; -exports.socketio = (hookName, args, cb) => { - const io = args.io.of('/settings'); - io.on('connection', (socket) => { +exports.socketio = (hookName, {io}, cb) => { + io.of('/settings').on('connection', (socket) => { const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; if (!isAdmin) return; From 46ad93024d44e5327c459530785de1df87b196b5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 10 Nov 2021 18:24:54 -0500 Subject: [PATCH 274/322] adminsettings: Promisify --- src/node/hooks/express/adminsettings.js | 42 +++++++++++-------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index e77595d11..7445158d8 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -1,12 +1,12 @@ 'use strict'; const eejs = require('../../eejs'); -const fs = require('fs'); +const fsp = require('fs').promises; const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugins'); const settings = require('../../utils/Settings'); -exports.expressCreateServer = (hookName, {app}, cb) => { +exports.expressCreateServer = (hookName, {app}) => { app.get('/admin/settings', (req, res) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { req, @@ -14,34 +14,31 @@ exports.expressCreateServer = (hookName, {app}, cb) => { errors: [], })); }); - return cb(); }; -exports.socketio = (hookName, {io}, cb) => { +exports.socketio = (hookName, {io}) => { io.of('/settings').on('connection', (socket) => { const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; if (!isAdmin) return; - socket.on('load', (query) => { - fs.readFile('settings.json', 'utf8', (err, data) => { - if (err) { - return console.log(err); - } - - // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result - if (settings.showSettingsInAdminPage === false) { - socket.emit('settings', {results: 'NOT_ALLOWED'}); - } else { - socket.emit('settings', {results: data}); - } - }); + socket.on('load', async (query) => { + let data; + try { + data = await fsp.readFile('settings.json', 'utf8'); + } catch (err) { + return console.log(err); + } + // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result + if (settings.showSettingsInAdminPage === false) { + socket.emit('settings', {results: 'NOT_ALLOWED'}); + } else { + socket.emit('settings', {results: data}); + } }); - socket.on('saveSettings', (settings) => { - fs.writeFile('settings.json', settings, (err) => { - if (err) throw err; - socket.emit('saveprogress', 'saved'); - }); + socket.on('saveSettings', async (settings) => { + await fsp.writeFile('settings.json', settings); + socket.emit('saveprogress', 'saved'); }); socket.on('restartServer', async () => { @@ -52,5 +49,4 @@ exports.socketio = (hookName, {io}, cb) => { await hooks.aCallAll('restartServer'); }); }); - return cb(); }; From 19e9c2f114a4b3b62595baf4f0730189f5685daa Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 10 Nov 2021 18:41:53 -0500 Subject: [PATCH 275/322] adminsettings: Pay attention to `--settings` --- src/node/hooks/express/adminsettings.js | 6 +++--- src/node/utils/Settings.js | 27 ++++++++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js index 7445158d8..792801dc7 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.js @@ -24,7 +24,7 @@ exports.socketio = (hookName, {io}) => { socket.on('load', async (query) => { let data; try { - data = await fsp.readFile('settings.json', 'utf8'); + data = await fsp.readFile(settings.settingsFilename, 'utf8'); } catch (err) { return console.log(err); } @@ -36,8 +36,8 @@ exports.socketio = (hookName, {io}) => { } }); - socket.on('saveSettings', async (settings) => { - await fsp.writeFile('settings.json', settings); + socket.on('saveSettings', async (newSettings) => { + await fsp.writeFile(settings.settingsFilename, newSettings); socket.emit('saveprogress', 'saved'); }); diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 1a8bb000e..814601f89 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -42,6 +42,12 @@ const _ = require('underscore'); const logger = log4js.getLogger('settings'); +// Exported values that settings.json and credentials.json cannot override. +const nonSettings = [ + 'credentialsFilename', + 'settingsFilename', +]; + // This is a function to make it easy to create a new instance. It is important to not reuse a // config object after passing it to log4js.configure() because that method mutates the object. :( const defaultLogConfig = () => ({appenders: [{type: 'console'}]}); @@ -65,6 +71,8 @@ initLogging(defaultLogLevel, defaultLogConfig()); exports.root = absolutePaths.findEtherpadRoot(); logger.info('All relative paths will be interpreted relative to the identified ' + `Etherpad base dir: ${exports.root}`); +exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); +exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); /** * The app title, visible e.g. in the browser window @@ -489,6 +497,11 @@ exports.getEpVersion = () => require('../../package.json').version; */ const storeSettings = (settingsObj) => { for (const i of Object.keys(settingsObj || {})) { + if (nonSettings.includes(i)) { + logger.warn(`Ignoring setting: '${i}'`); + continue; + } + // test if the setting starts with a lowercase character if (i.charAt(0).search('[a-z]') !== 0) { logger.warn(`Settings should start with a lowercase character: '${i}'`); @@ -708,18 +721,8 @@ const parseSettings = (settingsFilename, isSettings) => { }; exports.reloadSettings = () => { - // Discover where the settings file lives - const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); - - // Discover if a credential file exists - const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); - - // try to parse the settings - const settings = parseSettings(settingsFilename, true); - - // try to parse the credentials - const credentials = parseSettings(credentialsFilename, false); - + const settings = parseSettings(exports.settingsFilename, true); + const credentials = parseSettings(exports.credentialsFilename, false); storeSettings(settings); storeSettings(credentials); From 0f7c5a878295d066d81d75a6735ae5e348b9e614 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 10 Nov 2021 23:41:24 -0500 Subject: [PATCH 276/322] lint: Use `window.` to avoid global declaration --- src/static/js/admin/settings.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/static/js/admin/settings.js b/src/static/js/admin/settings.js index d7e089b18..ada694f81 100644 --- a/src/static/js/admin/settings.js +++ b/src/static/js/admin/settings.js @@ -1,9 +1,7 @@ 'use strict'; -/* global socketio */ - $(document).ready(() => { - const socket = socketio.connect('..', '/settings'); + const socket = window.socketio.connect('..', '/settings'); socket.on('connect', () => { socket.emit('load'); From 3c6aef11bd0f4d9b6e6b7f4153be03a271deb958 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 13 Nov 2021 03:02:40 -0500 Subject: [PATCH 277/322] lint: Add `'use strict';` --- src/tests/backend/specs/specialpages.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/backend/specs/specialpages.js b/src/tests/backend/specs/specialpages.js index 8b372c959..fa2260708 100644 --- a/src/tests/backend/specs/specialpages.js +++ b/src/tests/backend/specs/specialpages.js @@ -1,3 +1,5 @@ +'use strict'; + const common = require('../common'); const settings = require('../../../node/utils/Settings'); From 263105d185602ff2af6e89c4550f526a724e28ab Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 13 Nov 2021 03:03:01 -0500 Subject: [PATCH 278/322] tests: Remove overly aggressive timeouts --- src/tests/backend/specs/contentcollector.js | 1 - src/tests/backend/specs/regression-db.js | 1 - src/tests/backend/specs/specialpages.js | 1 - 3 files changed, 3 deletions(-) diff --git a/src/tests/backend/specs/contentcollector.js b/src/tests/backend/specs/contentcollector.js index de3dae39c..f7bc539e6 100644 --- a/src/tests/backend/specs/contentcollector.js +++ b/src/tests/backend/specs/contentcollector.js @@ -285,7 +285,6 @@ describe(__filename, function () { } it(testObj.description, async function () { - this.timeout(250); const {window: {document}} = new jsdom.JSDOM(testObj.html); // Create an empty attribute pool const apool = new AttributePool(); diff --git a/src/tests/backend/specs/regression-db.js b/src/tests/backend/specs/regression-db.js index 221193c3b..388b8346a 100644 --- a/src/tests/backend/specs/regression-db.js +++ b/src/tests/backend/specs/regression-db.js @@ -24,7 +24,6 @@ describe(__filename, function () { }); it('regression test for missing await in createAuthor (#5000)', async function () { - this.timeout(700); const {authorID} = await AuthorManager.createAuthor(); // Should block until db.set() finishes. assert(await AuthorManager.doesAuthorExist(authorID)); }); diff --git a/src/tests/backend/specs/specialpages.js b/src/tests/backend/specs/specialpages.js index fa2260708..93c8b3bc4 100644 --- a/src/tests/backend/specs/specialpages.js +++ b/src/tests/backend/specs/specialpages.js @@ -22,7 +22,6 @@ describe(__filename, function () { describe('/javascript', function () { it('/javascript -> 200', async function () { - this.timeout(200); await agent.get('/javascript').expect(200); }); }); From 085bc8cbb3160b392c3f67c69c4851868818b6e2 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 10 Nov 2021 17:34:09 -0500 Subject: [PATCH 279/322] plugins: Don't create `.ep_initialized` files These files cause problems with Docker images and read-only directories/mounts, and they have dubious value (any install-time setup should instead be done at startup). --- .gitignore | 1 - CHANGELOG.md | 2 ++ doc/api/hooks_server-side.md | 9 +++++---- src/bin/plugins/lib/gitignore | 2 -- src/static/js/pluginfw/plugins.js | 15 +-------------- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 09618cc83..60638c50a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ var/dirty.db *.patch npm-debug.log *.DS_Store -.ep_initialized *.crt *.key credentials.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 450a2d8c1..5ce91f39d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ for an example fix. * The `clientReady` server-side hook is deprecated; use the new `userJoin` hook instead. + * The `init_` server-side hooks are now run every time Etherpad + starts up, not just the first time after the named plugin is installed. * The `userLeave` server-side hook's context properties have changed: * `auth`: Deprecated. * `author`: Deprecated; use the new `authorId` property instead. diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index c99ea58aa..5e8832fd4 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -50,12 +50,13 @@ Things in context: If this hook returns an error, the callback to the install function gets an error, too. This seems useful for adding in features when a particular plugin is installed. -## init_`` -Called from: src/static/js/pluginfw/plugins.js +## `init_` -Things in context: None +Called from: `src/static/js/pluginfw/plugins.js` -This function is called after a specific plugin is initialized. This would probably be more useful than the previous two functions if you only wanted to add in features to one specific plugin. +Run during startup after the named plugin is initialized. + +Context properties: None ## expressConfigure Called from: src/node/hooks/express.js diff --git a/src/bin/plugins/lib/gitignore b/src/bin/plugins/lib/gitignore index 0719a85c1..153216eb9 100755 --- a/src/bin/plugins/lib/gitignore +++ b/src/bin/plugins/lib/gitignore @@ -1,5 +1,3 @@ -.ep_initialized .DS_Store node_modules/ -node_modules npm-debug.log diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 74fbbafc8..a4a2df40d 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -72,19 +72,6 @@ exports.formatHooks = (hookSetName, html) => { return lines.join('\n'); }; -const callInit = async () => { - await Promise.all(Object.keys(defs.plugins).map(async (pluginName) => { - const plugin = defs.plugins[pluginName]; - const epInit = path.join(plugin.package.path, '.ep_initialized'); - try { - await fs.stat(epInit); - } catch (err) { - await fs.writeFile(epInit, 'done'); - await hooks.aCallAll(`init_${pluginName}`, {}); - } - })); -}; - exports.pathNormalization = (part, hookFnName, hookName) => { const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. @@ -111,7 +98,7 @@ exports.update = async () => { defs.parts = sortParts(parts); defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization); defs.loaded = true; - await callInit(); + await Promise.all(Object.keys(defs.plugins).map((p) => hooks.aCallAll(`init_${p}`, {}))); }; exports.getPackages = async () => { From 4a65c2c8ff6036222a2b2c48e5161c82c199e8c9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 21 Mar 2021 14:13:50 -0400 Subject: [PATCH 280/322] Changeset: Unexport unnecessarily exported functions These functions aren't used outside of this file. --- CHANGELOG.md | 4 + src/static/js/Changeset.js | 121 ++++++++++++++------------- src/tests/frontend/specs/easysync.js | 14 ++-- 3 files changed, 75 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ce91f39d..0cf6d1ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,10 @@ * Changes to the `src/static/js/Changeset.js` library: * `opIterator()`: The unused start index parameter has been removed, as has the unused `lastIndex()` method on the returned object. + * Several functions that should have never been public are no longer + exported: `applyZip()`, `assert()`, `clearOp()`, `cloneOp()`, `copyOp()`, + `error()`, `followAttributes()`, `opString()`, `stringOp()`, + `textLinesMutator()`, `toBaseTen()`, `toSplices()`. ### Notable enhancements diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index f43a236cf..fa1962340 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -35,7 +35,7 @@ const AttributePool = require('./AttributePool'); * * @param {string} msg - Just some message */ -exports.error = (msg) => { +const error = (msg) => { const e = new Error(msg); e.easysync = true; throw e; @@ -49,8 +49,8 @@ exports.error = (msg) => { * @param {string} msg - error message to include in the exception * @type {(b: boolean, msg: string) => asserts b} */ -exports.assert = (b, msg) => { - if (!b) exports.error(`Failed assertion: ${msg}`); +const assert = (b, msg) => { + if (!b) error(`Failed assertion: ${msg}`); }; /** @@ -147,7 +147,7 @@ exports.opIterator = (opsStr) => { const nextRegexMatch = () => { const result = regex.exec(opsStr); if (result[0] === '?') { - exports.error('Hit error opcode in op stream'); + error('Hit error opcode in op stream'); } return result; @@ -163,7 +163,7 @@ exports.opIterator = (opsStr) => { op.chars = exports.parseNum(regexResult[4]); regexResult = nextRegexMatch(); } else { - exports.clearOp(op); + clearOp(op); } return op; }; @@ -181,7 +181,7 @@ exports.opIterator = (opsStr) => { * * @param {Op} op - object to clear */ -exports.clearOp = (op) => { +const clearOp = (op) => { op.opcode = ''; op.chars = 0; op.lines = 0; @@ -207,7 +207,7 @@ exports.newOp = (optOpcode) => ({ * @param {Op} op1 - src Op * @param {Op} op2 - dest Op */ -exports.copyOp = (op1, op2) => { +const copyOp = (op1, op2) => { op2.opcode = op1.opcode; op2.chars = op1.chars; op2.lines = op1.lines; @@ -280,13 +280,13 @@ exports.checkRep = (cs) => { break; case '-': oldPos += o.chars; - exports.assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`); + assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`); break; case '+': { calcNewLen += o.chars; numInserted += o.chars; - exports.assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`); + assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`); break; } } @@ -301,7 +301,7 @@ exports.checkRep = (cs) => { assem.endDocument(); const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank); - exports.assert(normalized === cs, 'Invalid changeset (checkRep failed)'); + assert(normalized === cs, 'Invalid changeset (checkRep failed)'); return cs; }; @@ -458,7 +458,7 @@ exports.mergingOpAssembler = () => { } } else { flush(); - exports.copyOp(op, bufOp); + copyOp(op, bufOp); } } }; @@ -474,7 +474,7 @@ exports.mergingOpAssembler = () => { const clear = () => { assem.clear(); - exports.clearOp(bufOp); + clearOp(bufOp); }; return { append, @@ -536,7 +536,7 @@ exports.stringIterator = (str) => { const getnewLines = () => newLines; const assertRemaining = (n) => { - exports.assert(n <= remaining(), `!(${n} <= ${remaining()})`); + assert(n <= remaining(), `!(${n} <= ${remaining()})`); }; const take = (n) => { @@ -633,7 +633,7 @@ exports.stringAssembler = () => { * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). * @returns {TextLinesMutator} */ -exports.textLinesMutator = (lines) => { +const textLinesMutator = (lines) => { /** * curSplice holds values that will be passed as arguments to lines.splice() to insert, delete, or * change lines: @@ -990,7 +990,7 @@ exports.textLinesMutator = (lines) => { * other out), `opOut.opcode` MUST be set to the empty string. * @returns {string} the integrated changeset */ -exports.applyZip = (in1, in2, func) => { +const applyZip = (in1, in2, func) => { const iter1 = exports.opIterator(in1); const iter2 = exports.opIterator(in2); const assem = exports.smartOpAssembler(); @@ -1020,7 +1020,7 @@ exports.unpack = (cs) => { const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; const headerMatch = headerRegex.exec(cs); if ((!headerMatch) || (!headerMatch[0])) { - exports.error(`Not a exports: ${cs}`); + error(`Not a exports: ${cs}`); } const oldLen = exports.parseNum(headerMatch[1]); const changeSign = (headerMatch[2] === '>') ? 1 : -1; @@ -1064,8 +1064,7 @@ exports.pack = (oldLen, newLen, opsStr, bank) => { */ exports.applyToText = (cs, str) => { const unpacked = exports.unpack(cs); - exports.assert( - str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); + assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); const csIter = exports.opIterator(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); const strIter = exports.stringIterator(str); @@ -1113,7 +1112,7 @@ exports.mutateTextLines = (cs, lines) => { const unpacked = exports.unpack(cs); const csIter = exports.opIterator(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); - const mut = exports.textLinesMutator(lines); + const mut = textLinesMutator(lines); while (csIter.hasNext()) { const op = csIter.next(); switch (op.opcode) { @@ -1205,12 +1204,12 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { * @param {Op} opOut - Mutated to hold the result of applying `csOp` to `attOp`. * @param {AttributePool} pool - Can be null if definitely not needed. */ -exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { +const slicerZipperFunc = (attOp, csOp, opOut, pool) => { if (attOp.opcode === '-') { - exports.copyOp(attOp, opOut); + copyOp(attOp, opOut); attOp.opcode = ''; } else if (!attOp.opcode) { - exports.copyOp(csOp, opOut); + copyOp(csOp, opOut); csOp.opcode = ''; } else { switch (csOp.opcode) { @@ -1247,7 +1246,7 @@ exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { case '+': { // insert - exports.copyOp(csOp, opOut); + copyOp(csOp, opOut); csOp.opcode = ''; break; } @@ -1281,7 +1280,7 @@ exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { } case '': { - exports.copyOp(attOp, opOut); + copyOp(attOp, opOut); attOp.opcode = ''; break; } @@ -1300,8 +1299,8 @@ exports._slicerZipperFunc = (attOp, csOp, opOut, pool) => { exports.applyToAttribution = (cs, astr, pool) => { const unpacked = exports.unpack(cs); - return exports.applyZip(astr, unpacked.ops, - (op1, op2, opOut) => exports._slicerZipperFunc(op1, op2, opOut, pool)); + return applyZip(astr, unpacked.ops, + (op1, op2, opOut) => slicerZipperFunc(op1, op2, opOut, pool)); }; exports.mutateAttributionLines = (cs, lines, pool) => { @@ -1310,7 +1309,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { const csBank = unpacked.charBank; let csBankIndex = 0; // treat the attribution lines as text lines, mutating a line at a time - const mut = exports.textLinesMutator(lines); + const mut = textLinesMutator(lines); /** @type {?OpIter} */ let lineIter = null; @@ -1336,7 +1335,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { } lineAssem.append(op); if (op.lines > 0) { - exports.assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`); + assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`); // ship it to the mut mut.insert(lineAssem.toString(), 1); lineAssem = null; @@ -1360,13 +1359,13 @@ exports.mutateAttributionLines = (cs, lines, pool) => { } else if (csOp.opcode === '+') { if (csOp.lines > 1) { const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; - exports.copyOp(csOp, opOut); + copyOp(csOp, opOut); csOp.chars -= firstLineLen; csOp.lines--; opOut.lines = 1; opOut.chars = firstLineLen; } else { - exports.copyOp(csOp, opOut); + copyOp(csOp, opOut); csOp.opcode = ''; } outputMutOp(opOut); @@ -1376,7 +1375,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { if ((!attOp.opcode) && isNextMutOp()) { nextMutOp(attOp); } - exports._slicerZipperFunc(attOp, csOp, opOut, pool); + slicerZipperFunc(attOp, csOp, opOut, pool); if (opOut.opcode) { outputMutOp(opOut); opOut.opcode = ''; @@ -1384,7 +1383,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { } } - exports.assert(!lineAssem, `line assembler not finished:${cs}`); + assert(!lineAssem, `line assembler not finished:${cs}`); mut.close(); }; @@ -1427,7 +1426,7 @@ exports.splitAttributionLines = (attrOps, text) => { let numLines = op.lines; while (numLines > 1) { const newlineEnd = text.indexOf('\n', pos) + 1; - exports.assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); + assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); op.chars = newlineEnd - pos; op.lines = 1; appendOp(op); @@ -1465,19 +1464,19 @@ exports.compose = (cs1, cs2, pool) => { const unpacked2 = exports.unpack(cs2); const len1 = unpacked1.oldLen; const len2 = unpacked1.newLen; - exports.assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); + assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); const len3 = unpacked2.newLen; const bankIter1 = exports.stringIterator(unpacked1.charBank); const bankIter2 = exports.stringIterator(unpacked2.charBank); const bankAssem = exports.stringAssembler(); - const newOps = exports.applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { const op1code = op1.opcode; const op2code = op2.opcode; if (op1code === '+' && op2code === '-') { bankIter1.skip(Math.min(op1.chars, op2.chars)); } - exports._slicerZipperFunc(op1, op2, opOut, pool); + slicerZipperFunc(op1, op2, opOut, pool); if (opOut.opcode === '+') { if (op2code === '+') { bankAssem.append(bankIter2.take(opOut.chars)); @@ -1560,7 +1559,7 @@ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextA * @param {string} cs - Changeset * @returns {[number, number, string][]} */ -exports.toSplices = (cs) => { +const toSplices = (cs) => { const unpacked = exports.unpack(cs); /** @type {[number, number, string][]} */ const splices = []; @@ -1601,7 +1600,7 @@ exports.toSplices = (cs) => { exports.characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { let newStartChar = startChar; let newEndChar = endChar; - const splices = exports.toSplices(cs); + const splices = toSplices(cs); let lengthChangeSoFar = 0; for (let i = 0; i < splices.length; i++) { const splice = splices[i]; @@ -1796,7 +1795,9 @@ exports.cloneAText = (atext) => { text: atext.text, attribs: atext.attribs, }; - } else { exports.error('atext is null'); } + } else { + error('atext is null'); + } }; /** @@ -2040,7 +2041,7 @@ exports.subattribution = (astr, start, optEnd) => { csOp.lines++; } - exports._slicerZipperFunc(attOp, csOp, opOut, null); + slicerZipperFunc(attOp, csOp, opOut, null); if (opOut.opcode) { assem.append(opOut); opOut.opcode = ''; @@ -2240,7 +2241,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const unpacked2 = exports.unpack(cs2); const len1 = unpacked1.oldLen; const len2 = unpacked2.oldLen; - exports.assert(len1 === len2, 'mismatched follow - cannot transform cs1 on top of cs2'); + assert(len1 === len2, 'mismatched follow - cannot transform cs1 on top of cs2'); const chars1 = exports.stringIterator(unpacked1.charBank); const chars2 = exports.stringIterator(unpacked2.charBank); @@ -2250,7 +2251,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); - const newOps = exports.applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { if (op1.opcode === '+' || op2.opcode === '+') { let whichToDo; if (op2.opcode !== '+') { @@ -2289,7 +2290,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { } else { // whichToDo == 2 chars2.skip(op2.chars); - exports.copyOp(op2, opOut); + copyOp(op2, opOut); op2.opcode = ''; } } else if (op1.opcode === '-') { @@ -2308,7 +2309,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { op2.opcode = ''; } } else if (op2.opcode === '-') { - exports.copyOp(op2, opOut); + copyOp(op2, opOut); if (!op1.opcode) { op2.opcode = ''; } else if (op2.chars <= op1.chars) { @@ -2328,17 +2329,17 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { op1.opcode = ''; } } else if (!op1.opcode) { - exports.copyOp(op2, opOut); + copyOp(op2, opOut); op2.opcode = ''; } else if (!op2.opcode) { // @NOTE: Critical bugfix for EPL issue #1625. We do not copy op1 here // in order to prevent attributes from leaking into result changesets. - // exports.copyOp(op1, opOut); + // copyOp(op1, opOut); op1.opcode = ''; } else { // both keeps opOut.opcode = '='; - opOut.attribs = exports.followAttributes(op1.attribs, op2.attribs, pool); + opOut.attribs = followAttributes(op1.attribs, op2.attribs, pool); if (op1.chars <= op2.chars) { opOut.chars = op1.chars; opOut.lines = op1.lines; @@ -2374,7 +2375,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { return exports.pack(oldLen, newLen, newOps, unpacked2.charBank); }; -exports.followAttributes = (att1, att2, pool) => { +const followAttributes = (att1, att2, pool) => { // The merge of two sets of attribute changes to the same text // takes the lexically-earlier value if there are two values // for the same key. Otherwise, all key/value changes from @@ -2416,19 +2417,19 @@ exports.composeWithDeletions = (cs1, cs2, pool) => { const unpacked2 = exports.unpack(cs2); const len1 = unpacked1.oldLen; const len2 = unpacked1.newLen; - exports.assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); + assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); const len3 = unpacked2.newLen; const bankIter1 = exports.stringIterator(unpacked1.charBank); const bankIter2 = exports.stringIterator(unpacked2.charBank); const bankAssem = exports.stringAssembler(); - const newOps = exports.applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { const op1code = op1.opcode; const op2code = op2.opcode; if (op1code === '+' && op2code === '-') { bankIter1.skip(Math.min(op1.chars, op2.chars)); } - exports._slicerZipperFuncWithDeletions(op1, op2, opOut, pool); + slicerZipperFuncWithDeletions(op1, op2, opOut, pool); if (opOut.opcode === '+') { if (op2code === '+') { bankAssem.append(bankIter2.take(opOut.chars)); @@ -2441,19 +2442,19 @@ exports.composeWithDeletions = (cs1, cs2, pool) => { return exports.pack(len1, len3, newOps, bankAssem.toString()); }; -// This function is 95% like _slicerZipperFunc, we just changed two lines to +// This function is 95% like slicerZipperFunc, we just changed two lines to // ensure it merges the attribs of deletions properly. // This is necassary for correct paddiff. But to ensure these changes doesn't // affect anything else, we've created a seperate function only used for paddiffs -exports._slicerZipperFuncWithDeletions = (attOp, csOp, opOut, pool) => { +const slicerZipperFuncWithDeletions = (attOp, csOp, opOut, pool) => { // attOp is the op from the sequence that is being operated on, either an // attribution string or the earlier of two exportss being composed. // pool can be null if definitely not needed. if (attOp.opcode === '-') { - exports.copyOp(attOp, opOut); + copyOp(attOp, opOut); attOp.opcode = ''; } else if (!attOp.opcode) { - exports.copyOp(csOp, opOut); + copyOp(csOp, opOut); csOp.opcode = ''; } else { switch (csOp.opcode) { @@ -2490,7 +2491,7 @@ exports._slicerZipperFuncWithDeletions = (attOp, csOp, opOut, pool) => { case '+': { // insert - exports.copyOp(csOp, opOut); + copyOp(csOp, opOut); csOp.opcode = ''; break; } @@ -2524,10 +2525,16 @@ exports._slicerZipperFuncWithDeletions = (attOp, csOp, opOut, pool) => { } case '': { - exports.copyOp(attOp, opOut); + copyOp(attOp, opOut); attOp.opcode = ''; break; } } } }; + +exports.exportedForTestingOnly = { + followAttributes, + textLinesMutator, + toSplices, +}; diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index 5c4c47ae4..121218407 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -92,7 +92,7 @@ describe('easysync', function () { const runMutationTest = (testId, origLines, muts, correct) => { it(`runMutationTest#${testId}`, async function () { let lines = origLines.slice(); - const mu = Changeset.textLinesMutator(lines); + const mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); applyMutations(mu, muts); mu.close(); expect(lines).to.eql(correct); @@ -211,7 +211,7 @@ describe('easysync', function () { const lines = ['1\n', '2\n', '3\n', '4\n']; let mu; - mu = Changeset.textLinesMutator(lines); + mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); expect(mu.hasMore()).to.be(true); mu.skip(8, 4); expect(mu.hasMore()).to.be(false); @@ -219,7 +219,7 @@ describe('easysync', function () { expect(mu.hasMore()).to.be(false); // still 1,2,3,4 - mu = Changeset.textLinesMutator(lines); + mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); expect(mu.hasMore()).to.be(true); mu.remove(2, 1); expect(mu.hasMore()).to.be(true); @@ -235,7 +235,7 @@ describe('easysync', function () { expect(mu.hasMore()).to.be(false); // 2,3,4,5 now - mu = Changeset.textLinesMutator(lines); + mu = Changeset.exportedForTestingOnly.textLinesMutator(lines); expect(mu.hasMore()).to.be(true); mu.remove(6, 3); expect(mu.hasMore()).to.be(true); @@ -610,8 +610,8 @@ describe('easysync', function () { const testFollow = (a, b, afb, bfa, merge) => { it(`testFollow manual #${++n}`, async function () { - expect(Changeset.followAttributes(a, b, p)).to.equal(afb); - expect(Changeset.followAttributes(b, a, p)).to.equal(bfa); + expect(Changeset.exportedForTestingOnly.followAttributes(a, b, p)).to.equal(afb); + expect(Changeset.exportedForTestingOnly.followAttributes(b, a, p)).to.equal(bfa); expect(Changeset.composeAttributes(a, afb, true, p)).to.equal(merge); expect(Changeset.composeAttributes(b, bfa, true, p)).to.equal(merge); }); @@ -701,7 +701,7 @@ describe('easysync', function () { [5, 8, '123456789'], [9, 17, 'abcdefghijk'], ]; - expect(Changeset.toSplices(cs)).to.eql(correctSplices); + expect(Changeset.exportedForTestingOnly.toSplices(cs)).to.eql(correctSplices); }); const testCharacterRangeFollow = (testId, cs, oldRange, insertionsAfter, correctNewRange) => { From b29e59419e2aabe6f85fe37fa3d4a287b0b9796e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 16 Oct 2021 17:54:03 -0400 Subject: [PATCH 281/322] Changeset: Factor out duplicate code --- src/node/utils/padDiff.js | 2 +- src/static/js/Changeset.js | 131 +++---------------------------------- 2 files changed, 9 insertions(+), 124 deletions(-) diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 67c5f68ff..193f24458 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -160,7 +160,7 @@ PadDiff.prototype._createDiffAtext = async function () { if (superChangeset == null) { superChangeset = changeset; } else { - superChangeset = Changeset.composeWithDeletions(superChangeset, changeset, this._pad.pool); + superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool); } } diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index fa1962340..fda143021 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1221,7 +1221,10 @@ const slicerZipperFunc = (attOp, csOp, opOut, pool) => { opOut.opcode = '-'; opOut.chars = csOp.chars; opOut.lines = csOp.lines; - opOut.attribs = ''; + // csOp is a remove op and remove ops normally never have any attributes, so this should + // normally be the empty string. However, padDiff.js adds attributes to remove ops and + // needs them preserved so they are copied here. + opOut.attribs = csOp.attribs; } attOp.chars -= csOp.chars; attOp.lines -= csOp.lines; @@ -1235,7 +1238,10 @@ const slicerZipperFunc = (attOp, csOp, opOut, pool) => { opOut.opcode = '-'; opOut.chars = attOp.chars; opOut.lines = attOp.lines; - opOut.attribs = ''; + // csOp is a remove op and remove ops normally never have any attributes, so this should + // normally be the empty string. However, padDiff.js adds attributes to remove ops and + // needs them preserved so they are copied here. + opOut.attribs = csOp.attribs; } csOp.chars -= attOp.chars; csOp.lines -= attOp.lines; @@ -2412,127 +2418,6 @@ const followAttributes = (att1, att2, pool) => { return buf.toString(); }; -exports.composeWithDeletions = (cs1, cs2, pool) => { - const unpacked1 = exports.unpack(cs1); - const unpacked2 = exports.unpack(cs2); - const len1 = unpacked1.oldLen; - const len2 = unpacked1.newLen; - assert(len2 === unpacked2.oldLen, 'mismatched composition of two changesets'); - const len3 = unpacked2.newLen; - const bankIter1 = exports.stringIterator(unpacked1.charBank); - const bankIter2 = exports.stringIterator(unpacked2.charBank); - const bankAssem = exports.stringAssembler(); - - const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { - const op1code = op1.opcode; - const op2code = op2.opcode; - if (op1code === '+' && op2code === '-') { - bankIter1.skip(Math.min(op1.chars, op2.chars)); - } - slicerZipperFuncWithDeletions(op1, op2, opOut, pool); - if (opOut.opcode === '+') { - if (op2code === '+') { - bankAssem.append(bankIter2.take(opOut.chars)); - } else { - bankAssem.append(bankIter1.take(opOut.chars)); - } - } - }); - - return exports.pack(len1, len3, newOps, bankAssem.toString()); -}; - -// This function is 95% like slicerZipperFunc, we just changed two lines to -// ensure it merges the attribs of deletions properly. -// This is necassary for correct paddiff. But to ensure these changes doesn't -// affect anything else, we've created a seperate function only used for paddiffs -const slicerZipperFuncWithDeletions = (attOp, csOp, opOut, pool) => { - // attOp is the op from the sequence that is being operated on, either an - // attribution string or the earlier of two exportss being composed. - // pool can be null if definitely not needed. - if (attOp.opcode === '-') { - copyOp(attOp, opOut); - attOp.opcode = ''; - } else if (!attOp.opcode) { - copyOp(csOp, opOut); - csOp.opcode = ''; - } else { - switch (csOp.opcode) { - case '-': - { - if (csOp.chars <= attOp.chars) { - // delete or delete part - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = csOp.attribs; // changed by yammer - } - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - csOp.opcode = ''; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // delete and keep going - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = csOp.attribs; // changed by yammer - } - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - attOp.opcode = ''; - } - break; - } - case '+': - { - // insert - copyOp(csOp, opOut); - csOp.opcode = ''; - break; - } - case '=': - { - if (csOp.chars <= attOp.chars) { - // keep or keep part - opOut.opcode = attOp.opcode; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - csOp.opcode = ''; - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // keep and keep going - opOut.opcode = attOp.opcode; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } - } - } -}; - exports.exportedForTestingOnly = { followAttributes, textLinesMutator, From 7fa9b0711621448b3aa289ed86dc137b4e7fd556 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 27 Sep 2021 17:25:31 -0400 Subject: [PATCH 282/322] Changeset: Invert conditions to improve readability --- src/static/js/Changeset.js | 371 +++++++++++++++++-------------------- 1 file changed, 169 insertions(+), 202 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index fda143021..b5e72f477 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -425,41 +425,39 @@ exports.mergingOpAssembler = () => { let bufOpAdditionalCharsAfterNewline = 0; const flush = (isEndDocument) => { - if (bufOp.opcode) { - if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) { - // final merged keep, leave it implicit - } else { + if (!bufOp.opcode) return; + if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) { + // final merged keep, leave it implicit + } else { + assem.append(bufOp); + if (bufOpAdditionalCharsAfterNewline) { + bufOp.chars = bufOpAdditionalCharsAfterNewline; + bufOp.lines = 0; assem.append(bufOp); - if (bufOpAdditionalCharsAfterNewline) { - bufOp.chars = bufOpAdditionalCharsAfterNewline; - bufOp.lines = 0; - assem.append(bufOp); - bufOpAdditionalCharsAfterNewline = 0; - } + bufOpAdditionalCharsAfterNewline = 0; } - bufOp.opcode = ''; } + bufOp.opcode = ''; }; const append = (op) => { - if (op.chars > 0) { - if (bufOp.opcode === op.opcode && bufOp.attribs === op.attribs) { - if (op.lines > 0) { - // bufOp and additional chars are all mergeable into a multi-line op - bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; - bufOp.lines += op.lines; - bufOpAdditionalCharsAfterNewline = 0; - } else if (bufOp.lines === 0) { - // both bufOp and op are in-line - bufOp.chars += op.chars; - } else { - // append in-line text to multi-line bufOp - bufOpAdditionalCharsAfterNewline += op.chars; - } + if (op.chars <= 0) return; + if (bufOp.opcode === op.opcode && bufOp.attribs === op.attribs) { + if (op.lines > 0) { + // bufOp and additional chars are all mergeable into a multi-line op + bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; + bufOp.lines += op.lines; + bufOpAdditionalCharsAfterNewline = 0; + } else if (bufOp.lines === 0) { + // both bufOp and op are in-line + bufOp.chars += op.chars; } else { - flush(); - copyOp(op, bufOp); + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; } + } else { + flush(); + copyOp(op, bufOp); } }; @@ -762,31 +760,28 @@ const textLinesMutator = (lines) => { * @param {boolean} includeInSplice - indicates if attributes are present */ const skipLines = (L, includeInSplice) => { - if (L) { - if (includeInSplice) { - if (!inSplice) { - enterSplice(); - } - // TODO(doc) should this count the number of characters that are skipped to check? - for (let i = 0; i < L; i++) { - curCol = 0; - putCurLineInSplice(); - curLine++; - } - } else { - if (inSplice) { - if (L > 1) { - // TODO(doc) figure out why single lines are incorporated into splice instead of ignored - leaveSplice(); - } else { - putCurLineInSplice(); - } - } - curLine += L; + if (!L) return; + if (includeInSplice) { + if (!inSplice) enterSplice(); + // TODO(doc) should this count the number of characters that are skipped to check? + for (let i = 0; i < L; i++) { curCol = 0; + putCurLineInSplice(); + curLine++; } - // tests case foo in remove(), which isn't otherwise covered in current impl + } else { + if (inSplice) { + if (L > 1) { + // TODO(doc) figure out why single lines are incorporated into splice instead of ignored + leaveSplice(); + } else { + putCurLineInSplice(); + } + } + curLine += L; + curCol = 0; } + // tests case foo in remove(), which isn't otherwise covered in current impl }; /** @@ -797,20 +792,17 @@ const textLinesMutator = (lines) => { * @param {boolean} includeInSplice - indicates if attributes are present */ const skip = (N, L, includeInSplice) => { - if (N) { - if (L) { - skipLines(L, includeInSplice); - } else { - if (includeInSplice && !inSplice) { - enterSplice(); - } - if (inSplice) { - // although the line is put into splice curLine is not increased, because - // only some chars are skipped, not the whole line - putCurLineInSplice(); - } - curCol += N; + if (!N) return; + if (L) { + skipLines(L, includeInSplice); + } else { + if (includeInSplice && !inSplice) enterSplice(); + if (inSplice) { + // although the line is put into splice curLine is not increased, because + // only some chars are skipped, not the whole line + putCurLineInSplice(); } + curCol += N; } }; @@ -821,41 +813,39 @@ const textLinesMutator = (lines) => { * @returns {string} */ const removeLines = (L) => { - let removed = ''; - if (L) { - if (!inSplice) { - enterSplice(); - } + if (!L) return ''; + if (!inSplice) enterSplice(); - /** - * Gets a string of joined lines after the end of the splice. - * - * @param {number} k - number of lines - * @returns {string} joined lines - */ - const nextKLinesText = (k) => { - const m = curSplice[0] + curSplice[1]; - return linesSlice(m, m + k).join(''); - }; - if (isCurLineInSplice()) { - if (curCol === 0) { - removed = curSplice[curSplice.length - 1]; - curSplice.length--; - removed += nextKLinesText(L - 1); - curSplice[1] += L - 1; - } else { - removed = nextKLinesText(L - 1); - curSplice[1] += L - 1; - const sline = curSplice.length - 1; - removed = curSplice[sline].substring(curCol) + removed; - curSplice[sline] = curSplice[sline].substring(0, curCol) + - linesGet(curSplice[0] + curSplice[1]); - curSplice[1] += 1; - } + /** + * Gets a string of joined lines after the end of the splice. + * + * @param {number} k - number of lines + * @returns {string} joined lines + */ + const nextKLinesText = (k) => { + const m = curSplice[0] + curSplice[1]; + return linesSlice(m, m + k).join(''); + }; + + let removed = ''; + if (isCurLineInSplice()) { + if (curCol === 0) { + removed = curSplice[curSplice.length - 1]; + curSplice.length--; + removed += nextKLinesText(L - 1); + curSplice[1] += L - 1; } else { - removed = nextKLinesText(L); - curSplice[1] += L; + removed = nextKLinesText(L - 1); + curSplice[1] += L - 1; + const sline = curSplice.length - 1; + removed = curSplice[sline].substring(curCol) + removed; + curSplice[sline] = curSplice[sline].substring(0, curCol) + + linesGet(curSplice[0] + curSplice[1]); + curSplice[1] += 1; } + } else { + removed = nextKLinesText(L); + curSplice[1] += L; } return removed; }; @@ -868,22 +858,15 @@ const textLinesMutator = (lines) => { * @returns {string} */ const remove = (N, L) => { - let removed = ''; - if (N) { - if (L) { - return removeLines(L); - } else { - if (!inSplice) { - enterSplice(); - } - // although the line is put into splice, curLine is not increased, because - // only some chars are removed not the whole line - const sline = putCurLineInSplice(); - removed = curSplice[sline].substring(curCol, curCol + N); - curSplice[sline] = curSplice[sline].substring(0, curCol) + - curSplice[sline].substring(curCol + N); - } - } + if (!N) return ''; + if (L) return removeLines(L); + if (!inSplice) enterSplice(); + // although the line is put into splice, curLine is not increased, because + // only some chars are removed not the whole line + const sline = putCurLineInSplice(); + const removed = curSplice[sline].substring(curCol, curCol + N); + curSplice[sline] = curSplice[sline].substring(0, curCol) + + curSplice[sline].substring(curCol + N); return removed; }; @@ -894,44 +877,41 @@ const textLinesMutator = (lines) => { * @param {number} L - number of newlines in text */ const insert = (text, L) => { - if (text) { - if (!inSplice) { - enterSplice(); - } - if (L) { - const newLines = exports.splitTextLines(text); - if (isCurLineInSplice()) { - const sline = curSplice.length - 1; - /** @type {string} */ - const theLine = curSplice[sline]; - const lineCol = curCol; - // insert the first new line - curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; - curLine++; - newLines.splice(0, 1); - // insert the remaining new lines - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - // insert the remaining chars from the "old" line (e.g. the line we were in - // when we started to insert new lines) - curSplice.push(theLine.substring(lineCol)); - curCol = 0; // TODO(doc) why is this not set to the length of last line? - } else { - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - } + if (!text) return; + if (!inSplice) enterSplice(); + if (L) { + const newLines = exports.splitTextLines(text); + if (isCurLineInSplice()) { + const sline = curSplice.length - 1; + /** @type {string} */ + const theLine = curSplice[sline]; + const lineCol = curCol; + // insert the first new line + curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + curLine++; + newLines.splice(0, 1); + // insert the remaining new lines + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; + // insert the remaining chars from the "old" line (e.g. the line we were in + // when we started to insert new lines) + curSplice.push(theLine.substring(lineCol)); + curCol = 0; // TODO(doc) why is this not set to the length of last line? } else { - // there are no additional lines - // although the line is put into splice, curLine is not increased, because - // there may be more chars in the line (newline is not reached) - const sline = putCurLineInSplice(); - if (!curSplice[sline]) { - console.error('curSplice[sline] not populated, actual curSplice contents is ', curSplice, '. Possibly related to https://github.com/ether/etherpad-lite/issues/2802'); - } - curSplice[sline] = curSplice[sline].substring(0, curCol) + text + - curSplice[sline].substring(curCol); - curCol += text.length; + Array.prototype.push.apply(curSplice, newLines); + curLine += newLines.length; } + } else { + // there are no additional lines + // although the line is put into splice, curLine is not increased, because + // there may be more chars in the line (newline is not reached) + const sline = putCurLineInSplice(); + if (!curSplice[sline]) { + console.error('curSplice[sline] not populated, actual curSplice contents is ', curSplice, '. Possibly related to https://github.com/ether/etherpad-lite/issues/2802'); + } + curSplice[sline] = curSplice[sline].substring(0, curCol) + text + + curSplice[sline].substring(curCol); + curCol += text.length; } }; @@ -1171,15 +1151,14 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { let found = false; for (let i = 0; i < atts.length; i++) { const oldPair = atts[i]; - if (oldPair[0] === pair[0]) { - if (pair[1] || resultIsMutation) { - oldPair[1] = pair[1]; - } else { - atts.splice(i, 1); - } - found = true; - break; + if (oldPair[0] !== pair[0]) continue; + if (pair[1] || resultIsMutation) { + oldPair[1] = pair[1]; + } else { + atts.splice(i, 1); } + found = true; + break; } if ((!found) && (pair[1] || resultIsMutation)) { atts.push(pair); @@ -1340,12 +1319,11 @@ exports.mutateAttributionLines = (cs, lines, pool) => { lineAssem = exports.mergingOpAssembler(); } lineAssem.append(op); - if (op.lines > 0) { - assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`); - // ship it to the mut - mut.insert(lineAssem.toString(), 1); - lineAssem = null; - } + if (op.lines <= 0) return; + assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`); + // ship it to the mut + mut.insert(lineAssem.toString(), 1); + lineAssem = null; }; const csOp = exports.newOp(); @@ -1505,16 +1483,11 @@ exports.compose = (cs1, cs2, pool) => { */ exports.attributeTester = (attribPair, pool) => { const never = (attribs) => false; - if (!pool) { - return never; - } + if (!pool) return never; const attribNum = pool.putAttrib(attribPair, true); - if (attribNum < 0) { - return never; - } else { - const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); - return (attribs) => re.test(attribs); - } + if (attribNum < 0) return never; + const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); + return (attribs) => re.test(attribs); }; /** @@ -1796,14 +1769,11 @@ exports.applyToAText = (cs, atext, pool) => ({ * @returns {AText} */ exports.cloneAText = (atext) => { - if (atext) { - return { - text: atext.text, - attribs: atext.attribs, - }; - } else { - error('atext is null'); - } + if (!atext) error('atext is null'); + return { + text: atext.text, + attribs: atext.attribs, + }; }; /** @@ -1902,14 +1872,13 @@ exports.opAttributeValue = (op, key, pool) => exports.attribsAttributeValue(op.a * @returns {string} */ exports.attribsAttributeValue = (attribs, key, pool) => { + if (!attribs) return ''; let value = ''; - if (attribs) { - exports.eachAttribNumber(attribs, (n) => { - if (pool.getAttribKey(n) === key) { - value = pool.getAttribValue(n); - } - }); - } + exports.eachAttribNumber(attribs, (n) => { + if (pool.getAttribKey(n) === key) { + value = pool.getAttribValue(n); + } + }); return value; }; @@ -2038,20 +2007,19 @@ exports.subattribution = (astr, start, optEnd) => { const opOut = exports.newOp(); const doCsOp = () => { - if (csOp.chars) { - while (csOp.opcode && (attOp.opcode || iter.hasNext())) { - if (!attOp.opcode) iter.next(attOp); + if (!csOp.chars) return; + while (csOp.opcode && (attOp.opcode || iter.hasNext())) { + if (!attOp.opcode) iter.next(attOp); - if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && - attOp.lines > 0 && csOp.lines <= 0) { - csOp.lines++; - } + if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && + attOp.lines > 0 && csOp.lines <= 0) { + csOp.lines++; + } - slicerZipperFunc(attOp, csOp, opOut, null); - if (opOut.opcode) { - assem.append(opOut); - opOut.opcode = ''; - } + slicerZipperFunc(attOp, csOp, opOut, null); + if (opOut.opcode) { + assem.append(opOut); + opOut.opcode = ''; } } }; @@ -2399,13 +2367,12 @@ const followAttributes = (att1, att2, pool) => { const pair1 = pool.getAttrib(exports.parseNum(a)); for (let i = 0; i < atts.length; i++) { const pair2 = atts[i]; - if (pair1[0] === pair2[0]) { - if (pair1[1] <= pair2[1]) { - // winner of merge is pair1, delete this attribute - atts.splice(i, 1); - } - break; + if (pair1[0] !== pair2[0]) continue; + if (pair1[1] <= pair2[1]) { + // winner of merge is pair1, delete this attribute + atts.splice(i, 1); } + break; } return ''; }); From 37bb297e76a3da6939941fcdd143dac9c06f333e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 4 Mar 2021 20:37:37 -0500 Subject: [PATCH 283/322] Changeset: Improve logged error message I saw this on a production system today and wanted more information. --- src/static/js/Changeset.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index b5e72f477..bdf25f7de 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -907,7 +907,11 @@ const textLinesMutator = (lines) => { // there may be more chars in the line (newline is not reached) const sline = putCurLineInSplice(); if (!curSplice[sline]) { - console.error('curSplice[sline] not populated, actual curSplice contents is ', curSplice, '. Possibly related to https://github.com/ether/etherpad-lite/issues/2802'); + const err = new Error( + 'curSplice[sline] not populated, actual curSplice contents is ' + + `${JSON.stringify(curSplice)}. Possibly related to ` + + 'https://github.com/ether/etherpad-lite/issues/2802'); + console.error(err.stack || err.toString()); } curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol); From 7ec0d5f385b9e27077b311a48d604f91c5aedfbc Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 21 Mar 2021 14:58:05 -0400 Subject: [PATCH 284/322] Changeset: Remove unnecessary `linesApplySplice()` --- src/static/js/Changeset.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index bdf25f7de..220a3947b 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -654,15 +654,6 @@ const textLinesMutator = (lines) => { // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then // curCol == 0 - /** - * Adds and/or removes entries at a specific offset in `lines`. Called when leaving the splice. - * - * @param {[number, number?, ...string[]?]} s - curSplice - */ - const linesApplySplice = (s) => { - lines.splice(...s); - }; - /** * Get a line from `lines` at given index. * @@ -724,7 +715,7 @@ const textLinesMutator = (lines) => { * close or TODO(doc). */ const leaveSplice = () => { - linesApplySplice(curSplice); + lines.splice(...curSplice); curSplice.length = 2; curSplice[0] = curSplice[1] = 0; inSplice = false; From 18a6b7279cb97deaa3f12c636365277eeacddbe1 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 21 Oct 2021 03:35:03 -0400 Subject: [PATCH 285/322] Changeset: Only pass strings to `parseNum()` --- src/static/js/Changeset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 220a3947b..020c34e10 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -158,7 +158,7 @@ exports.opIterator = (opsStr) => { const op = optOp || exports.newOp(); if (regexResult[0]) { op.attribs = regexResult[1]; - op.lines = exports.parseNum(regexResult[2] || 0); + op.lines = exports.parseNum(regexResult[2] || '0'); op.opcode = regexResult[3]; op.chars = exports.parseNum(regexResult[4]); regexResult = nextRegexMatch(); From 02ef78e174f6ac38fb76b8031bff583ab16f7582 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 2 Oct 2021 18:05:24 -0400 Subject: [PATCH 286/322] Changeset: Make sure `opOut` is cleared `slicerZipperFunc()` previously assumed the provided `opOut` argument was a null Op. Enforce this by clearing it at the beginning. --- src/static/js/Changeset.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 020c34e10..9dc101701 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1179,6 +1179,7 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { * @param {AttributePool} pool - Can be null if definitely not needed. */ const slicerZipperFunc = (attOp, csOp, opOut, pool) => { + clearOp(opOut); if (attOp.opcode === '-') { copyOp(attOp, opOut); attOp.opcode = ''; @@ -2221,6 +2222,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { + clearOp(opOut); if (op1.opcode === '+' || op2.opcode === '+') { let whichToDo; if (op2.opcode !== '+') { From 94f550767171f9986b84825de1f6d01c9e690873 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 2 Oct 2021 18:17:11 -0400 Subject: [PATCH 287/322] Changeset: Improve `copyOp()` API Use `Object.assign()` to implement `copyOp()`, which simplifies the code and provides a return value. Also make the second op optional. --- src/static/js/Changeset.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 9dc101701..4d3c30db5 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -205,14 +205,10 @@ exports.newOp = (optOpcode) => ({ * Copies op1 to op2 * * @param {Op} op1 - src Op - * @param {Op} op2 - dest Op + * @param {Op} [op2] - dest Op. If not given, a new Op is used. + * @returns {Op} `op2` */ -const copyOp = (op1, op2) => { - op2.opcode = op1.opcode; - op2.chars = op1.chars; - op2.lines = op1.lines; - op2.attribs = op1.attribs; -}; +const copyOp = (op1, op2 = exports.newOp()) => Object.assign(op2, op1); /** * Serializes a sequence of Ops. From 1955e7b263a6eb9cf9fa8d687e3a90ef7c585d94 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 13 Oct 2021 17:00:50 -0400 Subject: [PATCH 288/322] Changeset: Replace output params with return values This improves readability and reduces the chances of introducing a bug. --- src/node/utils/padDiff.js | 6 +-- src/static/js/Changeset.js | 100 +++++++++++++------------------------ 2 files changed, 39 insertions(+), 67 deletions(-) diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 193f24458..fa612c7c2 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -273,7 +273,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let curChar = 0; let curLineOpIter = null; let curLineOpIterLine; - const curLineNextOp = Changeset.newOp('+'); + let curLineNextOp = Changeset.newOp('+'); const unpacked = Changeset.unpack(cs); const csIter = Changeset.opIterator(unpacked.ops); @@ -287,7 +287,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let indexIntoLine = 0; let done = false; while (!done) { - curLineOpIter.next(curLineNextOp); + curLineNextOp = curLineOpIter.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); done = true; @@ -307,7 +307,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { } if (!curLineNextOp.chars) { - curLineOpIter.next(curLineNextOp); + curLineNextOp = curLineOpIter.next(); } const charsToUse = Math.min(numChars, curLineNextOp.chars); diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 4d3c30db5..ead0e97a9 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -947,7 +947,7 @@ const textLinesMutator = (lines) => { * @param {string} in2 - second Op string * @param {Function} func - Callback that applies an operation to another operation. Will be called * multiple times depending on the number of operations in `in1` and `in2`. `func` has signature - * `f(op1, op2, opOut)`: + * `opOut = f(op1, op2)`: * - `op1` is the current operation from `in1`. `func` is expected to mutate `op1` to * partially or fully consume it, and MUST set `op1.opcode` to the empty string once `op1` * is fully consumed. If `op1` is not fully consumed, `func` will be called again with the @@ -956,9 +956,9 @@ const textLinesMutator = (lines) => { * the empty string. * - `op2` is the current operation from `in2`, to apply to `op1`. Has the same consumption * and advancement semantics as `op1`. - * - `opOut` MUST be mutated to reflect the result of applying `op2` (before consumption) to - * `op1` (before consumption). If there is no result (perhaps `op1` and `op2` cancelled each - * other out), `opOut.opcode` MUST be set to the empty string. + * - `opOut` is the result of applying `op2` (before consumption) to `op1` (before + * consumption). If there is no result (perhaps `op1` and `op2` cancelled each other out), + * either `opOut` must be nullish or `opOut.opcode` must be the empty string. * @returns {string} the integrated changeset */ const applyZip = (in1, in2, func) => { @@ -967,15 +967,11 @@ const applyZip = (in1, in2, func) => { const assem = exports.smartOpAssembler(); const op1 = exports.newOp(); const op2 = exports.newOp(); - const opOut = exports.newOp(); while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) { if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); - func(op1, op2, opOut); - if (opOut.opcode) { - assem.append(opOut); - opOut.opcode = ''; - } + const opOut = func(op1, op2); + if (opOut && opOut.opcode) assem.append(opOut); } assem.endDocument(); return assem.toString(); @@ -1171,11 +1167,11 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { * @param {Op} attOp - The op from the sequence that is being operated on, either an attribution * string or the earlier of two exportss being composed. * @param {Op} csOp - - * @param {Op} opOut - Mutated to hold the result of applying `csOp` to `attOp`. * @param {AttributePool} pool - Can be null if definitely not needed. + * @returns {Op} The result of applying `csOp` to `attOp`. */ -const slicerZipperFunc = (attOp, csOp, opOut, pool) => { - clearOp(opOut); +const slicerZipperFunc = (attOp, csOp, pool) => { + const opOut = exports.newOp(); if (attOp.opcode === '-') { copyOp(attOp, opOut); attOp.opcode = ''; @@ -1263,6 +1259,7 @@ const slicerZipperFunc = (attOp, csOp, opOut, pool) => { } } } + return opOut; }; /** @@ -1275,9 +1272,7 @@ const slicerZipperFunc = (attOp, csOp, opOut, pool) => { */ exports.applyToAttribution = (cs, astr, pool) => { const unpacked = exports.unpack(cs); - - return applyZip(astr, unpacked.ops, - (op1, op2, opOut) => slicerZipperFunc(op1, op2, opOut, pool)); + return applyZip(astr, unpacked.ops, (op1, op2) => slicerZipperFunc(op1, op2, pool)); }; exports.mutateAttributionLines = (cs, lines, pool) => { @@ -1293,16 +1288,13 @@ exports.mutateAttributionLines = (cs, lines, pool) => { const isNextMutOp = () => (lineIter && lineIter.hasNext()) || mut.hasMore(); - const nextMutOp = (destOp) => { + const nextMutOp = () => { if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { const line = mut.removeLines(1); lineIter = exports.opIterator(line); } - if (lineIter && lineIter.hasNext()) { - lineIter.next(destOp); - } else { - destOp.opcode = ''; - } + if (!lineIter || !lineIter.hasNext()) return exports.newOp(); + return lineIter.next(); }; let lineAssem = null; @@ -1318,13 +1310,10 @@ exports.mutateAttributionLines = (cs, lines, pool) => { lineAssem = null; }; - const csOp = exports.newOp(); - const attOp = exports.newOp(); - const opOut = exports.newOp(); + let csOp = exports.newOp(); + let attOp = exports.newOp(); while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { - if ((!csOp.opcode) && csIter.hasNext()) { - csIter.next(csOp); - } + if (!csOp.opcode && csIter.hasNext()) csOp = csIter.next(); if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { break; // done } else if (csOp.opcode === '=' && csOp.lines > 0 && (!csOp.attribs) && @@ -1333,29 +1322,22 @@ exports.mutateAttributionLines = (cs, lines, pool) => { mut.skipLines(csOp.lines); csOp.opcode = ''; } else if (csOp.opcode === '+') { + const opOut = copyOp(csOp); if (csOp.lines > 1) { const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; - copyOp(csOp, opOut); csOp.chars -= firstLineLen; csOp.lines--; opOut.lines = 1; opOut.chars = firstLineLen; } else { - copyOp(csOp, opOut); csOp.opcode = ''; } outputMutOp(opOut); csBankIndex += opOut.chars; - opOut.opcode = ''; } else { - if ((!attOp.opcode) && isNextMutOp()) { - nextMutOp(attOp); - } - slicerZipperFunc(attOp, csOp, opOut, pool); - if (opOut.opcode) { - outputMutOp(opOut); - opOut.opcode = ''; - } + if (!attOp.opcode && isNextMutOp()) attOp = nextMutOp(); + const opOut = slicerZipperFunc(attOp, csOp, pool); + if (opOut.opcode) outputMutOp(opOut); } } @@ -1446,13 +1428,13 @@ exports.compose = (cs1, cs2, pool) => { const bankIter2 = exports.stringIterator(unpacked2.charBank); const bankAssem = exports.stringAssembler(); - const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => { const op1code = op1.opcode; const op2code = op2.opcode; if (op1code === '+' && op2code === '-') { bankIter1.skip(Math.min(op1.chars, op2.chars)); } - slicerZipperFunc(op1, op2, opOut, pool); + const opOut = slicerZipperFunc(op1, op2, pool); if (opOut.opcode === '+') { if (op2code === '+') { bankAssem.append(bankIter2.take(opOut.chars)); @@ -1460,6 +1442,7 @@ exports.compose = (cs1, cs2, pool) => { bankAssem.append(bankIter1.take(opOut.chars)); } } + return opOut; }); return exports.pack(len1, len3, newOps, bankAssem.toString()); @@ -1788,9 +1771,8 @@ exports.copyAText = (atext1, atext2) => { exports.appendATextToAssembler = (atext, assem) => { // intentionally skips last newline char of atext const iter = exports.opIterator(atext.attribs); - const op = exports.newOp(); while (iter.hasNext()) { - iter.next(op); + const op = iter.next(); if (!iter.hasNext()) { // last op, exclude final newline if (op.lines <= 1) { @@ -1994,25 +1976,19 @@ exports.makeAttribsString = (opcode, attribs, pool) => { exports.subattribution = (astr, start, optEnd) => { const iter = exports.opIterator(astr); const assem = exports.smartOpAssembler(); - const attOp = exports.newOp(); + let attOp = exports.newOp(); const csOp = exports.newOp(); - const opOut = exports.newOp(); const doCsOp = () => { if (!csOp.chars) return; while (csOp.opcode && (attOp.opcode || iter.hasNext())) { - if (!attOp.opcode) iter.next(attOp); - + if (!attOp.opcode) attOp = iter.next(); if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) { csOp.lines++; } - - slicerZipperFunc(attOp, csOp, opOut, null); - if (opOut.opcode) { - assem.append(opOut); - opOut.opcode = ''; - } + const opOut = slicerZipperFunc(attOp, csOp, null); + if (opOut.opcode) assem.append(opOut); } }; @@ -2025,10 +2001,7 @@ exports.subattribution = (astr, start, optEnd) => { if (attOp.opcode) { assem.append(attOp); } - while (iter.hasNext()) { - iter.next(attOp); - assem.append(attOp); - } + while (iter.hasNext()) assem.append(iter.next()); } else { csOp.opcode = '='; csOp.chars = optEnd - start; @@ -2067,7 +2040,7 @@ exports.inverse = (cs, lines, alines, pool) => { let curChar = 0; let curLineOpIter = null; let curLineOpIterLine; - const curLineNextOp = exports.newOp('+'); + let curLineNextOp = exports.newOp('+'); const unpacked = exports.unpack(cs); const csIter = exports.opIterator(unpacked.ops); @@ -2081,7 +2054,7 @@ exports.inverse = (cs, lines, alines, pool) => { let indexIntoLine = 0; let done = false; while (!done && curLineOpIter.hasNext()) { - curLineOpIter.next(curLineNextOp); + curLineNextOp = curLineOpIter.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); done = true; @@ -2099,9 +2072,7 @@ exports.inverse = (cs, lines, alines, pool) => { curLineNextOp.chars = 0; curLineOpIter = exports.opIterator(alinesGet(curLine)); } - if (!curLineNextOp.chars) { - curLineOpIter.next(curLineNextOp); - } + if (!curLineNextOp.chars) curLineNextOp = curLineOpIter.next(); const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && curLineNextOp.lines > 0); @@ -2217,8 +2188,8 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); - const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2, opOut) => { - clearOp(opOut); + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => { + const opOut = exports.newOp(); if (op1.opcode === '+' || op2.opcode === '+') { let whichToDo; if (op2.opcode !== '+') { @@ -2336,6 +2307,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { newLen += opOut.chars; break; } + return opOut; }); newLen += oldLen - oldPos; From 44d99733c63f33c85e97a8edce3d50c8ba192846 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 30 Sep 2021 13:39:02 -0400 Subject: [PATCH 289/322] Changeset: Check `.hasNext()` before calling `.next()` --- src/node/utils/padDiff.js | 4 ++-- src/static/js/Changeset.js | 4 +++- src/static/js/linestylefilter.js | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index fa612c7c2..900a452c7 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -286,7 +286,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { curLineOpIterLine = curLine; let indexIntoLine = 0; let done = false; - while (!done) { + while (!done && curLineOpIter.hasNext()) { curLineNextOp = curLineOpIter.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); @@ -307,7 +307,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { } if (!curLineNextOp.chars) { - curLineNextOp = curLineOpIter.next(); + curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : Changeset.newOp(); } const charsToUse = Math.min(numChars, curLineNextOp.chars); diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index ead0e97a9..07e17e5b7 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -2072,7 +2072,9 @@ exports.inverse = (cs, lines, alines, pool) => { curLineNextOp.chars = 0; curLineOpIter = exports.opIterator(alinesGet(curLine)); } - if (!curLineNextOp.chars) curLineNextOp = curLineOpIter.next(); + if (!curLineNextOp.chars) { + curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : exports.newOp(); + } const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && curLineNextOp.lines > 0); diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index ac8df82f4..84668ea46 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -108,7 +108,7 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool let nextOp, nextOpClasses; const goNextOp = () => { - nextOp = attributionIter.next(); + nextOp = attributionIter.hasNext() ? attributionIter.next() : Changeset.newOp(); nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); }; goNextOp(); From ca5bdddc599fe165c4c47e9df8daaf471d454efe Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 8 Nov 2021 23:35:03 -0500 Subject: [PATCH 290/322] Changeset: Use `break` instead of `done` variable --- src/node/utils/padDiff.js | 8 +++----- src/static/js/Changeset.js | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index 900a452c7..670e8d6a1 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -285,15 +285,13 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); curLineOpIterLine = curLine; let indexIntoLine = 0; - let done = false; - while (!done && curLineOpIter.hasNext()) { + while (curLineOpIter.hasNext()) { curLineNextOp = curLineOpIter.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); - done = true; - } else { - indexIntoLine += curLineNextOp.chars; + break; } + indexIntoLine += curLineNextOp.chars; } } diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 07e17e5b7..395dbc2dd 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -2052,15 +2052,13 @@ exports.inverse = (cs, lines, alines, pool) => { curLineOpIter = exports.opIterator(alinesGet(curLine)); curLineOpIterLine = curLine; let indexIntoLine = 0; - let done = false; - while (!done && curLineOpIter.hasNext()) { + while (curLineOpIter.hasNext()) { curLineNextOp = curLineOpIter.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); - done = true; - } else { - indexIntoLine += curLineNextOp.chars; + break; } + indexIntoLine += curLineNextOp.chars; } } From 42d4d8269c7e3c1323080aff3f49c2827f1c466b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 21 Mar 2021 19:48:30 -0400 Subject: [PATCH 291/322] Changeset: Refactor `appendATextToAssembler()` for readability --- src/static/js/Changeset.js | 43 ++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 395dbc2dd..9cf105760 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1771,33 +1771,26 @@ exports.copyAText = (atext1, atext2) => { exports.appendATextToAssembler = (atext, assem) => { // intentionally skips last newline char of atext const iter = exports.opIterator(atext.attribs); + let lastOp = null; while (iter.hasNext()) { - const op = iter.next(); - if (!iter.hasNext()) { - // last op, exclude final newline - if (op.lines <= 1) { - op.lines = 0; - op.chars--; - if (op.chars) { - assem.append(op); - } - } else { - const nextToLastNewlineEnd = - atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; - const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; - op.lines--; - op.chars -= (lastLineLength + 1); - assem.append(op); - op.lines = 0; - op.chars = lastLineLength; - if (op.chars) { - assem.append(op); - } - } - } else { - assem.append(op); - } + if (lastOp != null) assem.append(lastOp); + lastOp = iter.next(); } + if (lastOp == null) return; + // exclude final newline + if (lastOp.lines <= 1) { + lastOp.lines = 0; + lastOp.chars--; + } else { + const nextToLastNewlineEnd = atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; + const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; + lastOp.lines--; + lastOp.chars -= (lastLineLength + 1); + assem.append(lastOp); + lastOp.lines = 0; + lastOp.chars = lastLineLength; + } + if (lastOp.chars) assem.append(lastOp); }; /** From efeb69b4ea361fc28da0f475b0331b189b53f865 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 22 Mar 2021 20:18:09 -0400 Subject: [PATCH 292/322] Changeset: Simplify `slicerZipperFunc()` --- src/static/js/Changeset.js | 112 ++++++++++--------------------------- 1 file changed, 31 insertions(+), 81 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 9cf105760..b15c35f69 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1172,92 +1172,42 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { */ const slicerZipperFunc = (attOp, csOp, pool) => { const opOut = exports.newOp(); - if (attOp.opcode === '-') { + if (!attOp.opcode) { + copyOp(csOp, opOut); + csOp.opcode = ''; + } else if (!csOp.opcode) { copyOp(attOp, opOut); attOp.opcode = ''; - } else if (!attOp.opcode) { + } else if (attOp.opcode === '-') { + copyOp(attOp, opOut); + attOp.opcode = ''; + } else if (csOp.opcode === '+') { copyOp(csOp, opOut); csOp.opcode = ''; } else { - switch (csOp.opcode) { - case '-': - { - if (csOp.chars <= attOp.chars) { - // delete or delete part - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - // csOp is a remove op and remove ops normally never have any attributes, so this should - // normally be the empty string. However, padDiff.js adds attributes to remove ops and - // needs them preserved so they are copied here. - opOut.attribs = csOp.attribs; - } - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - csOp.opcode = ''; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // delete and keep going - if (attOp.opcode === '=') { - opOut.opcode = '-'; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - // csOp is a remove op and remove ops normally never have any attributes, so this should - // normally be the empty string. However, padDiff.js adds attributes to remove ops and - // needs them preserved so they are copied here. - opOut.attribs = csOp.attribs; - } - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - attOp.opcode = ''; - } - break; - } - case '+': - { - // insert - copyOp(csOp, opOut); - csOp.opcode = ''; - break; - } - case '=': - { - if (csOp.chars <= attOp.chars) { - // keep or keep part - opOut.opcode = attOp.opcode; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - csOp.opcode = ''; - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - if (!attOp.chars) { - attOp.opcode = ''; - } - } else { - // keep and keep going - opOut.opcode = attOp.opcode; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = exports.composeAttributes( - attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } - } + opOut.opcode = { + '+': { + '-': '', // The '-' cancels out (some of) the '+', leaving any remainder for the next call. + '=': '+', + }, + '=': { + '-': '-', + '=': '=', + }, + }[attOp.opcode][csOp.opcode]; + const [fullyConsumedOp, partiallyConsumedOp] = [attOp, csOp].sort((a, b) => a.chars - b.chars); + opOut.chars = fullyConsumedOp.chars; + opOut.lines = fullyConsumedOp.lines; + opOut.attribs = csOp.opcode === '-' + // csOp is a remove op and remove ops normally never have any attributes, so this should + // normally be the empty string. However, padDiff.js adds attributes to remove ops and needs + // them preserved so they are copied here. + ? csOp.attribs + : exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode === '=', pool); + partiallyConsumedOp.chars -= fullyConsumedOp.chars; + partiallyConsumedOp.lines -= fullyConsumedOp.lines; + if (!partiallyConsumedOp.chars) partiallyConsumedOp.opcode = ''; + fullyConsumedOp.opcode = ''; } return opOut; }; From 097f2623c63eeab475c060504e90a21ce40fd6f4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 22 Mar 2021 20:23:59 -0400 Subject: [PATCH 293/322] Changeset: Add sanity checks to `slicerZipperFunc()` --- src/static/js/Changeset.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index b15c35f69..50277e023 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1185,6 +1185,17 @@ const slicerZipperFunc = (attOp, csOp, pool) => { copyOp(csOp, opOut); csOp.opcode = ''; } else { + for (const op of [attOp, csOp]) { + assert(op.chars >= op.lines, `op has more newlines than chars: ${op.toString()}`); + } + assert( + attOp.chars < csOp.chars ? attOp.lines <= csOp.lines + : attOp.chars > csOp.chars ? attOp.lines >= csOp.lines + : attOp.lines === csOp.lines, + 'line count mismatch when composing changesets A*B; ' + + `opA: ${attOp.toString()} opB: ${csOp.toString()}`); + assert(['+', '='].includes(attOp.opcode), `unexpected opcode in op: ${attOp.toString()}`); + assert(['-', '='].includes(csOp.opcode), `unexpected opcode in op: ${csOp.toString()}`); opOut.opcode = { '+': { '-': '', // The '-' cancels out (some of) the '+', leaving any remainder for the next call. From 0ae8fb144192d7f2fbc894fb50e3502b7a4d2735 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 11 Oct 2021 04:31:28 -0400 Subject: [PATCH 294/322] Changeset: Use string concatenation instead of array join People report that string concatenation is faster. Also, I think it's more readable. --- src/static/js/Changeset.js | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 50277e023..85ee35700 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -482,24 +482,22 @@ exports.mergingOpAssembler = () => { * @returns {OpAssembler} */ exports.opAssembler = () => { - const pieces = []; + let serialized = ''; /** * @param {Op} op - Operation to add. Ownership remains with the caller. */ const append = (op) => { - pieces.push(op.attribs); - if (op.lines) { - pieces.push('|', exports.numToString(op.lines)); - } - pieces.push(op.opcode); - pieces.push(exports.numToString(op.chars)); + if (op.attribs != null) serialized += op.attribs; + if (op.lines) serialized += `|${exports.numToString(op.lines)}`; + if (op.opcode != null) serialized += op.opcode; + serialized += exports.numToString(op.chars); }; - const toString = () => pieces.join(''); + const toString = () => serialized; const clear = () => { - pieces.length = 0; + serialized = ''; }; return { append, @@ -573,22 +571,14 @@ exports.stringIterator = (str) => { /** * @returns {StringAssembler} */ -exports.stringAssembler = () => { - const pieces = []; - +exports.stringAssembler = () => ({ + _str: '', /** * @param {string} x - */ - const append = (x) => { - pieces.push(String(x)); - }; - - const toString = () => pieces.join(''); - return { - append, - toString, - }; -}; + append(x) { this._str += String(x); }, + toString() { return this._str; }, +}); /** * @typedef {object} StringArrayLike From 9c17b03660fba65a118ebc71f8fdbd001985e8a7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 3 Oct 2021 04:47:29 -0400 Subject: [PATCH 295/322] Changeset: Require Op opcode and attribs to be strings --- src/static/js/Changeset.js | 6 ++++-- src/static/js/contentcollector.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 85ee35700..426835915 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -488,9 +488,11 @@ exports.opAssembler = () => { * @param {Op} op - Operation to add. Ownership remains with the caller. */ const append = (op) => { - if (op.attribs != null) serialized += op.attribs; + if (!op.opcode) throw new TypeError('null op'); + if (typeof op.attribs !== 'string') throw new TypeError('attribs must be a string'); + serialized += op.attribs; if (op.lines) serialized += `|${exports.numToString(op.lines)}`; - if (op.opcode != null) serialized += op.opcode; + serialized += op.opcode; serialized += exports.numToString(op.chars); }; diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 3d2bd9aa8..54c628804 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -92,7 +92,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) attribsBuilder = Changeset.smartOpAssembler(); }, textOfLine: (i) => textArray[i], - appendText: (txt, attrString) => { + appendText: (txt, attrString = '') => { textArray[textArray.length - 1] += txt; op.attribs = attrString; op.chars = txt.length; From 6d5b737140979c03391efb89332d2d879134eda8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 11 Oct 2021 17:37:43 -0400 Subject: [PATCH 296/322] Changeset: Replace `.apply()` with spread operator --- src/static/js/Changeset.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 426835915..dab1e5afc 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -870,14 +870,14 @@ const textLinesMutator = (lines) => { curLine++; newLines.splice(0, 1); // insert the remaining new lines - Array.prototype.push.apply(curSplice, newLines); + curSplice.push(...newLines); curLine += newLines.length; // insert the remaining chars from the "old" line (e.g. the line we were in // when we started to insert new lines) curSplice.push(theLine.substring(lineCol)); curCol = 0; // TODO(doc) why is this not set to the length of last line? } else { - Array.prototype.push.apply(curSplice, newLines); + curSplice.push(...newLines); curLine += newLines.length; } } else { From 1cad5d881a4f383006d56fc3e7644be17dc24867 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 11 Oct 2021 18:14:01 -0400 Subject: [PATCH 297/322] Changeset: Use `for...of` iteration to improve readability --- src/static/js/Changeset.js | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index dab1e5afc..e70966d5f 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1146,9 +1146,9 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { }); atts.sort(); const buf = exports.stringAssembler(); - for (let i = 0; i < atts.length; i++) { + for (const att of atts) { buf.append('*'); - buf.append(exports.numToString(pool.putAttrib(atts[i]))); + buf.append(exports.numToString(pool.putAttrib(att))); } return buf.toString(); }; @@ -1306,8 +1306,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { */ exports.joinAttributionLines = (theAlines) => { const assem = exports.mergingOpAssembler(); - for (let i = 0; i < theAlines.length; i++) { - const aline = theAlines[i]; + for (const aline of theAlines) { const iter = exports.opIterator(aline); while (iter.hasNext()) { assem.append(iter.next()); @@ -1507,10 +1506,8 @@ const toSplices = (cs) => { exports.characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { let newStartChar = startChar; let newEndChar = endChar; - const splices = toSplices(cs); let lengthChangeSoFar = 0; - for (let i = 0; i < splices.length; i++) { - const splice = splices[i]; + for (const splice of toSplices(cs)) { const spliceStart = splice[0] + lengthChangeSoFar; const spliceEnd = splice[1] + lengthChangeSoFar; const newTextLength = splice[2].length; @@ -1906,8 +1903,7 @@ exports.makeAttribsString = (opcode, attribs, pool) => { attribs.sort(); } const result = []; - for (let i = 0; i < attribs.length; i++) { - const pair = attribs[i]; + for (const pair of attribs) { if (opcode === '=' || (opcode === '+' && pair[1])) { result.push(`*${exports.numToString(pool.putAttrib(pair))}`); } @@ -2072,23 +2068,15 @@ exports.inverse = (cs, lines, alines, pool) => { }; }; - const attribKeys = []; - const attribValues = []; while (csIter.hasNext()) { const csOp = csIter.next(); if (csOp.opcode === '=') { if (csOp.attribs) { - attribKeys.length = 0; - attribValues.length = 0; - exports.eachAttribNumber(csOp.attribs, (n) => { - attribKeys.push(pool.getAttribKey(n)); - attribValues.push(pool.getAttribValue(n)); - }); + const csAttribs = []; + exports.eachAttribNumber(csOp.attribs, (n) => csAttribs.push(pool.getAttrib(n))); const undoBackToAttribs = cachedStrFunc((attribs) => { const backAttribs = []; - for (let i = 0; i < attribKeys.length; i++) { - const appliedKey = attribKeys[i]; - const appliedValue = attribValues[i]; + for (const [appliedKey, appliedValue] of csAttribs) { const oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool); if (appliedValue !== oldValue) { backAttribs.push([appliedKey, oldValue]); @@ -2289,9 +2277,9 @@ const followAttributes = (att1, att2, pool) => { }); // we've only removed attributes, so they're already sorted const buf = exports.stringAssembler(); - for (let i = 0; i < atts.length; i++) { + for (const att of atts) { buf.append('*'); - buf.append(exports.numToString(pool.putAttrib(atts[i]))); + buf.append(exports.numToString(pool.putAttrib(att))); } return buf.toString(); }; From 9401ae876bd76ca6b2afb7753d6f48393a89e86f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 11 Oct 2021 21:11:28 -0400 Subject: [PATCH 298/322] Changeset: Sort attributes by keys, not full string rep --- src/static/js/Changeset.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index e70966d5f..78d5d3edc 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1089,6 +1089,15 @@ exports.mutateTextLines = (cs, lines) => { mut.close(); }; +/** + * Sorts an array of attributes by key. + * + * @param {Attribute[]} attribs - The array of attributes to sort in place. + * @returns {Attribute[]} The `attribs` array. + */ +const sortAttribs = + (attribs) => attribs.sort((a, b) => (a[0] > b[0] ? 1 : 0) - (a[0] < b[0] ? 1 : 0)); + /** * Composes two attribute strings (see below) into one. * @@ -1144,9 +1153,8 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { } return ''; }); - atts.sort(); const buf = exports.stringAssembler(); - for (const att of atts) { + for (const att of sortAttribs(atts)) { buf.append('*'); buf.append(exports.numToString(pool.putAttrib(att))); } @@ -1900,7 +1908,7 @@ exports.makeAttribsString = (opcode, attribs, pool) => { } else if (pool && attribs.length) { if (attribs.length > 1) { attribs = attribs.slice(); - attribs.sort(); + sortAttribs(attribs); } const result = []; for (const pair of attribs) { From b62534a6b22e498eadda95dd77dea13f055ba44f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 12 Oct 2021 02:00:56 -0400 Subject: [PATCH 299/322] Changeset: Use Maps to simplify attribute processing --- src/static/js/Changeset.js | 44 ++++++++++++-------------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 78d5d3edc..2df33ec46 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1129,32 +1129,23 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { return att2; } if (!att2) return att1; - const atts = []; + const atts = new Map(); att1.replace(/\*([0-9a-z]+)/g, (_, a) => { - atts.push(pool.getAttrib(exports.parseNum(a))); + const [key, val] = pool.getAttrib(exports.parseNum(a)); + atts.set(key, val); return ''; }); att2.replace(/\*([0-9a-z]+)/g, (_, a) => { - const pair = pool.getAttrib(exports.parseNum(a)); - let found = false; - for (let i = 0; i < atts.length; i++) { - const oldPair = atts[i]; - if (oldPair[0] !== pair[0]) continue; - if (pair[1] || resultIsMutation) { - oldPair[1] = pair[1]; - } else { - atts.splice(i, 1); - } - found = true; - break; - } - if ((!found) && (pair[1] || resultIsMutation)) { - atts.push(pair); + const [key, val] = pool.getAttrib(exports.parseNum(a)); + if (val || resultIsMutation) { + atts.set(key, val); + } else { + atts.delete(key); } return ''; }); const buf = exports.stringAssembler(); - for (const att of sortAttribs(atts)) { + for (const att of sortAttribs([...atts])) { buf.append('*'); buf.append(exports.numToString(pool.putAttrib(att))); } @@ -2265,22 +2256,15 @@ const followAttributes = (att1, att2, pool) => { // to produce the merged set. if ((!att2) || (!pool)) return ''; if (!att1) return att2; - const atts = []; + const atts = new Map(); att2.replace(/\*([0-9a-z]+)/g, (_, a) => { - atts.push(pool.getAttrib(exports.parseNum(a))); + const [key, val] = pool.getAttrib(exports.parseNum(a)); + atts.set(key, val); return ''; }); att1.replace(/\*([0-9a-z]+)/g, (_, a) => { - const pair1 = pool.getAttrib(exports.parseNum(a)); - for (let i = 0; i < atts.length; i++) { - const pair2 = atts[i]; - if (pair1[0] !== pair2[0]) continue; - if (pair1[1] <= pair2[1]) { - // winner of merge is pair1, delete this attribute - atts.splice(i, 1); - } - break; - } + const [key, val] = pool.getAttrib(exports.parseNum(a)); + if (atts.has(key) && val <= atts.get(key)) atts.delete(key); return ''; }); // we've only removed attributes, so they're already sorted From 4f4a775d9e44ae6caac8bdcad573d122aca00c7d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 05:19:35 -0400 Subject: [PATCH 300/322] Changeset: Improve handling of missing attribute in old pool --- src/static/js/Changeset.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 2df33ec46..d261db748 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -1563,18 +1563,10 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => { // order of attribs stays the same return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { const oldNum = exports.parseNum(a); - let pair = oldPool.getAttrib(oldNum); - - /* - * Setting an empty pair. Required for when delete pad contents / attributes - * while another user has the timeslider open. - * - * Fixes https://github.com/ether/etherpad-lite/issues/3932 - */ - if (!pair) { - pair = []; - } - + const pair = oldPool.getAttrib(oldNum); + // The attribute might not be in the old pool if the user is viewing the current revision in the + // timeslider and text is deleted. See: https://github.com/ether/etherpad-lite/issues/3932 + if (!pair) return ''; const newNum = newPool.putAttrib(pair); return `*${exports.numToString(newNum)}`; }) + fromDollar; From 1bbe0d921514cec6a118df4c3e1b7ba276c7ca7b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 05:17:18 -0400 Subject: [PATCH 301/322] Changeset: Use `in` check to help TypeScript narrowing --- src/static/js/Changeset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index d261db748..21de89c43 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -649,7 +649,7 @@ const textLinesMutator = (lines) => { * @returns {string} */ const linesGet = (idx) => { - if (lines.get) { + if ('get' in lines) { return lines.get(idx); } else { return lines[idx]; From eae814f5a066b0ea24361bf2e13ea9620a37ec6a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 25 Oct 2021 01:18:17 -0400 Subject: [PATCH 302/322] pad_utils: New `warnWithStack()` function --- src/static/js/pad_utils.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.js index 6fb46fde0..9bea959da 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.js @@ -89,6 +89,25 @@ const urlRegex = (() => { })(); const padutils = { + /** + * Prints a warning message followed by a stack trace (to make it easier to figure out what code + * is using the deprecated function). + * + * Most browsers include UI widget to examine the stack at the time of the warning, but this + * includes the stack in the log message for a couple of reasons: + * - This makes it possible to see the stack if the code runs in Node.js. + * - Users are more likely to paste the stack in bug reports they might file. + * + * @param {...*} args - Passed to `console.warn`, with a stack trace appended. + */ + warnWithStack: (...args) => { + const err = new Error(); + if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnWithStack); + err.name = ''; + if (err.stack) args.push(err.stack); + console.warn(...args); + }, + escapeHtml: (x) => Security.escapeHTML(String(x)), uniqueId: () => { const pad = require('./pad').pad; // Sidestep circular dependency From a65498e849c9d49b33a008c44af0ba3f8d2ba62c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 13 Oct 2021 00:19:09 -0400 Subject: [PATCH 303/322] Changeset: Move `SmartOpAssembler.appendOpWithText()` to a standalone function --- CHANGELOG.md | 3 ++ src/static/js/Changeset.js | 65 ++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cf6d1ded..98ccb583d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,9 @@ * Changes to the `src/static/js/Changeset.js` library: * `opIterator()`: The unused start index parameter has been removed, as has the unused `lastIndex()` method on the returned object. + * `smartOpAssembler()`: The returned object's `appendOpWithText()` method is + deprecated without a replacement available to plugins (if you need one, + let us know and we can make the private `opsFromText()` function public). * Several functions that should have never been public are no longer exported: `applyZip()`, `assert()`, `clearOp()`, `cloneOp()`, `copyOp()`, `error()`, `followAttributes()`, `opString()`, `stringOp()`, diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 21de89c43..a335f7ccb 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -23,6 +23,7 @@ */ const AttributePool = require('./AttributePool'); +const {padutils} = require('./pad_utils'); /** * A `[key, value]` pair of strings describing a text attribute. @@ -230,6 +231,36 @@ const copyOp = (op1, op2 = exports.newOp()) => Object.assign(op2, op1); * @property {Function} toString - */ +/** + * Generates operations from the given text and attributes. + * + * @param {('-'|'+'|'=')} opcode - The operator to use. + * @param {string} text - The text to remove/add/keep. + * @param {(string|Attribute[])} [attribs] - The attributes to apply to the operations. See + * `makeAttribsString`. + * @param {?AttributePool} [pool] - See `makeAttribsString`. + * @yields {Op} One or two ops (depending on the presense of newlines) that cover the given text. + * @returns {Generator} + */ +const opsFromText = function* (opcode, text, attribs = '', pool = null) { + const op = exports.newOp(opcode); + op.attribs = exports.makeAttribsString(opcode, attribs, pool); + const lastNewlinePos = text.lastIndexOf('\n'); + if (lastNewlinePos < 0) { + op.chars = text.length; + op.lines = 0; + yield op; + } else { + op.chars = lastNewlinePos + 1; + op.lines = text.match(/\n/g).length; + yield op; + const op2 = copyOp(op); + op2.chars = text.length - (lastNewlinePos + 1); + op2.lines = 0; + yield op2; + } +}; + /** * Creates an object that allows you to append operations (type Op) and also compresses them if * possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser @@ -353,6 +384,7 @@ exports.smartOpAssembler = () => { /** * Generates operations from the given text and attributes. * + * @deprecated Use `opsFromText` instead. * @param {('-'|'+'|'=')} opcode - The operator to use. * @param {string} text - The text to remove/add/keep. * @param {(string|Attribute[])} attribs - The attributes to apply to the operations. See @@ -360,21 +392,9 @@ exports.smartOpAssembler = () => { * @param {?AttributePool} pool - See `makeAttribsString`. */ const appendOpWithText = (opcode, text, attribs, pool) => { - const op = exports.newOp(opcode); - op.attribs = exports.makeAttribsString(opcode, attribs, pool); - const lastNewlinePos = text.lastIndexOf('\n'); - if (lastNewlinePos < 0) { - op.chars = text.length; - op.lines = 0; - append(op); - } else { - op.chars = lastNewlinePos + 1; - op.lines = text.match(/\n/g).length; - append(op); - op.chars = text.length - (lastNewlinePos + 1); - op.lines = 0; - append(op); - } + padutils.warnWithStack('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' + + 'use opsFromText() instead.'); + for (const op of opsFromText(opcode, text, attribs, pool)) append(op); }; const toString = () => { @@ -1450,9 +1470,12 @@ exports.makeSplice = (oldFullText, spliceStart, numRemoved, newText, optNewTextA const newLen = oldLen + newText.length - oldText.length; const assem = exports.smartOpAssembler(); - assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); - assem.appendOpWithText('-', oldText); - assem.appendOpWithText('+', newText, optNewTextAPairs, pool); + const ops = (function* () { + yield* opsFromText('=', oldFullText.substring(0, spliceStart)); + yield* opsFromText('-', oldText); + yield* opsFromText('+', newText, optNewTextAPairs, pool); + })(); + for (const op of ops) assem.append(op); assem.endDocument(); return exports.pack(oldLen, newLen, assem.toString(), newText); }; @@ -1580,7 +1603,7 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => { */ exports.makeAttribution = (text) => { const assem = exports.smartOpAssembler(); - assem.appendOpWithText('+', text); + for (const op of opsFromText('+', text)) assem.append(op); return assem.toString(); }; @@ -1839,7 +1862,7 @@ exports.builder = (oldLen) => { * @returns {Builder} this */ keepText: (text, attribs, pool) => { - assem.appendOpWithText('=', text, attribs, pool); + for (const op of opsFromText('=', text, attribs, pool)) assem.append(op); return self; }, @@ -1852,7 +1875,7 @@ exports.builder = (oldLen) => { * @returns {Builder} this */ insert: (text, attribs, pool) => { - assem.appendOpWithText('+', text, attribs, pool); + for (const op of opsFromText('+', text, attribs, pool)) assem.append(op); charBank.append(text); return self; }, From c94906f1f856e7bc7362253819c36f31eb464584 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 15 Nov 2021 03:13:25 -0500 Subject: [PATCH 304/322] MinifyWorker: Delete unhelpful comment The comment should have been a commit message instead. --- src/node/utils/MinifyWorker.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.js index 1ef6490ee..7b6378ad2 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.js @@ -13,32 +13,6 @@ const compressJS = (content) => Terser.minify(content); const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => { try { const absPath = path.resolve(ROOT_DIR, filename); - - /* - * Changes done to migrate CleanCSS 3.x -> 4.x: - * - * 1. Rework the rebase logic, because the API was simplified (but we have - * less control now). See: - * https://github.com/jakubpawlowicz/clean-css/blob/08f3a74925524d30bbe7ac450979de0a8a9e54b2/README.md#important-40-breaking-changes - * - * EXAMPLE: - * The URLs contained in a CSS file (including all the stylesheets - * imported by it) residing on disk at: - * /home/muxator/etherpad/src/static/css/pad.css - * - * Will be rewritten rebasing them to: - * /home/muxator/etherpad/src/static/css - * - * 2. CleanCSS.minify() can either receive a string containing the CSS, or - * an array of strings. In that case each array element is interpreted as - * an absolute local path from which the CSS file is read. - * - * In version 4.x, CleanCSS API was simplified, eliminating the - * relativeTo parameter, and thus we cannot use our already loaded - * "content" argument, but we have to wrap the absolute path to the CSS - * in an array and ask the library to read it by itself. - */ - const basePath = path.dirname(absPath); new CleanCSS({ From 98f0e931feb9f5d9cfd8c51f9f3403e37a5e5dae Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 15 Nov 2021 03:11:54 -0500 Subject: [PATCH 305/322] MinifyWorker: Promisify `compressCSS` --- src/node/utils/MinifyWorker.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.js index 7b6378ad2..5736bd3a1 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.js @@ -10,25 +10,21 @@ const Threads = require('threads'); const compressJS = (content) => Terser.minify(content); -const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => { +const compressCSS = async (filename, ROOT_DIR) => { try { const absPath = path.resolve(ROOT_DIR, filename); const basePath = path.dirname(absPath); - - new CleanCSS({ + const output = await new CleanCSS({ rebase: true, rebaseTo: basePath, - }).minify([absPath], (errors, minified) => { - if (errors) return rej(errors); - - return res(minified.styles); - }); + }).minify([absPath]); + return output.styles; } catch (error) { // on error, just yield the un-minified original, but write a log message console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`); callback(null, content); } -}); +}; Threads.expose({ compressJS, From 24b22e3a6df7781e5c147b7e189d78a230a696c0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 15 Nov 2021 03:14:04 -0500 Subject: [PATCH 306/322] MinifyWorker: Fix buggy error handling in `compressCSS` --- src/node/utils/MinifyWorker.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.js index 5736bd3a1..364ecc96c 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.js @@ -5,14 +5,15 @@ const CleanCSS = require('clean-css'); const Terser = require('terser'); +const fsp = require('fs').promises; const path = require('path'); const Threads = require('threads'); const compressJS = (content) => Terser.minify(content); const compressCSS = async (filename, ROOT_DIR) => { + const absPath = path.resolve(ROOT_DIR, filename); try { - const absPath = path.resolve(ROOT_DIR, filename); const basePath = path.dirname(absPath); const output = await new CleanCSS({ rebase: true, @@ -22,7 +23,7 @@ const compressCSS = async (filename, ROOT_DIR) => { } catch (error) { // on error, just yield the un-minified original, but write a log message console.error(`Unexpected error minifying ${filename} (${absPath}): ${error}`); - callback(null, content); + return await fsp.readFile(absPath, 'utf8'); } }; From b540c2bc481d73ff7ee5039a21d49471bdfb98f9 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 19 Nov 2021 15:27:40 +0000 Subject: [PATCH 307/322] release: Add version to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ccb583d..d5d4bd07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Next Release +# 1.8.15 ### Security fixes From 6e19d888d3b9b204307d4adbbba5c37f8c2ec2f0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 20 Nov 2021 01:09:42 -0500 Subject: [PATCH 308/322] docs: Check in `src/bin/doc/package-lock.json` Looks like this was accidentally deleted in commit 4a65acf417848a1f75d06e6019da9b26a19deeab. --- src/bin/doc/package-lock.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/bin/doc/package-lock.json diff --git a/src/bin/doc/package-lock.json b/src/bin/doc/package-lock.json new file mode 100644 index 000000000..40fe13e45 --- /dev/null +++ b/src/bin/doc/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "node-doc-generator", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "marked": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", + "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==" + } + } +} From db105e865000943fd7134b8a8278da871e34679b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 15 Nov 2021 03:15:57 -0500 Subject: [PATCH 309/322] ImportHtml: Get the logger once --- src/node/utils/ImportHtml.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 2749977d6..fc6a95bde 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -22,9 +22,9 @@ const jsdom = require('jsdom'); const rehype = require('rehype'); const minifyWhitespace = require('rehype-minify-whitespace'); -exports.setPadHTML = async (pad, html) => { - const apiLogger = log4js.getLogger('ImportHtml'); +const apiLogger = log4js.getLogger('ImportHtml'); +exports.setPadHTML = async (pad, html) => { rehype() .use(minifyWhitespace, {newlines: false}) .process(html, (err, output) => { From afdb01681a53735a3387fa09ec42b29210aef8c9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 15 Nov 2021 03:17:38 -0500 Subject: [PATCH 310/322] ImportHtml: Reuse the HTML processor --- src/node/utils/ImportHtml.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index fc6a95bde..cc3ecb09b 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -23,13 +23,12 @@ const rehype = require('rehype'); const minifyWhitespace = require('rehype-minify-whitespace'); const apiLogger = log4js.getLogger('ImportHtml'); +const processor = rehype().use(minifyWhitespace, {newlines: false}); exports.setPadHTML = async (pad, html) => { - rehype() - .use(minifyWhitespace, {newlines: false}) - .process(html, (err, output) => { - html = String(output); - }); + processor.process(html, (err, output) => { + html = String(output); + }); const {window: {document}} = new jsdom.JSDOM(html); From 50dce085c297e3c5f3c03f41a50eef52327cc1d5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 15 Nov 2021 14:18:04 -0500 Subject: [PATCH 311/322] ImportHtml: In-line an unnecessary function --- src/node/utils/ImportHtml.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index cc3ecb09b..807dab536 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -66,28 +66,24 @@ exports.setPadHTML = async (pad, html) => { apiLogger.debug(newText); const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`; - const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => { - const attribsIter = Changeset.opIterator(attribs); - let textIndex = 0; - const newTextStart = 0; - const newTextEnd = newText.length; - while (attribsIter.hasNext()) { - const op = attribsIter.next(); - const nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { - func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); - } - textIndex = nextIndex; - } - }; - // create a new changeset with a helper builder object const builder = Changeset.builder(1); // assemble each line into the builder - eachAttribRun(newAttribs, (start, end, attribs) => { - builder.insert(newText.substring(start, end), attribs); - }); + const attribsIter = Changeset.opIterator(newAttribs); + let textIndex = 0; + const newTextStart = 0; + const newTextEnd = newText.length; + while (attribsIter.hasNext()) { + const op = attribsIter.next(); + const nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { + const start = Math.max(newTextStart, textIndex); + const end = Math.min(newTextEnd, nextIndex); + builder.insert(newText.substring(start, end), op.attribs); + } + textIndex = nextIndex; + } // the changeset is ready! const theChangeset = builder.toString(); From abf4eeeb173f768f7350b63021d700527537f492 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 15 Nov 2021 03:18:27 -0500 Subject: [PATCH 312/322] ImportHtml: Wait for HTML processing to complete --- src/node/utils/ImportHtml.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 807dab536..58b79f3a1 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -26,10 +26,7 @@ const apiLogger = log4js.getLogger('ImportHtml'); const processor = rehype().use(minifyWhitespace, {newlines: false}); exports.setPadHTML = async (pad, html) => { - processor.process(html, (err, output) => { - html = String(output); - }); - + html = String(await processor.process(html)); const {window: {document}} = new jsdom.JSDOM(html); // Appends a line break, used by Etherpad to ensure a caret is available From 5c1177a3d9599d56662a2cafa2a1d78431be81f4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 02:12:54 -0500 Subject: [PATCH 313/322] PadMessageHandler: Switch from nodeify to util.callbackify --- src/node/handler/PadMessageHandler.js | 8 ++++---- src/package-lock.json | 5 ++++- src/package.json | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index c9f8856b4..ca706e325 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -37,8 +37,8 @@ const hooks = require('../../static/js/pluginfw/hooks.js'); const channels = require('channels'); const stats = require('../stats'); const assert = require('assert').strict; -const nodeify = require('nodeify'); const {RateLimiterMemory} = require('rate-limiter-flexible'); +const util = require('util'); const webaccess = require('../hooks/express/webaccess'); let rateLimiter; @@ -87,9 +87,8 @@ stats.gauge('activePads', () => { /** * A changeset queue per pad that is processed by handleUserChanges() */ -const padChannels = new channels.channels( - ({socket, message}, callback) => nodeify(handleUserChanges(socket, message), callback) -); +const padChannels = + new channels.channels(({socket, message}, cb) => handleUserChangesCb(socket, message, cb)); /** * This Method is called by server.js to tell the message handler on which socket it should send @@ -691,6 +690,7 @@ const handleUserChanges = async (socket, message) => { stopWatch.end(); }; +const handleUserChangesCb = util.callbackify(handleUserChanges); exports.updatePadClients = async (pad) => { // skip this if no-one is on this pad diff --git a/src/package-lock.json b/src/package-lock.json index 5f602c433..3565f4b40 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -3054,7 +3054,8 @@ "is-promise": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", - "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" + "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=", + "dev": true }, "is-redirect": { "version": "1.0.0", @@ -4012,6 +4013,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz", "integrity": "sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=", + "dev": true, "requires": { "is-promise": "~1.0.0", "promise": "~1.3.0" @@ -7509,6 +7511,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-1.3.0.tgz", "integrity": "sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=", + "dev": true, "requires": { "is-promise": "~1" } diff --git a/src/package.json b/src/package.json index f09968be5..3dba9c9db 100644 --- a/src/package.json +++ b/src/package.json @@ -53,7 +53,6 @@ "log4js": "0.6.38", "measured-core": "^2.0.0", "mime-types": "^2.1.33", - "nodeify": "1.0.1", "npm": "^6.14.15", "openapi-backend": "^4.2.0", "proxy-addr": "^2.0.7", @@ -90,6 +89,7 @@ "etherpad-cli-client": "^0.1.12", "mocha": "^9.1.1", "mocha-froth": "^0.2.10", + "nodeify": "^1.0.1", "openapi-schema-validation": "^0.4.2", "selenium-webdriver": "^4.0.0-rc-1", "set-cookie-parser": "^2.4.8", From 85919ff914067bb49a14ed08473125f9973446e0 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 02:51:36 -0500 Subject: [PATCH 314/322] PadMessageHandler: Replace `channels` package with async-friendly class --- src/node/handler/PadMessageHandler.js | 42 +++++++++++++++++++++++---- src/package-lock.json | 5 ---- src/package.json | 1 - 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index ca706e325..1b327a92d 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -34,11 +34,9 @@ const messageLogger = log4js.getLogger('message'); const accessLogger = log4js.getLogger('access'); const _ = require('underscore'); const hooks = require('../../static/js/pluginfw/hooks.js'); -const channels = require('channels'); const stats = require('../stats'); const assert = require('assert').strict; const {RateLimiterMemory} = require('rate-limiter-flexible'); -const util = require('util'); const webaccess = require('../hooks/express/webaccess'); let rateLimiter; @@ -84,11 +82,44 @@ stats.gauge('activePads', () => { return padIds.size; }); +/** + * Processes one task at a time per channel. + */ +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, task) => task(ch)) { + this._exec = exec; + this._promiseChains = new Map(); + } + + /** + * Schedules a task for execution. The task will be executed once all previously enqueued tasks + * for the named channel have completed. + * + * @param {any} ch - Identifies the channel. + * @param {any} task - The task to give to the executor. + * @returns {Promise} The value returned by the executor. + */ + async enqueue(ch, task) { + const p = (this._promiseChains.get(ch) || Promise.resolve()).then(() => this._exec(ch, task)); + const pc = p + .catch(() => {}) // Prevent rejections from halting the queue. + .then(() => { + // Clean up this._promiseChains if there are no more tasks for the channel. + if (this._promiseChains.get(ch) === pc) this._promiseChains.delete(ch); + }); + this._promiseChains.set(ch, pc); + return await p; + } +} + /** * A changeset queue per pad that is processed by handleUserChanges() */ -const padChannels = - new channels.channels(({socket, message}, cb) => handleUserChangesCb(socket, message, cb)); +const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(socket, message)); /** * This Method is called by server.js to tell the message handler on which socket it should send @@ -265,7 +296,7 @@ exports.handleMessage = async (socket, message) => { messageLogger.warn('Dropped message, COLLABROOM for readonly pad'); } else if (message.data.type === 'USER_CHANGES') { stats.counter('pendingEdits').inc(); - padChannels.emit(message.padId, {socket, message}); // add to pad queue + await padChannels.enqueue(message.padId, {socket, message}); } else if (message.data.type === 'USERINFO_UPDATE') { await handleUserInfoUpdate(socket, message); } else if (message.data.type === 'CHAT_MESSAGE') { @@ -690,7 +721,6 @@ const handleUserChanges = async (socket, message) => { stopWatch.end(); }; -const handleUserChangesCb = util.callbackify(handleUserChanges); exports.updatePadClients = async (pad) => { // skip this if no-one is on this pad diff --git a/src/package-lock.json b/src/package-lock.json index 3565f4b40..037648d44 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1233,11 +1233,6 @@ "supports-color": "^5.3.0" } }, - "channels": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/channels/-/channels-0.0.4.tgz", - "integrity": "sha1-G+4yPt6hUrue8E9BvG5rD1lIqUE=" - }, "character-entities-html4": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", diff --git a/src/package.json b/src/package.json index 3dba9c9db..2fd5e985e 100644 --- a/src/package.json +++ b/src/package.json @@ -31,7 +31,6 @@ ], "dependencies": { "async": "^3.2.1", - "channels": "0.0.4", "clean-css": "^5.2.1", "cookie-parser": "1.4.5", "cross-spawn": "^7.0.3", From 35b2aeb3b186b42c253da05c422d43888730963b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 15:45:48 -0500 Subject: [PATCH 315/322] PadMessageHandler: Factor out duplicate bad USER_CHANGES handling This also fixes a double-count bug. --- src/node/handler/PadMessageHandler.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 1b327a92d..1e2dcc408 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -645,9 +645,6 @@ const handleUserChanges = async (socket, message) => { // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); } catch (e) { - // There is an error in this changeset, so just refuse it - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); throw new Error(`Can't apply USER_CHANGES from Socket ${socket.id} because: ${e.message}`); } @@ -673,15 +670,11 @@ const handleUserChanges = async (socket, message) => { // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES // of that revision if (baseRev + 1 === r && c === changeset) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); throw new Error("Won't apply USER_CHANGES, as it contains an already accepted changeset"); } changeset = Changeset.follow(c, changeset, false, apool); } catch (e) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); throw new Error(`Can't apply USER_CHANGES, because ${e.message}`); } } @@ -689,19 +682,11 @@ const handleUserChanges = async (socket, message) => { const prevText = pad.text(); if (Changeset.oldLen(changeset) !== prevText.length) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ` + `${Changeset.oldLen(changeset)} to document of length ${prevText.length}`); } - try { - await pad.appendRevision(changeset, thisSession.author); - } catch (e) { - socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); - throw e; - } + await pad.appendRevision(changeset, thisSession.author); const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { @@ -716,6 +701,8 @@ const handleUserChanges = async (socket, message) => { await exports.updatePadClients(pad); } catch (err) { + socket.json.send({disconnect: 'badChangeset'}); + stats.meter('failedChangesets').mark(); console.warn(err.stack || err); } From 1447ab8899d45c3bbef9c5b0ad59a285c2f815ac Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 15:55:01 -0500 Subject: [PATCH 316/322] PadMessageHandler: Factor out common USER_CHANGES error message formatting --- src/node/handler/PadMessageHandler.js | 101 ++++++++++++-------------- 1 file changed, 46 insertions(+), 55 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 1e2dcc408..b72d14c89 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -601,53 +601,49 @@ const handleUserChanges = async (socket, message) => { // create the changeset try { - try { - // Verify that the changeset has valid syntax and is in canonical form - Changeset.checkRep(changeset); + // Verify that the changeset has valid syntax and is in canonical form + Changeset.checkRep(changeset); - // Verify that the attribute indexes used in the changeset are all - // defined in the accompanying attribute pool. - Changeset.eachAttribNumber(changeset, (n) => { - if (!wireApool.getAttrib(n)) { - throw new Error(`Attribute pool is missing attribute ${n} for changeset ${changeset}`); + // Verify that the attribute indexes used in the changeset are all + // defined in the accompanying attribute pool. + Changeset.eachAttribNumber(changeset, (n) => { + if (!wireApool.getAttrib(n)) { + throw new Error(`Attribute pool is missing attribute ${n} for changeset ${changeset}`); + } + }); + + // Validate all added 'author' attribs to be the same value as the current user + const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); + let op; + + while (iterator.hasNext()) { + op = iterator.next(); + + // + 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 - + // but do show up in the pool + + op.attribs.split('*').forEach((attr) => { + if (!attr) return; + + attr = wireApool.getAttrib(attr); + if (!attr) return; + + // the empty author is used in the clearAuthorship functionality so this + // should be the only exception + if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) { + throw new Error(`Author ${thisSession.author} tried to submit changes as author ` + + `${attr[1]} in changeset ${changeset}`); } }); - - // Validate all added 'author' attribs to be the same value as the current user - const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); - let op; - - while (iterator.hasNext()) { - op = iterator.next(); - - // + 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 - - // but do show up in the pool - - op.attribs.split('*').forEach((attr) => { - if (!attr) return; - - attr = wireApool.getAttrib(attr); - if (!attr) return; - - // the empty author is used in the clearAuthorship functionality so this - // should be the only exception - if ('author' === attr[0] && (attr[1] !== thisSession.author && attr[1] !== '')) { - throw new Error(`Author ${thisSession.author} tried to submit changes as author ` + - `${attr[1]} in changeset ${changeset}`); - } - }); - } - - // ex. adoptChangesetAttribs - - // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool - changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); - } catch (e) { - throw new Error(`Can't apply USER_CHANGES from Socket ${socket.id} because: ${e.message}`); } + // ex. adoptChangesetAttribs + + // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool + changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); + // ex. applyUserChanges const apool = pad.pool; let r = baseRev; @@ -665,24 +661,18 @@ const handleUserChanges = async (socket, message) => { // rebases "changeset" so that it is relative to revision r // and can be applied after "c". - try { - // a changeset can be based on an old revision with the same changes in it - // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES - // of that revision - if (baseRev + 1 === r && c === changeset) { - throw new Error("Won't apply USER_CHANGES, as it contains an already accepted changeset"); - } + // a changeset can be based on an old revision with the same changes in it + // prevent eplite from accepting it TODO: better send the client a NEW_CHANGES + // of that revision + if (baseRev + 1 === r && c === changeset) throw new Error('Changeset already accepted'); - changeset = Changeset.follow(c, changeset, false, apool); - } catch (e) { - throw new Error(`Can't apply USER_CHANGES, because ${e.message}`); - } + changeset = Changeset.follow(c, changeset, false, apool); } const prevText = pad.text(); if (Changeset.oldLen(changeset) !== prevText.length) { - throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ` + + throw new Error(`Can't apply changeset ${changeset} with oldLen ` + `${Changeset.oldLen(changeset)} to document of length ${prevText.length}`); } @@ -703,7 +693,8 @@ const handleUserChanges = async (socket, message) => { } catch (err) { socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); - console.warn(err.stack || err); + console.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` + + `(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`); } stopWatch.end(); From e961718ff23e62479235c254a6a765d07ba7d774 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 16:01:01 -0500 Subject: [PATCH 317/322] PadMessageHandler: Improve error logging Use a log4js logger instead of `console`, and clean up some of the log messages. --- src/node/handler/PadMessageHandler.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index b72d14c89..df112ac04 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -202,8 +202,8 @@ exports.handleMessage = async (socket, message) => { try { await rateLimiter.consume(socket.request.ip); // consume 1 point per event from IP } catch (e) { - console.warn(`Rate limited: ${socket.request.ip} to reduce the amount of rate limiting ` + - 'that happens edit the rateLimit values in settings.json'); + 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(); socket.json.send({disconnect: 'rateLimited'}); return; @@ -584,7 +584,7 @@ const handleUserChanges = async (socket, message) => { // and always use the copy. atm a message will be ignored if the session is gone even // if the session was valid when the message arrived in the first place if (!thisSession) { - messageLogger.warn('Dropped message, disconnect happened in the mean time'); + messageLogger.warn('Ignoring USER_CHANGES from disconnected user'); return; } @@ -693,8 +693,8 @@ const handleUserChanges = async (socket, message) => { } catch (err) { socket.json.send({disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); - console.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` + - `(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`); + messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` + + `(socket ${socket.id}) on pad ${thisSession.padId}: ${err.stack || err}`); } stopWatch.end(); @@ -969,7 +969,7 @@ const handleClientReady = async (socket, message) => { apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; } catch (e) { - console.error(e.stack || e); + messageLogger.error(e.stack || e); socket.json.send({disconnect: 'corruptPad'}); // pull the brakes return; @@ -1172,8 +1172,8 @@ const handleChangesetRequest = async (socket, message) => { data.requestID = message.data.requestID; socket.json.send({type: 'CHANGESET_REQ', data}); } catch (err) { - console.error(`Error while handling a changeset request for ${padIds.padId}`, - err.toString(), message.data); + messageLogger.error(`Error while handling a changeset request ${message.data} ` + + `for ${padIds.padId}: ${err.stack || err}`); } }; @@ -1316,7 +1316,8 @@ const composePadChangesets = async (padId, startNum, endNum) => { return changeset; } catch (e) { // r-1 indicates the rev that was build starting with startNum, applying startNum+1, +2, +3 - console.warn('failed to compose cs in pad:', padId, ' startrev:', startNum, ' current rev:', r); + messageLogger.warn( + `failed to compose cs in pad: ${padId} startrev: ${startNum} current rev: ${r}`); throw e; } }; From b1c1501e1efa23382b7e34e3a6f6d658be8983a8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 16:04:03 -0500 Subject: [PATCH 318/322] PadMessageHandler: Use object destructuring --- src/node/handler/PadMessageHandler.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index df112ac04..72a5e812a 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -560,18 +560,20 @@ const handleUserChanges = async (socket, message) => { // This one's no longer pending, as we're gonna process it now stats.counter('pendingEdits').dec(); + const {data: {baseRev, apool, changeset}} = message; + // Make sure all required fields are present - if (message.data.baseRev == null) { + if (baseRev == null) { messageLogger.warn('Dropped message, USER_CHANGES Message has no baseRev!'); return; } - if (message.data.apool == null) { + if (apool == null) { messageLogger.warn('Dropped message, USER_CHANGES Message has no apool!'); return; } - if (message.data.changeset == null) { + if (changeset == null) { messageLogger.warn('Dropped message, USER_CHANGES Message has no changeset!'); return; } @@ -588,10 +590,7 @@ const handleUserChanges = async (socket, message) => { return; } - // get all Vars we need - const baseRev = message.data.baseRev; - const wireApool = (new AttributePool()).fromJsonable(message.data.apool); - let changeset = message.data.changeset; + const wireApool = (new AttributePool()).fromJsonable(apool); // Measure time to process edit const stopWatch = stats.timer('edits').start(); @@ -642,10 +641,9 @@ const handleUserChanges = async (socket, message) => { // ex. adoptChangesetAttribs // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool - changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); + let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); // ex. applyUserChanges - const apool = pad.pool; let r = baseRev; // The client's changeset might not be based on the latest revision, @@ -666,17 +664,18 @@ const handleUserChanges = async (socket, message) => { // of that revision if (baseRev + 1 === r && c === changeset) throw new Error('Changeset already accepted'); - changeset = Changeset.follow(c, changeset, false, apool); + rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool); } const prevText = pad.text(); - if (Changeset.oldLen(changeset) !== prevText.length) { - throw new Error(`Can't apply changeset ${changeset} with oldLen ` + - `${Changeset.oldLen(changeset)} to document of length ${prevText.length}`); + if (Changeset.oldLen(rebasedChangeset) !== prevText.length) { + throw new Error( + `Can't apply changeset ${rebasedChangeset} with oldLen ` + + `${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`); } - await pad.appendRevision(changeset, thisSession.author); + await pad.appendRevision(rebasedChangeset, thisSession.author); const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool); if (correctionChangeset) { From 97e382e5d2c0a7b57d9420d974318bb8cf7dc45e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 16:05:28 -0500 Subject: [PATCH 319/322] PadMessageHandler: Move USER_CHANGES processing inside `try` This improves the accuracy of the timing stats, and it simplifies error handling. --- src/node/handler/PadMessageHandler.js | 32 ++++++--------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 72a5e812a..3f9cbfaba 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -560,24 +560,6 @@ const handleUserChanges = async (socket, message) => { // This one's no longer pending, as we're gonna process it now stats.counter('pendingEdits').dec(); - const {data: {baseRev, apool, changeset}} = message; - - // Make sure all required fields are present - if (baseRev == null) { - messageLogger.warn('Dropped message, USER_CHANGES Message has no baseRev!'); - return; - } - - if (apool == null) { - messageLogger.warn('Dropped message, USER_CHANGES Message has no apool!'); - return; - } - - if (changeset == null) { - messageLogger.warn('Dropped message, USER_CHANGES Message has no changeset!'); - return; - } - // The client might disconnect between our callbacks. We should still // finish processing the changeset, so keep a reference to the session. const thisSession = sessioninfos[socket.id]; @@ -590,16 +572,16 @@ const handleUserChanges = async (socket, message) => { return; } - const wireApool = (new AttributePool()).fromJsonable(apool); - // Measure time to process edit const stopWatch = stats.timer('edits').start(); - - // get the pad - const pad = await padManager.getPad(thisSession.padId); - - // create the changeset 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); + // Verify that the changeset has valid syntax and is in canonical form Changeset.checkRep(changeset); From a052580bd161c9fe2cdcde76bfb9dabbaa47aed7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 03:46:27 -0500 Subject: [PATCH 320/322] PadMessageHandler: Stop the USER_CHANGES timer on exception --- src/node/handler/PadMessageHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 3f9cbfaba..04ea4f4a5 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -676,9 +676,9 @@ const handleUserChanges = async (socket, message) => { stats.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 { + stopWatch.end(); } - - stopWatch.end(); }; exports.updatePadClients = async (pad) => { From ae1741c25b81939c7037c4319cf7bce016094069 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 16 Nov 2021 17:59:33 -0500 Subject: [PATCH 321/322] PadMessageHandler: Add missing `parseNum()` for attribute lookup --- src/node/handler/PadMessageHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 04ea4f4a5..0735ce97f 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -608,7 +608,7 @@ const handleUserChanges = async (socket, message) => { op.attribs.split('*').forEach((attr) => { if (!attr) return; - attr = wireApool.getAttrib(attr); + attr = wireApool.getAttrib(Changeset.parseNum(attr)); if (!attr) return; // the empty author is used in the clearAuthorship functionality so this From 2e19087cb98fd668f2ef9a5c7a0a50db40d7b67c Mon Sep 17 00:00:00 2001 From: John McLear Date: Sat, 20 Nov 2021 15:20:35 +0000 Subject: [PATCH 322/322] bump version --- src/package-lock.json | 2 +- src/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 037648d44..bccb0f8c0 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "ep_etherpad-lite", - "version": "1.8.14", + "version": "1.8.15", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/package.json b/src/package.json index 2fd5e985e..90ac13b36 100644 --- a/src/package.json +++ b/src/package.json @@ -246,6 +246,6 @@ "test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "mocha --timeout 5000 tests/container/specs/api" }, - "version": "1.8.14", + "version": "1.8.15", "license": "Apache-2.0" }