diff --git a/node_modules/ep_etherpad-lite b/node_modules/ep_etherpad-lite deleted file mode 120000 index 5cd551cf269..00000000000 --- a/node_modules/ep_etherpad-lite +++ /dev/null @@ -1 +0,0 @@ -../src \ No newline at end of file diff --git a/src/.babelrc b/src/.babelrc new file mode 100644 index 00000000000..1320b9a3272 --- /dev/null +++ b/src/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} diff --git a/src/.eslintrc.cjs b/src/.eslintrc.cjs index 95c9efa07f2..0930b688898 100644 --- a/src/.eslintrc.cjs +++ b/src/.eslintrc.cjs @@ -1,10 +1,6 @@ +import "eslint-config-etherpad/patch/modern-module-resolution"; 'use strict'; - -// This is a workaround for https://github.com/eslint/eslint/issues/3458 -require('eslint-config-etherpad/patch/modern-module-resolution'); - -module.exports = { - ignorePatterns: [ +export const ignorePatterns = [ '/static/js/admin/jquery.autosize.js', '/static/js/admin/minify.json.js', '/static/js/vendors/browser.js', @@ -14,128 +10,132 @@ module.exports = { '/static/js/vendors/jquery.js', '/static/js/vendors/nice-select.js', '/tests/frontend/lib/', - ], - overrides: [ +]; +export const overrides = [ { - files: [ - '**/.eslintrc.*', - ], - extends: 'etherpad/node', + files: [ + '**/.eslintrc.*', + ], + extends: 'etherpad/node', }, { - files: [ - '**/*', - ], - excludedFiles: [ - '**/.eslintrc.*', - 'tests/frontend/**/*', - ], - extends: 'etherpad/node', + files: [ + '**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + 'tests/frontend/**/*', + ], + extends: 'etherpad/node', }, { - files: [ - 'static/**/*', - 'tests/frontend/helper.js', - 'tests/frontend/helper/**/*', - ], - excludedFiles: [ - '**/.eslintrc.*', - ], - extends: 'etherpad/browser', - env: { - 'shared-node-browser': true, - }, - overrides: [ - { - files: [ + files: [ + 'static/**/*', + 'tests/frontend/helper.js', 'tests/frontend/helper/**/*', - ], - globals: { - helper: 'readonly', - }, + ], + excludedFiles: [ + '**/.eslintrc.*', + ], + extends: 'etherpad/browser', + env: { + 'shared-node-browser': true, }, - ], - }, - { - files: [ - 'tests/**/*', - ], - excludedFiles: [ - '**/.eslintrc.*', - 'tests/frontend/cypress/**/*', - 'tests/frontend/helper.js', - 'tests/frontend/helper/**/*', - 'tests/frontend/travis/**/*', - 'tests/ratelimit/**/*', - ], - extends: 'etherpad/tests', - rules: { - 'mocha/no-exports': 'off', - 'mocha/no-top-level-hooks': 'off', - }, + overrides: [ + { + files: [ + 'tests/frontend/helper/**/*', + ], + globals: { + helper: 'readonly', + }, + }, + ], }, { - files: [ - 'tests/backend/**/*', - ], - excludedFiles: [ - '**/.eslintrc.*', - ], - extends: 'etherpad/tests/backend', - overrides: [ - { - files: [ - 'tests/backend/**/*', - ], - excludedFiles: [ - 'tests/backend/specs/**/*', - ], - rules: { + files: [ + 'tests/**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + 'tests/frontend/cypress/**/*', + 'tests/frontend/helper.js', + 'tests/frontend/helper/**/*', + 'tests/frontend/travis/**/*', + 'tests/ratelimit/**/*', + ], + extends: 'etherpad/tests', + rules: { 'mocha/no-exports': 'off', 'mocha/no-top-level-hooks': 'off', - }, }, - ], }, { - files: [ - 'tests/frontend/**/*', - ], - excludedFiles: [ - '**/.eslintrc.*', - 'tests/frontend/cypress/**/*', - 'tests/frontend/helper.js', - 'tests/frontend/helper/**/*', - 'tests/frontend/travis/**/*', - ], - extends: 'etherpad/tests/frontend', - overrides: [ - { - files: [ + files: [ + 'tests/backend/**/*', + ], + excludedFiles: [ + '**/.eslintrc.*', + ], + extends: 'etherpad/tests/backend', + overrides: [ + { + files: [ + 'tests/backend/**/*', + ], + excludedFiles: [ + 'tests/backend/specs/**/*', + ], + rules: { + 'mocha/no-exports': 'off', + 'mocha/no-top-level-hooks': 'off', + }, + }, + ], + }, + { + files: [ 'tests/frontend/**/*', - ], - excludedFiles: [ - 'tests/frontend/specs/**/*', - ], - rules: { - 'mocha/no-exports': 'off', - 'mocha/no-top-level-hooks': 'off', - }, - }, - ], + ], + excludedFiles: [ + '**/.eslintrc.*', + 'tests/frontend/cypress/**/*', + 'tests/frontend/helper.js', + 'tests/frontend/helper/**/*', + 'tests/frontend/travis/**/*', + ], + extends: 'etherpad/tests/frontend', + overrides: [ + { + files: [ + 'tests/frontend/**/*', + ], + excludedFiles: [ + 'tests/frontend/specs/**/*', + ], + rules: { + 'mocha/no-exports': 'off', + 'mocha/no-top-level-hooks': 'off', + }, + }, + ], }, { - files: [ - 'tests/frontend/cypress/**/*', - ], - extends: 'etherpad/tests/cypress', + files: [ + 'tests/frontend/cypress/**/*', + ], + extends: 'etherpad/tests/cypress', }, { - files: [ - 'tests/frontend/travis/**/*', - ], - extends: 'etherpad/node', + files: [ + 'tests/frontend/travis/**/*', + ], + extends: 'etherpad/node', }, - ], - root: true, +]; +export const root = true; +export default { + ignorePatterns, + overrides, + root }; diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 00000000000..81cd8b481cc --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules diff --git a/src/bin/buildDebian.sh b/src/bin/buildDebian.sh old mode 100755 new mode 100644 diff --git a/src/bin/buildForWindows.sh b/src/bin/buildForWindows.sh old mode 100755 new mode 100644 diff --git a/src/bin/cleanRun.sh b/src/bin/cleanRun.sh old mode 100755 new mode 100644 diff --git a/src/bin/createRelease.sh b/src/bin/createRelease.sh old mode 100755 new mode 100644 diff --git a/src/bin/deb-src/DEBIAN/postinst b/src/bin/deb-src/DEBIAN/postinst old mode 100755 new mode 100644 diff --git a/src/bin/deb-src/DEBIAN/preinst b/src/bin/deb-src/DEBIAN/preinst old mode 100755 new mode 100644 diff --git a/src/bin/deb-src/DEBIAN/prerm b/src/bin/deb-src/DEBIAN/prerm old mode 100755 new mode 100644 diff --git a/src/bin/debugRun.sh b/src/bin/debugRun.sh old mode 100755 new mode 100644 diff --git a/src/bin/etherpad-healthcheck b/src/bin/etherpad-healthcheck old mode 100755 new mode 100644 diff --git a/src/bin/fastRun.sh b/src/bin/fastRun.sh old mode 100755 new mode 100644 diff --git a/src/bin/installDeps.sh b/src/bin/installDeps.sh old mode 100755 new mode 100644 diff --git a/src/bin/nsis/brand.ico b/src/bin/nsis/brand.ico index cb7d6a01979..e69de29bb2d 100644 Binary files a/src/bin/nsis/brand.ico and b/src/bin/nsis/brand.ico differ diff --git a/src/bin/plugins/README.md b/src/bin/plugins/README.md old mode 100755 new mode 100644 diff --git a/src/bin/plugins/checkPlugin.js b/src/bin/plugins/checkPlugin.js old mode 100755 new mode 100644 diff --git a/src/bin/plugins/getCorePlugins.sh b/src/bin/plugins/getCorePlugins.sh old mode 100755 new mode 100644 diff --git a/src/bin/plugins/lib/README.md b/src/bin/plugins/lib/README.md old mode 100755 new mode 100644 diff --git a/src/bin/plugins/lib/gitignore b/src/bin/plugins/lib/gitignore old mode 100755 new mode 100644 diff --git a/src/bin/plugins/listOfficialPlugins b/src/bin/plugins/listOfficialPlugins old mode 100755 new mode 100644 diff --git a/src/bin/plugins/reTestAllPlugins.sh b/src/bin/plugins/reTestAllPlugins.sh old mode 100755 new mode 100644 diff --git a/src/bin/plugins/updateAllPluginsScript.sh b/src/bin/plugins/updateAllPluginsScript.sh old mode 100755 new mode 100644 diff --git a/src/bin/plugins/updateCorePlugins.sh b/src/bin/plugins/updateCorePlugins.sh old mode 100755 new mode 100644 diff --git a/src/bin/run.sh b/src/bin/run.sh old mode 100755 new mode 100644 index b655e96354a..0e681bdf67b --- a/src/bin/run.sh +++ b/src/bin/run.sh @@ -32,4 +32,4 @@ src/bin/installDeps.sh "$@" || exit 1 # Move to the node folder and start log "Starting Etherpad..." -exec node src/node/server.js "$@" +exec node src/dist/node/server.js "$@" diff --git a/src/bin/safeRun.sh b/src/bin/safeRun.sh old mode 100755 new mode 100644 diff --git a/src/bin/updatePlugins.sh b/src/bin/updatePlugins.sh old mode 100755 new mode 100644 diff --git a/src/node/db/API.js b/src/node/db/API.ts similarity index 81% rename from src/node/db/API.js rename to src/node/db/API.ts index 9b2ecadc74a..5f07339085b 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.ts @@ -19,59 +19,49 @@ * limitations under the License. */ -const Changeset = require('../../static/js/Changeset'); -const ChatMessage = require('../../static/js/ChatMessage'); -const CustomError = require('../utils/customError'); -const padManager = require('./PadManager'); -const padMessageHandler = require('../handler/PadMessageHandler'); -const readOnlyManager = require('./ReadOnlyManager'); -const groupManager = require('./GroupManager'); -const authorManager = require('./AuthorManager'); -const sessionManager = require('./SessionManager'); -const exportHtml = require('../utils/ExportHtml'); -const exportTxt = require('../utils/ExportTxt'); -const importHtml = require('../utils/ImportHtml'); +import {builder, deserializeOps} from '../../static/js/Changeset'; +import ChatMessage from '../../static/js/ChatMessage'; +import {CustomError} from '../utils/customError'; +import {doesPadExist, getPad, isValidPadId, listAllPads} from './PadManager'; +import { + handleCustomMessage, + sendChatMessageToPadClients, + sessioninfos, + updatePadClients +} from '../handler/PadMessageHandler'; +import {getPadId, getReadOnlyId} from './ReadOnlyManager'; +import { + createGroup, + createGroupIfNotExistsFor, + createGroupPad, + deleteGroup, + listAllGroups, + listPads +} from './GroupManager'; +import {createAuthor, createAuthorIfNotExistsFor, getAuthorName, listPadsOfAuthor} from './AuthorManager'; +import {} from './SessionManager'; +import {getTXTFromAtext} from '../utils/ExportTxt'; +import {setPadHTML} from '../utils/ImportHtml'; const cleanText = require('./Pad').cleanText; -const PadDiff = require('../utils/padDiff'); +import {PadDiff} from '../utils/padDiff'; +import {getPadHTMLDocument} from "../utils/ExportHtml"; /* ******************** * GROUP FUNCTIONS **** ******************** */ -exports.listAllGroups = groupManager.listAllGroups; -exports.createGroup = groupManager.createGroup; -exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; -exports.deleteGroup = groupManager.deleteGroup; -exports.listPads = groupManager.listPads; -exports.createGroupPad = groupManager.createGroupPad; - /* ******************** * PADLIST FUNCTION *** ******************** */ -exports.listAllPads = padManager.listAllPads; - /* ******************** * AUTHOR FUNCTIONS *** ******************** */ -exports.createAuthor = authorManager.createAuthor; -exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; -exports.getAuthorName = authorManager.getAuthorName; -exports.listPadsOfAuthor = authorManager.listPadsOfAuthor; -exports.padUsers = padMessageHandler.padUsers; -exports.padUsersCount = padMessageHandler.padUsersCount; - /* ******************** * SESSION FUNCTIONS ** ******************** */ -exports.createSession = sessionManager.createSession; -exports.deleteSession = sessionManager.deleteSession; -exports.getSessionInfo = sessionManager.getSessionInfo; -exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup; -exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; - /* *********************** * PAD CONTENT FUNCTIONS * *********************** */ @@ -103,7 +93,7 @@ Example returns: } */ -exports.getAttributePool = async (padID) => { +export const getAttributePool = async (padID: string) => { const pad = await getPadSafe(padID, true); return {pool: pad.pool}; }; @@ -121,7 +111,7 @@ Example returns: } */ -exports.getRevisionChangeset = async (padID, rev) => { +export const getRevisionChangeset = async (padID, rev) => { // try to parse the revision number if (rev !== undefined) { rev = checkValidRev(rev); @@ -154,7 +144,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getText = async (padID, rev) => { +export const getText = async (padID, rev) => { // try to parse the revision number if (rev !== undefined) { rev = checkValidRev(rev); @@ -179,7 +169,7 @@ exports.getText = async (padID, rev) => { } // the client wants the latest text, lets return it to him - const text = exportTxt.getTXTFromAtext(pad, pad.atext); + const text = getTXTFromAtext(pad, pad.atext); return {text}; }; @@ -192,7 +182,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.setText = async (padID, text, authorId = '') => { +export const setText = async (padID, text, authorId = '') => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -202,7 +192,7 @@ exports.setText = async (padID, text, authorId = '') => { const pad = await getPadSafe(padID, true); await pad.setText(text, authorId); - await padMessageHandler.updatePadClients(pad); + await updatePadClients(pad); }; /** @@ -214,7 +204,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} {code: 1, message:"text too long", data: null} */ -exports.appendText = async (padID, text, authorId = '') => { +export const appendText = async (padID, text, authorId = '') => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -222,7 +212,7 @@ exports.appendText = async (padID, text, authorId = '') => { const pad = await getPadSafe(padID, true); await pad.appendText(text, authorId); - await padMessageHandler.updatePadClients(pad); + await updatePadClients(pad); }; /** @@ -233,7 +223,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getHTML = async (padID, rev) => { +export const getHTML = async (padID, rev) => { if (rev !== undefined) { rev = checkValidRev(rev); } @@ -250,7 +240,7 @@ exports.getHTML = async (padID, rev) => { } // get the html of this revision - let html = await exportHtml.getPadHTML(pad, rev); + let html = await getPadHTMLDocument(pad, rev); // wrap the HTML html = `${html}`; @@ -265,7 +255,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setHTML = async (padID, html, authorId = '') => { +export const setHTML = async (padID, html, authorId = '') => { // html string is required if (typeof html !== 'string') { throw new CustomError('html is not a string', 'apierror'); @@ -276,13 +266,13 @@ exports.setHTML = async (padID, html, authorId = '') => { // add a new changeset with the new html to the pad try { - await importHtml.setPadHTML(pad, cleanText(html), authorId); + await setPadHTML(pad, cleanText(html), authorId); } catch (e) { throw new CustomError('HTML is malformed', 'apierror'); } // update the clients on the pad - padMessageHandler.updatePadClients(pad); + updatePadClients(pad); }; /* **************** @@ -303,7 +293,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHistory = async (padID, start, end) => { +export const getChatHistory = async (padID, start, end) => { if (start && end) { if (start < 0) { throw new CustomError('start is below zero', 'apierror'); @@ -349,7 +339,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.appendChatMessage = async (padID, text, authorID, time) => { +export const appendChatMessage = async (padID, text, authorID, time) => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -363,7 +353,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(new ChatMessage(text, authorID, time), padID); + await sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID); }; /* *************** @@ -378,7 +368,7 @@ Example returns: {code: 0, message:"ok", data: {revisions: 56}} {code: 1, message:"padID does not exist", data: null} */ -exports.getRevisionsCount = async (padID) => { +export const getRevisionsCount = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {revisions: pad.getHeadRevisionNumber()}; @@ -392,7 +382,7 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getSavedRevisionsCount = async (padID) => { +export const getSavedRevisionsCount = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsNumber()}; @@ -406,7 +396,7 @@ Example returns: {code: 0, message:"ok", data: {savedRevisions: [2, 42, 1337]}} {code: 1, message:"padID does not exist", data: null} */ -exports.listSavedRevisions = async (padID) => { +export const listSavedRevisions = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsList()}; @@ -420,7 +410,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.saveRevision = async (padID, rev) => { +export const saveRevision = async (padID, rev) => { // check if rev is a number if (rev !== undefined) { rev = checkValidRev(rev); @@ -439,7 +429,7 @@ exports.saveRevision = async (padID, rev) => { rev = pad.getHeadRevisionNumber(); } - const author = await authorManager.createAuthor('API'); + const author = await createAuthor('API'); await pad.addSavedRevision(rev, author.authorID, 'Saved through API call'); }; @@ -451,7 +441,7 @@ Example returns: {code: 0, message:"ok", data: {lastEdited: 1340815946602}} {code: 1, message:"padID does not exist", data: null} */ -exports.getLastEdited = async (padID) => { +export const getLastEdited = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); const lastEdited = await pad.getLastEdit(); @@ -466,7 +456,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"pad does already exist", data: null} */ -exports.createPad = async (padID, text, authorId = '') => { +export const createPad = async (padID, text, authorId = '') => { if (padID) { // ensure there is no $ in the padID if (padID.indexOf('$') !== -1) { @@ -491,7 +481,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.deletePad = async (padID) => { +export const deletePad = async (padID) => { const pad = await getPadSafe(padID, true); await pad.remove(); }; @@ -504,7 +494,7 @@ exports.deletePad = async (padID) => { {code:0, message:"ok", data:null} {code: 1, message:"padID does not exist", data: null} */ -exports.restoreRevision = async (padID, rev, authorId = '') => { +export const restoreRevision = async (padID, rev, authorId = '') => { // check if rev is a number if (rev === undefined) { throw new CustomError('rev is not defined', 'apierror'); @@ -528,7 +518,7 @@ exports.restoreRevision = async (padID, rev, authorId = '') => { let textIndex = 0; const newTextStart = 0; const newTextEnd = atext.text.length; - for (const op of Changeset.deserializeOps(attribs)) { + for (const op of deserializeOps(attribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); @@ -538,25 +528,25 @@ exports.restoreRevision = async (padID, rev, authorId = '') => { }; // create a new changeset with a helper builder object - const builder = Changeset.builder(oldText.length); + const builder2 = builder(oldText.length); // assemble each line into the builder eachAttribRun(atext.attribs, (start, end, attribs) => { - builder.insert(atext.text.substring(start, end), attribs); + builder2.insert(atext.text.substring(start, end), attribs); }); const lastNewlinePos = oldText.lastIndexOf('\n'); if (lastNewlinePos < 0) { - builder.remove(oldText.length - 1, 0); + builder2.remove(oldText.length - 1, 0); } else { - builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); - builder.remove(oldText.length - lastNewlinePos - 1, 0); + builder2.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); + builder2.remove(oldText.length - lastNewlinePos - 1, 0); } const changeset = builder.toString(); await pad.appendRevision(changeset, authorId); - await padMessageHandler.updatePadClients(pad); + await updatePadClients(pad); }; /** @@ -568,7 +558,7 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.copyPad = async (sourceID, destinationID, force) => { +export const copyPad = async (sourceID, destinationID, force) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); }; @@ -582,7 +572,7 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => { +export const copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => { const pad = await getPadSafe(sourceID, true); await pad.copyPadWithoutHistory(destinationID, force, authorId); }; @@ -596,7 +586,7 @@ Example returns: {code: 0, message:"ok", data: {padID: destinationID}} {code: 1, message:"padID does not exist", data: null} */ -exports.movePad = async (sourceID, destinationID, force) => { +export const movePad = async (sourceID, destinationID, force) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); await pad.remove(); @@ -610,12 +600,12 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.getReadOnlyID = async (padID) => { +export const getReadOnlyID = async (padID) => { // we don't need the pad object, but this function does all the security stuff for us await getPadSafe(padID, true); // get the readonlyId - const readOnlyID = await readOnlyManager.getReadOnlyId(padID); + const readOnlyID = await getReadOnlyId(padID); return {readOnlyID}; }; @@ -628,9 +618,9 @@ Example returns: {code: 0, message:"ok", data: {padID: padID}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPadID = async (roID) => { +export const getPadID = async (roID) => { // get the PadId - const padID = await readOnlyManager.getPadId(roID); + const padID = await getPadId(roID); if (padID == null) { throw new CustomError('padID does not exist', 'apierror'); } @@ -646,7 +636,7 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"padID does not exist", data: null} */ -exports.setPublicStatus = async (padID, publicStatus) => { +export const setPublicStatus = async (padID, publicStatus) => { // ensure this is a group pad checkGroupPad(padID, 'publicStatus'); @@ -669,7 +659,7 @@ Example returns: {code: 0, message:"ok", data: {publicStatus: true}} {code: 1, message:"padID does not exist", data: null} */ -exports.getPublicStatus = async (padID) => { +export const getPublicStatus = async (padID) => { // ensure this is a group pad checkGroupPad(padID, 'publicStatus'); @@ -686,7 +676,7 @@ Example returns: {code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]} {code: 1, message:"padID does not exist", data: null} */ -exports.listAuthorsOfPad = async (padID) => { +export const listAuthorsOfPad = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); const authorIDs = pad.getAllAuthors(); @@ -716,9 +706,9 @@ Example returns: {code: 1, message:"padID does not exist"} */ -exports.sendClientsMessage = async (padID, msg) => { +export const sendClientsMessage = async (padID, msg) => { await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist. - padMessageHandler.handleCustomMessage(padID, msg); + handleCustomMessage(padID, msg); }; /** @@ -729,7 +719,7 @@ Example returns: {"code":0,"message":"ok","data":null} {"code":4,"message":"no or wrong API Key","data":null} */ -exports.checkToken = async () => { +export const checkToken = async () => { }; /** @@ -740,7 +730,7 @@ Example returns: {code: 0, message:"ok", data: {chatHead: 42}} {code: 1, message:"padID does not exist", data: null} */ -exports.getChatHead = async (padID) => { +export const getChatHead = async (padID) => { // get the pad const pad = await getPadSafe(padID, true); return {chatHead: pad.chatHead}; @@ -764,7 +754,7 @@ Example returns: {"code":4,"message":"no or wrong API Key","data":null} */ -exports.createDiffHTML = async (padID, startRev, endRev) => { +export const createDiffHTML = async (padID, startRev, endRev) => { // check if startRev is a number if (startRev !== undefined) { startRev = checkValidRev(startRev); @@ -803,13 +793,13 @@ exports.createDiffHTML = async (padID, startRev, endRev) => { {"code":4,"message":"no or wrong API Key","data":null} */ -exports.getStats = async () => { - const sessionInfos = padMessageHandler.sessioninfos; +export const getStats = async () => { + const sessionInfos = sessioninfos; const sessionKeys = Object.keys(sessionInfos); const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId)); - const {padIDs} = await padManager.listAllPads(); + const {padIDs} = await listAllPads(); return { totalPads: padIDs.length, @@ -826,19 +816,19 @@ exports.getStats = async () => { const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value); // gets a pad safe -const getPadSafe = async (padID, shouldExist, text, authorId = '') => { +const getPadSafe = async (padID, shouldExist, text?, authorId = '') => { // check if padID is a string if (typeof padID !== 'string') { throw new CustomError('padID is not a string', 'apierror'); } // check if the padID maches the requirements - if (!padManager.isValidPadId(padID)) { + if (!isValidPadId(padID)) { throw new CustomError('padID did not match requirements', 'apierror'); } // check if the pad exists - const exists = await padManager.doesPadExists(padID); + const exists = await doesPadExist(padID); if (!exists && shouldExist) { // does not exist, but should @@ -851,7 +841,7 @@ const getPadSafe = async (padID, shouldExist, text, authorId = '') => { } // pad exists, let's get it - return padManager.getPad(padID, text, authorId); + return getPad(padID, text, authorId); }; // checks if a rev is a legal number diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.ts similarity index 77% rename from src/node/db/AuthorManager.js rename to src/node/db/AuthorManager.ts index 7049be5dba3..3dd316f675d 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.ts @@ -19,12 +19,13 @@ * limitations under the License. */ -const db = require('./DB'); -const CustomError = require('../utils/customError'); -const hooks = require('../../static/js/pluginfw/hooks.js'); +import {db} from './DB'; +import {CustomError} from '../utils/customError'; +import {aCallFirst} from '../../static/js/pluginfw/hooks.js'; + const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); -exports.getColorPalette = () => [ +export const getColorPalette = () => [ '#ffc7c7', '#fff1c7', '#e3ffc7', @@ -94,26 +95,23 @@ exports.getColorPalette = () => [ /** * Checks if the author exists */ -exports.doesAuthorExist = async (authorID) => { +export const doesAuthorExist = async (authorID: string) => { const author = await db.get(`globalAuthor:${authorID}`); return author != null; -}; - -/* exported for backwards compatibility */ -exports.doesAuthorExists = exports.doesAuthorExist; +} -const getAuthor4Token = async (token) => { +const getAuthor4Token2 = async (token: string) => { const author = await mapAuthorWithDBKey('token2author', token); // return only the sub value authorID return author ? author.authorID : author; }; -exports.getAuthorId = async (token, user) => { +export const getAuthorId = async (token, user) => { const context = {dbKey: token, token, user}; - let [authorId] = await hooks.aCallFirst('getAuthorId', context); - if (!authorId) authorId = await getAuthor4Token(context.dbKey); + let [authorId] = await aCallFirst('getAuthorId', context); + if (!authorId) authorId = await getAuthor4Token2(context.dbKey); return authorId; }; @@ -123,23 +121,23 @@ exports.getAuthorId = async (token, user) => { * @deprecated Use `getAuthorId` instead. * @param {String} token The token */ -exports.getAuthor4Token = async (token) => { +export const getAuthor4Token = async (token) => { warnDeprecated( 'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead'); - return await getAuthor4Token(token); + return await getAuthor4Token2(token); }; /** * Returns the AuthorID for a mapper. - * @param {String} token The mapper + * @param authorMapper * @param {String} name The name of the author (optional) */ -exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { +export const createAuthorIfNotExistsFor = async (authorMapper, name: string) => { const author = await mapAuthorWithDBKey('mapper2author', authorMapper); if (name) { // set the name of this author - await exports.setAuthorName(author.authorID, name); + await setAuthorName(author.authorID, name); } return author; @@ -151,13 +149,13 @@ exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { * @param {String} mapperkey The database key name for this mapper * @param {String} mapper The mapper */ -const mapAuthorWithDBKey = async (mapperkey, mapper) => { +export const mapAuthorWithDBKey = async (mapperkey: string, mapper) => { // try to map to an author const author = await db.get(`${mapperkey}:${mapper}`); if (author == null) { // there is no author with this mapper, so create one - const author = await exports.createAuthor(null); + const author = await createAuthor(null); // create the token2author relation await db.set(`${mapperkey}:${mapper}`, author.authorID); @@ -178,13 +176,13 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => { * Internal function that creates the database entry for an author * @param {String} name The name of the author */ -exports.createAuthor = async (name) => { +export const createAuthor = async (name) => { // create the new author name const author = `a.${randomString(16)}`; // create the globalAuthors db entry const authorObj = { - colorId: Math.floor(Math.random() * (exports.getColorPalette().length)), + colorId: Math.floor(Math.random() * (getColorPalette().length)), name, timestamp: Date.now(), }; @@ -199,41 +197,41 @@ exports.createAuthor = async (name) => { * Returns the Author Obj of the author * @param {String} author The id of the author */ -exports.getAuthor = async (author) => await db.get(`globalAuthor:${author}`); +export const getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`); /** * Returns the color Id of the author * @param {String} author The id of the author */ -exports.getAuthorColorId = async (author) => await db.getSub(`globalAuthor:${author}`, ['colorId']); +export const getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']); /** * Sets the color Id of the author * @param {String} author The id of the author * @param {String} colorId The color id of the author */ -exports.setAuthorColorId = async (author, colorId) => await db.setSub( +export const setAuthorColorId = async (author: string, colorId: string) => await db.setSub( `globalAuthor:${author}`, ['colorId'], colorId); /** * Returns the name of the author * @param {String} author The id of the author */ -exports.getAuthorName = async (author) => await db.getSub(`globalAuthor:${author}`, ['name']); +export const getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']); /** * Sets the name of the author * @param {String} author The id of the author * @param {String} name The name of the author */ -exports.setAuthorName = async (author, name) => await db.setSub( +export const setAuthorName = async (author: string, name:string) => await db.setSub( `globalAuthor:${author}`, ['name'], name); /** * Returns an array of all pads this author contributed to - * @param {String} author The id of the author + * @param {String} authorID The id of the author */ -exports.listPadsOfAuthor = async (authorID) => { +export const listPadsOfAuthor = async (authorID:string) => { /* There are two other places where this array is manipulated: * (1) When the author is added to a pad, the author object is also updated * (2) When a pad is deleted, each author of that pad is also updated @@ -255,10 +253,10 @@ exports.listPadsOfAuthor = async (authorID) => { /** * Adds a new pad to the list of contributions - * @param {String} author The id of the author + * @param {String} authorID The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.addPad = async (authorID, padID) => { +export const addPad = async (authorID: string, padID: string) => { // get the entry const author = await db.get(`globalAuthor:${authorID}`); @@ -282,10 +280,10 @@ exports.addPad = async (authorID, padID) => { /** * Removes a pad from the list of contributions - * @param {String} author The id of the author + * @param {String} authorID The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.removePad = async (authorID, padID) => { +export const removePad = async (authorID: string, padID?: string) => { const author = await db.get(`globalAuthor:${authorID}`); if (author == null) return; diff --git a/src/node/db/DB.js b/src/node/db/DB.ts similarity index 62% rename from src/node/db/DB.js rename to src/node/db/DB.ts index 02e83f85de7..563ba5ec57d 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.ts @@ -21,40 +21,43 @@ * limitations under the License. */ -const ueberDB = require('ueberdb2'); -const settings = require('../utils/Settings'); -const log4js = require('log4js'); -const stats = require('../stats'); - +import {Database} from 'ueberdb2'; +import {dbSettings, dbType} from '../utils/Settings'; +import log4js from 'log4js'; +import {shutdown as statsShutdown,createCollection} from '../stats'; +import {} from 'measured-core' const logger = log4js.getLogger('ueberDB'); /** * The UeberDB Object that provides the database functions */ -exports.db = null; +let db = null; /** * Initializes the database with the settings provided by the settings module */ -exports.init = async () => { - exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger); - await exports.db.init(); - if (exports.db.metrics != null) { - for (const [metric, value] of Object.entries(exports.db.metrics)) { +const init = async () => { + db = new Database(dbType, dbSettings, null, logger); + await db.init(); + if (db.metrics != null) { + for (const [metric, value] of Object.entries(db.metrics)) { if (typeof value !== 'number') continue; - stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]); + // FIXME find a better replacement for measure-core + createCollection.gauge(`ueberdb_${metric}`, () => db.metrics[metric]); } } for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) { - const f = exports.db[fn]; - exports[fn] = async (...args) => await f.call(exports.db, ...args); + const f = db[fn]; + exports[fn] = async (...args) => await f.call(db, ...args); Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f)); Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f)); } }; -exports.shutdown = async (hookName, context) => { - if (exports.db != null) await exports.db.close(); - exports.db = null; +const shutdown = async (hookName, context) => { + if (db != null) await db.close(); + db = null; logger.log('Database closed'); }; + +export {db,init,shutdown} diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.ts similarity index 81% rename from src/node/db/GroupManager.js rename to src/node/db/GroupManager.ts index 4302048c409..5df48b2e31b 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.ts @@ -19,13 +19,13 @@ * limitations under the License. */ -const CustomError = require('../utils/customError'); +import {CustomError} from '../utils/customError'; const randomString = require('../../static/js/pad_utils').randomString; -const db = require('./DB'); -const padManager = require('./PadManager'); -const sessionManager = require('./SessionManager'); +import {db} from './DB'; +import {doesPadExist, getPad} from './PadManager'; +import {deleteSession} from './SessionManager'; -exports.listAllGroups = async () => { +export const listAllGroups = async () => { let groups = await db.get('groups'); groups = groups || {}; @@ -33,7 +33,7 @@ exports.listAllGroups = async () => { return {groupIDs}; }; -exports.deleteGroup = async (groupID) => { +export const deleteGroup = async (groupID) => { const group = await db.get(`group:${groupID}`); // ensure group exists @@ -44,7 +44,7 @@ exports.deleteGroup = async (groupID) => { // iterate through all pads of this group and delete them (in parallel) await Promise.all(Object.keys(group.pads).map(async (padId) => { - const pad = await padManager.getPad(padId); + const pad = await getPad(padId); await pad.remove(); })); @@ -52,7 +52,7 @@ exports.deleteGroup = async (groupID) => { // record because deleting a session updates the group2sessions record. const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {}; await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => { - await sessionManager.deleteSession(sessionId); + await deleteSession(sessionId); })); await Promise.all([ @@ -68,14 +68,14 @@ exports.deleteGroup = async (groupID) => { await db.remove(`group:${groupID}`); }; -exports.doesGroupExist = async (groupID) => { +export const doesGroupExist = async (groupID) => { // try to get the group entry const group = await db.get(`group:${groupID}`); return (group != null); }; -exports.createGroup = async () => { +export const createGroup = async () => { const groupID = `g.${randomString(16)}`; await db.set(`group:${groupID}`, {pads: {}, mappings: {}}); // Add the group to the `groups` record after the group's individual record is created so that @@ -85,13 +85,13 @@ exports.createGroup = async () => { return {groupID}; }; -exports.createGroupIfNotExistsFor = async (groupMapper) => { +export const createGroupIfNotExistsFor = async (groupMapper) => { if (typeof groupMapper !== 'string') { throw new CustomError('groupMapper is not a string', 'apierror'); } const groupID = await db.get(`mapper2group:${groupMapper}`); - if (groupID && await exports.doesGroupExist(groupID)) return {groupID}; - const result = await exports.createGroup(); + if (groupID && await doesGroupExist(groupID)) return {groupID}; + const result = await createGroup(); await Promise.all([ db.set(`mapper2group:${groupMapper}`, result.groupID), // Remember the mapping in the group record so that it can be cleaned up when the group is @@ -103,19 +103,19 @@ exports.createGroupIfNotExistsFor = async (groupMapper) => { return result; }; -exports.createGroupPad = async (groupID, padName, text, authorId = '') => { +export const createGroupPad = async (groupID, padName, text, authorId = '') => { // create the padID const padID = `${groupID}$${padName}`; // ensure group exists - const groupExists = await exports.doesGroupExist(groupID); + const groupExists = await doesGroupExist(groupID); if (!groupExists) { throw new CustomError('groupID does not exist', 'apierror'); } // ensure pad doesn't exist already - const padExists = await padManager.doesPadExists(padID); + const padExists = await doesPadExist(padID); if (padExists) { // pad exists already @@ -123,7 +123,7 @@ exports.createGroupPad = async (groupID, padName, text, authorId = '') => { } // create the pad - await padManager.getPad(padID, text, authorId); + await getPad(padID, text, authorId); // create an entry in the group for this pad await db.setSub(`group:${groupID}`, ['pads', padID], 1); @@ -131,8 +131,8 @@ exports.createGroupPad = async (groupID, padName, text, authorId = '') => { return {padID}; }; -exports.listPads = async (groupID) => { - const exists = await exports.doesGroupExist(groupID); +export const listPads = async (groupID) => { + const exists = await doesGroupExist(groupID); // ensure the group exists if (!exists) { diff --git a/src/node/db/Pad.js b/src/node/db/Pad.ts similarity index 82% rename from src/node/db/Pad.js rename to src/node/db/Pad.ts index b692962f1c9..db5c5a4ca3e 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.ts @@ -3,46 +3,63 @@ * The pad object, defined with joose */ -const AttributeMap = require('../../static/js/AttributeMap'); -const Changeset = require('../../static/js/Changeset'); -const ChatMessage = require('../../static/js/ChatMessage'); -const AttributePool = require('../../static/js/AttributePool'); -const Stream = require('../utils/Stream'); -const assert = require('assert').strict; -const db = require('./DB'); -const settings = require('../utils/Settings'); -const authorManager = require('./AuthorManager'); -const padManager = require('./PadManager'); -const padMessageHandler = require('../handler/PadMessageHandler'); -const groupManager = require('./GroupManager'); -const CustomError = require('../utils/customError'); -const readOnlyManager = require('./ReadOnlyManager'); -const randomString = require('../utils/randomstring'); -const hooks = require('../../static/js/pluginfw/hooks'); -const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); -const promises = require('../utils/promises'); - +import AttributeMap from '../../static/js/AttributeMap'; +import { + applyToAText, checkRep, + copyAText, deserializeOps, + makeAText, + makeSplice, + opsFromAText, + pack, + smartOpAssembler, unpack +} from '../../static/js/Changeset'; +import ChatMessage from '../../static/js/ChatMessage'; +import {AttributePool} from '../../static/js/AttributePool'; +import {Stream} from '../utils/Stream'; +import assert, {strict} from 'assert' +import {db} from './DB'; +import {defaultPadText} from '../utils/Settings'; +import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager'; +import {Revision} from "../models/Revision"; +import {doesPadExist, getPad} from './PadManager'; +import {kickSessionsFromPad} from '../handler/PadMessageHandler'; +import {doesGroupExist} from './GroupManager'; +import {CustomError} from '../utils/customError'; +import {getReadOnlyId} from './ReadOnlyManager'; +import {randomString} from '../utils/randomstring'; +import {aCallAll} from '../../static/js/pluginfw/hooks'; +import {timesLimit} from '../utils/promises'; +import {padutils} from '../../static/js/pad_utils'; /** * Copied from the Etherpad source code. It converts Windows line breaks to Unix * line breaks and convert Tabs to spaces * @param txt */ -exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n') +export const cleanText = (txt) => txt.replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .replace(/\t/g, ' ') .replace(/\xa0/g, ' '); -class Pad { +export class Pad { + private db: any; + private atext: any; + private pool: AttributePool; + private head: number; + private chatHead: number; + private publicStatus: boolean; + private id: string; + private savedRevisions: Revision[]; /** + * @param id the id of this pad * @param [database] - Database object to access this pad's records (and only this pad's records; * the shared global Etherpad database object is still used for all other pad accesses, such * as copying the pad). Defaults to the shared global Etherpad database object. This parameter * can be used to shard pad storage across multiple database backends, to put each pad in its * own database table, or to validate imported pad data before it is written to the database. */ - constructor(id, database = db) { + constructor(id: string, database = db) { this.db = database; - this.atext = Changeset.makeAText('\n'); + this.atext = makeAText('\n'); this.pool = new AttributePool(); this.head = -1; this.chatHead = -1; @@ -74,11 +91,11 @@ class Pad { } async appendRevision(aChangeset, authorId = '') { - const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); + const newAText = applyToAText(aChangeset, this.atext, this.pool); if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs) { return this.head; } - Changeset.copyAText(newAText, this.atext); + copyAText(newAText, this.atext); const newRev = ++this.head; @@ -99,16 +116,16 @@ class Pad { }, }), this.saveToDatabase(), - authorId && authorManager.addPad(authorId, this.id), - hooks.aCallAll(hook, { + authorId && addPad(authorId, this.id), + aCallAll(hook, { pad: this, authorId, get author() { - warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); + padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); return this.authorId; }, set author(authorId) { - warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); + padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); this.authorId = authorId; }, ...this.head === 0 ? {} : { @@ -121,7 +138,7 @@ class Pad { } toJSON() { - const o = {...this, pool: this.pool.toJsonable()}; + const o:{db: any, id: any} = {...this, pool: this.pool.toJsonable()} delete o.db; delete o.id; return o; @@ -179,7 +196,7 @@ class Pad { ]); const apool = this.apool(); let atext = keyAText; - for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool); + for (const cs of changesets) atext = applyToAText(cs, atext, apool); return atext; } @@ -190,10 +207,10 @@ class Pad { async getAllAuthorColors() { const authorIds = this.getAllAuthors(); const returnTable = {}; - const colorPalette = authorManager.getColorPalette(); + const colorPalette = getColorPalette(); await Promise.all( - authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId) => { + authorIds.map((authorId) => getAuthorColorId(authorId).then((colorId) => { // colorId might be a hex color or an number out of the palette returnTable[authorId] = colorPalette[colorId] || colorId; }))); @@ -250,14 +267,14 @@ class Pad { const orig = this.text(); assert(orig.endsWith('\n')); if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text'); - ins = exports.cleanText(ins); + ins = cleanText(ins); const willEndWithNewline = start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline). ins.endsWith('\n') || (!ins && start > 0 && orig[start - 1] === '\n'); if (!willEndWithNewline) ins += '\n'; if (ndel === 0 && ins.length === 0) return; - const changeset = Changeset.makeSplice(orig, start, ndel, ins); + const changeset = makeSplice(orig, start, ndel, ins); await this.appendRevision(changeset, authorId); } @@ -315,7 +332,7 @@ class Pad { const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`); if (entry == null) return null; const message = ChatMessage.fromObject(entry); - message.displayName = await authorManager.getAuthorName(message.authorId); + message.displayName = await getAuthorName(message.authorId); return message; } @@ -352,15 +369,15 @@ class Pad { if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool); } else { if (text == null) { - const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText}; - await hooks.aCallAll('padDefaultContent', context); + const context = {pad: this, authorId, type: 'text', content: defaultPadText}; + await aCallAll('padDefaultContent', context); if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`); - text = exports.cleanText(context.content); + text = cleanText(context.content); } - const firstChangeset = Changeset.makeSplice('\n', 0, 0, text); + const firstChangeset = makeSplice('\n', 0, 0, text); await this.appendRevision(firstChangeset, authorId); } - await hooks.aCallAll('padLoad', {pad: this}); + await aCallAll('padLoad', {pad: this}); } async copy(destinationID, force) { @@ -393,16 +410,16 @@ class Pad { for (const p of new Stream(promises).batch(100).buffer(99)) await p; // Initialize the new pad (will update the listAllPads cache) - const dstPad = await padManager.getPad(destinationID, null); + const dstPad = await getPad(destinationID, null); // let the plugins know the pad was copied - await hooks.aCallAll('padCopy', { + await aCallAll('padCopy', { get originalPad() { - warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); + padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); return this.srcPad; }, get destinationID() { - warnDeprecated( + padutils.warnDeprecated( 'padCopy destinationID context property is deprecated; use dstPad.id instead'); return this.dstPad.id; }, @@ -418,7 +435,7 @@ class Pad { if (destinationID.indexOf('$') >= 0) { destGroupID = destinationID.split('$')[0]; - const groupExists = await groupManager.doesGroupExist(destGroupID); + const groupExists = await doesGroupExist(destGroupID); // group does not exist if (!groupExists) { @@ -430,7 +447,7 @@ class Pad { async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) { // if the pad exists, we should abort, unless forced. - const exists = await padManager.doesPadExist(destinationID); + const exists = await doesPadExist(destinationID); // allow force to be a string if (typeof force === 'string') { @@ -446,7 +463,7 @@ class Pad { } // exists and forcing - const pad = await padManager.getPad(destinationID); + const pad = await getPad(destinationID); await pad.remove(); } } @@ -454,7 +471,7 @@ class Pad { async copyAuthorInfoToDestinationPad(destinationID) { // add the new sourcePad to all authors who contributed to the old one await Promise.all(this.getAllAuthors().map( - (authorID) => authorManager.addPad(authorID, destinationID))); + (authorID) => addPad(authorID, destinationID))); } async copyPadWithoutHistory(destinationID, force, authorId = '') { @@ -475,14 +492,14 @@ class Pad { } // initialize the pad with a new line to avoid getting the defaultText - const dstPad = await padManager.getPad(destinationID, '\n', authorId); + const dstPad = await getPad(destinationID, '\n', authorId); dstPad.pool = this.pool.clone(); const oldAText = this.atext; // based on Changeset.makeSplice - const assem = Changeset.smartOpAssembler(); - for (const op of Changeset.opsFromAText(oldAText)) assem.append(op); + const assem = smartOpAssembler(); + for (const op of opsFromAText(oldAText)) assem.append(op); assem.endDocument(); // although we have instantiated the dstPad with '\n', an additional '\n' is @@ -494,16 +511,16 @@ class Pad { // create a changeset that removes the previous text and add the newText with // all atributes present on the source pad - const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); + const changeset = pack(oldLength, newLength, assem.toString(), newText); dstPad.appendRevision(changeset, authorId); - await hooks.aCallAll('padCopy', { + await aCallAll('padCopy', { get originalPad() { - warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); + padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); return this.srcPad; }, get destinationID() { - warnDeprecated( + padutils.warnDeprecated( 'padCopy destinationID context property is deprecated; use dstPad.id instead'); return this.dstPad.id; }, @@ -519,7 +536,7 @@ class Pad { const p = []; // kick everyone from this pad - padMessageHandler.kickSessionsFromPad(padID); + kickSessionsFromPad(padID); // delete all relations - the original code used async.parallel but // none of the operations except getting the group depended on callbacks @@ -540,31 +557,31 @@ class Pad { } // remove the readonly entries - p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => { + p.push(getReadOnlyId(padID).then(async (readonlyID) => { await db.remove(`readonly2pad:${readonlyID}`); })); p.push(db.remove(`pad2readonly:${padID}`)); // delete all chat messages - p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => { + p.push(timesLimit(this.chatHead + 1, 500, async (i) => { await this.db.remove(`pad:${this.id}:chat:${i}`, null); })); // delete all revisions - p.push(promises.timesLimit(this.head + 1, 500, async (i) => { + p.push(timesLimit(this.head + 1, 500, async (i) => { await this.db.remove(`pad:${this.id}:revs:${i}`, null); })); // remove pad from all authors who contributed this.getAllAuthors().forEach((authorId) => { - p.push(authorManager.removePad(authorId, padID)); + p.push(removePad(authorId, padID)); }); // delete the pad entry and delete pad from padManager - p.push(padManager.removePad(padID)); - p.push(hooks.aCallAll('padRemove', { + p.push(removePad(padID)); + p.push(aCallAll('padRemove', { get padID() { - warnDeprecated('padRemove padID context property is deprecated; use pad.id instead'); + padutils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead'); return this.pad.id; }, pad: this, @@ -587,12 +604,13 @@ class Pad { } // build the saved revision object - const savedRevision = {}; - savedRevision.revNum = revNum; - savedRevision.savedById = savedById; - savedRevision.label = label || `Revision ${revNum}`; - savedRevision.timestamp = Date.now(); - savedRevision.id = randomString(10); + const savedRevision:Revision = { + label: label || `Revision ${revNum}`, + revNum: revNum, + savedById: savedById, + timestamp: Date.now(), + id: randomString(10) + } // save this new saved revision this.savedRevisions.push(savedRevision); @@ -667,7 +685,7 @@ class Pad { } }) .batch(100).buffer(99); - let atext = Changeset.makeAText('\n'); + let atext = makeAText('\n'); for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) { try { assert(authorId != null); @@ -678,10 +696,10 @@ class Pad { assert(timestamp > 0); assert(changeset != null); assert.equal(typeof changeset, 'string'); - Changeset.checkRep(changeset); - const unpacked = Changeset.unpack(changeset); + checkRep(changeset); + const unpacked = unpack(changeset); let text = atext.text; - for (const op of Changeset.deserializeOps(unpacked.ops)) { + for (const op of deserializeOps(unpacked.ops)) { if (['=', '-'].includes(op.opcode)) { assert(text.length >= op.chars); const consumed = text.slice(0, op.chars); @@ -692,7 +710,7 @@ class Pad { } assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString()); } - atext = Changeset.applyToAText(changeset, atext, pool); + atext = applyToAText(changeset, atext, pool); if (isKeyRev) assert.deepEqual(keyAText, atext); } catch (err) { err.message = `(pad ${this.id} revision ${r}) ${err.message}`; @@ -720,7 +738,6 @@ class Pad { .batch(100).buffer(99); for (const p of chats) await p; - await hooks.aCallAll('padCheck', {pad: this}); + await aCallAll('padCheck', {pad: this}); } } -exports.Pad = Pad; diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.ts similarity index 86% rename from src/node/db/PadManager.js rename to src/node/db/PadManager.ts index e419e839231..a48473546d8 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.ts @@ -19,9 +19,9 @@ * limitations under the License. */ -const CustomError = require('../utils/customError'); -const Pad = require('../db/Pad'); -const db = require('./DB'); +import {CustomError} from '../utils/customError'; +import {Pad} from './Pad'; +import {db} from './DB'; /** * A cache of all loaded Pads. @@ -50,6 +50,9 @@ const globalPads = { * Updated without db access as new pads are created/old ones removed. */ const padList = new class { + private _cachedList: any[]; + private _list: Set; + private _loaded: Promise constructor() { this._cachedList = null; this._list = new Set(); @@ -94,9 +97,9 @@ const padList = new class { * @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if * applicable). */ -exports.getPad = async (id, text, authorId = '') => { +export const getPad = async (id, text?, authorId = '') => { // check if this is a valid padId - if (!exports.isValidPadId(id)) { + if (!isValidPadId(id)) { throw new CustomError(`${id} is not a valid padId`, 'apierror'); } @@ -121,7 +124,7 @@ exports.getPad = async (id, text, authorId = '') => { } // try to load pad - pad = new Pad.Pad(id); + pad = new Pad(id); // initialize the pad await pad.init(text, authorId); @@ -131,21 +134,18 @@ exports.getPad = async (id, text, authorId = '') => { return pad; }; -exports.listAllPads = async () => { +export const listAllPads = async () => { const padIDs = await padList.getPads(); return {padIDs}; }; // checks if a pad exists -exports.doesPadExist = async (padId) => { +export const doesPadExist = async (padId) => { const value = await db.get(`pad:${padId}`); return (value != null && value.atext); -}; - -// alias for backwards compatibility -exports.doesPadExists = exports.doesPadExist; +} /** * An array of padId transformations. These represent changes in pad name policy over @@ -157,9 +157,9 @@ const padIdTransforms = [ ]; // returns a sanitized padId, respecting legacy pad id formats -exports.sanitizePadId = async (padId) => { +export const sanitizePadId = async (padId) => { for (let i = 0, n = padIdTransforms.length; i < n; ++i) { - const exists = await exports.doesPadExist(padId); + const exists = await doesPadExist(padId); if (exists) { return padId; @@ -174,19 +174,19 @@ exports.sanitizePadId = async (padId) => { return padId; }; -exports.isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); +export const isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); /** * Removes the pad from database and unloads it. */ -exports.removePad = async (padId) => { +export const removePad = async (padId) => { const p = db.remove(`pad:${padId}`); - exports.unloadPad(padId); + unloadPad(padId); padList.removePad(padId); await p; }; // removes a pad from the cache -exports.unloadPad = (padId) => { +export const unloadPad = (padId) => { globalPads.remove(padId); }; diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.ts similarity index 72% rename from src/node/db/ReadOnlyManager.js rename to src/node/db/ReadOnlyManager.ts index 33ce2930a4b..a96ad53ed60 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.ts @@ -20,21 +20,21 @@ */ -const db = require('./DB'); -const randomString = require('../utils/randomstring'); +import {db} from './DB'; +import {randomString} from '../utils/randomstring'; /** * checks if the id pattern matches a read-only pad id - * @param {String} the pad's id + * @param {String} id the pad's id */ -exports.isReadOnlyId = (id) => id.startsWith('r.'); +export const isReadOnlyId = (id: string) => id.startsWith('r.'); /** * returns a read only id for a pad * @param {String} padId the id of the pad */ -exports.getReadOnlyId = async (padId) => { +export const getReadOnlyId = async (padId) => { // check if there is a pad2readonly entry let readOnlyId = await db.get(`pad2readonly:${padId}`); @@ -54,18 +54,18 @@ exports.getReadOnlyId = async (padId) => { * returns the padId for a read only id * @param {String} readOnlyId read only id */ -exports.getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`); +export const getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`); /** * returns the padId and readonlyPadId in an object for any id - * @param {String} padIdOrReadonlyPadId read only id or real pad id + * @param {String} id padIdOrReadonlyPadId read only id or real pad id */ -exports.getIds = async (id) => { - const readonly = exports.isReadOnlyId(id); +export const getIds = async (id: string) => { + const readonly = isReadOnlyId(id); // Might be null, if this is an unknown read-only id - const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); - const padId = readonly ? await exports.getPadId(id) : id; + const readOnlyPadId = readonly ? id : await getReadOnlyId(id); + const padId = readonly ? await getPadId(id) : id; return {readOnlyPadId, padId, readonly}; }; diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.ts similarity index 76% rename from src/node/db/SecurityManager.js rename to src/node/db/SecurityManager.ts index 280c753bba4..f0fc26551fe 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.ts @@ -19,18 +19,29 @@ * limitations under the License. */ -const authorManager = require('./AuthorManager'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const padManager = require('./PadManager'); -const readOnlyManager = require('./ReadOnlyManager'); -const sessionManager = require('./SessionManager'); -const settings = require('../utils/Settings'); -const webaccess = require('../hooks/express/webaccess'); -const log4js = require('log4js'); +import {getAuthorId} from "./AuthorManager"; + +import {callAll} from "../../static/js/pluginfw/hooks.js"; + +import {doesPadExist, getPad} from "./PadManager"; + +import {getPadId} from "./ReadOnlyManager"; + +import {findAuthorID} from "./SessionManager"; + +import {editOnly, loadTest, requireAuthentication, requireSession} from "../utils/Settings"; + +import {normalizeAuthzLevel} from "../hooks/express/webaccess"; + +import log4js from "log4js"; + +import {padutils} from "../../static/js/pad_utils"; +import {isReadOnlyId} from "./ReadOnlyManager.js"; + const authLogger = log4js.getLogger('auth'); -const {padutils} = require('../../static/js/pad_utils'); -const DENY = Object.freeze({accessStatus: 'deny'}); + +const DENY = Object.freeze({accessStatus: 'deny', authorID: null}); /** * Determines whether the user can access a pad. @@ -50,17 +61,17 @@ const DENY = Object.freeze({accessStatus: 'deny'}); * WARNING: Tokens and session IDs MUST be kept secret, otherwise users will be able to impersonate * each other (which might allow them to gain privileges). */ -exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { +export const checkAccess = async (padID, sessionCookie, token, userSettings) => { if (!padID) { authLogger.debug('access denied: missing padID'); return DENY; } - let canCreate = !settings.editOnly; + let canCreate = !editOnly; - if (readOnlyManager.isReadOnlyId(padID)) { + if (isReadOnlyId(padID)) { canCreate = false; - padID = await readOnlyManager.getPadId(padID); + padID = await getPadId(padID); if (padID == null) { authLogger.debug('access denied: read-only pad ID for a pad that does not exist'); return DENY; @@ -68,10 +79,10 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { } // Authentication and authorization checks. - if (settings.loadTest) { + if (loadTest) { console.warn( 'bypassing socket.io authentication and authorization checks due to settings.loadTest'); - } else if (settings.requireAuthentication) { + } else if (requireAuthentication) { if (userSettings == null) { authLogger.debug('access denied: authentication is required'); return DENY; @@ -81,7 +92,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { // Note: userSettings.padAuthorizations should still be populated even if // settings.requireAuthorization is false. const padAuthzs = userSettings.padAuthorizations || {}; - const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]); + const level = normalizeAuthzLevel(padAuthzs[padID]); if (!level) { authLogger.debug('access denied: unauthorized'); return DENY; @@ -91,19 +102,19 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { // allow plugins to deny access const isFalse = (x) => x === false; - if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) { + if (callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) { authLogger.debug('access denied: an onAccessCheck hook function returned false'); return DENY; } - const padExists = await padManager.doesPadExist(padID); + const padExists = await doesPadExist(padID); if (!padExists && !canCreate) { authLogger.debug('access denied: user attempted to create a pad, which is prohibited'); return DENY; } - const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie); - if (settings.requireSession && !sessionAuthorID) { + const sessionAuthorID = await findAuthorID(padID.split('$')[0], sessionCookie); + if (requireSession && !sessionAuthorID) { authLogger.debug('access denied: HTTP API session is required'); return DENY; } @@ -115,7 +126,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { const grant = { accessStatus: 'grant', - authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings), + authorID: sessionAuthorID || await getAuthorId(token, userSettings), }; if (!padID.includes('$')) { @@ -132,7 +143,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { return grant; } - const pad = await padManager.getPad(padID); + const pad = await getPad(padID); if (!pad.getPublicStatus() && sessionAuthorID == null) { authLogger.debug('access denied: must have an HTTP API session to access private group pads'); diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.ts similarity index 93% rename from src/node/db/SessionManager.js rename to src/node/db/SessionManager.ts index 5ef75acfaf4..341252e26a0 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.ts @@ -36,7 +36,7 @@ const authorManager = require('./AuthorManager'); * sessionCookie, and is bound to a group with the given ID, then this returns the author ID * bound to the session. Otherwise, returns undefined. */ -exports.findAuthorID = async (groupID, sessionCookie) => { +export const findAuthorID = async (groupID, sessionCookie) => { if (!sessionCookie) return undefined; /* * Sometimes, RFC 6265-compliant web servers may send back a cookie whose @@ -64,7 +64,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => { const sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(','); const sessionInfoPromises = sessionIDs.map(async (id) => { try { - return await exports.getSessionInfo(id); + return await getSessionInfo(id); } catch (err) { if (err.message === 'sessionID does not exist') { console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`); @@ -81,7 +81,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => { return sessionInfo.authorID; }; -exports.doesSessionExist = async (sessionID) => { +export const doesSessionExist = async (sessionID) => { // check if the database entry of this session exists const session = await db.get(`session:${sessionID}`); return (session != null); @@ -90,7 +90,7 @@ exports.doesSessionExist = async (sessionID) => { /** * Creates a new session between an author and a group */ -exports.createSession = async (groupID, authorID, validUntil) => { +export const createSession = async (groupID, authorID, validUntil) => { // check if the group exists const groupExists = await groupManager.doesGroupExist(groupID); if (!groupExists) { @@ -146,7 +146,7 @@ exports.createSession = async (groupID, authorID, validUntil) => { return {sessionID}; }; -exports.getSessionInfo = async (sessionID) => { +export const getSessionInfo = async (sessionID) => { // check if the database entry of this session exists const session = await db.get(`session:${sessionID}`); @@ -162,7 +162,7 @@ exports.getSessionInfo = async (sessionID) => { /** * Deletes a session */ -exports.deleteSession = async (sessionID) => { +export const deleteSession = async (sessionID) => { // ensure that the session exists const session = await db.get(`session:${sessionID}`); if (session == null) { @@ -186,7 +186,7 @@ exports.deleteSession = async (sessionID) => { await db.remove(`session:${sessionID}`); }; -exports.listSessionsOfGroup = async (groupID) => { +export const listSessionsOfGroup = async (groupID) => { // check that the group exists const exists = await groupManager.doesGroupExist(groupID); if (!exists) { @@ -197,7 +197,7 @@ exports.listSessionsOfGroup = async (groupID) => { return sessions; }; -exports.listSessionsOfAuthor = async (authorID) => { +export const listSessionsOfAuthor = async (authorID) => { // check that the author exists const exists = await authorManager.doesAuthorExist(authorID); if (!exists) { @@ -218,8 +218,7 @@ const listSessionsWithDBKey = async (dbkey) => { // iterate through the sessions and get the sessioninfos for (const sessionID of Object.keys(sessions || {})) { try { - const sessionInfo = await exports.getSessionInfo(sessionID); - sessions[sessionID] = sessionInfo; + sessions[sessionID] = await getSessionInfo(sessionID); } catch (err) { if (err.name === 'apierror') { console.warn(`Found bad session ${sessionID} in ${dbkey}`); diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.ts similarity index 88% rename from src/node/db/SessionStore.js rename to src/node/db/SessionStore.ts index 40e5e90d069..bd6ab9d92e0 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.ts @@ -1,13 +1,19 @@ 'use strict'; -const DB = require('./DB'); -const Store = require('express-session').Store; -const log4js = require('log4js'); -const util = require('util'); +import {db} from "./DB"; + +import {Store} from "express-session"; + +import log4js from "log4js"; + +import util from "util"; +import {SessionModel} from "../models/SessionModel"; const logger = log4js.getLogger('SessionStore'); class SessionStore extends Store { + private _refresh: any; + private _expirations: Map; /** * @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's * database record with the cookie's latest expiration time. If the difference between the @@ -34,10 +40,10 @@ class SessionStore extends Store { for (const {timeout} of this._expirations.values()) clearTimeout(timeout); } - async _updateExpirations(sid, sess, updateDbExp = true) { + async _updateExpirations(sid, sess: SessionModel, updateDbExp = true) { const exp = this._expirations.get(sid) || {}; clearTimeout(exp.timeout); - const {cookie: {expires} = {}} = sess || {}; + const {cookie: {expires} = {expires: sess.cookie.expires}} = sess || {cookie:{expires:undefined}}; if (expires) { const sessExp = new Date(expires).getTime(); if (updateDbExp) exp.db = sessExp; @@ -64,12 +70,12 @@ class SessionStore extends Store { } async _write(sid, sess) { - await DB.set(`sessionstorage:${sid}`, sess); + await db.set(`sessionstorage:${sid}`, sess); } async _get(sid) { logger.debug(`GET ${sid}`); - const s = await DB.get(`sessionstorage:${sid}`); + const s = await db.get(`sessionstorage:${sid}`); return await this._updateExpirations(sid, s); } @@ -83,7 +89,7 @@ class SessionStore extends Store { logger.debug(`DESTROY ${sid}`); clearTimeout((this._expirations.get(sid) || {}).timeout); this._expirations.delete(sid); - await DB.remove(`sessionstorage:${sid}`); + await db.remove(`sessionstorage:${sid}`); } // Note: express-session might call touch() before it calls set() for the first time. Ideally this @@ -110,4 +116,4 @@ for (const m of ['get', 'set', 'destroy', 'touch']) { SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]); } -module.exports = SessionStore; +export default SessionStore; diff --git a/src/node/eejs/index.js b/src/node/eejs/index.ts similarity index 53% rename from src/node/eejs/index.js rename to src/node/eejs/index.ts index 1bf29634b1b..ff125e4c373 100644 --- a/src/node/eejs/index.js +++ b/src/node/eejs/index.ts @@ -20,56 +20,61 @@ * require("./index").require("./path/to/template.ejs") */ -const ejs = require('ejs'); -const fs = require('fs'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const path = require('path'); -const resolve = require('resolve'); -const settings = require('../utils/Settings'); +import ejs from 'ejs'; +import fs from "fs"; + +import {callAll} from "../../static/js/pluginfw/hooks.js"; + +import path from "path"; + +import resolve from "resolve"; + +import {maxAge} from "../utils/Settings"; const templateCache = new Map(); -exports.info = { +export const info = { __output_stack: [], block_stack: [], file_stack: [], - args: [], + args: [], __output: undefined + }; -const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1]; +const getCurrentFile = () => info.file_stack[info.file_stack.length - 1]; -exports._init = (b, recursive) => { - exports.info.__output_stack.push(exports.info.__output); - exports.info.__output = b; +export const _init = (b, recursive) => { + info.__output_stack.push(info.__output) + info.__output = b }; -exports._exit = (b, recursive) => { - exports.info.__output = exports.info.__output_stack.pop(); +export const _exit = (b, recursive) => { + info.__output = info.__output_stack.pop(); }; -exports.begin_block = (name) => { - exports.info.block_stack.push(name); - exports.info.__output_stack.push(exports.info.__output.get()); - exports.info.__output.set(''); +export const begin_block = (name) => { + info.block_stack.push(name); + info.__output_stack.push(info.__output.get()); + info.__output.set(''); }; -exports.end_block = () => { - const name = exports.info.block_stack.pop(); - const renderContext = exports.info.args[exports.info.args.length - 1]; - const content = exports.info.__output.get(); - exports.info.__output.set(exports.info.__output_stack.pop()); +export const end_block = () => { + const name = info.block_stack.pop(); + const renderContext = info.args[info.args.length - 1]; + const content = info.__output.get(); + info.__output.set(info.__output_stack.pop()); const args = {content, renderContext}; - hooks.callAll(`eejsBlock_${name}`, args); - exports.info.__output.set(exports.info.__output.get().concat(args.content)); + callAll(`eejsBlock_${name}`, args); + info.__output.set(info.__output.get().concat(args.content)); }; -exports.require = (name, args, mod) => { +export const required = (name, args?, mod?) => { if (args == null) args = {}; let basedir = __dirname; let paths = []; - if (exports.info.file_stack.length) { + if (info.file_stack.length) { basedir = path.dirname(getCurrentFile().path); } if (mod) { @@ -82,18 +87,18 @@ exports.require = (name, args, mod) => { args.e = exports; args.require = require; - const cache = settings.maxAge !== 0; + const cache = maxAge !== 0; const template = cache && templateCache.get(ejspath) || ejs.compile( '<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' + `${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`, {filename: ejspath}); if (cache) templateCache.set(ejspath, template); - exports.info.args.push(args); - exports.info.file_stack.push({path: ejspath}); + info.args.push(args); + info.file_stack.push({path: ejspath}); const res = template(args); - exports.info.file_stack.pop(); - exports.info.args.pop(); + info.file_stack.pop(); + info.args.pop(); return res; }; diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.ts similarity index 88% rename from src/node/handler/APIHandler.js rename to src/node/handler/APIHandler.ts index 2c060c23953..5b485f33a4a 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.ts @@ -19,12 +19,12 @@ * limitations under the License. */ -const absolutePaths = require('../utils/AbsolutePaths'); -const fs = require('fs'); -const api = require('../db/API'); -const log4js = require('log4js'); -const padManager = require('../db/PadManager'); -const randomString = require('../utils/randomstring'); +import {makeAbsolute} from '../utils/AbsolutePaths'; +import fs from 'fs'; +import * as api from '../db/API'; +import log4js from 'log4js'; +import {sanitizePadId} from '../db/PadManager'; +import {randomString} from '../utils/randomstring'; const argv = require('../utils/Cli').argv; const createHTTPError = require('http-errors'); @@ -32,7 +32,7 @@ const apiHandlerLogger = log4js.getLogger('APIHandler'); // ensure we have an apikey let apikey = null; -const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); +const apikeyFilename = makeAbsolute(argv.apikey || './APIKEY.txt'); try { apikey = fs.readFileSync(apikeyFilename, 'utf8'); @@ -45,7 +45,7 @@ try { } // a list of all functions -const version = {}; +export const version = {}; version['1'] = { createGroup: [], @@ -158,10 +158,9 @@ version['1.3.0'] = { }; // set the latest available API version here -exports.latestApiVersion = '1.3.0'; +export const latestApiVersion = '1.3.0'; // exports the versions so it can be used by the new Swagger endpoint -exports.version = version; /** * Handles a HTTP API call @@ -170,7 +169,7 @@ exports.version = version; * @req express request object * @res express response object */ -exports.handle = async function (apiVersion, functionName, fields, req, res) { +export const handle = async function (apiVersion, functionName, fields, req, res) { // say goodbye if this is an unknown API version if (!(apiVersion in version)) { throw new createHTTPError.NotFound('no such api version'); @@ -190,13 +189,13 @@ exports.handle = async function (apiVersion, functionName, fields, req, res) { // sanitize any padIDs before continuing if (fields.padID) { - fields.padID = await padManager.sanitizePadId(fields.padID); + fields.padID = await sanitizePadId(fields.padID); } // there was an 'else' here before - removed it to ensure // that this sanitize step can't be circumvented by forcing // the first branch to be taken if (fields.padName) { - fields.padName = await padManager.sanitizePadId(fields.padName); + fields.padName = await sanitizePadId(fields.padName); } // put the function parameters in an array @@ -206,6 +205,6 @@ exports.handle = async function (apiVersion, functionName, fields, req, res) { return api[functionName].apply(this, functionParams); }; -exports.exportedForTestingOnly = { +export const exportedForTestingOnly = { apiKey: apikey, }; diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.ts similarity index 98% rename from src/node/handler/ExportHandler.js rename to src/node/handler/ExportHandler.ts index f3fde047cd5..a06c2839331 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.ts @@ -38,7 +38,7 @@ const tempDirectory = os.tmpdir(); /** * do a requested export */ -exports.doExport = async (req, res, padId, readOnlyId, type) => { +export const doExport = async (req, res, padId, readOnlyId, type) => { // avoid naming the read-only file as the original pad's id let fileName = readOnlyId ? readOnlyId : padId; @@ -114,4 +114,4 @@ exports.doExport = async (req, res, padId, readOnlyId, type) => { await fsp_unlink(destFile); } -}; +} diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.ts similarity index 84% rename from src/node/handler/ImportHandler.js rename to src/node/handler/ImportHandler.ts index c1fbc94d03a..d83c15c6721 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.ts @@ -21,22 +21,24 @@ * limitations under the License. */ -const padManager = require('../db/PadManager'); -const padMessageHandler = require('./PadMessageHandler'); -const fs = require('fs').promises; -const path = require('path'); -const settings = require('../utils/Settings'); -const {Formidable} = require('formidable'); -const os = require('os'); -const importHtml = require('../utils/ImportHtml'); -const importEtherpad = require('../utils/ImportEtherpad'); -const log4js = require('log4js'); -const hooks = require('../../static/js/pluginfw/hooks.js'); +import {getPad, unloadPad} from '../db/PadManager'; +import {updatePadClients} from './PadMessageHandler'; +import path from 'path'; +import {promises as fs} from "fs"; + +import {abiword, allowUnknownFileEnds, importMaxFileSize, soffice} from '../utils/Settings'; +import {Formidable} from 'formidable'; +import os from 'os'; +import {setPadHTML} from '../utils/ImportHtml'; +import {setPadRaw} from '../utils/ImportEtherpad'; +import log4js from 'log4js'; +import {aCallAll} from '../../static/js/pluginfw/hooks.js'; const logger = log4js.getLogger('ImportHandler'); // `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`. class ImportError extends Error { + public status: any; constructor(status, ...args) { super(...args); if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError); @@ -59,12 +61,12 @@ let converter = null; let exportExtension = 'htm'; // load abiword only if it is enabled and if soffice is disabled -if (settings.abiword != null && settings.soffice == null) { +if (abiword != null && soffice == null) { converter = require('../utils/Abiword'); } // load soffice only if it is enabled -if (settings.soffice != null) { +if (soffice != null) { converter = require('../utils/LibreOffice'); exportExtension = 'html'; } @@ -86,11 +88,11 @@ const doImport = async (req, res, padId, authorId) => { const form = new Formidable({ keepExtensions: true, uploadDir: tmpDirectory, - maxFileSize: settings.importMaxFileSize, + maxFileSize: importMaxFileSize, }); // locally wrapped Promise, since form.parse requires a callback - let srcFile = await new Promise((resolve, reject) => { + let srcFile = await new Promise((resolve, reject) => { form.parse(req, (err, fields, files) => { if (err != null) { logger.warn(`Import failed due to form error: ${err.stack || err}`); @@ -118,7 +120,7 @@ const doImport = async (req, res, padId, authorId) => { if (fileEndingUnknown) { // the file ending is not known - if (settings.allowUnknownFileEnds === true) { + if (allowUnknownFileEnds === true) { // we need to rename this file with a .txt ending const oldSrcFile = srcFile; @@ -132,7 +134,7 @@ const doImport = async (req, res, padId, authorId) => { const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); const context = {srcFile, destFile, fileEnding, padId, ImportError}; - const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x) => x); + const importHandledByPlugin = (await aCallAll('import', context)).some((x) => x); const fileIsEtherpad = (fileEnding === '.etherpad'); const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsTXT = (fileEnding === '.txt'); @@ -140,7 +142,7 @@ const doImport = async (req, res, padId, authorId) => { let directDatabaseAccess = false; if (fileIsEtherpad) { // Use '\n' to avoid the default pad text if the pad doesn't yet exist. - const pad = await padManager.getPad(padId, '\n', authorId); + const pad = await getPad(padId, '\n', authorId); const headCount = pad.head; if (headCount >= 10) { logger.warn('Aborting direct database import attempt of a pad that already has content'); @@ -148,7 +150,7 @@ const doImport = async (req, res, padId, authorId) => { } const text = await fs.readFile(srcFile, 'utf8'); directDatabaseAccess = true; - await importEtherpad.setPadRaw(padId, text, authorId); + await setPadRaw(padId, text, authorId); } // convert file to html if necessary @@ -186,7 +188,7 @@ const doImport = async (req, res, padId, authorId) => { } // Use '\n' to avoid the default pad text if the pad doesn't yet exist. - let pad = await padManager.getPad(padId, '\n', authorId); + let pad = await getPad(padId, '\n', authorId); // read the text let text; @@ -205,7 +207,7 @@ const doImport = async (req, res, padId, authorId) => { if (!directDatabaseAccess) { if (importHandledByPlugin || useConverter || fileIsHTML) { try { - await importHtml.setPadHTML(pad, text, authorId); + await setPadHTML(pad, text, authorId); } catch (err) { logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`); } @@ -215,16 +217,16 @@ const doImport = async (req, res, padId, authorId) => { } // Load the Pad into memory then broadcast updates to all clients - padManager.unloadPad(padId); - pad = await padManager.getPad(padId, '\n', authorId); - padManager.unloadPad(padId); + unloadPad(padId); + pad = await getPad(padId, '\n', authorId); + unloadPad(padId); // 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 - await padMessageHandler.updatePadClients(pad); + await updatePadClients(pad); // clean up temporary files rm(srcFile); @@ -233,7 +235,7 @@ const doImport = async (req, res, padId, authorId) => { return false; }; -exports.doImport = async (req, res, padId, authorId = '') => { +export const doImport2 = async (req, res, padId, authorId = '') => { let httpStatus = 200; let code = 0; let message = 'ok'; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.ts similarity index 82% rename from src/node/handler/PadMessageHandler.js rename to src/node/handler/PadMessageHandler.ts index 9a1885b736e..52f47092612 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.ts @@ -19,34 +19,72 @@ * limitations under the License. */ -const AttributeMap = require('../../static/js/AttributeMap'); -const padManager = require('../db/PadManager'); -const Changeset = require('../../static/js/Changeset'); -const ChatMessage = require('../../static/js/ChatMessage'); -const AttributePool = require('../../static/js/AttributePool'); -const AttributeManager = require('../../static/js/AttributeManager'); -const authorManager = require('../db/AuthorManager'); -const {padutils} = require('../../static/js/pad_utils'); -const readOnlyManager = require('../db/ReadOnlyManager'); -const settings = require('../utils/Settings'); +import AttributeMap from '../../static/js/AttributeMap'; +import {getPad} from '../db/PadManager'; +import { + builder, + checkRep, cloneAText, compose, + deserializeOps, + follow, inverse, makeAText, + makeSplice, + moveOpsToNewPool, mutateAttributionLines, mutateTextLines, + oldLen, prepareForWire, splitAttributionLines, splitTextLines, + unpack +} from '../../static/js/Changeset'; +import ChatMessage from '../../static/js/ChatMessage'; +import {AttributePool} from '../../static/js/AttributePool'; +import AttributeManager from '../../static/js/AttributeManager'; +import { + getAuthor, + getAuthorColorId, + getAuthorName, + getColorPalette, + setAuthorColorId, + setAuthorName +} from '../db/AuthorManager'; +import {padutils} from '../../static/js/pad_utils'; +import {getIds} from '../db/ReadOnlyManager'; +import { + abiwordAvailable, + automaticReconnectionTimeout, + commitRateLimiting, + cookie, + disableIPlogging, + exportAvailable, + indentationOnNewLine, + padOptions, + padShortcutEnabled, + scrollWhenFocusLineIsOutOfViewport, + skinName, + skinVariants, + sofficeAvailable +} from '../utils/Settings'; +import {parts, plugins} from '../../static/js/pluginfw/plugin_defs.js'; +import log4js from "log4js"; +import {aCallAll, deprecationNotices} from '../../static/js/pluginfw/hooks.js'; +import {createCollection} from '../stats'; +import {strict as assert} from "assert"; + +import {RateLimiterMemory} from 'rate-limiter-flexible'; +import {userCanModify} from '../hooks/express/webaccess'; +import {ErrorCaused} from "../models/ErrorCaused"; +import {Pad} from "../db/Pad"; +import {SessionInfo} from "../models/SessionInfo"; +import {randomString} from "../utils/randomstring"; +import {identity} from "lodash"; + const securityManager = require('../db/SecurityManager'); -const plugins = require('../../static/js/pluginfw/plugin_defs.js'); -const log4js = require('log4js'); + const messageLogger = log4js.getLogger('message'); const accessLogger = log4js.getLogger('access'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const stats = require('../stats'); -const assert = require('assert').strict; -const {RateLimiterMemory} = require('rate-limiter-flexible'); -const webaccess = require('../hooks/express/webaccess'); let rateLimiter; let socketio = null; -hooks.deprecationNotices.clientReady = 'use the userJoin hook instead'; +deprecationNotices.clientReady = 'use the userJoin hook instead'; -const addContextToError = (err, pfx) => { - const newErr = new Error(`${pfx}${err.message}`, {cause: err}); +const addContextToError = (err: Error, pfx) => { + const newErr = new ErrorCaused(`${pfx}${err.message}`, err); if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError); // Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10. if (newErr.cause === err) return newErr; @@ -54,11 +92,11 @@ const addContextToError = (err, pfx) => { return err; }; -exports.socketio = () => { +export const socketiofn = () => { // The rate limiter is created in this hook so that restarting the server resets the limiter. The // settings.commitRateLimiting object is passed directly to the rate limiter so that the limits // can be dynamically changed during runtime by modifying its properties. - rateLimiter = new RateLimiterMemory(settings.commitRateLimiting); + rateLimiter = new RateLimiterMemory(commitRateLimiting); }; /** @@ -79,11 +117,10 @@ exports.socketio = () => { * - readonly: Whether the client has read-only access (true) or read/write access (false). * - rev: The last revision that was sent to the client. */ -const sessioninfos = {}; -exports.sessioninfos = sessioninfos; +export const sessioninfos: SessionInfo = {}; -stats.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0); -stats.gauge('activePads', () => { +createCollection.gauge('totalUsers', () => socketio ? Object.keys(socketio.sockets.sockets).length : 0); +createCollection.gauge('activePads', () => { const padIds = new Set(); for (const {padId} of Object.values(sessioninfos)) { if (!padId) continue; @@ -96,6 +133,8 @@ stats.gauge('activePads', () => { * Processes one task at a time per channel. */ class Channels { + private readonly _exec: (ch, task) => any; + private _promiseChains: Map; /** * @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. @@ -135,7 +174,7 @@ const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(so * This Method is called by server.js to tell the message handler on which socket it should send * @param socket_io The Socket */ -exports.setSocketIO = (socket_io) => { +export const setSocketIO = (socket_io) => { socketio = socket_io; }; @@ -143,17 +182,21 @@ exports.setSocketIO = (socket_io) => { * Handles the connection of a new user * @param socket the socket.io Socket object for the new connection from the client */ -exports.handleConnect = (socket) => { - stats.meter('connects').mark(); +export const handleConnect = (socket) => { + createCollection.meter('connects').mark(); // Initialize sessioninfos for this new session - sessioninfos[socket.id] = {}; + sessioninfos[socket.id] = { + rev: 0, time: undefined, + auth: {padID: undefined, sessionID: undefined, token: undefined}, + readOnlyPadId: undefined, + padId:undefined,readonly:false,author:undefined}; }; /** * Kicks all sessions from a pad */ -exports.kickSessionsFromPad = (padID) => { +export const kickSessionsFromPad = (padID) => { if (typeof socketio.sockets.clients !== 'function') return; // skip if there is nobody on this pad @@ -167,18 +210,18 @@ exports.kickSessionsFromPad = (padID) => { * Handles the disconnection of a user * @param socket the socket.io Socket object for the client */ -exports.handleDisconnect = async (socket) => { - stats.meter('disconnects').mark(); +export const handleDisconnect = async (socket) => { + createCollection.meter('disconnects').mark(); const session = sessioninfos[socket.id]; delete sessioninfos[socket.id]; // session.padId can be nullish if the user disconnects before sending CLIENT_READY. if (!session || !session.author || !session.padId) return; - const {session: {user} = {}} = socket.client.request; + const {session: {user} = {}}: SessionSocketModel = 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}` + + ` IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + ` authorID:${session.author}` + (user && user.username ? ` username:${user.username}` : '')); /* eslint-enable prefer-template */ @@ -187,12 +230,12 @@ exports.handleDisconnect = async (socket) => { data: { type: 'USER_LEAVE', userInfo: { - colorId: await authorManager.getAuthorColorId(session.author), + colorId: await getAuthorColorId(session.author), userId: session.author, }, }, }); - await hooks.aCallAll('userLeave', { + await aCallAll('userLeave', { ...session, // For backwards compatibility. authorId: session.author, readOnly: session.readonly, @@ -205,7 +248,7 @@ exports.handleDisconnect = async (socket) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -exports.handleMessage = async (socket, message) => { +export const handleMessage = async (socket, message) => { const env = process.env.NODE_ENV || 'development'; if (env === 'production') { @@ -214,7 +257,7 @@ exports.handleMessage = async (socket, message) => { } catch (err) { messageLogger.warn(`Rate limited IP ${socket.request.ip}. To reduce the amount of rate ` + 'limiting that happens edit the rateLimit values in settings.json'); - stats.meter('rateLimited').mark(); + createCollection.meter('rateLimited').mark(); socket.json.send({disconnect: 'rateLimited'}); throw err; } @@ -235,11 +278,11 @@ exports.handleMessage = async (socket, message) => { padID: message.padId, token: message.token, }; - const padIds = await readOnlyManager.getIds(thisSession.auth.padID); + const padIds = await getIds(thisSession.auth.padID); thisSession.padId = padIds.padId; thisSession.readOnlyPadId = padIds.readOnlyPadId; thisSession.readonly = - padIds.readonly || !webaccess.userCanModify(thisSession.auth.padID, socket.client.request); + padIds.readonly || !userCanModify(thisSession.auth.padID, socket.client.request); } // Outside of the checks done by this function, message.padId must not be accessed because it is // too easy to introduce a security vulnerability that allows malicious users to read or modify @@ -252,12 +295,12 @@ exports.handleMessage = async (socket, message) => { const auth = thisSession.auth; if (!auth) { - const ip = settings.disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || ''); + const ip = disableIPlogging ? 'ANONYMOUS' : (socket.request.ip || ''); const msg = JSON.stringify(message, null, 2); throw new Error(`pre-CLIENT_READY message from IP ${ip}: ${msg}`); } - const {session: {user} = {}} = socket.client.request; + const {session: {user} = {}}:SessionSocketModel = socket.client.request; const {accessStatus, authorID} = await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user); if (accessStatus !== 'grant') { @@ -269,7 +312,7 @@ exports.handleMessage = async (socket, message) => { throw new Error([ 'Author ID changed mid-session. Bad or missing token or sessionID?', `socket:${socket.id}`, - `IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}`, + `IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}`, `originalAuthorID:${thisSession.author}`, `newAuthorID:${authorID}`, ...(user && user.username) ? [`username:${user.username}`] : [], @@ -295,7 +338,7 @@ exports.handleMessage = async (socket, message) => { return this.socket; }, }; - for (const res of await hooks.aCallAll('handleMessageSecurity', context)) { + for (const res of await aCallAll('handleMessageSecurity', context)) { switch (res) { case true: padutils.warnDeprecated( @@ -313,7 +356,7 @@ exports.handleMessage = async (socket, message) => { } // Call handleMessage hook. If a plugin returns null, the message will be dropped. - if ((await hooks.aCallAll('handleMessage', context)).some((m) => m == null)) { + if ((await aCallAll('handleMessage', context)).some((m) => m == null)) { return; } @@ -331,7 +374,7 @@ exports.handleMessage = async (socket, message) => { try { switch (type) { case 'USER_CHANGES': - stats.counter('pendingEdits').inc(); + createCollection.counter('pendingEdits').inc(); await padChannels.enqueue(thisSession.padId, {socket, message}); break; case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break; @@ -372,7 +415,7 @@ exports.handleMessage = async (socket, message) => { */ const handleSaveRevisionMessage = async (socket, message) => { const {padId, author: authorId} = sessioninfos[socket.id]; - const pad = await padManager.getPad(padId, null, authorId); + const pad = await getPad(padId, null, authorId); await pad.addSavedRevision(pad.head, authorId); }; @@ -383,7 +426,7 @@ const handleSaveRevisionMessage = async (socket, message) => { * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = (msg, sessionID) => { +export const handleCustomObjectMessage = (msg, sessionID) => { if (msg.data.type === 'CUSTOM') { if (sessionID) { // a sessionID is targeted: directly to this sessionID @@ -401,7 +444,7 @@ exports.handleCustomObjectMessage = (msg, sessionID) => { * @param padID {Pad} the pad to which we're sending this message * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = (padID, msgString) => { +export const handleCustomMessage = (padID, msgString) => { const time = Date.now(); const msg = { type: 'COLLABROOM', @@ -424,7 +467,7 @@ const handleChatMessage = async (socket, message) => { // Don't trust the user-supplied values. chatMessage.time = Date.now(); chatMessage.authorId = authorId; - await exports.sendChatMessageToPadClients(chatMessage, padId); + await sendChatMessageToPadClients(chatMessage, padId); }; /** @@ -438,15 +481,15 @@ const handleChatMessage = async (socket, message) => { * @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message * object as the first argument and the destination pad ID as the second argument instead. */ -exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => { +export const 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, null, message.authorId); - await hooks.aCallAll('chatNewMessage', {message, pad, padId}); + const pad = await getPad(padId, null, message.authorId); + await aCallAll('chatNewMessage', {message, pad, padId}); // pad.appendChatMessage() ignores the displayName property so we don't need to wait for // authorManager.getAuthorName() to resolve before saving the message to the database. const promise = pad.appendChatMessage(message); - message.displayName = await authorManager.getAuthorName(message.authorId); + message.displayName = await getAuthorName(message.authorId); socketio.sockets.in(padId).json.send({ type: 'COLLABROOM', data: {type: 'CHAT_MESSAGE', message}, @@ -465,7 +508,7 @@ const handleGetChatMessages = async (socket, {data: {start, end}}) => { const count = end - start; if (count < 0 || count > 100) throw new Error(`invalid number of messages: ${count}`); const {padId, author: authorId} = sessioninfos[socket.id]; - const pad = await padManager.getPad(padId, null, authorId); + const pad = await getPad(padId, null, authorId); const chatMessages = await pad.getChatMessages(start, end); const infoMsg = { @@ -517,8 +560,8 @@ const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}}) // Tell the authorManager about the new attributes const p = Promise.all([ - authorManager.setAuthorColorId(author, colorId), - authorManager.setAuthorName(author, name), + setAuthorColorId(author, colorId), + setAuthorName(author, name), ]); const padId = session.padId; @@ -555,7 +598,7 @@ const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}}) */ const handleUserChanges = async (socket, message) => { // This one's no longer pending, as we're gonna process it now - stats.counter('pendingEdits').dec(); + createCollection.counter('pendingEdits').dec(); // The client might disconnect between our callbacks. We should still // finish processing the changeset, so keep a reference to the session. @@ -567,20 +610,20 @@ const handleUserChanges = async (socket, message) => { if (!thisSession) throw new Error('client disconnected'); // Measure time to process edit - const stopWatch = stats.timer('edits').start(); + const stopWatch = createCollection.timer('edits').start(); try { const {data: {baseRev, apool, changeset}} = message; if (baseRev == null) throw new Error('missing baseRev'); if (apool == null) throw new Error('missing apool'); if (changeset == null) throw new Error('missing changeset'); const wireApool = (new AttributePool()).fromJsonable(apool); - const pad = await padManager.getPad(thisSession.padId, null, thisSession.author); + const pad = await getPad(thisSession.padId, null, thisSession.author); // Verify that the changeset has valid syntax and is in canonical form - Changeset.checkRep(changeset); + checkRep(changeset); // Validate all added 'author' attribs to be the same value as the current user - for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) { + for (const op of deserializeOps(unpack(changeset).ops)) { // + can add text with attribs // = can change or add attribs // - can have attribs, but they are discarded and don't show up in the attribs - @@ -599,7 +642,7 @@ const handleUserChanges = async (socket, message) => { // ex. adoptChangesetAttribs // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool - let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); + let rebasedChangeset = moveOpsToNewPool(changeset, wireApool, pad.pool); // ex. applyUserChanges let r = baseRev; @@ -612,21 +655,21 @@ const handleUserChanges = async (socket, message) => { const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r); if (changeset === c && thisSession.author === authorId) { // Assume this is a retransmission of an already applied changeset. - rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen); + rebasedChangeset = identity(unpack(changeset).oldLen); } // At this point, both "c" (from the pad) and "changeset" (from the // client) are relative to revision r - 1. The follow function // rebases "changeset" so that it is relative to revision r // and can be applied after "c". - rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool); + rebasedChangeset = follow(c, rebasedChangeset, false, pad.pool); } const prevText = pad.text(); - if (Changeset.oldLen(rebasedChangeset) !== prevText.length) { + if (oldLen(rebasedChangeset) !== prevText.length) { throw new Error( `Can't apply changeset ${rebasedChangeset} with oldLen ` + - `${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`); + `${oldLen(rebasedChangeset)} to document of length ${prevText.length}`); } const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author); @@ -641,7 +684,7 @@ const handleUserChanges = async (socket, message) => { // Make sure the pad always ends with an empty line. if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) { - const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); + const nlChangeset = makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); await pad.appendRevision(nlChangeset, thisSession.author); } @@ -651,10 +694,10 @@ const handleUserChanges = async (socket, message) => { socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}}); thisSession.rev = newRev; if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev); - await exports.updatePadClients(pad); + await updatePadClients(pad); } catch (err) { socket.json.send({disconnect: 'badChangeset'}); - stats.meter('failedChangesets').mark(); + createCollection.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 { @@ -662,7 +705,7 @@ const handleUserChanges = async (socket, message) => { } }; -exports.updatePadClients = async (pad) => { +export const updatePadClients = async (pad) => { // skip this if no-one is on this pad const roomSockets = _getRoomSockets(pad.id); if (roomSockets.length === 0) return; @@ -696,7 +739,7 @@ exports.updatePadClients = async (pad) => { const revChangeset = revision.changeset; const currentTime = revision.meta.timestamp; - const forWire = Changeset.prepareForWire(revChangeset, pad.pool); + const forWire = prepareForWire(revChangeset, pad.pool); const msg = { type: 'COLLABROOM', data: { @@ -731,7 +774,7 @@ const _correctMarkersInPad = (atext, apool) => { // that aren't at the start of a line const badMarkers = []; let offset = 0; - for (const op of Changeset.deserializeOps(atext.attribs)) { + for (const op of deserializeOps(atext.attribs)) { const attribs = AttributeMap.fromString(op.attribs, apool); const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a)); if (hasMarker) { @@ -753,17 +796,18 @@ const _correctMarkersInPad = (atext, apool) => { // create changeset that removes these bad markers offset = 0; - const builder = Changeset.builder(text.length); + const builder2 = builder(text.length); badMarkers.forEach((pos) => { - builder.keepText(text.substring(offset, pos)); - builder.remove(1); + builder2.keepText(text.substring(offset, pos)); + builder2.remove(1); offset = pos + 1; }); - return builder.toString(); + return builder2.toString(); }; +export let clientVars:any /** * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client * to the server. The Client sends his token @@ -776,7 +820,7 @@ const handleClientReady = async (socket, message) => { if (sessionInfo == null) throw new Error('client disconnected'); assert(sessionInfo.author); - await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context. + await aCallAll('clientReady', message); // Deprecated due to awkward context. let {colorId: authorColorId, name: authorName} = message.userInfo || {}; if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) { @@ -784,13 +828,13 @@ const handleClientReady = async (socket, message) => { authorColorId = null; } await Promise.all([ - authorName && authorManager.setAuthorName(sessionInfo.author, authorName), - authorColorId && authorManager.setAuthorColorId(sessionInfo.author, authorColorId), + authorName && setAuthorName(sessionInfo.author, authorName), + authorColorId && setAuthorColorId(sessionInfo.author, authorColorId), ]); - ({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author)); + ({colorId: authorColorId, name: authorName} = await getAuthor(sessionInfo.author)); // load the pad-object from the database - const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author); + const pad = await createCollection.getPad(sessionInfo.padId, null, sessionInfo.author); // these db requests all need the pad object (timestamp of latest revision, author data) const authors = pad.getAllAuthors(); @@ -801,7 +845,7 @@ const handleClientReady = async (socket, message) => { // get all author data out of the database (in parallel) const historicalAuthorData = {}; await Promise.all(authors.map(async (authorId) => { - const author = await authorManager.getAuthor(authorId); + const author = await getAuthor(authorId); if (!author) { messageLogger.error(`There is no author for authorId: ${authorId}. ` + 'This is possibly related to https://github.com/ether/etherpad-lite/issues/2802'); @@ -825,18 +869,18 @@ const handleClientReady = async (socket, message) => { 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[otherSocket.id] = {}; + sessioninfos[otherSocket.id] = undefined otherSocket.leave(sessionInfo.padId); otherSocket.json.send({disconnect: 'userdup'}); } } - const {session: {user} = {}} = socket.client.request; + const {session: {user} = {}}:SessionSocketModel = socket.client.request; /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + ` pad:${sessionInfo.padId}` + ` socket:${socket.id}` + - ` IP:${settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + + ` IP:${disableIPlogging ? 'ANONYMOUS' : socket.request.ip}` + ` authorID:${sessionInfo.author}` + (user && user.username ? ` username:${user.username}` : '')); /* eslint-enable prefer-template */ @@ -884,7 +928,7 @@ const handleClientReady = async (socket, message) => { // return pending changesets for (const r of revisionsNeeded) { - const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool); + const forWire = prepareForWire(changesets[r].changeset, pad.pool); const wireMsg = {type: 'COLLABROOM', data: {type: 'CLIENT_RECONNECT', headRev: pad.getHeadRevisionNumber(), @@ -909,8 +953,8 @@ const handleClientReady = async (socket, message) => { let apool; // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted try { - atext = Changeset.cloneAText(pad.atext); - const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + atext = cloneAText(pad.atext); + const attribsForWire = prepareForWire(atext.attribs, pad.pool); apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; } catch (e) { @@ -921,14 +965,14 @@ const handleClientReady = async (socket, message) => { // 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, - randomVersionString: settings.randomVersionString, + clientVars = { + skinName: skinName, + skinVariants: skinVariants, + randomVersionString: randomString(4), accountPrivs: { maxRevisions: 100, }, - automaticReconnectionTimeout: settings.automaticReconnectionTimeout, + automaticReconnectionTimeout: automaticReconnectionTimeout, initialRevisionList: [], initialOptions: {}, savedRevisions: pad.getSavedRevisions(), @@ -941,12 +985,12 @@ const handleClientReady = async (socket, message) => { rev: pad.getHeadRevisionNumber(), time: currentTime, }, - colorPalette: authorManager.getColorPalette(), + colorPalette: getColorPalette(), clientIp: '127.0.0.1', userColor: authorColorId, padId: sessionInfo.auth.padID, - padOptions: settings.padOptions, - padShortcutEnabled: settings.padShortcutEnabled, + padOptions: padOptions, + padShortcutEnabled: padShortcutEnabled, initialTitle: `Pad: ${sessionInfo.auth.padID}`, opts: {}, // tell the client the number of the latest chat-message, which will be @@ -956,29 +1000,30 @@ const handleClientReady = async (socket, message) => { readOnlyId: sessionInfo.readOnlyPadId, readonly: sessionInfo.readonly, serverTimestamp: Date.now(), - sessionRefreshInterval: settings.cookie.sessionRefreshInterval, + sessionRefreshInterval: cookie.sessionRefreshInterval, userId: sessionInfo.author, - abiwordAvailable: settings.abiwordAvailable(), - sofficeAvailable: settings.sofficeAvailable(), - exportAvailable: settings.exportAvailable(), + abiwordAvailable: abiwordAvailable(), + sofficeAvailable: sofficeAvailable(), + exportAvailable: exportAvailable(), plugins: { - plugins: plugins.plugins, - parts: plugins.parts, + plugins: plugins, + parts: parts, }, - indentationOnNewLine: settings.indentationOnNewLine, + indentationOnNewLine: indentationOnNewLine, scrollWhenFocusLineIsOutOfViewport: { percentage: { editionAboveViewport: - settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, + scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, editionBelowViewport: - settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, + scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, }, - duration: settings.scrollWhenFocusLineIsOutOfViewport.duration, + duration: scrollWhenFocusLineIsOutOfViewport.duration, scrollWhenCaretIsInTheLastLineOfViewport: - settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, + scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, percentageToScrollWhenUserPressesArrowUp: - settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, + scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, }, + userName: undefined, initialChangesets: [], // FIXME: REMOVE THIS SHIT }; @@ -988,7 +1033,7 @@ const handleClientReady = async (socket, message) => { } // call the clientVars-hook so plugins can modify them before they get sent to the client - const messages = await hooks.aCallAll('clientVars', {clientVars, pad, socket}); + const messages = await aCallAll('clientVars', {clientVars, pad, socket}); // combine our old object with the new attributes from the hook for (const msg of messages) { @@ -1034,7 +1079,7 @@ const handleClientReady = async (socket, message) => { if (authorId == null) return; // reuse previously created cache of author's data - const authorInfo = historicalAuthorData[authorId] || await authorManager.getAuthor(authorId); + const authorInfo = historicalAuthorData[authorId] || await getAuthor(authorId); if (authorInfo == null) { messageLogger.error( `Author ${authorId} connected via socket.io session ${roomSocket.id} is missing from ` + @@ -1059,7 +1104,7 @@ const handleClientReady = async (socket, message) => { socket.json.send(msg); })); - await hooks.aCallAll('userJoin', { + await aCallAll('userJoin', { authorId: sessionInfo.author, displayName: authorName, padId: sessionInfo.padId, @@ -1079,7 +1124,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques if (requestID == null) throw new Error('mising requestID'); const end = start + (100 * granularity); const {padId, author: authorId} = sessioninfos[socket.id]; - const pad = await padManager.getPad(padId, null, authorId); + const pad = await createCollection.getPad(padId, null, authorId); const data = await getChangesetInfo(pad, start, end, granularity); data.requestID = requestID; socket.json.send({type: 'CHANGESET_REQ', data}); @@ -1089,7 +1134,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ -const getChangesetInfo = async (pad, startNum, endNum, granularity) => { +const getChangesetInfo = async (pad: Pad, startNum: number, endNum: number, granularity: number) => { const headRevision = pad.getHeadRevisionNumber(); // calculate the last full endnum @@ -1120,8 +1165,7 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => { getPadLines(pad, startNum - 1), // Get all needed composite Changesets. ...compositesChangesetNeeded.map(async (item) => { - const changeset = await composePadChangesets(pad, item.start, item.end); - composedChangesets[`${item.start}/${item.end}`] = changeset; + composedChangesets[`${item.start}/${item.end}`] = await composePadChangesets(pad, item.start, item.end); }), // Get all needed revision Dates. ...revTimesNeeded.map(async (revNum) => { @@ -1141,13 +1185,13 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => { if (compositeEnd > endNum || compositeEnd > headRevision + 1) break; const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`]; - const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + const backwards = inverse(forwards, lines.textlines, lines.alines, pad.apool()); - Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); - Changeset.mutateTextLines(forwards, lines.textlines); + mutateAttributionLines(forwards, lines.alines, pad.apool()); + mutateTextLines(forwards, lines.textlines); - const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); - const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool); + const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool); const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1]; const t2 = revisionDate[compositeEnd - 1]; @@ -1159,7 +1203,9 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => { return {forwardsChangesets, backwardsChangesets, apool: apool.toJsonable(), actualEndNum: endNum, - timeDeltas, start: startNum, granularity}; + timeDeltas, start: startNum, granularity, requestID: undefined + + }; }; /** @@ -1173,12 +1219,12 @@ const getPadLines = async (pad, revNum) => { if (revNum >= 0) { atext = await pad.getInternalRevisionAText(revNum); } else { - atext = Changeset.makeAText('\n'); + atext = makeAText('\n'); } return { - textlines: Changeset.splitTextLines(atext.text), - alines: Changeset.splitAttributionLines(atext.attribs, atext.text), + textlines: splitTextLines(atext.text), + alines: splitAttributionLines(atext.attribs, atext.text), }; }; @@ -1213,7 +1259,7 @@ const composePadChangesets = async (pad, startNum, endNum) => { for (r = startNum + 1; r < endNum; r++) { const cs = changesets[r]; - changeset = Changeset.compose(changeset, cs, pool); + changeset = compose(changeset, cs, pool); } return changeset; } catch (e) { @@ -1238,21 +1284,21 @@ const _getRoomSockets = (padID) => { /** * Get the number of users in a pad */ -exports.padUsersCount = (padID) => ({ +export const padUsersCount = (padID) => ({ padUsersCount: _getRoomSockets(padID).length, }); /** * Get the list of users in a pad */ -exports.padUsers = async (padID) => { +export const padUsers = async (padID) => { const padUsers = []; // iterate over all clients (in parallel) await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { const s = sessioninfos[roomSocket.id]; if (s) { - const author = await authorManager.getAuthor(s.author); + const author = await getAuthor(s.author); // Fixes: https://github.com/ether/etherpad-lite/issues/4120 // On restart author might not be populated? if (author) { @@ -1263,6 +1309,4 @@ exports.padUsers = async (padID) => { })); return {padUsers}; -}; - -exports.sessioninfos = sessioninfos; +} diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.ts similarity index 80% rename from src/node/handler/SocketIORouter.js rename to src/node/handler/SocketIORouter.ts index 863401aceb4..9f7875566b1 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.ts @@ -20,9 +20,10 @@ * limitations under the License. */ -const log4js = require('log4js'); -const settings = require('../utils/Settings'); -const stats = require('../stats'); +import log4js from 'log4js'; +import {disableIPlogging} from "../utils/Settings"; + +import {createCollection} from '../stats'; const logger = log4js.getLogger('socket.io'); @@ -38,22 +39,22 @@ let io; /** * adds a component */ -exports.addComponent = (moduleName, module) => { - if (module == null) return exports.deleteComponent(moduleName); +export const addComponent = (moduleName, module) => { + if (module == null) return deleteComponent(moduleName); components[moduleName] = module; module.setSocketIO(io); }; -exports.deleteComponent = (moduleName) => { delete components[moduleName]; }; +export const deleteComponent = (moduleName) => { delete components[moduleName]; }; /** * sets the socket.io and adds event functions for routing */ -exports.setSocketIO = (_io) => { +export const setSocketIO = (_io) => { io = _io; io.sockets.on('connection', (socket) => { - const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip; + const ip = disableIPlogging ? 'ANONYMOUS' : socket.request.ip; logger.debug(`${socket.id} connected from IP ${ip}`); // wrap the original send function to log the messages @@ -68,14 +69,14 @@ exports.setSocketIO = (_io) => { components[i].handleConnect(socket); } - socket.on('message', (message, ack = () => {}) => (async () => { + socket.on('message', (message, ack = (p: { name: any; message: any }) => {}) => (async () => { if (!message.component || !components[message.component]) { throw new Error(`unknown message component: ${message.component}`); } logger.debug(`from ${socket.id}:`, message); return await components[message.component].handleMessage(socket, message); })().then( - (val) => ack(null, val), + (val) => ack({name: val.name, message: val.message}), (err) => { logger.error( `Error handling ${message.component} message from ${socket.id}: ${err.stack || err}`); @@ -88,7 +89,7 @@ exports.setSocketIO = (_io) => { // when the last user disconnected. If your activePads is 0 and totalUsers is 0 // you can say, if there has been no active pads or active users for 10 minutes // this instance can be brought out of a scaling cluster. - stats.gauge('lastDisconnect', () => Date.now()); + createCollection.gauge('lastDisconnect', () => Date.now()); // tell all components about this disconnect for (const i of Object.keys(components)) { components[i].handleDisconnect(socket); diff --git a/src/node/hooks/express.js b/src/node/hooks/express.ts similarity index 64% rename from src/node/hooks/express.js rename to src/node/hooks/express.ts index 9c42fd6d867..ef43d451685 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.ts @@ -1,35 +1,58 @@ 'use strict'; -const _ = require('underscore'); -const cookieParser = require('cookie-parser'); -const events = require('events'); -const express = require('express'); -const expressSession = require('express-session'); -const fs = require('fs'); -const hooks = require('../../static/js/pluginfw/hooks'); -const log4js = require('log4js'); -const SessionStore = require('../db/SessionStore'); -const settings = require('../utils/Settings'); -const stats = require('../stats'); -const util = require('util'); -const webaccess = require('./express/webaccess'); +import _ from 'underscore'; +import cookieParser from 'cookie-parser'; +import events from "events"; + +import express from "express"; + +import fs from "fs"; + +import expressSession from "express-session"; + +import {aCallAll} from "../../static/js/pluginfw/hooks"; + +import log4js from "log4js"; + +import SessionStore from "../db/SessionStore"; + +import { + cookie, + exposeVersion, + getEpVersion, + getGitCommit, + ip, + loglevel, + port, + sessionKey, + ssl, sslKeys, + trustProxy, + users +} from "../utils/Settings"; + +import {createCollection} from "../stats"; + +import util from "util"; + +import {checkAccess, checkAccess2} from "./express/webaccess"; +import {Socket} from "net"; const logger = log4js.getLogger('http'); let serverName; let sessionStore; -const sockets = new Set(); +const sockets = new Set(); const socketsEvents = new events.EventEmitter(); -const startTime = stats.settableGauge('httpStartTime'); - -exports.server = null; +const startTime = createCollection.settableGauge('httpStartTime'); +export let server = null; +export let sessionMiddleware; const closeServer = async () => { - if (exports.server != null) { + if (server != null) { logger.info('Closing HTTP server...'); // Call exports.server.close() to reject new connections but don't await just yet because the // Promise won't resolve until all preexisting connections are closed. - const p = util.promisify(exports.server.close.bind(exports.server))(); - await hooks.aCallAll('expressCloseServer'); + const p = util.promisify(server.close.bind(server))(); + await aCallAll('expressCloseServer'); // Give existing connections some time to close on their own before forcibly terminating. The // time should be long enough to avoid interrupting most preexisting transmissions but short // enough to avoid a noticeable outage. @@ -47,7 +70,7 @@ const closeServer = async () => { } await p; clearTimeout(timeout); - exports.server = null; + server = null; startTime.setValue(0); logger.info('HTTP server closed'); } @@ -55,71 +78,73 @@ const closeServer = async () => { sessionStore = null; }; -exports.createServer = async () => { - console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); +export const createServer = async () => { + logger.info('Report bugs at https://github.com/ether/etherpad-lite/issues'); - serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; + serverName = `Etherpad ${getGitCommit()} (https://etherpad.org)`; - console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); + logger.info(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`); - await exports.restartServer(); + await restartServer(); - if (settings.ip === '') { + if (ip.length===0) { // using Unix socket for connectivity - console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`); + logger.info(`You can access your Etherpad instance using the Unix socket at ${port}`); } else { - console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`); + logger.info(`You can access your Etherpad instance at http://${ip}:${port}/`); } - if (!_.isEmpty(settings.users)) { - console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`); + if (!_.isEmpty(users)) { + logger.info(`The plugin admin page is at http://${ip}:${port}/admin/plugins`); } else { - console.warn('Admin username and password not set in settings.json. ' + + logger.info('Admin username and password not set in settings.json. ' + 'To access admin please uncomment and edit "users" in settings.json'); } const env = process.env.NODE_ENV || 'development'; if (env !== 'production') { - console.warn('Etherpad is running in Development mode. This mode is slower for users and ' + + logger.warn('Etherpad is running in Development mode. This mode is slower for users and ' + 'less secure than production mode. You should set the NODE_ENV environment ' + 'variable to production by using: export NODE_ENV=production'); } }; +export const app = express(); + +import http from 'http' +import https from 'https' -exports.restartServer = async () => { +export const restartServer = async () => { await closeServer(); - const app = express(); // New syntax for express v3 + // New syntax for express v3 - if (settings.ssl) { + if (ssl) { console.log('SSL -- enabled'); - console.log(`SSL -- server key file: ${settings.ssl.key}`); - console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); + console.log(`SSL -- server key file: ${sslKeys.key}`); + console.log(`SSL -- Certificate Authority's certificate file: ${sslKeys.cert}`); const options = { - key: fs.readFileSync(settings.ssl.key), - cert: fs.readFileSync(settings.ssl.cert), + key: fs.readFileSync(sslKeys.key), + cert: fs.readFileSync(sslKeys.cert), + ca: undefined }; - if (settings.ssl.ca) { + if (sslKeys.ca) { options.ca = []; - for (let i = 0; i < settings.ssl.ca.length; i++) { - const caFileName = settings.ssl.ca[i]; + for (let i = 0; i < sslKeys.ca.length; i++) { + const caFileName = sslKeys.ca[i]; options.ca.push(fs.readFileSync(caFileName)); } } - - const https = require('https'); - exports.server = https.createServer(options, app); + server = https.createServer(options, app); } else { - const http = require('http'); - exports.server = http.createServer(app); + server = http.createServer(app); } app.use((req, res, next) => { // res.header("X-Frame-Options", "deny"); // breaks embedded pads - if (settings.ssl) { + if (ssl) { // we use SSL res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } @@ -138,14 +163,14 @@ exports.restartServer = async () => { res.header('Referrer-Policy', 'same-origin'); // send git version in the Server response header if exposeVersion is true. - if (settings.exposeVersion) { + if (exposeVersion) { res.header('Server', serverName); } next(); }); - if (settings.trustProxy) { + if (trustProxy) { /* * If 'trust proxy' === true, the client’s IP address in req.ip will be the * left-most entry in the X-Forwarded-* header. @@ -157,8 +182,10 @@ exports.restartServer = async () => { // Measure response time app.use((req, res, next) => { - const stopWatch = stats.timer('httpRequests').start(); + const stopWatch = createCollection.timer('httpRequests').start(); const sendFn = res.send.bind(res); + // FIXME Check if this is still needed + // @ts-ignore res.send = (...args) => { stopWatch.end(); sendFn(...args); }; next(); }); @@ -167,20 +194,20 @@ exports.restartServer = async () => { // starts listening to requests as reported in issue #158. Not installing the log4js connect // logger when the log level has a higher severity than INFO since it would not log at that level // anyway. - if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { + if (!(loglevel === 'WARN') && loglevel === 'ERROR') { app.use(log4js.connectLogger(logger, { - level: log4js.levels.DEBUG, + level: loglevel, format: ':status, :method :url', })); } - app.use(cookieParser(settings.sessionKey, {})); + app.use(cookieParser(sessionKey, {})); - sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval); - exports.sessionMiddleware = expressSession({ + sessionStore = new SessionStore(cookie.sessionRefreshInterval); +sessionMiddleware = expressSession({ propagateTouch: true, rolling: true, - secret: settings.sessionKey, + secret: sessionKey, store: sessionStore, resave: false, saveUninitialized: false, @@ -188,8 +215,8 @@ exports.restartServer = async () => { // cleaner :) name: 'express_sid', cookie: { - maxAge: settings.cookie.sessionLifetime || null, // Convert 0 to null. - sameSite: settings.cookie.sameSite, + maxAge: cookie.sessionLifetime || null, // Convert 0 to null. + sameSite: cookie.sameSite, // The automatic express-session mechanism for determining if the application is being served // over ssl is similar to the one used for setting the language cookie, which check if one of @@ -214,16 +241,16 @@ exports.restartServer = async () => { // Give plugins an opportunity to install handlers/middleware before the express-session // middleware. This allows plugins to avoid creating an express-session record in the database // when it is not needed (e.g., public static content). - await hooks.aCallAll('expressPreSession', {app}); - app.use(exports.sessionMiddleware); + await aCallAll('expressPreSession', {app}); + app.use(sessionMiddleware); - app.use(webaccess.checkAccess); + app.use(checkAccess2); await Promise.all([ - hooks.aCallAll('expressConfigure', {app}), - hooks.aCallAll('expressCreateServer', {app, server: exports.server}), + aCallAll('expressConfigure', {app}), + aCallAll('expressCreateServer', {app, server: server}), ]); - exports.server.on('connection', (socket) => { + server.on('connection', (socket) => { sockets.add(socket); socketsEvents.emit('updated'); socket.on('close', () => { @@ -231,11 +258,11 @@ exports.restartServer = async () => { socketsEvents.emit('updated'); }); }); - await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); + await util.promisify(server.listen).bind(server)(port, ip); startTime.setValue(Date.now()); logger.info('HTTP server listening for connections'); }; -exports.shutdown = async (hookName, context) => { +export const shutdown = async (hookName, context) => { await closeServer(); }; diff --git a/src/node/hooks/express/admin.js b/src/node/hooks/express/admin.js deleted file mode 100644 index 7636983327b..00000000000 --- a/src/node/hooks/express/admin.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; -const eejs = require('../../eejs'); - -exports.expressCreateServer = (hookName, args, cb) => { - args.app.get('/admin', (req, res) => { - if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); - res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req})); - }); - return cb(); -}; diff --git a/src/node/hooks/express/admin.ts b/src/node/hooks/express/admin.ts new file mode 100644 index 00000000000..d55f49ea572 --- /dev/null +++ b/src/node/hooks/express/admin.ts @@ -0,0 +1,10 @@ +'use strict'; +import {required} from '../../eejs'; + +export const expressCreateServer = (hookName:string, args, cb) => { + args.app.get('/admin', (req, res) => { + if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); + res.send(required('ep_etherpad-lite/templates/admin/index.html', {req})); + }); + return cb(); +}; diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.ts similarity index 57% rename from src/node/hooks/express/adminplugins.js rename to src/node/hooks/express/adminplugins.ts index 543f04c0fb9..03a62655f55 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.ts @@ -1,33 +1,38 @@ 'use strict'; -const eejs = require('../../eejs'); -const settings = require('../../utils/Settings'); -const installer = require('../../../static/js/pluginfw/installer'); -const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); -const plugins = require('../../../static/js/pluginfw/plugins'); -const semver = require('semver'); -const UpdateCheck = require('../../utils/UpdateCheck'); - -exports.expressCreateServer = (hookName, args, cb) => { +import {required} from '../../eejs'; +import {getEpVersion, getGitCommit} from "../../utils/Settings"; + +import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer"; + +import {plugins} from "../../../static/js/pluginfw/plugin_defs"; + +import {formatHooks, formatParts, formatPlugins} from "../../../static/js/pluginfw/plugins"; + +import semver from "semver"; + +import UpdateCheck from "../../utils/UpdateCheck"; + +export const expressCreateServer = (hookName, args, cb) => { args.app.get('/admin/plugins', (req, res) => { - res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', { - plugins: pluginDefs.plugins, + res.send(required('ep_etherpad-lite/templates/admin/plugins.html', { + plugins: plugins, req, errors: [], })); }); args.app.get('/admin/plugins/info', (req, res) => { - const gitCommit = settings.getGitCommit(); - const epVersion = settings.getEpVersion(); + const gitCommit = getGitCommit(); + const epVersion = getEpVersion(); - res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', { + res.send(required('ep_etherpad-lite/templates/admin/plugins-info.html', { gitCommit, epVersion, - installedPlugins: `
${plugins.formatPlugins().replace(/, /g, '\n')}
`, - installedParts: `
${plugins.formatParts()}
`, - installedServerHooks: `
${plugins.formatHooks('hooks', true)}
`, - installedClientHooks: `
${plugins.formatHooks('client_hooks', true)}
`, + installedPlugins: `
${formatPlugins().replace(/, /g, '\n')}
`, + installedParts: `
${formatParts()}
`, + installedServerHooks: `
${formatHooks('hooks', true)}
`, + installedClientHooks: `
${formatHooks('client_hooks', true)}
`, latestVersion: UpdateCheck.getLatestVersion(), req, })); @@ -36,16 +41,16 @@ exports.expressCreateServer = (hookName, args, cb) => { return cb(); }; -exports.socketio = (hookName, args, cb) => { +export const socketio = (hookName, args, cb) => { const io = args.io.of('/pluginfw/installer'); io.on('connection', (socket) => { - const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; + const {session: {user: {is_admin: isAdmin} = {}} = {}}:SessionSocketModel = socket.conn.request; if (!isAdmin) return; socket.on('getInstalled', (query) => { // send currently installed plugins const installed = - Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package); + Object.keys(plugins).map((plugin) => plugins[plugin].package); socket.emit('results:installed', {installed}); }); @@ -53,13 +58,13 @@ exports.socketio = (hookName, args, cb) => { socket.on('checkUpdates', async () => { // Check plugins for updates try { - const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); + const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10); - const updatable = Object.keys(pluginDefs.plugins).filter((plugin) => { + const updatable = Object.keys(plugins).filter((plugin) => { if (!results[plugin]) return false; const latestVersion = results[plugin].version; - const currentVersion = pluginDefs.plugins[plugin].package.version; + const currentVersion = plugins[plugin].package.version; return semver.gt(latestVersion, currentVersion); }); @@ -74,7 +79,7 @@ exports.socketio = (hookName, args, cb) => { socket.on('getAvailable', async (query) => { try { - const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false); + const results = await getAvailablePlugins(/* maxCacheAge:*/ false); socket.emit('results:available', results); } catch (er) { console.error(er); @@ -84,10 +89,10 @@ exports.socketio = (hookName, args, cb) => { socket.on('search', async (query) => { try { - const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); + const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); let res = Object.keys(results) .map((pluginName) => results[pluginName]) - .filter((plugin) => !pluginDefs.plugins[plugin.name]); + .filter((plugin) => !plugins[plugin.name]); res = sortPluginList(res, query.sortBy, query.sortDir) .slice(query.offset, query.offset + query.limit); socket.emit('results:search', {results: res, query}); @@ -99,7 +104,7 @@ exports.socketio = (hookName, args, cb) => { }); socket.on('install', (pluginName) => { - installer.install(pluginName, (err) => { + install(pluginName, (err) => { if (err) console.warn(err.stack || err.toString()); socket.emit('finished:install', { @@ -111,7 +116,7 @@ exports.socketio = (hookName, args, cb) => { }); socket.on('uninstall', (pluginName) => { - installer.uninstall(pluginName, (err) => { + uninstall(pluginName, (err) => { if (err) console.warn(err.stack || err.toString()); socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null}); diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.js deleted file mode 100644 index 792801dc72e..00000000000 --- a/src/node/hooks/express/adminsettings.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const eejs = require('../../eejs'); -const fsp = require('fs').promises; -const hooks = require('../../../static/js/pluginfw/hooks'); -const plugins = require('../../../static/js/pluginfw/plugins'); -const settings = require('../../utils/Settings'); - -exports.expressCreateServer = (hookName, {app}) => { - app.get('/admin/settings', (req, res) => { - res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { - req, - settings: '', - errors: [], - })); - }); -}; - -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', async (query) => { - let data; - try { - data = await fsp.readFile(settings.settingsFilename, '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', async (newSettings) => { - await fsp.writeFile(settings.settingsFilename, newSettings); - socket.emit('saveprogress', 'saved'); - }); - - socket.on('restartServer', async () => { - console.log('Admin request to restart server through a socket on /admin/settings'); - settings.reloadSettings(); - await plugins.update(); - await hooks.aCallAll('loadSettings', {settings}); - await hooks.aCallAll('restartServer'); - }); - }); -}; diff --git a/src/node/hooks/express/adminsettings.ts b/src/node/hooks/express/adminsettings.ts new file mode 100644 index 00000000000..404d6eefd75 --- /dev/null +++ b/src/node/hooks/express/adminsettings.ts @@ -0,0 +1,58 @@ +'use strict'; + +import {required} from '../../eejs'; +import {promises as fsp} from "fs"; + +import {aCallAll} from "../../../static/js/pluginfw/hooks"; + +import {update} from "../../../static/js/pluginfw/plugins"; + +import {reloadSettings, settingsFilename, showSettingsInAdminPage} from "../../utils/Settings"; +import * as settings from "../../utils/Settings"; + +export const expressCreateServer = (hookName, {app}) => { + app.get('/admin/settings', (req, res) => { + res.send(required('ep_etherpad-lite/templates/admin/settings.html', { + req, + settings: '', + errors: [], + })); + }); +}; + +export const socketio = (hookName, {io}) => { + io.of('/settings').on('connection', (socket) => { + const {session: {user: {is_admin: isAdmin} = {}} = {}}:SessionSocketModel = socket.conn.request; + if (!isAdmin) return; + + socket.on('load', async (query) => { + let data; + try { + data = await fsp.readFile(settingsFilename, 'utf8'); + } catch (err) { + return console.log(err); + } + // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result + //FIXME Is this intentional to never change + // @ts-ignore + if (showSettingsInAdminPage === false) { + socket.emit('settings', {results: 'NOT_ALLOWED'}); + } else { + socket.emit('settings', {results: data}); + } + }); + + socket.on('saveSettings', async (newSettings) => { + await fsp.writeFile(settingsFilename, newSettings); + socket.emit('saveprogress', 'saved'); + }); + + socket.on('restartServer', async () => { + console.log('Admin request to restart server through a socket on /admin/settings'); + reloadSettings(); + await update(); + await aCallAll('loadSettings', {}); + await aCallAll('restartServer'); + }); + }); +}; diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.ts similarity index 80% rename from src/node/hooks/express/apicalls.js rename to src/node/hooks/express/apicalls.ts index 010ab14e520..dc15d893849 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.ts @@ -1,12 +1,15 @@ 'use strict'; -const log4js = require('log4js'); -const clientLogger = log4js.getLogger('client'); -const {Formidable} = require('formidable'); -const apiHandler = require('../../handler/APIHandler'); -const util = require('util'); +import log4js from "log4js"; + +import {Formidable} from "formidable"; + +import {latestApiVersion} from "../../handler/APIHandler"; -exports.expressPreSession = async (hookName, {app}) => { +import util from "util"; + +const clientLogger = log4js.getLogger('client'); +export const expressPreSession = async (hookName, {app}) => { // The Etherpad client side sends information about how a disconnect happened app.post('/ep/pad/connection-diagnostic-info', (req, res) => { new Formidable().parse(req, (err, fields, files) => { @@ -26,7 +29,7 @@ exports.expressPreSession = async (hookName, {app}) => { // The Etherpad client side sends information about client side javscript errors app.post('/jserror', (req, res, next) => { (async () => { - const data = JSON.parse(await parseJserrorForm(req)); + const data = JSON.parse(await parseJserrorForm(req)); clientLogger.warn(`${data.msg} --`, { [util.inspect.custom]: (depth, options) => { // Depth is forced to infinity to ensure that all of the provided data is logged. @@ -40,6 +43,6 @@ exports.expressPreSession = async (hookName, {app}) => { // Provide a possibility to query the latest available API version app.get('/api', (req, res) => { - res.json({currentVersion: apiHandler.latestApiVersion}); - }); -}; + res.json({currentVersion: latestApiVersion}); + }) +} diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.ts similarity index 66% rename from src/node/hooks/express/errorhandling.js rename to src/node/hooks/express/errorhandling.ts index 884ca9be0b6..9e2992251f7 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.ts @@ -1,9 +1,10 @@ 'use strict'; -const stats = require('../../stats'); +import {createCollection} from '../../stats'; -exports.expressCreateServer = (hook_name, args, cb) => { - exports.app = args.app; +export let app:any; +export const expressCreateServer = (hook_name, args, cb) => { + app = args.app; // Handle errors args.app.use((err, req, res, next) => { @@ -12,7 +13,7 @@ exports.expressCreateServer = (hook_name, args, cb) => { // allowing you to respond however you like res.status(500).send({error: 'Sorry, something bad happened!'}); console.error(err.stack ? err.stack : err.toString()); - stats.meter('http500').mark(); + createCollection.meter('http500').mark(); }); return cb(); diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.ts similarity index 63% rename from src/node/hooks/express/importexport.js rename to src/node/hooks/express/importexport.ts index 96f0efab484..5d4101ead49 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.ts @@ -1,23 +1,23 @@ 'use strict'; -const hasPadAccess = require('../../padaccess'); -const settings = require('../../utils/Settings'); -const exportHandler = require('../../handler/ExportHandler'); -const importHandler = require('../../handler/ImportHandler'); -const padManager = require('../../db/PadManager'); -const readOnlyManager = require('../../db/ReadOnlyManager'); -const rateLimit = require('express-rate-limit'); -const securityManager = require('../../db/SecurityManager'); -const webaccess = require('./webaccess'); +import hasPadAccess from '../../padaccess'; +import {exportAvailable, importExportRateLimiting} from '../../utils/Settings'; +import {doExport} from '../../handler/ExportHandler'; +import {doImport2} from '../../handler/ImportHandler'; +import {doesPadExist} from '../../db/PadManager'; +import {getPadId, isReadOnlyId} from '../../db/ReadOnlyManager'; +import rateLimit from 'express-rate-limit'; +import {checkAccess} from '../../db/SecurityManager'; +import {userCanModify} from './webaccess'; -exports.expressCreateServer = (hookName, args, cb) => { - settings.importExportRateLimiting.onLimitReached = (req, res, options) => { +export const expressCreateServer = (hookName, args, cb) => { + importExportRateLimiting.onLimitReached = (req, res, options) => { // when the rate limiter triggers, write a warning in the logs console.warn('Import/Export rate limiter triggered on ' + `"${req.originalUrl}" for IP address ${req.ip}`); }; // The rate limiter is created in this hook so that restarting the server resets the limiter. - const limiter = rateLimit(settings.importExportRateLimiting); + const limiter = rateLimit(importExportRateLimiting); // handle export requests args.app.use('/p/:pad/:rev?/export/:type', limiter); @@ -30,7 +30,7 @@ exports.expressCreateServer = (hookName, args, cb) => { } // if abiword is disabled, and this is a format we only support with abiword, output a message - if (settings.exportAvailable() === 'no' && + if (exportAvailable() === 'no' && ['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` + ' There is no converter configured'); @@ -48,19 +48,19 @@ exports.expressCreateServer = (hookName, args, cb) => { let padId = req.params.pad; let readOnlyId = null; - if (readOnlyManager.isReadOnlyId(padId)) { + if (isReadOnlyId(padId)) { readOnlyId = padId; - padId = await readOnlyManager.getPadId(readOnlyId); + padId = await getPadId(readOnlyId); } - const exists = await padManager.doesPadExists(padId); + const exists = await doesPadExist(padId); if (!exists) { console.warn(`Someone tried to export a pad that doesn't exist (${padId})`); return next(); } console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`); - await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type); + await doExport(req, res, padId, readOnlyId, req.params.type); } })().catch((err) => next(err || new Error(err))); }); @@ -69,13 +69,13 @@ exports.expressCreateServer = (hookName, args, cb) => { args.app.use('/p/:pad/import', limiter); args.app.post('/p/:pad/import', (req, res, next) => { (async () => { - const {session: {user} = {}} = req; - const {accessStatus, authorID: authorId} = await securityManager.checkAccess( + const {session: {user} = {}}:SessionSocketModel = req; + const {accessStatus, authorID: authorId} = await checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); - if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { + if (accessStatus !== 'grant' || !userCanModify(req.params.pad, req)) { return res.status(403).send('Forbidden'); } - await importHandler.doImport(req, res, req.params.pad, authorId); + await doImport2(req, res, req.params.pad, authorId); })().catch((err) => next(err || new Error(err))); }); diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.ts similarity index 98% rename from src/node/hooks/express/openapi.js rename to src/node/hooks/express/openapi.ts index 0531854aa5b..68001bce7d4 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.ts @@ -1,5 +1,8 @@ 'use strict'; +import {ResponseSpec} from "../../models/ResponseSpec"; +import {APIResource} from "../../models/APIResource"; + /** * node/hooks/express/openapi.js * @@ -54,7 +57,7 @@ const APIPathStyle = { }; // API resources - describe your API endpoints here -const resources = { +const resources: APIResource = { // Group group: { create: { @@ -375,7 +378,8 @@ const defaultResponses = { const defaultResponseRefs = { 200: { - $ref: '#/components/responses/Success', + $ref: '#/components/responses/Success', content: undefined + }, 400: { $ref: '#/components/responses/ApiError', @@ -491,7 +495,11 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { // build operations for (const funcName of Object.keys(apiHandler.version[version])) { - let operation = {}; + let operation: ResponseSpec = { + parameters: undefined, + _restPath: "", + operationId: undefined + }; if (operations[funcName]) { operation = {...operations[funcName]}; } else { @@ -540,7 +548,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { return definition; }; -exports.expressPreSession = async (hookName, {app}) => { +export const expressPreSession = async (hookName, {app}) => { // create openapi-backend handlers for each api version under /api/{version}/* for (const version of Object.keys(apiHandler.version)) { // we support two different styles of api: flat + rest diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.ts similarity index 78% rename from src/node/hooks/express/padurlsanitize.js rename to src/node/hooks/express/padurlsanitize.ts index ff1afa47700..233f6f97c1a 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.ts @@ -1,18 +1,18 @@ 'use strict'; -const padManager = require('../../db/PadManager'); +import {isValidPadId, sanitizePadId} from '../../db/PadManager'; -exports.expressCreateServer = (hookName, args, cb) => { +export const expressCreateServer = (hookName, args, cb) => { // redirects browser to the pad's sanitized url if needed. otherwise, renders the html args.app.param('pad', (req, res, next, padId) => { (async () => { // ensure the padname is valid and the url doesn't end with a / - if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { + if (!isValidPadId(padId) || /\/$/.test(req.url)) { res.status(404).send('Such a padname is forbidden'); return; } - const sanitizedPadId = await padManager.sanitizePadId(padId); + const sanitizedPadId = await sanitizePadId(padId); if (sanitizedPadId === padId) { // the pad id was fine, so just render it diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.ts similarity index 85% rename from src/node/hooks/express/socketio.js rename to src/node/hooks/express/socketio.ts index edb6799405b..23c2623299d 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.ts @@ -1,21 +1,21 @@ 'use strict'; -const events = require('events'); -const express = require('../express'); -const log4js = require('log4js'); -const proxyaddr = require('proxy-addr'); -const settings = require('../../utils/Settings'); -const socketio = require('socket.io'); -const socketIORouter = require('../../handler/SocketIORouter'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const padMessageHandler = require('../../handler/PadMessageHandler'); +import events from 'events'; +import {sessionMiddleware} from '../express'; +import log4js from 'log4js'; +import proxyaddr from 'proxy-addr'; +import {socketIo, socketTransportProtocols, trustProxy} from '../../utils/Settings'; +import socketio from 'socket.io'; +import {addComponent, setSocketIO} from '../../handler/SocketIORouter'; +import {callAll} from '../../../static/js/pluginfw/hooks'; +import * as padMessageHandler from '../../handler/PadMessageHandler'; let io; const logger = log4js.getLogger('socket.io'); const sockets = new Set(); const socketsEvents = new events.EventEmitter(); -exports.expressCloseServer = async () => { +export const expressCloseServer = async () => { if (io == null) return; logger.info('Closing socket.io engine...'); // Close the socket.io engine to disconnect existing clients and reject new clients. Don't call @@ -46,13 +46,13 @@ exports.expressCloseServer = async () => { logger.info('All socket.io clients have disconnected'); }; -exports.expressCreateServer = (hookName, args, cb) => { +export const expressCreateServer = (hookName, args, cb) => { // init socket.io and redirect all requests to the MessageHandler // there shouldn't be a browser that isn't compatible to all // transports in this list at once // e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling io = socketio({ - transports: settings.socketTransportProtocols, + transports: socketTransportProtocols, }).listen(args.server, { /* * Do not set the "io" cookie. @@ -74,7 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => { * https://github.com/socketio/socket.io/issues/2276#issuecomment-147184662 (not totally true, actually, see above) */ cookie: false, - maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, + maxHttpBufferSize: socketIo.maxHttpBufferSize, }); io.on('connect', (socket) => { @@ -90,7 +90,7 @@ exports.expressCreateServer = (hookName, args, cb) => { const req = socket.request; // Express sets req.ip but socket.io does not. Replicate Express's behavior here. if (req.ip == null) { - if (settings.trustProxy) { + if (trustProxy) { req.ip = proxyaddr(req, args.app.get('trust proxy fn')); } else { req.ip = socket.handshake.address; @@ -102,7 +102,7 @@ exports.expressCreateServer = (hookName, args, cb) => { req.headers.cookie = socket.handshake.query.cookie; } // See: https://socket.io/docs/faq/#Usage-with-express-session - express.sessionMiddleware(req, {}, next); + sessionMiddleware(req, {}, next); }); io.use((socket, next) => { @@ -130,10 +130,10 @@ exports.expressCreateServer = (hookName, args, cb) => { // if(settings.minify) io.enable('browser client minification'); // Initialize the Socket.IO Router - socketIORouter.setSocketIO(io); - socketIORouter.addComponent('pad', padMessageHandler); + setSocketIO(io); + addComponent('pad', padMessageHandler); - hooks.callAll('socketio', {app: args.app, io, server: args.server}); + callAll('socketio', {app: args.app, io, server: args.server}); return cb(); }; diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.ts similarity index 58% rename from src/node/hooks/express/specialpages.js rename to src/node/hooks/express/specialpages.ts index 4f41d8cd02f..8a21beaa830 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.ts @@ -1,41 +1,41 @@ 'use strict'; -const path = require('path'); -const eejs = require('../../eejs'); -const fs = require('fs'); +import path from 'path'; +import {required} from '../../eejs'; +import fs from 'fs'; const fsp = fs.promises; -const toolbar = require('../../utils/toolbar'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const settings = require('../../utils/Settings'); -const util = require('util'); -const webaccess = require('./webaccess'); +import {} from '../../utils/toolbar'; +import {callAll} from '../../../static/js/pluginfw/hooks'; +import {favicon, getEpVersion, maxAge, root, skinName} from '../../utils/Settings'; +import util from 'util'; +import {userCanModify} from './webaccess'; -exports.expressPreSession = async (hookName, {app}) => { +export const expressPreSession = async (hookName, {app}) => { // This endpoint is intended to conform to: // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html app.get('/health', (req, res) => { res.set('Content-Type', 'application/health+json'); res.json({ status: 'pass', - releaseId: settings.getEpVersion(), + releaseId: getEpVersion(), }); }); app.get('/stats', (req, res) => { - res.json(require('../../stats').toJSON()); + res.json(required('../../stats').toJSON()); }); app.get('/javascript', (req, res) => { - res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); + res.send(required('ep_etherpad-lite/templates/javascript.html', {req})); }); app.get('/robots.txt', (req, res) => { let filePath = - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); + path.join(root, 'src', 'static', 'skins', skinName, 'robots.txt'); res.sendFile(filePath, (err) => { // there is no custom robots.txt, send the default robots.txt which dissallows all if (err) { - filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); + filePath = path.join(root, 'src', 'static', 'robots.txt'); res.sendFile(filePath); } }); @@ -44,9 +44,9 @@ exports.expressPreSession = async (hookName, {app}) => { app.get('/favicon.ico', (req, res, next) => { (async () => { const fns = [ - ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), - path.join(settings.root, 'src', 'static', 'favicon.ico'), + ...(favicon ? [path.resolve(root, favicon)] : []), + path.join(root, 'src', 'static', 'skins', skinName, 'favicon.ico'), + path.join(root, 'src', 'static', 'favicon.ico'), ]; for (const fn of fns) { try { @@ -54,7 +54,7 @@ exports.expressPreSession = async (hookName, {app}) => { } catch (err) { continue; } - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + res.setHeader('Cache-Control', `public, max-age=${maxAge}`); await util.promisify(res.sendFile.bind(res))(fn); return; } @@ -63,25 +63,25 @@ exports.expressPreSession = async (hookName, {app}) => { }); }; -exports.expressCreateServer = (hookName, args, cb) => { +export const expressCreateServer = (hookName, args, cb) => { // serve index.html under / args.app.get('/', (req, res) => { - res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); + res.send(required('ep_etherpad-lite/templates/index.html', {req})); }); // serve pad.html under /p args.app.get('/p/:pad', (req, res, next) => { // The below might break for pads being rewritten - const isReadOnly = !webaccess.userCanModify(req.params.pad, req); + const isReadOnly = !userCanModify(req.params.pad, req); - hooks.callAll('padInitToolbar', { + callAll('padInitToolbar', { toolbar, isReadOnly, }); // can be removed when require-kernel is dropped res.header('Feature-Policy', 'sync-xhr \'self\''); - res.send(eejs.require('ep_etherpad-lite/templates/pad.html', { + res.send(required('ep_etherpad-lite/templates/pad.html', { req, toolbar, isReadOnly, @@ -90,11 +90,11 @@ exports.expressCreateServer = (hookName, args, cb) => { // serve timeslider.html under /p/$padname/timeslider args.app.get('/p/:pad/timeslider', (req, res, next) => { - hooks.callAll('padInitToolbar', { + callAll('padInitToolbar', { toolbar, }); - res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { + res.send(required('ep_etherpad-lite/templates/timeslider.html', { req, toolbar, })); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.ts similarity index 86% rename from src/node/hooks/express/static.js rename to src/node/hooks/express/static.ts index 26c18995ab4..962b845f5f0 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.ts @@ -19,8 +19,9 @@ const getTar = async () => { }; const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8'); const tar = {}; - for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { - const files = relativeFiles.map(prefixLocalLibraryPath); + for (let [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { + const relativeFilesMapped = relativeFiles as string[]; + const files = relativeFilesMapped.map(prefixLocalLibraryPath); tar[prefixLocalLibraryPath(key)] = files .concat(files.map((p) => p.replace(/\.js$/, ''))) .concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`)); @@ -28,7 +29,7 @@ const getTar = async () => { return tar; }; -exports.expressPreSession = async (hookName, {app}) => { +export const expressPreSession = async (hookName, {app}) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); @@ -62,8 +63,9 @@ exports.expressPreSession = async (hookName, {app}) => { const clientParts = plugins.parts.filter((part) => part.client_hooks != null); const clientPlugins = {}; for (const name of new Set(clientParts.map((part) => part.plugin))) { - clientPlugins[name] = {...plugins.plugins[name]}; - delete clientPlugins[name].package; + const mappedName = name as any + clientPlugins[mappedName] = {...plugins.plugins[mappedName]}; + delete clientPlugins[mappedName].package; } res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.ts similarity index 79% rename from src/node/hooks/express/tests.js rename to src/node/hooks/express/tests.ts index 66b47d2af95..5ecfef43b57 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.ts @@ -1,11 +1,14 @@ 'use strict'; -const path = require('path'); -const fsp = require('fs').promises; -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const sanitizePathname = require('../../utils/sanitizePathname'); -const settings = require('../../utils/Settings'); +import path from 'path'; +import {promises as fsp} from "fs"; +import {plugins} from "../../../static/js/pluginfw/plugin_defs"; + +import sanitizePathname from "../../utils/sanitizePathname"; + +import {enableAdminUITests, root} from "../../utils/Settings"; +import {Presession} from "../../models/Presession"; // Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/' // instead of path.sep to separate pathname components. const findSpecs = async (specDir) => { @@ -29,16 +32,17 @@ const findSpecs = async (specDir) => { return specs; }; -exports.expressPreSession = async (hookName, {app}) => { +export const expressPreSession = async (hookName, {app}) => { app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => { (async () => { const modules = []; - await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { - let {package: {path: pluginPath}} = def; + await Promise.all(Object.entries(plugins).map(async ([plugin, def]) => { + const mappedDef = def as Presession; + let {package: {path: pluginPath}} = mappedDef; if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep; const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; for (const spec of await findSpecs(path.join(pluginPath, specDir))) { - if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests && + if (plugin === 'ep_etherpad-lite' && !enableAdminUITests && spec.startsWith('admin')) continue; modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`); } @@ -57,7 +61,7 @@ exports.expressPreSession = async (hookName, {app}) => { })().catch((err) => next(err || new Error(err))); }); - const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); + const rootTestFolder = path.join(root, 'src/tests/frontend/'); app.get('/tests/frontend/index.html', (req, res) => { res.redirect(['./', ...req.url.split('?').slice(1)].join('?')); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.ts similarity index 81% rename from src/node/hooks/express/webaccess.js rename to src/node/hooks/express/webaccess.ts index 81ed69b0748..a7ea8c573be 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.ts @@ -1,23 +1,31 @@ 'use strict'; -const assert = require('assert').strict; -const log4js = require('log4js'); +import {strict as assert} from "assert"; + +import log4js from "log4js"; + +import {requireAuthentication, requireAuthorization, setUsers, users} from "../../utils/Settings"; + +import {deprecationNotices} from "../../../static/js/pluginfw/hooks"; + +import {getPadId, isReadOnlyId} from "../../db/ReadOnlyManager"; +import {UserIndexedModel} from "../../models/UserIndexedModel"; + const httpLogger = log4js.getLogger('http'); -const settings = require('../../utils/Settings'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const readOnlyManager = require('../../db/ReadOnlyManager'); -hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; +import {aCallFirst as hookCall} from "../../../static/js/pluginfw/hooks"; + +deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; // Promisified wrapper around hooks.aCallFirst. const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => { - hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred); + hookCall(hookName, context, (err, r) => err != null ? reject(err) : resolve(r)); }); const aCallFirst0 = async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0]; -exports.normalizeAuthzLevel = (level) => { +export const normalizeAuthzLevel = (level) => { if (!level) return false; switch (level) { case true: @@ -32,20 +40,20 @@ exports.normalizeAuthzLevel = (level) => { return false; }; -exports.userCanModify = (padId, req) => { - if (readOnlyManager.isReadOnlyId(padId)) return false; - if (!settings.requireAuthentication) return true; - const {session: {user} = {}} = req; +export const userCanModify = (padId, req) => { + if (isReadOnlyId(padId)) return false; + if (!requireAuthentication) return true; + const {session: {user} = {}}:SessionSocketModel = req; if (!user || user.readOnly) return false; assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. - const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); + const level = normalizeAuthzLevel(user.padAuthorizations[padId]); return level && level !== 'readOnly'; }; // Exported so that tests can set this to 0 to avoid unnecessary test slowness. -exports.authnFailureDelayMs = 1000; +export let authnFailureDelayMs = 1000; -const checkAccess = async (req, res, next) => { +export const checkAccess = async (req, res, next) => { const requireAdmin = req.path.toLowerCase().startsWith('/admin'); // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -88,16 +96,16 @@ const checkAccess = async (req, res, next) => { // authentication is checked and once after (if settings.requireAuthorization is true). const authorize = async () => { const grant = async (level) => { - level = exports.normalizeAuthzLevel(level); + level = normalizeAuthzLevel(level); if (!level) return false; const user = req.session.user; if (user == null) return true; // This will happen if authentication is not required. const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1]; if (encodedPadId == null) return true; let padId = decodeURIComponent(encodedPadId); - if (readOnlyManager.isReadOnlyId(padId)) { + if (isReadOnlyId(padId)) { // pad is read-only, first get the real pad ID - padId = await readOnlyManager.getPadId(padId); + padId = await getPadId(padId); if (padId == null) return false; } // The user was granted access to a pad. Remember the authorization level in the user's @@ -108,11 +116,11 @@ const checkAccess = async (req, res, next) => { }; const isAuthenticated = req.session && req.session.user; if (isAuthenticated && req.session.user.is_admin) return await grant('create'); - const requireAuthn = requireAdmin || settings.requireAuthentication; + const requireAuthn = requireAdmin || requireAuthentication; if (!requireAuthn) return await grant('create'); if (!isAuthenticated) return await grant(false); if (requireAdmin && !req.session.user.is_admin) return await grant(false); - if (!settings.requireAuthorization) return await grant('create'); + if (!requireAuthorization) return await grant('create'); return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); }; @@ -131,8 +139,10 @@ const checkAccess = async (req, res, next) => { // page). // /////////////////////////////////////////////////////////////////////////////////////////////// - if (settings.users == null) settings.users = {}; - const ctx = {req, res, users: settings.users, next}; + if (users == null) setUsers({}); + const ctx = {req, res, users: users, next, username: undefined, + password: undefined + }; // If the HTTP basic auth header is present, extract the username and password so it can be given // to authn plugins. const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); @@ -148,7 +158,7 @@ const checkAccess = async (req, res, next) => { } if (!(await aCallFirst0('authenticate', ctx))) { // Fall back to HTTP basic auth. - const {[ctx.username]: {password} = {}} = settings.users; + const {[ctx.username]: {password} = {password: undefined}}:UserIndexedModel = users; if (!httpBasicAuth || !ctx.username || password == null || password !== ctx.password) { httpLogger.info(`Failed authentication from IP ${req.ip}`); if (await aCallFirst0('authnFailure', {req, res})) return; @@ -156,14 +166,14 @@ const checkAccess = async (req, res, next) => { // No plugin handled the authentication failure. Fall back to basic authentication. res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); // Delay the error response for 1s to slow down brute force attacks. - await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs)); + await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs)); res.status(401).send('Authentication Required'); return; } - settings.users[ctx.username].username = ctx.username; + users[ctx.username].username = ctx.username; // Make a shallow copy so that the password property can be deleted (to prevent it from // appearing in logs or in the database) without breaking future authentication attempts. - req.session.user = {...settings.users[ctx.username]}; + req.session.user = {...users[ctx.username]}; delete req.session.user.password; } if (req.session.user == null) { @@ -190,6 +200,12 @@ const checkAccess = async (req, res, next) => { * Express middleware to authenticate the user and check authorization. Must be installed after the * express-session middleware. */ -exports.checkAccess = (req, res, next) => { +// FIXME why same method twice? +export const checkAccess2 = (req, res, next) => { checkAccess(req, res, next).catch((err) => next(err || new Error(err))); }; + +// Setters +export const setauthnFailureDelayMs = (value) => { + authnFailureDelayMs = value; +} diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.ts similarity index 73% rename from src/node/hooks/i18n.js rename to src/node/hooks/i18n.ts index c54348867c8..001e87bd07d 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.ts @@ -1,12 +1,13 @@ 'use strict'; -const languages = require('languages4translatewiki'); -const fs = require('fs'); -const path = require('path'); -const _ = require('underscore'); -const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js'); -const existsSync = require('../utils/path_exists'); -const settings = require('../utils/Settings'); +import languages from 'languages4translatewiki'; +import fs from 'fs'; +import path from 'path'; +import _ from 'underscore'; +import {plugins} from '../../static/js/pluginfw/plugin_defs.js'; +import {check} from '../utils/path_exists'; +import {customLocaleStrings, maxAge, root} from '../utils/Settings'; +import {Presession} from "../models/Presession"; // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} @@ -17,7 +18,7 @@ const getAllLocales = () => { // into `locales2paths` (files from various dirs are grouped by lang code) // (only json files with valid language code as name) const extractLangs = (dir) => { - if (!existsSync(dir)) return; + if (!check(dir)) return; let stat = fs.lstatSync(dir); if (!stat.isDirectory() || stat.isSymbolicLink()) return; @@ -37,18 +38,19 @@ const getAllLocales = () => { }; // add core supported languages first - extractLangs(path.join(settings.root, 'src/locales')); + extractLangs(path.join(root, 'src/locales')); // add plugins languages (if any) - for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) { + for (const val of Object.values(plugins)) { + const pluginPath:Presession = val as Presession // plugin locales should overwrite etherpad's core locales - if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue; - extractLangs(path.join(pluginPath, 'locales')); + if (pluginPath.package.path.endsWith('/ep_etherpad-lite') === true) continue; + extractLangs(path.join(pluginPath.package.path, 'locales')); } // Build a locale index (merge all locale data other than user-supplied overrides) const locales = {}; - _.each(locales2paths, (files, langcode) => { + _.each(locales2paths, (files:[], langcode) => { locales[langcode] = {}; files.forEach((file) => { @@ -68,9 +70,9 @@ const getAllLocales = () => { const wrongFormatErr = Error( 'customLocaleStrings in wrong format. See documentation ' + 'for Customization for Administrators, under Localization.'); - if (settings.customLocaleStrings) { - if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; - _.each(settings.customLocaleStrings, (overrides, langcode) => { + if (customLocaleStrings) { + if (typeof customLocaleStrings !== 'object') throw wrongFormatErr; + _.each(customLocaleStrings, (overrides, langcode) => { if (typeof overrides !== 'object') throw wrongFormatErr; _.each(overrides, (localeString, key) => { if (typeof localeString !== 'string') throw wrongFormatErr; @@ -84,7 +86,7 @@ const getAllLocales = () => { // returns a hash of all available languages availables with nativeName and direction // e.g. { es: {nativeName: "español", direction: "ltr"}, ... } -const getAvailableLangs = (locales) => { +export const getAvailableLangs = (locales) => { const result = {}; for (const langcode of Object.keys(locales)) { result[langcode] = languages.getLanguageInfo(langcode); @@ -102,17 +104,17 @@ const generateLocaleIndex = (locales) => { }; -exports.expressPreSession = async (hookName, {app}) => { +export const expressPreSession = async (hookName, {app}) => { // regenerate locales on server restart const locales = getAllLocales(); const localeIndex = generateLocaleIndex(locales); - exports.availableLangs = getAvailableLangs(locales); + let availableLangs = getAvailableLangs(locales); app.get('/locales/:locale', (req, res) => { // works with /locale/en and /locale/en.json requests const locale = req.params.locale.split('.')[0]; - if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) { - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + if (Object.prototype.hasOwnProperty.call(availableLangs, locale)) { + res.setHeader('Cache-Control', `public, max-age=${maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); } else { @@ -121,7 +123,7 @@ exports.expressPreSession = async (hookName, {app}) => { }); app.get('/locales.json', (req, res) => { - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + res.setHeader('Cache-Control', `public, max-age=${maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); }); diff --git a/src/node/models/APIResource.ts b/src/node/models/APIResource.ts new file mode 100644 index 00000000000..862e15aec5d --- /dev/null +++ b/src/node/models/APIResource.ts @@ -0,0 +1,48 @@ +import {ResponseSpec} from "./ResponseSpec"; + +export type APIResource = { + group:{ + create:ResponseSpec, + createIfNotExistsFor: ResponseSpec, + delete: ResponseSpec, + listPads: ResponseSpec, + createPad: ResponseSpec, + listSessions: ResponseSpec, + list: ResponseSpec, + }, + author: { + create: ResponseSpec, + createIfNotExistsFor: ResponseSpec, + listPads: ResponseSpec, + listSessions: ResponseSpec, + getName: ResponseSpec, + }, + session:{ + create: ResponseSpec, + delete: ResponseSpec, + info: ResponseSpec, + }, + pad:{ + listAll: ResponseSpec, + createDiffHTML: ResponseSpec, + create: ResponseSpec, + getText: ResponseSpec, + setText: ResponseSpec, + getHTML: ResponseSpec, + setHTML: ResponseSpec, + getRevisionsCount: ResponseSpec, + getLastEdited: ResponseSpec, + delete: ResponseSpec, + getReadOnlyID: ResponseSpec, + setPublicStatus: ResponseSpec, + getPublicStatus: ResponseSpec, + authors: ResponseSpec, + usersCount: ResponseSpec, + users: ResponseSpec, + sendClientsMessage: ResponseSpec, + checkToken: ResponseSpec, + getChatHistory: ResponseSpec, + getChatHead: ResponseSpec, + appendChatMessage: ResponseSpec, + } +} diff --git a/src/node/models/CMDArgv.ts b/src/node/models/CMDArgv.ts new file mode 100644 index 00000000000..9e663cfa751 --- /dev/null +++ b/src/node/models/CMDArgv.ts @@ -0,0 +1,3 @@ +export type CMDArgv = { + [key: string]: any +} diff --git a/src/node/models/CMDOptions.ts b/src/node/models/CMDOptions.ts new file mode 100644 index 00000000000..925170e1d32 --- /dev/null +++ b/src/node/models/CMDOptions.ts @@ -0,0 +1,11 @@ +export type CMDOptions = { + cwd?: string, + stdio?: string|any[], + env?: NodeJS.ProcessEnv +} + +export type CMDPromise = { + stdout: string, + stderr: string, + child: any +} diff --git a/src/node/models/ErrorCaused.ts b/src/node/models/ErrorCaused.ts new file mode 100644 index 00000000000..1b821bc9d68 --- /dev/null +++ b/src/node/models/ErrorCaused.ts @@ -0,0 +1,14 @@ +import ts from "typescript/lib/tsserverlibrary"; + +export class ErrorCaused extends Error{ + cause: Error; + constructor(message: string, cause: Error) { + super(message); + this.cause = cause; + this.name = "ErrorCaused"; + } +} + +type ErrorCause = { + +} diff --git a/src/node/models/LogLevel.ts b/src/node/models/LogLevel.ts new file mode 100644 index 00000000000..bd007c23f22 --- /dev/null +++ b/src/node/models/LogLevel.ts @@ -0,0 +1 @@ +export type LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR" diff --git a/src/node/models/PadDiffModel.ts b/src/node/models/PadDiffModel.ts new file mode 100644 index 00000000000..c201b1fb4dc --- /dev/null +++ b/src/node/models/PadDiffModel.ts @@ -0,0 +1,3 @@ +export type PadDiffModel = { + _authors: any[] +} diff --git a/src/node/models/Plugin.ts b/src/node/models/Plugin.ts new file mode 100644 index 00000000000..08abf0d6918 --- /dev/null +++ b/src/node/models/Plugin.ts @@ -0,0 +1,6 @@ +export type Plugin = { + package: { + name: string, + version: string + } +} diff --git a/src/node/models/Presession.ts b/src/node/models/Presession.ts new file mode 100644 index 00000000000..bd8146bc340 --- /dev/null +++ b/src/node/models/Presession.ts @@ -0,0 +1,5 @@ +export type Presession = { + package:{ + path: string, + } +} diff --git a/src/node/models/ResponseSpec.ts b/src/node/models/ResponseSpec.ts new file mode 100644 index 00000000000..db32008af14 --- /dev/null +++ b/src/node/models/ResponseSpec.ts @@ -0,0 +1,43 @@ +export type ResponseSpec = { + parameters?: ParameterSpec[], + _restPath?: string, + operationId?: string, + responses?: any + description?: string, + summary?: string, + responseSchema?:{ + groupID?: APIResponseSpecType + groupIDs?: APIResponseSpecType + padIDs?: APIResponseSpecType, + sessions?: APIResponseSpecType, + authorID?: APIResponseSpecType, + info?: APIResponseSpecType, + sessionID?: APIResponseSpecType, + text?: APIResponseSpecType, + html?: APIResponseSpecType, + revisions?: APIResponseSpecType, + lastEdited?: APIResponseSpecType, + readOnlyID?: APIResponseSpecType, + publicStatus?: APIResponseSpecType, + authorIDs?: APIResponseSpecType, + padUsersCount?: APIResponseSpecType, + padUsers?: APIResponseSpecType, + messages?: APIResponseSpecType, + chatHead?: APIResponseSpecType, + } +} + + +export type APIResponseSpecType = { + type?:string, + items?: { + type?:string, + $ref?:string + }, + $ref?:string +} + + +export type ParameterSpec = { + $ref?: string, +} diff --git a/src/node/models/Revision.ts b/src/node/models/Revision.ts new file mode 100644 index 00000000000..a57be4e0fcb --- /dev/null +++ b/src/node/models/Revision.ts @@ -0,0 +1,7 @@ +export type Revision = { + revNum: number, + savedById: string, + label: string, + timestamp: number, + id: string, +} diff --git a/src/node/models/SessionInfo.ts b/src/node/models/SessionInfo.ts new file mode 100644 index 00000000000..520b21a2678 --- /dev/null +++ b/src/node/models/SessionInfo.ts @@ -0,0 +1,10 @@ +export type SessionInfo = { + [key: string]:{ + time: any; + rev: number; + readOnlyPadId: any; + auth: { padID: any; sessionID: any; token: any }; + readonly: boolean; + padId: string, + author: string + }} diff --git a/src/node/models/SessionModel.ts b/src/node/models/SessionModel.ts new file mode 100644 index 00000000000..2b2d5e80acb --- /dev/null +++ b/src/node/models/SessionModel.ts @@ -0,0 +1,5 @@ +export type SessionModel = { + cookie: { + expires?: string + } +} diff --git a/src/node/models/SessionSocketModel.ts b/src/node/models/SessionSocketModel.ts new file mode 100644 index 00000000000..ff0c730aa0f --- /dev/null +++ b/src/node/models/SessionSocketModel.ts @@ -0,0 +1,10 @@ +type SessionSocketModel = { + session:{ + user?: { + username?: string, + is_admin?: boolean + readOnly?: boolean, + padAuthorizations?: any + } + } +} diff --git a/src/node/models/UserIndexedModel.ts b/src/node/models/UserIndexedModel.ts new file mode 100644 index 00000000000..0c808a41b69 --- /dev/null +++ b/src/node/models/UserIndexedModel.ts @@ -0,0 +1,5 @@ +export type UserIndexedModel = { + [key: string]: { + password?: string|undefined, + } +} diff --git a/src/node/padaccess.js b/src/node/padaccess.ts similarity index 62% rename from src/node/padaccess.js rename to src/node/padaccess.ts index e9cc7cde5ec..a18bd83eda3 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.ts @@ -1,10 +1,10 @@ 'use strict'; -const securityManager = require('./db/SecurityManager'); +import {checkAccess} from './db/SecurityManager'; // checks for padAccess -module.exports = async (req, res) => { - const {session: {user} = {}} = req; - const accessObj = await securityManager.checkAccess( +export default async (req, res) => { + const {session: {user} = {}}:SessionSocketModel = req; + const accessObj = await checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); if (accessObj.accessStatus === 'grant') { diff --git a/src/node/server.js b/src/node/server.ts old mode 100755 new mode 100644 similarity index 79% rename from src/node/server.js rename to src/node/server.ts index 6a494fe1730..3546e458423 --- a/src/node/server.js +++ b/src/node/server.ts @@ -24,10 +24,22 @@ * limitations under the License. */ -const log4js = require('log4js'); -log4js.replaceConsole(); - -const settings = require('./utils/Settings'); +import log4js from 'log4js'; +import * as settings from "./utils/Settings"; +/* + * early check for version compatibility before calling + * any modules that require newer versions of NodeJS + */ +import {checkDeprecationStatus, enforceMinNodeVersion} from './utils/NodeVersion' +import {Gate} from './utils/promises'; +import * as UpdateCheck from "./utils/UpdateCheck"; +import {Plugin} from "./models/Plugin"; +import {db} from './db/DB' +import {createServer, server} from './hooks/express'; +import {aCallAll} from '../static/js/pluginfw/hooks' +import * as pluginDefs from '../static/js/pluginfw/plugin_defs' +import * as plugins from "../static/js/pluginfw/plugins"; +import {createCollection} from "./stats"; let wtfnode; if (settings.dumpOnUncleanExit) { @@ -36,24 +48,11 @@ if (settings.dumpOnUncleanExit) { wtfnode = require('wtfnode'); } -/* - * early check for version compatibility before calling - * any modules that require newer versions of NodeJS - */ -const NodeVersion = require('./utils/NodeVersion'); -NodeVersion.enforceMinNodeVersion('12.17.0'); -NodeVersion.checkDeprecationStatus('12.17.0', '1.9.0'); - -const UpdateCheck = require('./utils/UpdateCheck'); -const db = require('./db/DB'); -const express = require('./hooks/express'); -const hooks = require('../static/js/pluginfw/hooks'); -const pluginDefs = require('../static/js/pluginfw/plugin_defs'); -const plugins = require('../static/js/pluginfw/plugins'); -const {Gate} = require('./utils/promises'); -const stats = require('./stats'); +enforceMinNodeVersion('12.17.0'); +checkDeprecationStatus('12.17.0', '1.9.0'); const logger = log4js.getLogger('server'); +console.log = logger.info.bind(logger); // do the same for others - console.debug, etc. const State = { INITIAL: 1, @@ -70,22 +69,22 @@ let state = State.INITIAL; const removeSignalListener = (signal, listener) => { logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` + - `Function code:\n${listener.toString()}\n` + - `Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`); + `Function code:\n${listener.toString()}\n` + + `Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`); process.off(signal, listener); }; let startDoneGate; -exports.start = async () => { +export const start = async () => { switch (state) { case State.INITIAL: break; case State.STARTING: await startDoneGate; // Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED. - return await exports.start(); + return await start(); case State.RUNNING: - return express.server; + return server; case State.STOPPING: case State.STOPPED: case State.EXITING: @@ -100,16 +99,16 @@ exports.start = async () => { state = State.STARTING; try { // Check if Etherpad version is up-to-date - UpdateCheck.check(); + UpdateCheck.default.check(); - stats.gauge('memoryUsage', () => process.memoryUsage().rss); - stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); + createCollection.gauge('memoryUsage', () => process.memoryUsage().rss); + createCollection.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); process.on('uncaughtException', (err) => { logger.debug(`uncaught exception: ${err.stack || err}`); // eslint-disable-next-line promise/no-promise-in-callback - exports.exit(err) + exit(err) .catch((err) => { logger.error('Error in process exit', err); // eslint-disable-next-line n/no-process-exit @@ -118,7 +117,8 @@ exports.start = async () => { }); // As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an // unhandled rejection into an uncaught exception, which does cause Node.js to exit. - process.on('unhandledRejection', (err) => { + process.on('unhandledRejection', (err:Error) => { + logger.debug(`unhandled rejection: ${err.stack || err}`); throw err; }); @@ -128,10 +128,10 @@ exports.start = async () => { // done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a // problematic listener. This means that exports.exit is solely responsible for performing all // necessary cleanup tasks. - for (const listener of process.listeners(signal)) { + for (const listener of process.listeners(signal as any)) { removeSignalListener(signal, listener); } - process.on(signal, exports.exit); + process.on(signal, exit); // Prevent signal listeners from being added in the future. process.on('newListener', (event, listener) => { if (event !== signal) return; @@ -141,37 +141,36 @@ exports.start = async () => { await db.init(); await plugins.update(); - const installedPlugins = Object.values(pluginDefs.plugins) + const installedPlugins = (Object.values(pluginDefs.plugins) as Plugin[]) .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') .map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .join(', '); logger.info(`Installed plugins: ${installedPlugins}`); logger.debug(`Installed parts:\n${plugins.formatParts()}`); logger.debug(`Installed server-side hooks:\n${plugins.formatHooks('hooks', false)}`); - await hooks.aCallAll('loadSettings', {settings}); - await hooks.aCallAll('createServer'); + await aCallAll('loadSettings', {settings}); + await aCallAll(createServer()); } catch (err) { logger.error('Error occurred while starting Etherpad'); state = State.STATE_TRANSITION_FAILED; startDoneGate.resolve(); - return await exports.exit(err); + return await exit(err); } logger.info('Etherpad is running'); state = State.RUNNING; startDoneGate.resolve(); - // Return the HTTP server to make it easier to write tests. - return express.server; + return server; }; const stopDoneGate = new Gate(); -exports.stop = async () => { +export const stop = async () => { switch (state) { case State.STARTING: - await exports.start(); + await start(); // Don't fall through to State.RUNNING in case another caller is also waiting for startup. - return await exports.stop(); + return await stop(); case State.RUNNING: break; case State.STOPPING: @@ -191,7 +190,7 @@ exports.stop = async () => { try { let timeout = null; await Promise.race([ - hooks.aCallAll('shutdown'), + aCallAll('shutdown'), new Promise((resolve, reject) => { timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000); }), @@ -200,24 +199,26 @@ exports.stop = async () => { } catch (err) { logger.error('Error occurred while stopping Etherpad'); state = State.STATE_TRANSITION_FAILED; + // @ts-ignore stopDoneGate.resolve(); - return await exports.exit(err); + return await exit(err); } logger.info('Etherpad stopped'); state = State.STOPPED; + // @ts-ignore stopDoneGate.resolve(); }; let exitGate; let exitCalled = false; -exports.exit = async (err = null) => { +export const exit = async (err = null) => { /* eslint-disable no-process-exit */ if (err === 'SIGTERM') { // Termination from SIGTERM is not treated as an abnormal termination. logger.info('Received SIGTERM signal'); err = null; } else if (err != null) { - logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`); + logger.error(`Metrics at time of fatal error:\n${JSON.stringify(createCollection.toJSON(), null, 2)}`); logger.error(err.stack || err.toString()); process.exitCode = 1; if (exitCalled) { @@ -231,11 +232,11 @@ exports.exit = async (err = null) => { case State.STARTING: case State.RUNNING: case State.STOPPING: - await exports.stop(); + await stop(); // Don't fall through to State.STOPPED in case another caller is also waiting for stop(). // Don't pass err to exports.exit() because this err has already been processed. (If err is // passed again to exit() then exit() will think that a second error occurred while exiting.) - return await exports.exit(); + return await exit(); case State.INITIAL: case State.STOPPED: case State.STATE_TRANSITION_FAILED: @@ -257,13 +258,13 @@ exports.exit = async (err = null) => { // on the timeout so that the timeout itself does not prevent Node.js from exiting. setTimeout(() => { logger.error('Something that should have been cleaned up during the shutdown hook (such as ' + - 'a timer, worker thread, or open connection) is preventing Node.js from exiting'); + 'a timer, worker thread, or open connection) is preventing Node.js from exiting'); if (settings.dumpOnUncleanExit) { wtfnode.dump(); } else { logger.error('Enable `dumpOnUncleanExit` setting to get a dump of objects preventing a ' + - 'clean exit'); + 'clean exit'); } logger.error('Forcing an unclean exit...'); @@ -272,7 +273,7 @@ exports.exit = async (err = null) => { logger.info('Waiting for Node.js to exit...'); state = State.WAITING_FOR_EXIT; - /* eslint-enable no-process-exit */ }; -if (require.main === module) exports.start(); +start() + .then(c=>logger.info("Server started")); diff --git a/src/node/stats.js b/src/node/stats.js deleted file mode 100644 index cecaca20d16..00000000000 --- a/src/node/stats.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const measured = require('measured-core'); - -module.exports = measured.createCollection(); - -module.exports.shutdown = async (hookName, context) => { - module.exports.end(); -}; diff --git a/src/node/stats.ts b/src/node/stats.ts new file mode 100644 index 00000000000..9d35395d55f --- /dev/null +++ b/src/node/stats.ts @@ -0,0 +1,9 @@ +'use strict'; + +import measured from 'measured-core' + +export const createCollection = measured.createCollection(); + +export const shutdown = async (hookName, context) => { + module.exports.end(); +} diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.ts similarity index 77% rename from src/node/utils/Abiword.js rename to src/node/utils/Abiword.ts index 1ed487ae14a..90cd88bdd53 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.ts @@ -19,20 +19,23 @@ * limitations under the License. */ -const spawn = require('child_process').spawn; -const async = require('async'); -const settings = require('./Settings'); -const os = require('os'); +import {spawn} from "child_process"; + +import async from 'async'; +import {abiword} from './Settings'; +import os from 'os'; + +export let convertFile; // on windows we have to spawn a process for each convertion, // cause the plugin abicommand doesn't exist on this platform if (os.type().indexOf('Windows') > -1) { - exports.convertFile = async (srcFile, destFile, type) => { - const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]); + convertFile = async (srcFile, destFile, type) => { + const abiword2 = spawn(abiword, [`--to=${destFile}`, srcFile]); let stdoutBuffer = ''; - abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); - abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - await new Promise((resolve, reject) => { + abiword2.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); + abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); + await new Promise((resolve, reject) => { abiword.on('exit', (code) => { if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`)); if (stdoutBuffer !== '') { @@ -46,14 +49,14 @@ if (os.type().indexOf('Windows') > -1) { // communicate with it via stdin/stdout // thats much faster, about factor 10 } else { - let abiword; + let abiword2; let stdoutCallback = null; const spawnAbiword = () => { - abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); + abiword2 = spawn(abiword, ['--plugin', 'AbiCommand']); let stdoutBuffer = ''; let firstPrompt = true; - abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - abiword.on('exit', (code) => { + abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); + abiword2.on('exit', (code) => { spawnAbiword(); if (stdoutCallback != null) { stdoutCallback(new Error(`Abiword died with exit code ${code}`)); @@ -84,7 +87,7 @@ if (os.type().indexOf('Windows') > -1) { }; }, 1); - exports.convertFile = async (srcFile, destFile, type) => { + convertFile = async (srcFile, destFile, type) => { await queue.pushAsync({srcFile, destFile, type}); }; } diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.ts similarity index 93% rename from src/node/utils/AbsolutePaths.js rename to src/node/utils/AbsolutePaths.ts index 73a96bb676d..e5514c6699e 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.ts @@ -19,9 +19,10 @@ * limitations under the License. */ -const log4js = require('log4js'); -const path = require('path'); -const _ = require('underscore'); +import log4js from 'log4js'; +import path from "path"; + +import _ from "underscore"; const absPathLogger = log4js.getLogger('AbsolutePaths'); @@ -75,7 +76,7 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => { * @return {string} The identified absolute base path. If such path cannot be * identified, prints a log and exits the application. */ -exports.findEtherpadRoot = () => { +export const findEtherpadRoot = () => { if (etherpadRoot != null) { return etherpadRoot; } @@ -90,7 +91,7 @@ exports.findEtherpadRoot = () => { * * \src */ - let maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['src']); + let maybeEtherpadRoot = popIfEndsWith(splitFoundRoot, ['dist']); if ((maybeEtherpadRoot === false) && (process.platform === 'win32')) { /* @@ -131,12 +132,12 @@ exports.findEtherpadRoot = () => { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = (somePath) => { +export const makeAbsolute = (somePath) => { if (path.isAbsolute(somePath)) { return somePath; } - const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath); + const rewrittenPath = path.join(findEtherpadRoot(), somePath); absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`); return rewrittenPath; @@ -150,7 +151,7 @@ exports.makeAbsolute = (somePath) => { * a subdirectory of the base one * @return {boolean} */ -exports.isSubdir = (parent, arbitraryDir) => { +export const isSubdir = (parent, arbitraryDir) => { // modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825 const relative = path.relative(parent, arbitraryDir); const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.ts similarity index 79% rename from src/node/utils/Cli.js rename to src/node/utils/Cli.ts index a5cdee83af5..260478cce10 100644 --- a/src/node/utils/Cli.js +++ b/src/node/utils/Cli.ts @@ -21,33 +21,35 @@ */ // An object containing the parsed command-line options -exports.argv = {}; +import {CMDArgv} from "../models/CMDArgv"; -const argv = process.argv.slice(2); -let arg, prevArg; +const argvcmd = process.argv.slice(2); +let arg, prevArg + +export const argv:CMDArgv|undefined = {}; // Loop through args -for (let i = 0; i < argv.length; i++) { +for (let i = 0; i < argvcmd.length; i++) { arg = argv[i]; // Override location of settings.json file if (prevArg === '--settings' || prevArg === '-s') { - exports.argv.settings = arg; + argv.settings = arg; } // Override location of credentials.json file if (prevArg === '--credentials') { - exports.argv.credentials = arg; + argv.credentials = arg; } // Override location of settings.json file if (prevArg === '--sessionkey') { - exports.argv.sessionkey = arg; + argv.sessionkey = arg; } // Override location of settings.json file if (prevArg === '--apikey') { - exports.argv.apikey = arg; + argv.apikey = arg; } prevArg = arg; diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.ts similarity index 81% rename from src/node/utils/ExportEtherpad.js rename to src/node/utils/ExportEtherpad.ts index e20739ad3ef..c20741840bd 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.ts @@ -14,18 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import {Stream} from "./Stream"; -const Stream = require('./Stream'); -const assert = require('assert').strict; -const authorManager = require('../db/AuthorManager'); -const hooks = require('../../static/js/pluginfw/hooks'); -const padManager = require('../db/PadManager'); +import {strict as assert} from "assert"; -exports.getPadRaw = async (padId, readOnlyId) => { +import {getAuthor} from "../db/AuthorManager"; + +import {aCallAll} from "../../static/js/pluginfw/hooks"; + +import {getPad} from "../db/PadManager"; + +export const getPadRaw = async (padId, readOnlyId) => { const dstPfx = `pad:${readOnlyId || padId}`; const [pad, customPrefixes] = await Promise.all([ - padManager.getPad(padId), - hooks.aCallAll('exportEtherpadAdditionalContent'), + getPad(padId), + aCallAll('exportEtherpadAdditionalContent'), ]); const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => { const srcPfx = `${customPrefix}:${padId}`; @@ -43,7 +46,7 @@ exports.getPadRaw = async (padId, readOnlyId) => { const records = (function* () { for (const authorId of pad.getAllAuthors()) { yield [`globalAuthor:${authorId}`, (async () => { - const authorEntry = await authorManager.getAuthor(authorId); + const authorEntry = await getAuthor(authorId); if (!authorEntry) return undefined; // Becomes unset when converted to JSON. if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId; return authorEntry; @@ -55,7 +58,7 @@ exports.getPadRaw = async (padId, readOnlyId) => { })(); const data = {[dstPfx]: pad}; for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p; - await hooks.aCallAll('exportEtherpad', { + await aCallAll('exportEtherpad', { pad, data, dstPadId: readOnlyId || padId, diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.ts similarity index 77% rename from src/node/utils/ExportHelper.js rename to src/node/utils/ExportHelper.ts index 7962476e8a1..814ceef335d 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.ts @@ -19,14 +19,13 @@ * limitations under the License. */ -const AttributeMap = require('../../static/js/AttributeMap'); -const Changeset = require('../../static/js/Changeset'); +import AttributeMap from '../../static/js/AttributeMap'; +import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset'; -exports.getPadPlainText = (pad, revNum) => { - const _analyzeLine = exports._analyzeLine; +export const getPadPlainText = (pad, revNum) => { const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); const textLines = atext.text.slice(0, -1).split('\n'); - const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const attribLines = splitAttributionLines(atext.attribs, atext.text); const apool = pad.pool; const pieces = []; @@ -45,14 +44,20 @@ exports.getPadPlainText = (pad, revNum) => { }; -exports._analyzeLine = (text, aline, apool) => { - const line = {}; +export const _analyzeLine = (text, aline, apool) => { + const line = { + listLevel: undefined, + text: undefined, + listTypeName: undefined, + aline: undefined, + start: undefined + }; // identify list let lineMarker = 0; line.listLevel = 0; if (aline) { - const [op] = Changeset.deserializeOps(aline); + const [op] = deserializeOps(aline); if (op != null) { const attribs = AttributeMap.fromString(op.attribs, apool); let listType = attribs.get('list'); @@ -72,7 +77,7 @@ exports._analyzeLine = (text, aline, apool) => { } if (lineMarker) { line.text = text.substring(1); - line.aline = Changeset.subattribution(aline, 1); + line.aline = subattribution(aline, 1); } else { line.text = text; line.aline = aline; @@ -81,5 +86,5 @@ exports._analyzeLine = (text, aline, apool) => { }; -exports._encodeWhitespace = +export const _encodeWhitespace = (s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.ts similarity index 89% rename from src/node/utils/ExportHtml.js rename to src/node/utils/ExportHtml.ts index d14f40e6e7b..5de740f5516 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.ts @@ -15,16 +15,27 @@ * limitations under the License. */ -const Changeset = require('../../static/js/Changeset'); -const attributes = require('../../static/js/attributes'); -const padManager = require('../db/PadManager'); -const _ = require('underscore'); -const Security = require('../../static/js/security'); -const hooks = require('../../static/js/pluginfw/hooks'); -const eejs = require('../eejs'); -const _analyzeLine = require('./ExportHelper')._analyzeLine; -const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; -const padutils = require('../../static/js/pad_utils').padutils; +import { + deserializeOps, + splitAttributionLines, + stringAssembler, + stringIterator, + subattribution +} from '../../static/js/Changeset'; +import {decodeAttribString} from "../../static/js/attributes"; + +import {getPad} from "../db/PadManager"; + +import _ from "underscore"; + +// FIXME this is a hack to get around the fact that we don't have a good way +// @ts-ignore +import {escapeHTML,escapeHTMLAttribute} from '../../static/js/security'; +import {aCallAll} from '../../static/js/pluginfw/hooks'; +import {required} from '../eejs'; +import {_analyzeLine, _encodeWhitespace} from "./ExportHelper"; + +import {padutils} from "../../static/js/pad_utils"; const getPadHTML = async (pad, revNum) => { let atext = pad.atext; @@ -38,17 +49,17 @@ const getPadHTML = async (pad, revNum) => { return await getHTMLFromAtext(pad, atext); }; -const getHTMLFromAtext = async (pad, atext, authorColors) => { +export const getHTMLFromAtext = async (pad, atext, authorColors?) => { const apool = pad.apool(); const textLines = atext.text.slice(0, -1).split('\n'); - const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const attribLines = splitAttributionLines(atext.attribs, atext.text); const tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; await Promise.all([ // prepare tags stored as ['tag', true] to be exported - hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => { + aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => { newProps.forEach((prop) => { tags.push(prop); props.push(prop); @@ -56,7 +67,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { }), // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags // like - hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => { + aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => { newProps.forEach((prop) => { tags.push(`span data-${prop[0]}="${prop[1]}"`); props.push(prop); @@ -124,8 +135,8 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { // Just bold Bold and italics Just italics // becomes // Just bold Bold and italics Just italics - const taker = Changeset.stringIterator(text); - const assem = Changeset.stringAssembler(); + const taker = stringIterator(text); + const assem = stringAssembler(); const openTags = []; const getSpanClassFor = (i) => { @@ -197,7 +208,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { return; } - const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars)); + const ops = deserializeOps(subattribution(attribs, idx, idx + numChars)); idx += numChars; // this iterates over every op string and decides which tags to open or to close @@ -206,7 +217,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { const usedAttribs = []; // mark all attribs as used - for (const a of attributes.decodeAttribString(o.attribs)) { + for (const a of decodeAttribString(o.attribs)) { if (a in anumMap) { usedAttribs.push(anumMap[a]); // i = 0 => bold, etc. } @@ -246,7 +257,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { // from but they break the abiword parser and are completly useless s = s.replace(String.fromCharCode(12), ''); - assem.append(_encodeWhitespace(Security.escapeHTML(s))); + assem.append(_encodeWhitespace(escapeHTML(s))); } // end iteration over spans in line // close all the tags that are open after the last op @@ -269,7 +280,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener // https://mathiasbynens.github.io/rel-noopener/ // https://github.com/ether/etherpad-lite/pull/3636 - assem.append(``); + assem.append(``); processNextChars(urlLength); assem.append(''); }); @@ -311,7 +322,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { if (i < textLines.length) { nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool); } - await hooks.aCallAll('getLineHTMLForExport', context); + await aCallAll('getLineHTMLForExport', context); // To create list parent elements if ((!prevLine || prevLine.listLevel !== line.listLevel) || (line.listTypeName !== prevLine.listTypeName)) { @@ -448,7 +459,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { padId: pad.id, }; - await hooks.aCallAll('getLineHTMLForExport', context); + await aCallAll('getLineHTMLForExport', context); pieces.push(context.lineContent, '
'); } } @@ -456,25 +467,25 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { return pieces.join(''); }; -exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => { - const pad = await padManager.getPad(padId); +export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => { + const pad = await getPad(padId); // Include some Styles into the Head for Export let stylesForExportCSS = ''; - const stylesForExport = await hooks.aCallAll('stylesForExport', padId); + const stylesForExport = await aCallAll('stylesForExport', padId); stylesForExport.forEach((css) => { stylesForExportCSS += css; }); let html = await getPadHTML(pad, revNum); - for (const hookHtml of await hooks.aCallAll('exportHTMLAdditionalContent', {padId})) { + for (const hookHtml of await aCallAll('exportHTMLAdditionalContent', {padId})) { html += hookHtml; } - return eejs.require('ep_etherpad-lite/templates/export_html.html', { + return required('ep_etherpad-lite/templates/export_html.html', { body: html, - padId: Security.escapeHTML(readOnlyId || padId), + padId: escapeHTML(readOnlyId || padId), extraCSS: stylesForExportCSS, }); }; @@ -525,7 +536,4 @@ const _processSpaces = (s) => { } } return parts.join(''); -}; - -exports.getPadHTML = getPadHTML; -exports.getHTMLFromAtext = getHTMLFromAtext; +} diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.ts similarity index 97% rename from src/node/utils/ExportTxt.js rename to src/node/utils/ExportTxt.ts index 9511dd0e7a3..7915725fa6d 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.ts @@ -39,7 +39,7 @@ const getPadTXT = async (pad, revNum) => { // This is different than the functionality provided in ExportHtml as it provides formatting // functionality that is designed specifically for TXT exports -const getTXTFromAtext = (pad, atext, authorColors) => { +export const getTXTFromAtext = (pad, atext, authorColors?) => { const apool = pad.apool(); const textLines = atext.text.slice(0, -1).split('\n'); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); @@ -56,7 +56,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { }); const getLineTXT = (text, attribs) => { - const propVals = [false, false, false]; + const propVals:(boolean|number)[] = [false, false, false]; const ENTER = 1; const STAY = 2; const LEAVE = 0; @@ -256,9 +256,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => { return pieces.join(''); }; -exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = async (padId, revNum) => { +export const getPadTXTDocument = async (padId, revNum) => { const pad = await padManager.getPad(padId); return getPadTXT(pad, revNum); }; diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.ts similarity index 82% rename from src/node/utils/ImportEtherpad.js rename to src/node/utils/ImportEtherpad.ts index da7e750ffb5..9258159915d 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.ts @@ -16,30 +16,32 @@ * limitations under the License. */ -const AttributePool = require('../../static/js/AttributePool'); -const {Pad} = require('../db/Pad'); -const Stream = require('./Stream'); -const authorManager = require('../db/AuthorManager'); -const db = require('../db/DB'); -const hooks = require('../../static/js/pluginfw/hooks'); -const log4js = require('log4js'); -const supportedElems = require('../../static/js/contentcollector').supportedElems; -const ueberdb = require('ueberdb2'); +import {AttributePool} from '../../static/js/AttributePool'; +import {Pad} from '../db/Pad'; +import {Stream} from './Stream'; +import {addPad, doesAuthorExist} from '../db/AuthorManager'; +import {db} from '../db/DB'; +import {aCallAll, callAll} from '../../static/js/pluginfw/hooks'; +import log4js from "log4js"; + +import {supportedElems} from "../../static/js/contentcollector"; + +import ueberdb from 'ueberdb2'; const logger = log4js.getLogger('ImportEtherpad'); -exports.setPadRaw = async (padId, r, authorId = '') => { +export const setPadRaw = async (padId, r, authorId = '') => { const records = JSON.parse(r); // get supported block Elements from plugins, we will use this later. - hooks.callAll('ccRegisterBlockElements').forEach((element) => { + callAll('ccRegisterBlockElements').forEach((element) => { supportedElems.add(element); }); // DB key prefixes for pad records. Each key is expected to have the form `${prefix}:${padId}` or // `${prefix}:${padId}:${otherstuff}`. const padKeyPrefixes = [ - ...await hooks.aCallAll('exportEtherpadAdditionalContent'), + ...await aCallAll('exportEtherpadAdditionalContent'), 'pad', ]; @@ -69,7 +71,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => { throw new TypeError('globalAuthor padIDs subkey is not a string'); } checkOriginalPadId(value.padIDs); - if (await authorManager.doesAuthorExist(id)) { + if (await doesAuthorExist(id)) { existingAuthors.add(id); return; } @@ -101,7 +103,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => { const pad = new Pad(padId, padDb); await pad.init(null, authorId); - await hooks.aCallAll('importEtherpad', { + await aCallAll('importEtherpad', { pad, // Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but // it's not worth the added complexity. @@ -115,7 +117,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => { const writeOps = (function* () { for (const [k, v] of data) yield db.set(k, v); - for (const a of existingAuthors) yield authorManager.addPad(a, padId); + for (const a of existingAuthors) yield addPad(a as string, padId); })(); for (const op of new Stream(writeOps).batch(100).buffer(99)) await op; }; diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.ts similarity index 83% rename from src/node/utils/ImportHtml.js rename to src/node/utils/ImportHtml.ts index d7b2172b057..34dfb05e42e 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.ts @@ -15,15 +15,15 @@ * limitations under the License. */ -const log4js = require('log4js'); -const Changeset = require('../../static/js/Changeset'); -const contentcollector = require('../../static/js/contentcollector'); -const jsdom = require('jsdom'); +import log4js from 'log4js'; +import {builder, deserializeOps} from '../../static/js/Changeset'; +import {makeContentCollector} from '../../static/js/contentcollector'; +import jsdom from 'jsdom'; const apiLogger = log4js.getLogger('ImportHtml'); let processor; -exports.setPadHTML = async (pad, html, authorId = '') => { +export const setPadHTML = async (pad, html, authorId = '') => { if (processor == null) { const [{rehype}, {default: minifyWhitespace}] = await Promise.all([import('rehype'), import('rehype-minify-whitespace')]); @@ -42,7 +42,7 @@ exports.setPadHTML = async (pad, html, authorId = '') => { // Convert a dom tree into a list of lines and attribute liens // using the content collector object - const cc = contentcollector.makeContentCollector(true, null, pad.pool); + const cc = makeContentCollector(true, null, pad.pool); try { // we use a try here because if the HTML is bad it will blow up cc.collectContent(document.body); @@ -68,26 +68,26 @@ exports.setPadHTML = async (pad, html, authorId = '') => { const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`; // create a new changeset with a helper builder object - const builder = Changeset.builder(1); + const builder2 = builder(1); // assemble each line into the builder let textIndex = 0; const newTextStart = 0; const newTextEnd = newText.length; - for (const op of Changeset.deserializeOps(newAttribs)) { + for (const op of deserializeOps(newAttribs)) { 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); + builder2.insert(newText.substring(start, end), op.attribs); } textIndex = nextIndex; } // the changeset is ready! - const theChangeset = builder.toString(); + const theChangeset = builder2.toString(); apiLogger.debug(`The changeset: ${theChangeset}`); await pad.setText('\n', authorId); await pad.appendRevision(theChangeset, authorId); -}; +} diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.ts similarity index 91% rename from src/node/utils/LibreOffice.js rename to src/node/utils/LibreOffice.ts index 33920919461..26f818d30ea 100644 --- a/src/node/utils/LibreOffice.js +++ b/src/node/utils/LibreOffice.ts @@ -17,20 +17,23 @@ * limitations under the License. */ -const async = require('async'); -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'); +import async from 'async'; +import log4js from 'log4js'; +import {promises as fs} from "fs"; + +import os from 'os'; +import path from "path"; + +import exportCMD from "./run_cmd"; + +import {soffice} from "./Settings"; const logger = log4js.getLogger('LibreOffice'); const doConvertTask = async (task) => { const tmpDir = os.tmpdir(); - const p = runCmd([ - settings.soffice, + const p = await exportCMD([ + soffice, '--headless', '--invisible', '--nologo', @@ -81,7 +84,7 @@ const queue = async.queue(doConvertTask, 1); * @param {String} type The type to convert into * @param {Function} callback Standard callback function */ -exports.convertFile = async (srcFile, destFile, type) => { +export const convertFile = async (srcFile, destFile, type) => { // Used for the moving of the file, not the conversion const fileExtension = type; diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.ts similarity index 90% rename from src/node/utils/Minify.js rename to src/node/utils/Minify.ts index 2e8a2d960fc..830bd9c1baf 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.ts @@ -21,21 +21,28 @@ * limitations under the License. */ -const settings = require('./Settings'); -const fs = require('fs').promises; -const path = require('path'); -const plugins = require('../../static/js/pluginfw/plugin_defs'); -const RequireKernel = require('etherpad-require-kernel'); -const mime = require('mime-types'); -const Threads = require('threads'); -const log4js = require('log4js'); +import {maxAge, root} from './Settings'; +import {promises as fs} from "fs"; + +import path from "path"; + +import {plugins} from "../../static/js/pluginfw/plugin_defs"; + +import RequireKernel from "etherpad-require-kernel"; + +import mime from "mime-types"; + +import Threads from "threads"; + +import log4js from "log4js"; + const sanitizePathname = require('./sanitizePathname'); const logger = log4js.getLogger('Minify'); -const ROOT_DIR = path.join(settings.root, 'src/static/'); +const ROOT_DIR = path.join(root, 'src/static/'); -const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); +const threadsPool = Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); const LIBRARY_WHITELIST = [ 'async', @@ -89,7 +96,7 @@ const requestURI = async (url, method, headers) => { return await p; }; -const requestURIs = (locations, method, headers, callback) => { +export const requestURIs = (locations, method, headers, callback) => { Promise.all(locations.map(async (loc) => { try { return await requestURI(loc, method, headers); @@ -148,8 +155,8 @@ const minify = async (req, res) => { const library = match[1]; const libraryPath = match[2] || ''; - if (plugins.plugins[library] && match[3]) { - const plugin = plugins.plugins[library]; + if (plugins[library] && match[3]) { + const plugin = plugins[library]; const pluginPath = plugin.package.realPath; filename = path.join(pluginPath, libraryPath); // On Windows, path.relative converts forward slashes to backslashes. Convert them back @@ -176,10 +183,10 @@ const minify = async (req, res) => { date.setMilliseconds(0); res.setHeader('last-modified', date.toUTCString()); res.setHeader('date', (new Date()).toUTCString()); - if (settings.maxAge !== undefined) { - const expiresDate = new Date(Date.now() + settings.maxAge * 1000); + if (maxAge !== undefined) { + const expiresDate = new Date(Date.now() + maxAge * 1000); res.setHeader('expires', expiresDate.toUTCString()); - res.setHeader('cache-control', `max-age=${settings.maxAge}`); + res.setHeader('cache-control', `max-age=${maxAge}`); } } @@ -271,7 +278,7 @@ const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n` const getFileCompressed = async (filename, contentType) => { let content = await getFile(filename); - if (!content || !settings.minify) { + if (!content || !minify) { return content; } else if (contentType === 'application/javascript') { return await new Promise((resolve) => { @@ -317,10 +324,8 @@ const getFile = async (filename) => { return await fs.readFile(path.resolve(ROOT_DIR, filename)); }; -exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err))); - -exports.requestURIs = requestURIs; +export const minifyExp = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err))); -exports.shutdown = async (hookName, context) => { +export const shutdown = async (hookName, context) => { await threadsPool.terminate(); }; diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.ts similarity index 80% rename from src/node/utils/MinifyWorker.js rename to src/node/utils/MinifyWorker.ts index 364ecc96cca..f5307cc9a9d 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.ts @@ -3,11 +3,14 @@ * Worker thread to minify JS & CSS files out of the main NodeJS thread */ -const CleanCSS = require('clean-css'); -const Terser = require('terser'); -const fsp = require('fs').promises; -const path = require('path'); -const Threads = require('threads'); +import CleanCSS from 'clean-css'; +import Terser from "terser"; + +import {promises as fsp} from "fs"; + +import path from "path"; + +import Threads from "threads"; const compressJS = (content) => Terser.minify(content); diff --git a/src/node/utils/NodeVersion.js b/src/node/utils/NodeVersion.ts similarity index 84% rename from src/node/utils/NodeVersion.js rename to src/node/utils/NodeVersion.ts index ca5412f23c6..27de68d9b01 100644 --- a/src/node/utils/NodeVersion.js +++ b/src/node/utils/NodeVersion.ts @@ -26,19 +26,19 @@ const semver = require('semver'); * * @param {String} minNodeVersion Minimum required Node version */ -exports.enforceMinNodeVersion = (minNodeVersion) => { +const enforceMinNodeVersion = (minNodeVersion:string) => { const currentNodeVersion = process.version; // we cannot use template literals, since we still do not know if we are // running under Node >= 4.0 if (semver.lt(currentNodeVersion, minNodeVersion)) { console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. ` + - `Please upgrade at least to Node ${minNodeVersion}`); + `Please upgrade at least to Node ${minNodeVersion}`); process.exit(1); } console.debug(`Running on Node ${currentNodeVersion} ` + - `(minimum required Node version: ${minNodeVersion})`); + `(minimum required Node version: ${minNodeVersion})`); }; /** @@ -49,7 +49,7 @@ exports.enforceMinNodeVersion = (minNodeVersion) => { * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated * Node releases */ -exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => { +const checkDeprecationStatus = (lowestNonDeprecatedNodeVersion:string, epRemovalVersion:string) => { const currentNodeVersion = process.version; if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { @@ -58,3 +58,5 @@ exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersi `Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`); } }; + +export {checkDeprecationStatus, enforceMinNodeVersion} diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.ts similarity index 72% rename from src/node/utils/Settings.js rename to src/node/utils/Settings.ts index 51f48237aa8..e785160143a 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.ts @@ -27,18 +27,24 @@ * limitations under the License. */ -const absolutePaths = require('./AbsolutePaths'); -const deepEqual = require('fast-deep-equal/es6'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const argv = require('./Cli').argv; -const jsonminify = require('jsonminify'); -const log4js = require('log4js'); -const randomString = require('./randomstring'); +import exp from "constants"; +// FIXME Is there a better way to enter dynamic package.json path +// @ts-ignore +import packageJSON from '../../package.json' +import {findEtherpadRoot, makeAbsolute, isSubdir} from './AbsolutePaths'; +import deepEqual from 'fast-deep-equal/es6'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import jsonminify from 'jsonminify'; +import log4js from 'log4js'; +import {LogLevel} from "../models/LogLevel"; +import {argv} from "./Cli"; + +import {randomString} from './randomstring'; const suppressDisableMsg = ' -- To suppress these warning messages change ' + 'suppressErrorsInPadText to true in your settings.json\n'; -const _ = require('underscore'); +import _ from 'underscore'; const logger = log4js.getLogger('settings'); @@ -50,15 +56,18 @@ const nonSettings = [ // 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 defaultLogConfig = () => ({appenders: { console: { type: 'console' } }, +categories:{ + default: { appenders: ['console'], level: 'info'} +}}); 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(); + log4js.getLogger("console"); + console.log = logger.info.bind(logger) // Log the warning after configuring log4js to increase the chances the user will see it. if (!logConfigIsDefault) logger.warn('The logconfig setting is deprecated.'); }; @@ -68,16 +77,16 @@ const initLogging = (logLevel, config) => { initLogging(defaultLogLevel, defaultLogConfig()); /* Root path of the installation */ -exports.root = absolutePaths.findEtherpadRoot(); +export const root = 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'); + `Etherpad base dir: ${root}`); +export const settingsFilename = makeAbsolute(argv.settings || 'settings.json'); +export const credentialsFilename = makeAbsolute(argv.credentials || 'credentials.json'); /** * The app title, visible e.g. in the browser window */ -exports.title = 'Etherpad'; +export const title = 'Etherpad'; /** * Pathname of the favicon you want to use. If null, the skin's favicon is @@ -85,7 +94,7 @@ exports.title = 'Etherpad'; * is used. If this is a relative path it is interpreted as relative to the * Etherpad root directory. */ -exports.favicon = null; +export const favicon = null; /* * Skin name. @@ -93,37 +102,43 @@ exports.favicon = null; * Initialized to null, so we can spot an old configuration file and invite the * user to update it before falling back to the default. */ -exports.skinName = null; +export let skinName = null; -exports.skinVariants = 'super-light-toolbar super-light-editor light-background'; +export const skinVariants = 'super-light-toolbar super-light-editor light-background'; /** * The IP ep-lite should listen to */ -exports.ip = '0.0.0.0'; +export let ip:String = '0.0.0.0'; /** * The Port ep-lite should listen to */ -exports.port = process.env.PORT || 9001; +export let port = process.env.PORT || 9001; /** * Should we suppress Error messages from being in Pad Contents */ -exports.suppressErrorsInPadText = false; +export const suppressErrorsInPadText = false; /** * The SSL signed server key and the Certificate Authority's own certificate * default case: ep-lite does *not* use SSL. A signed server key is not required in this case. */ -exports.ssl = false; +export let ssl = false; + +export const sslKeys = { + cert: undefined, + key: undefined, + ca: undefined, +} /** * socket.io transport methods **/ -exports.socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile']; +export const socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile']; -exports.socketIo = { +export const socketIo = { /** * Maximum permitted client message size (in bytes). * @@ -138,20 +153,20 @@ exports.socketIo = { /* * The Type of the database */ -exports.dbType = 'dirty'; +export const dbType = 'dirty'; /** * This setting is passed with dbType to ueberDB to set up the database */ -exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')}; +export const dbSettings = {filename: path.join(root, 'var/dirty.db')}; /** * The default Text of a new pad */ -exports.defaultPadText = [ +export let defaultPadText = [ 'Welcome to Etherpad!', '', 'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' + - 'text. This allows you to collaborate seamlessly on documents!', + 'text. This allows you to collaborate seamlessly on documents!', '', 'Etherpad on Github: https://github.com/ether/etherpad-lite', ].join('\n'); @@ -159,7 +174,7 @@ exports.defaultPadText = [ /** * The default Pad Settings for a user (Can be overridden by changing the setting */ -exports.padOptions = { +export const padOptions = { noColors: false, showControls: true, showChat: true, @@ -176,7 +191,7 @@ exports.padOptions = { /** * Whether certain shortcut keys are enabled for a user in the pad */ -exports.padShortcutEnabled = { +export const padShortcutEnabled = { altF9: true, altC: true, delete: true, @@ -204,7 +219,7 @@ exports.padShortcutEnabled = { /** * The toolbar buttons and order. */ -exports.toolbar = { +export const toolbar = { left: [ ['bold', 'italic', 'underline', 'strikethrough'], ['orderedlist', 'unorderedlist', 'indent', 'outdent'], @@ -224,92 +239,92 @@ exports.toolbar = { /** * A flag that requires any user to have a valid session (via the api) before accessing a pad */ -exports.requireSession = false; +export const requireSession = false; /** * A flag that prevents users from creating new pads */ -exports.editOnly = false; +export const editOnly = false; /** * Max age that responses will have (affects caching layer). */ -exports.maxAge = 1000 * 60 * 60 * 6; // 6 hours +export const maxAge = 1000 * 60 * 60 * 6; // 6 hours /** * A flag that shows if minification is enabled or not */ -exports.minify = true; +export const minify = true; /** * The path of the abiword executable */ -exports.abiword = null; +export let abiword = null; /** * The path of the libreoffice executable */ -exports.soffice = null; +export let soffice = null; /** * The path of the tidy executable */ -exports.tidyHtml = null; +export const tidyHtml = null; /** * Should we support none natively supported file types on import? */ -exports.allowUnknownFileEnds = true; +export const allowUnknownFileEnds = true; /** * The log level of log4js */ -exports.loglevel = defaultLogLevel; +export const loglevel:LogLevel = defaultLogLevel; /** * Disable IP logging */ -exports.disableIPlogging = false; +export const disableIPlogging = false; /** * Number of seconds to automatically reconnect pad */ -exports.automaticReconnectionTimeout = 0; +export const automaticReconnectionTimeout = 0; /** * Disable Load Testing */ -exports.loadTest = false; +export const loadTest = false; /** * Disable dump of objects preventing a clean exit */ -exports.dumpOnUncleanExit = false; +export const dumpOnUncleanExit = false; /** * Enable indentation on new lines */ -exports.indentationOnNewLine = true; +export const indentationOnNewLine = true; /* * log4js appender configuration */ -exports.logconfig = defaultLogConfig(); +export const logconfig = defaultLogConfig(); /* * Session Key, do not sure this. */ -exports.sessionKey = false; +export let sessionKey: string|boolean = false; /* * Trust Proxy, whether or not trust the x-forwarded-for header. */ -exports.trustProxy = false; +export let trustProxy = false; /* * Settings controlling the session cookie issued by Etherpad. */ -exports.cookie = { +export const cookie = { /* * Value of the SameSite cookie property. "Lax" is recommended unless * Etherpad will be embedded in an iframe from another site, in which case @@ -331,20 +346,20 @@ exports.cookie = { * authorization. Note: /admin always requires authentication, and * either authorization by a module, or a user with is_admin set */ -exports.requireAuthentication = false; -exports.requireAuthorization = false; -exports.users = {}; +export const requireAuthentication = false; +export const requireAuthorization = false; +export let users = {}; /* * Show settings in admin page, by default it is true */ -exports.showSettingsInAdminPage = true; +export const showSettingsInAdminPage = true; /* * By default, when caret is moved out of viewport, it scrolls the minimum * height needed to make this line visible. */ -exports.scrollWhenFocusLineIsOutOfViewport = { +export const scrollWhenFocusLineIsOutOfViewport = { /* * Percentage of viewport height to be additionally scrolled. */ @@ -377,12 +392,16 @@ exports.scrollWhenFocusLineIsOutOfViewport = { * * Do not enable on production machines. */ -exports.exposeVersion = false; +export const exposeVersion = false; /* * Override any strings found in locale directories */ -exports.customLocaleStrings = {}; +export const customLocaleStrings = {}; + +export const setUsers = (newUsers:any) => { + users = newUsers; +} /* * From Etherpad 1.8.3 onwards, import and export of pads is always rate @@ -393,12 +412,13 @@ exports.customLocaleStrings = {}; * * See https://github.com/nfriedly/express-rate-limit for more options */ -exports.importExportRateLimiting = { +export let importExportRateLimiting = { // duration of the rate limit window (milliseconds) windowMs: 90000, // maximum number of requests per IP to allow during the rate limit window - max: 10, + max: 10, onLimitReached: undefined + }; /* @@ -409,7 +429,7 @@ exports.importExportRateLimiting = { * * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options */ -exports.commitRateLimiting = { +export let commitRateLimiting = { // duration of the rate limit window (seconds) duration: 1, @@ -423,34 +443,34 @@ exports.commitRateLimiting = { * * File size is specified in bytes. Default is 50 MB. */ -exports.importMaxFileSize = 50 * 1024 * 1024; +export const importMaxFileSize = 50 * 1024 * 1024; /* * Disable Admin UI tests */ -exports.enableAdminUITests = false; +export const enableAdminUITests = false; // checks if abiword is avaiable -exports.abiwordAvailable = () => { - if (exports.abiword != null) { +export const abiwordAvailable = () => { + if (abiword != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; } }; -exports.sofficeAvailable = () => { - if (exports.soffice != null) { +export const sofficeAvailable = () => { + if (soffice != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; } }; -exports.exportAvailable = () => { - const abiword = exports.abiwordAvailable(); - const soffice = exports.sofficeAvailable(); +export const exportAvailable = () => { + const abiword = abiwordAvailable(); + const soffice = sofficeAvailable(); if (abiword === 'no' && soffice === 'no') { return 'no'; @@ -463,10 +483,10 @@ exports.exportAvailable = () => { }; // Provide git version if available -exports.getGitCommit = () => { +export const getGitCommit = () => { let version = ''; try { - let rootPath = exports.root; + let rootPath = root; if (fs.lstatSync(`${rootPath}/.git`).isFile()) { rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); rootPath = rootPath.split(' ').pop().trim(); @@ -482,13 +502,14 @@ exports.getGitCommit = () => { } version = version.substring(0, 7); } catch (e) { - logger.warn(`Can't get git version for server header\n${e.message}`); + const errorCast = e as Error + logger.warn(`Can't get git version for server header\n${errorCast.message}`); } return version; }; // Return etherpad version from package.json -exports.getEpVersion = () => require('../../package.json').version; +export const getEpVersion = () => packageJSON.version; /** * Receives a settingsObj and, if the property name is a valid configuration @@ -497,7 +518,8 @@ exports.getEpVersion = () => require('../../package.json').version; * This code refactors a previous version that copied & pasted the same code for * both "settings.json" and "credentials.json". */ -const storeSettings = (settingsObj) => { +//FIXME find out what settingsObj is +const storeSettings = (settingsObj: any) => { for (const i of Object.keys(settingsObj || {})) { if (nonSettings.includes(i)) { logger.warn(`Ignoring setting: '${i}'`); @@ -536,9 +558,10 @@ const storeSettings = (settingsObj) => { * short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result * in the literal string "null", instead. */ -const coerceValue = (stringValue) => { +const coerceValue = (stringValue: string) => { // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number - const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue)); + const numberToEvaluate = Number(stringValue) + const isNumeric = !isNaN(numberToEvaluate) && !isNaN(parseFloat(stringValue)) && isFinite(numberToEvaluate); if (isNumeric) { // detected numeric string. Coerce to a number @@ -591,7 +614,7 @@ const coerceValue = (stringValue) => { * * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter */ -const lookupEnvironmentVariables = (obj) => { +const lookupEnvironmentVariables = (obj: any) => { const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => { /* * the first invocation of replacer() is with an empty key. Just go on, or @@ -636,9 +659,9 @@ const lookupEnvironmentVariables = (obj) => { if ((envVarValue === undefined) && (defaultValue === undefined)) { 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.'); + `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 @@ -649,9 +672,12 @@ const lookupEnvironmentVariables = (obj) => { if ((envVarValue === undefined) && (defaultValue !== undefined)) { logger.debug(`Environment variable "${envVarName}" not found for ` + - `configuration key "${key}". Falling back to default value.`); + `configuration key "${key}". Falling back to default value.`); return coerceValue(defaultValue); + } else if ((envVarValue === undefined)) + { + return coerceValue("none") } // envVarName contained some value. @@ -666,9 +692,7 @@ const lookupEnvironmentVariables = (obj) => { return coerceValue(envVarValue); }); - const newSettings = JSON.parse(stringifiedAndReplaced); - - return newSettings; + return JSON.parse(stringifiedAndReplaced); }; /** @@ -679,7 +703,7 @@ const lookupEnvironmentVariables = (obj) => { * * The isSettings variable only controls the error logging. */ -const parseSettings = (settingsFilename, isSettings) => { +const parseSettings = (settingsFilename: string, isSettings:boolean) => { let settingsStr = ''; let settingsType, notFoundMessage, notFoundFunction; @@ -711,127 +735,129 @@ const parseSettings = (settingsFilename, isSettings) => { logger.info(`${settingsType} loaded from: ${settingsFilename}`); - const replacedSettings = lookupEnvironmentVariables(settings); - - return replacedSettings; + return lookupEnvironmentVariables(settings); } catch (e) { + const error = e as Error logger.error(`There was an error processing your ${settingsType} ` + - `file from ${settingsFilename}: ${e.message}`); + `file from ${settingsFilename}: ${error.message}`); process.exit(1); } -}; +} + -exports.reloadSettings = () => { - const settings = parseSettings(exports.settingsFilename, true); - const credentials = parseSettings(exports.credentialsFilename, false); +export const reloadSettings = () => { + const settings = parseSettings(settingsFilename, true); + const credentials = parseSettings(credentialsFilename, false); storeSettings(settings); storeSettings(credentials); - initLogging(exports.loglevel, exports.logconfig); + initLogging(loglevel, logconfig); - if (!exports.skinName) { + if (!skinName) { logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + - 'update your settings.json. Falling back to the default "colibris".'); - exports.skinName = 'colibris'; + 'update your settings.json. Falling back to the default "colibris".'); + skinName = 'colibris'; } // checks if skinName has an acceptable value, otherwise falls back to "colibris" - if (exports.skinName) { - const skinBasePath = path.join(exports.root, 'src', 'static', 'skins'); - const countPieces = exports.skinName.split(path.sep).length; + if (skinName) { + const skinBasePath = path.join(root, 'src', 'static', 'skins'); + const countPieces = skinName.split(path.sep).length; if (countPieces !== 1) { logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + - `not valid: "${exports.skinName}". Falling back to the default "colibris".`); + `not valid: "${skinName}". Falling back to the default "colibris".`); - exports.skinName = 'colibris'; + skinName = 'colibris'; } // informative variable, just for the log messages - let skinPath = path.join(skinBasePath, exports.skinName); + let skinPath = path.join(skinBasePath, skinName); // what if someone sets skinName == ".." or "."? We catch him! - if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { + if (isSubdir(skinBasePath, skinPath) === false) { logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + - 'Falling back to the default "colibris".'); + 'Falling back to the default "colibris".'); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + skinName = 'colibris'; + skinPath = path.join(skinBasePath, skinName); } if (fs.existsSync(skinPath) === false) { logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + skinName = 'colibris'; + skinPath = path.join(skinBasePath, skinName); } - logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); + logger.info(`Using skin "${skinName}" in dir: ${skinPath}`); } - if (exports.abiword) { + if (abiword) { // Check abiword actually exists - if (exports.abiword != null) { - fs.exists(exports.abiword, (exists) => { + if (abiword != null) { + fs.exists(abiword, (exists: boolean) => { if (!exists) { const abiwordError = 'Abiword does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; } - logger.error(`${abiwordError} File location: ${exports.abiword}`); - exports.abiword = null; + logger.error(`${abiwordError} File location: ${abiword}`); + abiword = null; } }); } } - if (exports.soffice) { - fs.exists(exports.soffice, (exists) => { + if (soffice) { + fs.exists(soffice, (exists: boolean) => { if (!exists) { const sofficeError = 'soffice (libreoffice) does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; } - logger.error(`${sofficeError} File location: ${exports.soffice}`); - exports.soffice = null; + logger.error(`${sofficeError} File location: ${soffice}`); + soffice = null; } }); } - if (!exports.sessionKey) { - const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); + if (!sessionKey) { + const sessionkeyFilename = makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); try { - exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); + sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); logger.info(`Session key loaded from: ${sessionkeyFilename}`); } catch (e) { logger.info( `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); - exports.sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); + sessionKey = randomString(32); + // FIXME Check out why this can be string boolean or Array + // @ts-ignore + fs.writeFileSync(sessionkeyFilename, sessionKey, 'utf8'); } } else { 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.'); + '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') { + if (dbType === 'dirty') { const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; } - exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); + dbSettings.filename = makeAbsolute(dbSettings.filename); + logger.warn(`${dirtyWarning} File location: ${dbSettings.filename}`); } - if (exports.ip === '') { + if (ip === '') { // using Unix socket for connectivity logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + - '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); + '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); } /* @@ -845,13 +871,40 @@ exports.reloadSettings = () => { * ACHTUNG: this may prevent caching HTTP proxies to work * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead */ - exports.randomVersionString = randomString(4); - logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); + const randomVersionStringWith4Chars = randomString(4); + logger.info(`Random string used for versioning assets: ${randomVersionStringWith4Chars}`); + return settings }; -exports.exportedForTestingOnly = { +export const exportedForTestingOnly = { parseSettings, }; // initially load settings -exports.reloadSettings(); +reloadSettings(); + + +// Setters +export const setPort = (value: number) => { + port = value; +} + +export const setIp = (value: string) => { + ip = value; +} + +export const setTrustProxy = (value: boolean) => { + trustProxy = value; +} + +export const setSsl = (value: boolean) => { + ssl = value; +} + +export const setimportExportRateLimiting = (value: any) => { + importExportRateLimiting = value; +} + +export const setCommitRateLimiting = (value: any) => { + commitRateLimiting = value; +} diff --git a/src/node/utils/Stream.js b/src/node/utils/Stream.ts similarity index 98% rename from src/node/utils/Stream.js rename to src/node/utils/Stream.ts index 611b83b3372..0a083af8493 100644 --- a/src/node/utils/Stream.js +++ b/src/node/utils/Stream.ts @@ -4,7 +4,9 @@ * Wrapper around any iterable that adds convenience methods that standard JavaScript iterable * objects lack. */ -class Stream { +export class Stream { + private readonly _iter: any; + private _next: null; /** * @returns {Stream} A Stream that yields values in the half-open range [start, end). */ @@ -130,5 +132,3 @@ class Stream { */ [Symbol.iterator]() { return this._iter; } } - -module.exports = Stream; diff --git a/src/node/utils/TidyHtml.js b/src/node/utils/TidyHtml.ts similarity index 80% rename from src/node/utils/TidyHtml.js rename to src/node/utils/TidyHtml.ts index 5b48cdbad37..56fa68972ed 100644 --- a/src/node/utils/TidyHtml.js +++ b/src/node/utils/TidyHtml.ts @@ -3,16 +3,17 @@ * Tidy up the HTML in a given file */ -const log4js = require('log4js'); -const settings = require('./Settings'); -const spawn = require('child_process').spawn; +import log4js from 'log4js'; +import {tidyHtml} from "./Settings"; -exports.tidy = (srcFile) => { +import {spawn} from "child_process"; + +export const tidy = (srcFile) => { const logger = log4js.getLogger('TidyHtml'); return new Promise((resolve, reject) => { // Don't do anything if Tidy hasn't been enabled - if (!settings.tidyHtml) { + if (!tidyHtml) { logger.debug('tidyHtml has not been configured yet, ignoring tidy request'); return resolve(null); } @@ -21,7 +22,7 @@ exports.tidy = (srcFile) => { // Spawn a new tidy instance that cleans up the file inline logger.debug(`Tidying ${srcFile}`); - const tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); + const tidy = spawn(tidyHtml, ['-modify', srcFile]); // Keep track of any error messages tidy.stderr.on('data', (data) => { diff --git a/src/node/utils/UpdateCheck.js b/src/node/utils/UpdateCheck.js deleted file mode 100644 index cfbd9dad9fc..00000000000 --- a/src/node/utils/UpdateCheck.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; -const semver = require('semver'); -const settings = require('./Settings'); -const request = require('request'); - -let infos; - -const loadEtherpadInformations = () => new Promise((resolve, reject) => { - request('https://static.etherpad.org/info.json', (er, response, body) => { - if (er) return reject(er); - - try { - infos = JSON.parse(body); - return resolve(infos); - } catch (err) { - return reject(err); - } - }); -}); - -exports.getLatestVersion = () => { - exports.needsUpdate(); - return infos.latestVersion; -}; - -exports.needsUpdate = (cb) => { - loadEtherpadInformations().then((info) => { - if (semver.gt(info.latestVersion, settings.getEpVersion())) { - if (cb) return cb(true); - } - }).catch((err) => { - console.error(`Can not perform Etherpad update check: ${err}`); - if (cb) return cb(false); - }); -}; - -exports.check = () => { - exports.needsUpdate((needsUpdate) => { - if (needsUpdate) { - console.warn(`Update available: Download the actual version ${infos.latestVersion}`); - } - }); -}; diff --git a/src/node/utils/UpdateCheck.ts b/src/node/utils/UpdateCheck.ts new file mode 100644 index 00000000000..b80bfb954d3 --- /dev/null +++ b/src/node/utils/UpdateCheck.ts @@ -0,0 +1,58 @@ +'use strict'; +import semver from 'semver'; +import {getEpVersion} from './Settings'; +import request from 'request'; + + +type InfoModel = { + latestVersion: string +} + +let infos: InfoModel|undefined; + +const loadEtherpadInformations = () => new Promise((resolve, reject) => { + request('https://static.etherpad.org/info.json', (er, response, body) => { + if (er) reject(er); + + try { + infos = JSON.parse(body); + if (infos === undefined|| infos === null){ + reject("Could not retrieve current version") + return + } + resolve(infos); + } catch (err) { + reject(err); + } + }); +}); + +const getLatestVersion = () => { + needsUpdate(); + if(infos === undefined){ + throw new Error("Could not retrieve latest version") + } + + return infos.latestVersion; +} + +export const needsUpdate = (cb?:(arg0: boolean)=>void) => { + loadEtherpadInformations().then((info) => { + if (semver.gt(info.latestVersion, getEpVersion())) { + if (cb) return cb(true); + } + }).catch((err) => { + console.error(`Can not perform Etherpad update check: ${err}`); + if (cb) return cb(false); + }) +} + +const check = () => { + needsUpdate((needsUpdate)=>{ + if (needsUpdate) { + console.warn(`Update available: Download the actual version ${infos.latestVersion}`); + } + }) +} + +export default {check, getLatestVersion} diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.ts similarity index 90% rename from src/node/utils/caching_middleware.js rename to src/node/utils/caching_middleware.ts index 3cc4daf271b..93512e1eec1 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.ts @@ -16,15 +16,20 @@ * limitations under the License. */ -const Buffer = require('buffer').Buffer; -const fs = require('fs'); -const fsp = fs.promises; -const path = require('path'); -const zlib = require('zlib'); -const settings = require('./Settings'); -const existsSync = require('./path_exists'); -const util = require('util'); +import {Buffer} from "buffer"; + +import fs, {Stats} from "fs"; + +import path from "path"; + +import {check} from './path_exists' +import zlib from "zlib"; + +import {root} from "./Settings"; + +import util from "util"; +const fsp = fs.promises; /* * The crypto module can be absent on reduced node installations. * @@ -45,8 +50,8 @@ try { _crypto = undefined; } -let CACHE_DIR = path.join(settings.root, 'var/'); -CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; +let CACHE_DIR = path.join(root, 'var/'); +CACHE_DIR = check(CACHE_DIR) ? CACHE_DIR : undefined; const responseCache = {}; @@ -78,7 +83,7 @@ if (_crypto) { should replace this. */ -module.exports = class CachingMiddleware { +export class CachingMiddleware { handle(req, res, next) { this._handle(req, res, next).catch((err) => next(err || new Error(err))); } @@ -88,8 +93,15 @@ module.exports = class CachingMiddleware { return next(undefined, req, res); } - const oldReq = {}; - const oldRes = {}; + const oldReq = { + method: undefined + }; + const oldRes = { + write: undefined, + end: undefined, + setHeader: undefined, + writeHead: undefined + }; const supportsGzip = (req.get('Accept-Encoding') || '').indexOf('gzip') !== -1; @@ -100,7 +112,7 @@ module.exports = class CachingMiddleware { const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {}); const modifiedSince = req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']); - if (stats != null && stats.mtime && responseCache[cacheKey]) { + if (stats != null && stats instanceof Object && "mtime" in stats && responseCache[cacheKey]) { req.headers['if-modified-since'] = stats.mtime.toUTCString(); } else { delete req.headers['if-modified-since']; @@ -200,4 +212,4 @@ module.exports = class CachingMiddleware { next(undefined, req, res); } -}; +} diff --git a/src/node/utils/customError.js b/src/node/utils/customError.ts similarity index 84% rename from src/node/utils/customError.js rename to src/node/utils/customError.ts index 24ad181e6fa..b90946f65be 100644 --- a/src/node/utils/customError.js +++ b/src/node/utils/customError.ts @@ -7,7 +7,10 @@ * @class CustomError * @extends {Error} */ -class CustomError extends Error { +export class CustomError extends Error { + code: any; + signal: any; + easysync: boolean /** * Creates an instance of CustomError. * @param {*} message @@ -20,5 +23,3 @@ class CustomError extends Error { Error.captureStackTrace(this, this.constructor); } } - -module.exports = CustomError; diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.ts similarity index 79% rename from src/node/utils/padDiff.js rename to src/node/utils/padDiff.ts index 4ab276b4b66..737f830676a 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.ts @@ -1,11 +1,21 @@ 'use strict'; -const AttributeMap = require('../../static/js/AttributeMap'); -const Changeset = require('../../static/js/Changeset'); -const attributes = require('../../static/js/attributes'); -const exportHtml = require('./ExportHtml'); - -function PadDiff(pad, fromRev, toRev) { +import AttributeMap from '../../static/js/AttributeMap'; +import { + applyToAText, + builder, checkRep, + compose, + deserializeOps, + numToString, Op, + opAssembler, pack, splitAttributionLines, splitTextLines, stringAssembler, + unpack +} from '../../static/js/Changeset'; +import {attribsFromString} from '../../static/js/attributes'; +import {getHTMLFromAtext} from './ExportHtml'; +// @ts-ignore +import {PadDiffModel} from "ep_etherpad-lite/node/models/PadDiffModel"; + +export const PadDiff = (pad, fromRev, toRev)=> { // check parameters if (!pad || !pad.id || !pad.atext || !pad.pool) { throw new Error('Invalid pad'); @@ -14,16 +24,22 @@ function PadDiff(pad, fromRev, toRev) { const range = pad.getValidRevisionRange(fromRev, toRev); if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`); + // FIXME How to fix this? + // @ts-ignore this._pad = pad; + // @ts-ignore this._fromRev = range.startRev; + // @ts-ignore this._toRev = range.endRev; + // @ts-ignore this._html = null; + // @ts-ignore this._authors = []; } PadDiff.prototype._isClearAuthorship = function (changeset) { // unpack - const unpacked = Changeset.unpack(changeset); + const unpacked = unpack(changeset); // check if there is nothing in the charBank if (unpacked.charBank !== '') { @@ -35,7 +51,7 @@ PadDiff.prototype._isClearAuthorship = function (changeset) { return false; } - const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops); + const [clearOperator, anotherOp] = deserializeOps(unpacked.ops); // check if there is only one operator if (anotherOp != null) return false; @@ -52,7 +68,7 @@ PadDiff.prototype._isClearAuthorship = function (changeset) { } const [appliedAttribute, anotherAttribute] = - attributes.attribsFromString(clearOperator.attribs, this._pad.pool); + attribsFromString(clearOperator.attribs, this._pad.pool); // Check that the operation has exactly one attribute. if (appliedAttribute == null || anotherAttribute != null) return false; @@ -69,9 +85,9 @@ PadDiff.prototype._createClearAuthorship = async function (rev) { const atext = await this._pad.getInternalRevisionAText(rev); // build clearAuthorship changeset - const builder = Changeset.builder(atext.text.length); - builder.keepText(atext.text, [['author', '']], this._pad.pool); - const changeset = builder.toString(); + const builder2 = builder(atext.text.length); + builder2.keepText(atext.text, [['author', '']], this._pad.pool); + const changeset = builder2.toString(); return changeset; }; @@ -84,7 +100,7 @@ PadDiff.prototype._createClearStartAtext = async function (rev) { const changeset = await this._createClearAuthorship(rev); // apply the clearAuthorship changeset - const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool); + const newAText = applyToAText(changeset, atext, this._pad.pool); return newAText; }; @@ -108,9 +124,11 @@ PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) { return {changesets, authors}; }; -PadDiff.prototype._addAuthors = function (authors) { - const self = this; - +PadDiff.prototype._addAuthors = (authors)=> { + let self: undefined|PadDiffModel = this; + if(!self){ + self = {_authors: []} + } // add to array if not in the array authors.forEach((author) => { if (self._authors.indexOf(author) === -1) { @@ -151,7 +169,7 @@ PadDiff.prototype._createDiffAtext = async function () { if (superChangeset == null) { superChangeset = changeset; } else { - superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool); + superChangeset = compose(superChangeset, changeset, this._pad.pool); } } @@ -165,10 +183,10 @@ PadDiff.prototype._createDiffAtext = async function () { const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); // apply the superChangeset, which includes all addings - atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool); + atext = applyToAText(superChangeset, atext, this._pad.pool); // apply the deletionChangeset, which adds a deletions - atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool); + atext = applyToAText(deletionChangeset, atext, this._pad.pool); } return atext; @@ -187,7 +205,7 @@ PadDiff.prototype.getHtml = async function () { const authorColors = await this._pad.getAllAuthorColors(); // convert the atext to html - this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); + this._html = await getHTMLFromAtext(this._pad, atext, authorColors); return this._html; }; @@ -198,28 +216,32 @@ PadDiff.prototype.getAuthors = async function () { if (this._html == null) { await this.getHtml(); } + let self: undefined|PadDiffModel = this; + if(!self){ + self = {_authors: []} + } return self._authors; }; PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { // unpack - const unpacked = Changeset.unpack(changeset); + const unpacked = unpack(changeset); - const assem = Changeset.opAssembler(); + const assem = opAssembler(); // create deleted attribs const authorAttrib = apool.putAttrib(['author', author || '']); const deletedAttrib = apool.putAttrib(['removed', true]); - const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`; + const attribs = `*${numToString(authorAttrib)}*${numToString(deletedAttrib)}`; - for (const operator of Changeset.deserializeOps(unpacked.ops)) { + for (const operator of deserializeOps(unpacked.ops)) { if (operator.opcode === '-') { // this is a delete operator, extend it with the author operator.attribs = attribs; } else if (operator.opcode === '=' && operator.attribs) { // this is operator changes only attributes, let's mark which author did that - operator.attribs += `*${Changeset.numToString(authorAttrib)}`; + operator.attribs += `*${numToString(authorAttrib)}`; } // append the new operator to our assembler @@ -227,21 +249,21 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { } // return the modified changeset - return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank); + return pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank); }; // this method is 80% like Changeset.inverse. I just changed so instead of reverting, // it adds deletions and attribute changes to to the atext. PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { - const lines = Changeset.splitTextLines(startAText.text); - const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text); + const lines = splitTextLines(startAText.text); + const alines = splitAttributionLines(startAText.attribs, startAText.text); // lines and alines are what the exports is meant to apply to. // They may be arrays or objects with .get(i) and .length methods. // They include final newlines on lines. const linesGet = (idx) => { - if (lines.get) { + if ("get" in lines && lines.get instanceof Function) { return lines.get(idx); } else { return lines[idx]; @@ -249,7 +271,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { }; const aLinesGet = (idx) => { - if (alines.get) { + if ("get" in alines && alines.get instanceof Function) { return alines.get(idx); } else { return alines[idx]; @@ -261,14 +283,14 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let curLineOps = null; let curLineOpsNext = null; let curLineOpsLine; - let curLineNextOp = new Changeset.Op('+'); + let curLineNextOp = new Op('+'); - const unpacked = Changeset.unpack(cs); - const builder = Changeset.builder(unpacked.newLen); + const unpacked = unpack(cs); + const builder2 = builder(unpacked.newLen); const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { if (!curLineOps || curLineOpsLine !== curLine) { - curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOps = deserializeOps(aLinesGet(curLine)); curLineOpsNext = curLineOps.next(); curLineOpsLine = curLine; let indexIntoLine = 0; @@ -289,13 +311,13 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { curChar = 0; curLineOpsLine = curLine; curLineNextOp.chars = 0; - curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOps = deserializeOps(aLinesGet(curLine)); curLineOpsNext = curLineOps.next(); } if (!curLineNextOp.chars) { if (curLineOpsNext.done) { - curLineNextOp = new Changeset.Op(); + curLineNextOp = new Op(); } else { curLineNextOp = curLineOpsNext.value; curLineOpsNext = curLineOps.next(); @@ -330,7 +352,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { const nextText = (numChars) => { let len = 0; - const assem = Changeset.stringAssembler(); + const assem = stringAssembler(); const firstString = linesGet(curLine).substring(curChar); len += firstString.length; assem.append(firstString); @@ -358,7 +380,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { }; }; - for (const csOp of Changeset.deserializeOps(unpacked.ops)) { + for (const csOp of deserializeOps(unpacked.ops)) { if (csOp.opcode === '=') { const textBank = nextText(csOp.chars); @@ -404,7 +426,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { textLeftToProcess = textLeftToProcess.substr(lengthToProcess); if (lineBreak) { - builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak + builder2.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak // consume the attributes of this linebreak consumeAttribRuns(1, () => {}); @@ -416,31 +438,31 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { // get the old attributes back const oldAttribs = undoBackToAttribs(attribs); - builder.insert(processText.substr(textBankIndex, len), oldAttribs); + builder2.insert(processText.substr(textBankIndex, len), oldAttribs); textBankIndex += len; }); - builder.keep(lengthToProcess, 0); + builder2.keep(lengthToProcess, 0); } } } else { skip(csOp.chars, csOp.lines); - builder.keep(csOp.chars, csOp.lines); + builder2.keep(csOp.chars, csOp.lines); } } else if (csOp.opcode === '+') { - builder.keep(csOp.chars, csOp.lines); + builder2.keep(csOp.chars, csOp.lines); } else if (csOp.opcode === '-') { const textBank = nextText(csOp.chars); let textBankIndex = 0; consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { - builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); + builder2.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); textBankIndex += len; }); } } - return Changeset.checkRep(builder.toString()); + return checkRep(builder.toString()); }; // export the constructor diff --git a/src/node/utils/path_exists.js b/src/node/utils/path_exists.ts similarity index 71% rename from src/node/utils/path_exists.js rename to src/node/utils/path_exists.ts index 0b4c8fe94fc..91b6dd12726 100644 --- a/src/node/utils/path_exists.js +++ b/src/node/utils/path_exists.ts @@ -1,7 +1,7 @@ 'use strict'; -const fs = require('fs'); +import fs from 'fs'; -const check = (path) => { +export const check = (path) => { const existsSync = fs.statSync || fs.existsSync || path.existsSync; let result; @@ -11,6 +11,4 @@ const check = (path) => { result = false; } return result; -}; - -module.exports = check; +} diff --git a/src/node/utils/promises.js b/src/node/utils/promises.ts similarity index 93% rename from src/node/utils/promises.js rename to src/node/utils/promises.ts index bc9f8c2dc06..9ebb07da4c5 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.ts @@ -7,7 +7,7 @@ // `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if // `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as // the predicate. -exports.firstSatisfies = (promises, predicate) => { +export const firstSatisfies = (promises, predicate) => { if (predicate == null) predicate = (x) => x; // Transform each original Promise into a Promise that never resolves if the original resolved @@ -42,7 +42,7 @@ exports.firstSatisfies = (promises, predicate) => { // `total` is greater than `concurrency`, then `concurrency` Promises will be created right away, // and each remaining Promise will be created once one of the earlier Promises resolves.) This async // function resolves once all `total` Promises have resolved. -exports.timesLimit = async (total, concurrency, promiseCreator) => { +export const timesLimit = async (total, concurrency, promiseCreator) => { if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive'); let next = 0; const addAnother = () => promiseCreator(next++).finally(() => { @@ -55,11 +55,14 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => { await Promise.all(promises); }; + /** * An ordinary Promise except the `resolve` and `reject` executor functions are exposed as * properties. */ -class Gate extends Promise { +//FIXME Why is the constructor diviating from the Promise constructor? +// @ts-ignore +export class Gate extends Promise { // Coax `.then()` into returning an ordinary Promise, not a Gate. See // https://stackoverflow.com/a/65669070 for the rationale. static get [Symbol.species]() { return Promise; } @@ -73,4 +76,3 @@ class Gate extends Promise { Object.assign(this, props); } } -exports.Gate = Gate; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.ts similarity index 50% rename from src/node/utils/randomstring.js rename to src/node/utils/randomstring.ts index 4ffd3e8ae77..152239a1955 100644 --- a/src/node/utils/randomstring.js +++ b/src/node/utils/randomstring.ts @@ -3,8 +3,6 @@ * Generates a random String with the given length. Is needed to generate the * Author, Group, readonly, session Ids */ -const crypto = require('crypto'); +import crypto from 'crypto' -const randomString = (len) => crypto.randomBytes(len).toString('hex'); - -module.exports = randomString; +export const randomString = (len) => crypto.randomBytes(len).toString('hex'); diff --git a/src/node/utils/run_cmd.js b/src/node/utils/run_cmd.ts similarity index 84% rename from src/node/utils/run_cmd.js rename to src/node/utils/run_cmd.ts index bf5515c8466..a4ee806fe92 100644 --- a/src/node/utils/run_cmd.js +++ b/src/node/utils/run_cmd.ts @@ -1,12 +1,22 @@ + 'use strict'; -const spawn = require('cross-spawn'); -const log4js = require('log4js'); -const path = require('path'); -const settings = require('./Settings'); +import spawn from 'cross-spawn'; +import log4js from 'log4js'; +import path from 'path'; +import {root} from './Settings'; +import {CustomError} from "./customError"; const logger = log4js.getLogger('runCmd'); +type CMDPromise = { + stdout: Promise, + stderr: Promise, + status: Promise, + signal: Promise, +} + + const logLines = (readable, logLineFn) => { readable.setEncoding('utf8'); // The process won't necessarily write full lines every time -- it might write a part of a line @@ -69,18 +79,30 @@ const logLines = (readable, logLineFn) => { * - `stderr`: Similar to `stdout` but for stderr. * - `child`: The ChildProcess object. */ -module.exports = exports = (args, opts = {}) => { - logger.debug(`Executing command: ${args.join(' ')}`); - opts = {cwd: settings.root, ...opts}; - logger.debug(`cwd: ${opts.cwd}`); +type RunOpts = { + cwd?: string; + env?: any; + stdio?: any; + detached?: boolean; + uid?: number +} + + +const run_cmd = (args, opts:RunOpts = { + stdio: undefined +}) => { + logger.info(`Executing command: ${args.join(' ')}`); + + opts = {cwd: root, ...opts}; + logger.info(`cwd: ${opts.cwd}`); // Log stdout and stderr by default. const stdio = Array.isArray(opts.stdio) ? opts.stdio.slice() // Copy to avoid mutating the caller's array. - : typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio] - : opts.stdio === 'string' ? [null, 'string', 'string'] - : Array(3).fill(opts.stdio); + : typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio] + : opts.stdio === 'string' ? [null, 'string', 'string'] + : Array(3).fill(opts.stdio); const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`); if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line); if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line); @@ -107,8 +129,8 @@ module.exports = exports = (args, opts = {}) => { opts.env = { ...env, // Copy env to avoid modifying process.env or the caller's supplied env. [pathVarName]: [ - path.join(settings.root, 'src', 'node_modules', '.bin'), - path.join(settings.root, 'node_modules', '.bin'), + path.join(root, 'src', 'node_modules', '.bin'), + path.join(root, 'node_modules', '.bin'), ...(PATH ? PATH.split(path.delimiter) : []), ].join(path.delimiter), }; @@ -116,14 +138,16 @@ module.exports = exports = (args, opts = {}) => { // Create an error object to use in case the process fails. This is done here rather than in the // process's `exit` handler so that we get a useful stack trace. - const procFailedErr = new Error(); + const procFailedErr = new CustomError({}); const proc = spawn(args[0], args.slice(1), opts); const streams = [undefined, proc.stdout, proc.stderr]; let px; - const p = new Promise((resolve, reject) => { px = {resolve, reject}; }); + const p = new Promise((resolve, reject) => { px = {resolve, reject}; }); + // @ts-ignore [, p.stdout, p.stderr] = streams; + // @ts-ignore p.child = proc; const stdioStringPromises = [undefined, Promise.resolve(), Promise.resolve()]; @@ -132,6 +156,7 @@ module.exports = exports = (args, opts = {}) => { if (stdioLoggers[fd] != null) { logLines(streams[fd], stdioLoggers[fd]); } else if (stdioSaveString[fd]) { + // @ts-ignore p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => { const chunks = []; for await (const chunk of streams[fd]) chunks.push(chunk); @@ -155,3 +180,4 @@ module.exports = exports = (args, opts = {}) => { }); return p; }; +export default run_cmd; diff --git a/src/node/utils/sanitizePathname.js b/src/node/utils/sanitizePathname.ts similarity index 94% rename from src/node/utils/sanitizePathname.js rename to src/node/utils/sanitizePathname.ts index 61b61116661..b9e492661a3 100644 --- a/src/node/utils/sanitizePathname.js +++ b/src/node/utils/sanitizePathname.ts @@ -1,10 +1,10 @@ 'use strict'; -const path = require('path'); +import path from 'path'; // Normalizes p and ensures that it is a relative path that does not reach outside. See // https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. -module.exports = (p, pathApi = path) => { +export default (p, pathApi = path) => { // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., @@ -20,4 +20,4 @@ module.exports = (p, pathApi = path) => { // pathname would not be normalized away before being converted to '../'. if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); return p; -}; +} diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.ts similarity index 98% rename from src/node/utils/toolbar.js rename to src/node/utils/toolbar.ts index 40a47687818..57aed942f31 100644 --- a/src/node/utils/toolbar.js +++ b/src/node/utils/toolbar.ts @@ -2,7 +2,7 @@ /** * The Toolbar Module creates and renders the toolbars and buttons */ -const _ = require('underscore'); +import _ from 'underscore'; const removeItem = (array, what) => { let ax; @@ -12,13 +12,13 @@ const removeItem = (array, what) => { return array; }; -const defaultButtonAttributes = (name, overrides) => ({ +const defaultButtonAttributes = (name, overrides?) => ({ command: name, localizationId: `pad.toolbar.${name}.title`, class: `buttonicon buttonicon-${name}`, }); -const tag = (name, attributes, contents) => { +const tag = (name, attributes, contents?) => { const aStr = tagAttributes(attributes); if (_.isString(contents) && contents.length > 0) { diff --git a/src/package-lock.json b/src/package-lock.json index abf18216eb6..c421274846f 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -4,6 +4,22 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@apidevtools/json-schema-ref-parser": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-10.1.0.tgz", @@ -14,13 +30,6 @@ "@types/lodash.clonedeep": "^4.5.7", "js-yaml": "^4.1.0", "lodash.clonedeep": "^4.5.0" - }, - "dependencies": { - "@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" - } } }, "@azure/abort-controller": { @@ -28,350 +37,1669 @@ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "requires": { - "tslib": "^2.2.0" + "tslib": "^2.2.0" + } + }, + "@azure/core-auth": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", + "integrity": "sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.7.3.tgz", + "integrity": "sha512-kleJ1iUTxcO32Y06dH9Pfi9K4U+Tlb111WXEnbt7R/ne+NLRwppZiTGJuTD5VVoxTMK5NTbEtm5t2vcdNCFe2g==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-http-compat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-1.3.0.tgz", + "integrity": "sha512-ZN9avruqbQ5TxopzG3ih3KRy52n8OAbitX3fnZT5go4hzu0J+KVPSzkL+Wt3hpJpdG8WIfg1sBD1tWkgUdEpBA==", + "requires": { + "@azure/abort-controller": "^1.0.4", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + } + }, + "@azure/core-lro": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.3.tgz", + "integrity": "sha512-ubkOf2YCnVtq7KqEJQqAI8dDD5rH1M6OP5kW0KO/JQyTaxLA0N0pjFWvvaysCj9eHMNBcuuoZXhhl0ypjod2DA==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-paging": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", + "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-rest-pipeline": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.11.0.tgz", + "integrity": "sha512-nB4KXl6qAyJmBVLWA7SakT4tzpYZTCk4pvRBeI+Ye0WYSOrlTqlMhc4MSS/8atD3ufeYWdkN380LLoXlUUzThw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-util": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.2.tgz", + "integrity": "sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/identity": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-2.1.0.tgz", + "integrity": "sha512-BPDz1sK7Ul9t0l9YKLEa8PHqWU4iCfhGJ+ELJl6c8CP3TpJt2urNCbm0ZHsthmxRsYoMPbz2Dvzj30zXZVmAFw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^2.26.0", + "@azure/msal-common": "^7.0.0", + "@azure/msal-node": "^1.10.0", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@azure/keyvault-keys": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.7.1.tgz", + "integrity": "sha512-zfmlZQCw1Yz+aPhgZmWOYBUzaKmfBzR2yceAE4S6hKDl7YZraTguuXmtFbCqjRvpz+pIMKAK25fENay9mFy1hQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^1.3.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/logger": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/msal-browser": { + "version": "2.37.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.37.1.tgz", + "integrity": "sha512-EoKQISEpIY39Ru1OpWkeFZBcwp6Y0bG81bVmdyy4QJebPPDdVzfm62PSU0XFIRc3bqjZ4PBKBLMYLuo9NZYAow==", + "requires": { + "@azure/msal-common": "13.1.0" + }, + "dependencies": { + "@azure/msal-common": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.1.0.tgz", + "integrity": "sha512-wj+ULrRB0HTuMmtrMjg8j3guCx32GE2BCPbsMCZkHgL1BZetC3o/Su5UJEQMX1HNc9CrIaQNx5WaKWHygYDe0g==" + } + } + }, + "@azure/msal-common": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-7.6.0.tgz", + "integrity": "sha512-XqfbglUTVLdkHQ8F9UQJtKseRr3sSnr9ysboxtoswvaMVaEfvyLtMoHv9XdKUfOc0qKGzNgRFd9yRjIWVepl6Q==" + }, + "@azure/msal-node": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.17.3.tgz", + "integrity": "sha512-slsa+388bQQWnWH1V91KL+zV57rIp/0OQFfF0EmVMY8gnEIkAnpWWFUVBTTMbxEyjEFMk5ZW9xiHvHBcYFHzDw==", + "requires": { + "@azure/msal-common": "13.1.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@azure/msal-common": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.1.0.tgz", + "integrity": "sha512-wj+ULrRB0HTuMmtrMjg8j3guCx32GE2BCPbsMCZkHgL1BZetC3o/Su5UJEQMX1HNc9CrIaQNx5WaKWHygYDe0g==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@babel/cli": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.6.tgz", + "integrity": "sha512-Be3/RfEDmkMRGT1+ru5nTkfcvWz5jDOYg1V9rXqTz2u9Qt96O1ryboGvxVBp7wOnYWDB8DNHIWb6DThrpudfOw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.5" + } + }, + "@babel/compat-data": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "dev": true + }, + "@babel/core": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2" + }, + "dependencies": { + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "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 + } + } + }, + "@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-validator-option": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" + }, + "dependencies": { + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.6.tgz", + "integrity": "sha512-iwdzgtSiBxF6ni6mzVnZCF3xt5qE6cEA0J7nFt8QOAWZ0zjCFceEgpn3vtb2V7WFR6QzP2jmIFOHMTRo7eNJjQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@nicolo-ribaudo/semver-v6": "^6.3.3" + }, + "dependencies": { + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.6.tgz", + "integrity": "sha512-nBookhLKxAWo/TUCmhnaEJyLz2dekjQvv5SRpE9epWQBcpedWLKt8aZdsuT9XV5ovzR3fENLjRXVT0GsSlGGhA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "regexpu-core": "^5.3.1" + }, + "dependencies": { + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + } + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", + "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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 + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", + "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", + "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", + "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + } + }, + "@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", + "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", + "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", + "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz", + "integrity": "sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" } }, - "@azure/core-asynciterator-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz", - "integrity": "sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw==" + "@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } }, - "@azure/core-auth": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.3.2.tgz", - "integrity": "sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==", + "@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, "requires": { - "@azure/abort-controller": "^1.0.0", - "tslib": "^2.2.0" + "@babel/helper-plugin-utils": "^7.22.5" } }, - "@azure/core-client": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.6.0.tgz", - "integrity": "sha512-YhSf4cb61ApSjItscp9XoaLq8KRnacPDAhmjAZSMnn/gs6FhFbZNfOBOErG2dDj7JRknVtCmJ5mLmfR2sLa11A==", + "@babel/plugin-transform-unicode-escapes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-rest-pipeline": "^1.5.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.0.0", - "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" - }, - "dependencies": { - "@azure/core-tracing": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", - "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", - "requires": { - "tslib": "^2.2.0" - } - } + "@babel/helper-plugin-utils": "^7.22.5" } }, - "@azure/core-http": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-2.3.2.tgz", - "integrity": "sha512-Z4dfbglV9kNZO177CNx4bo5ekFuYwwsvjLiKdZI4r84bYGv3irrbQz7JC3/rUfFH2l4T/W6OFleJaa2X0IaQqw==", + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.13", - "@azure/core-util": "^1.1.1", - "@azure/logger": "^1.0.0", - "@types/node-fetch": "^2.5.0", - "@types/tunnel": "^0.0.3", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "process": "^0.11.10", - "tough-cookie": "^4.0.0", - "tslib": "^2.2.0", - "tunnel": "^0.0.6", - "uuid": "^8.3.0", - "xml2js": "^0.5.0" - }, - "dependencies": { - "@azure/core-util": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.3.2.tgz", - "integrity": "sha512-2bECOUh88RvL1pMZTcc6OzfobBeWDBf5oBbhjIhT1MV9otMVWCzpOJkkiKtrnO88y5GGBelgY8At73KGAdbkeQ==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "tslib": "^2.2.0" - } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, - "xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - } + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, - "@azure/core-lro": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.2.4.tgz", - "integrity": "sha512-e1I2v2CZM0mQo8+RSix0x091Av493e4bnT22ds2fcQGslTHzM2oTbswkB65nP4iEpCxBrFxOSDPKExmTmjCVtQ==", + "@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-tracing": "1.0.0-preview.13", - "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, - "@azure/core-paging": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.2.1.tgz", - "integrity": "sha512-UtH5iMlYsvg+nQYIl4UHlvvSrsBjOlRF4fs0j7mxd3rWdAStrKYrh2durOpHs5C9yZbVhsVDaisoyaf/lL1EVA==", + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, "requires": { - "@azure/core-asynciterator-polyfill": "^1.0.0", - "tslib": "^2.2.0" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" } }, - "@azure/core-rest-pipeline": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.8.1.tgz", - "integrity": "sha512-R/XpxZcDgGbnneEifnsAcjLoR2NCmlDxKDmzd8oi5jx5YEnPE6gsxHQWAk2+uY55Ka717x/fdctyoCYKnumrqw==", + "@babel/preset-env": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.6.tgz", + "integrity": "sha512-IHr0AXHGk8oh8HYSs45Mxuv6iySUBwDTIzJSnXN7PURqHdxJVQlCoXmKJgyvSS9bcNf9NVRVE35z+LkCvGmi6w==", + "dev": true, "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.0.0", - "@azure/logger": "^1.0.0", - "form-data": "^4.0.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0", - "uuid": "^8.3.0" + "@babel/compat-data": "^7.22.6", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.5", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.6", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.6", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "core-js-compat": "^3.31.0" }, "dependencies": { - "@azure/core-tracing": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", - "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", - "requires": { - "tslib": "^2.2.0" - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true } } }, - "@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-util": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.0.0.tgz", - "integrity": "sha512-yWshY9cdPthlebnb3Zuz/j0Lv4kjU6u7PR5sW7A9FF7EX+0irMRJAtyTq5TPiDHJfjH8gTSlnIYFj9m7Ed76IQ==", + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, "requires": { - "tslib": "^2.2.0" + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" } }, - "@azure/identity": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-2.0.4.tgz", - "integrity": "sha512-ZgFubAsmo7dji63NLPaot6O7pmDfceAUPY57uphSCr0hmRj+Cakqb4SUz5SohCHFtscrhcmejRU903Fowz6iXg==", + "@babel/register": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.5.tgz", + "integrity": "sha512-vV6pm/4CijSQ8Y47RH5SopXzursN35RQINfGJkmOlcpAtGuf94miFvIPhCKGQN7WGIcsgG1BHEX2KVdTYwTwUQ==", + "dev": true, "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.4.0", - "@azure/core-rest-pipeline": "^1.1.0", - "@azure/core-tracing": "1.0.0-preview.13", - "@azure/core-util": "^1.0.0-beta.1", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^2.16.0", - "@azure/msal-common": "^4.5.1", - "@azure/msal-node": "^1.3.0", - "events": "^3.0.0", - "jws": "^4.0.0", - "open": "^8.0.0", - "stoppable": "^1.1.0", - "tslib": "^2.2.0", - "uuid": "^8.3.0" + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.5", + "source-map-support": "^0.5.16" }, "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, - "@azure/keyvault-keys": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.4.0.tgz", - "integrity": "sha512-W9sPZebXYa3aar7BGIA+fAsq/sy1nf2TZAETbkv7DRawzVLrWv8QoVVceqNHjy3cigT4HNxXjaPYCI49ez5CUA==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-http": "^2.0.0", - "@azure/core-lro": "^2.2.0", - "@azure/core-paging": "^1.1.1", - "@azure/core-tracing": "1.0.0-preview.13", - "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" - } + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true }, - "@azure/logger": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.3.tgz", - "integrity": "sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==", + "@babel/runtime": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", + "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", + "dev": true, "requires": { - "tslib": "^2.2.0" + "regenerator-runtime": "^0.13.11" } }, - "@azure/msal-browser": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.24.0.tgz", - "integrity": "sha512-P4Z8mQ6hTuA9ss3HCltso7fRmuX1raaU6444G35c0FhaD6hfqViFYRa7hk16AiAs9HkUQHbBaL3gLjKMpX3heA==", + "@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, "requires": { - "@azure/msal-common": "^6.3.0" - }, - "dependencies": { - "@azure/msal-common": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-6.3.0.tgz", - "integrity": "sha512-ZyLq9GdnLBi/83YpysE86TFKbA0TuvfNAN5Psqu20cdAjLo/4rw4ttiItdh1G//XeGErHk9qn57gi2AYU1b5/Q==" - } + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" } }, - "@azure/msal-common": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-4.5.1.tgz", - "integrity": "sha512-/i5dXM+QAtO+6atYd5oHGBAx48EGSISkXNXViheliOQe+SIFMDo3gSq3lL54W0suOSAsVPws3XnTaIHlla0PIQ==", + "@babel/traverse": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "dev": true, "requires": { - "debug": "^4.1.1" + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" }, "dependencies": { "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "requires": { "ms": "2.1.2" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "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==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, - "@azure/msal-node": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.14.6.tgz", - "integrity": "sha512-em/qqFL5tLMxMPl9vormAs13OgZpmQoJbiQ/GlWr+BA77eCLoL+Ehr5xRHowYo+LFe5b+p+PJVkRvT+mLvOkwA==", + "@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, "requires": { - "@azure/msal-common": "^9.0.2", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + } + }, + "@elastic/transport": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.3.2.tgz", + "integrity": "sha512-ZiBYRVPj6pwYW99fueyNU4notDf7ZPs7Ix+4T1btIJsKJmeaORIItIfs+0O7KV4vV+DcvyMhkY1FXQx7kQOODw==", + "requires": { + "debug": "^4.3.4", + "hpagent": "^1.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.4.0", + "undici": "^5.22.1" }, "dependencies": { - "@azure/msal-common": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-9.0.2.tgz", - "integrity": "sha512-qzwxuF8kZAp+rNUactMCgJh8fblq9D4lSqrrIxMDzLjgSZtjN32ix7r/HBe8QdOr76II9SVVPcMkX4sPzPfQ7w==" - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" + "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==" + } } }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" } } }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, "@eslint/eslintrc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", - "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", + "espree": "^9.6.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "dependencies": { @@ -410,6 +1738,12 @@ } } }, + "@eslint/js": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "dev": true + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -417,14 +1751,14 @@ "optional": true }, "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "dependencies": { "debug": { @@ -444,6 +1778,12 @@ } } }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, "@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -451,9 +1791,10 @@ "dev": true }, "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -463,40 +1804,40 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true }, "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.4.tgz", + "integrity": "sha512-KE/SxsDqNs3rrWwFHcRh15ZLVFrI0YoZtgAdIyIq9k5hUNmiWRXXThPomIxHuL20sLdgzbDFyvkUMna14bvtrw==" }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" } }, "@js-joda/core": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-4.3.1.tgz", - "integrity": "sha512-oeaetlodcqVsiZDxnEcqsbs+sXBkASxua0mXs5OXuPQXz3/wdPTMlxwfQ4z2HKcOik3S9voW3QJkp/KLWDhvRQ==" + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.5.3.tgz", + "integrity": "sha512-7dqNYwG8gCt4hfg5PKgM7xLEcgSBcx/UgC92OMnhMmvAnq11QzDFPrxUkNR/u5kn17WWLZ8beZ4A3Qrz4pZcmQ==" }, "@jsdevtools/ono": { "version": "7.1.3", @@ -532,9 +1873,9 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, "@mapbox/node-pre-gyp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", - "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", "optional": true, "requires": { "detect-libc": "^2.0.0", @@ -548,11 +1889,17 @@ "tar": "^6.1.11" } }, + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -561,14 +1908,12 @@ "@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, "@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -594,10 +1939,39 @@ "rimraf": "^3.0.2" } }, - "@opentelemetry/api": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.1.0.tgz", - "integrity": "sha512-hf+3bwuBwtXsugA2ULBc95qxrOqP2pOekLz34BJhcAKawt94vfeNyUKpYc0lZQ/3sCP6LqRa7UAdHA7i5UODzQ==" + "@pkgr/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-JOqwkgFEyi+OROIyq7l4Jy28h/WwhDnG/cPkXG2Z1iFbubB6jsHW1NDvmyOzTBxHr3yg68YGirmh1JUgMqa+9w==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.2.12", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.5.0" + }, + "dependencies": { + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, + "open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "requires": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + } + } + } }, "@redis/bloom": { "version": "1.2.0", @@ -634,16 +2008,95 @@ "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==" }, + "@rollup/plugin-commonjs": { + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.2.tgz", + "integrity": "sha512-NGTwaJxIO0klMs+WSFFtBP7b9TdTJ3K76HZkewT8/+yHzMiUGVQgaPtLQxNVYIgT5F7lxkEyVID+yS3K7bhCow==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@rollup/plugin-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", + "integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==", + "requires": { + "@rollup/pluginutils": "^5.0.1" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + } + } + }, + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, "@rushstack/eslint-patch": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz", - "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz", + "integrity": "sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==", "dev": true }, "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", "dev": true, "requires": { "type-detect": "4.0.8" @@ -659,9 +2112,9 @@ } }, "@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.3.tgz", + "integrity": "sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -670,26 +2123,94 @@ } }, "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, "@tediousjs/connection-string": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.3.0.tgz", - "integrity": "sha512-d/keJiNKfpHo+GmSB8QcsAwBx8h+V1UbdozA5TD+eSLXprNY53JAYub47J9evsSKWDdNG5uVj0FiMozLKuzowQ==" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.4.2.tgz", + "integrity": "sha512-1R9UC7Qc5wief2oJL+c1+d7v1/oPBayL85u8L/jV2DzIKput1TZ8ZUjj2nxQaSfzu210zp0oFWUrYUiUs8NhBQ==" }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" }, - "@types/es-aggregate-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.2.tgz", - "integrity": "sha512-erqUpFXksaeR2kejKnhnjZjbFxUpGZx4Z7ydNL9ie8tEhXPiZTsLeUDJ6aR1F8j5wWUAtOAQWUqkc7givBJbBA==", + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cross-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", + "integrity": "sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + }, + "@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@types/fs-extra": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.2.tgz", + "integrity": "sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, "requires": { + "@types/minimatch": "*", "@types/node": "*" } }, @@ -701,17 +2222,36 @@ "@types/unist": "*" } }, - "@types/json-schema": { - "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/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "dev": true + }, + "@types/jquery": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", + "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/js-cookie": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz", + "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==", "dev": true }, + "@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, "@types/lodash": { "version": "4.14.195", @@ -731,64 +2271,99 @@ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, - "@types/node": { - "version": "17.0.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", - "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==" + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true }, - "@types/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==", - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - }, - "dependencies": { - "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" - } - } - } + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "@types/node": { + "version": "20.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", + "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==" }, "@types/parse5": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" }, - "@types/tunnel": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", - "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "dev": true, "requires": { + "@types/http-errors": "*", + "@types/mime": "*", "@types/node": "*" } }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "@types/underscore": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.5.tgz", + "integrity": "sha512-b8e//LrIlhoXaaBcMC0J/s2/lIF9y5VJYKqbW4nA+tW/nqqDk1Dacd1ULLT7zgGsKs7PGbSnqCPzqEniZ0RxYg==", + "dev": true + }, "@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, "@typescript-eslint/eslint-plugin": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.22.0.tgz", - "integrity": "sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz", + "integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/type-utils": "5.22.0", - "@typescript-eslint/utils": "5.22.0", - "debug": "^4.3.2", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.2.0", - "semver": "^7.3.5", + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/type-utils": "5.60.1", + "@typescript-eslint/utils": "5.60.1", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", "tsutils": "^3.21.0" }, "dependencies": { @@ -810,15 +2385,15 @@ } }, "@typescript-eslint/parser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", - "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz", + "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/typescript-estree": "5.22.0", - "debug": "^4.3.2" + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", + "debug": "^4.3.4" }, "dependencies": { "debug": { @@ -839,23 +2414,24 @@ } }, "@typescript-eslint/scope-manager": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", - "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0" + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" } }, "@typescript-eslint/type-utils": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.22.0.tgz", - "integrity": "sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz", + "integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.22.0", - "debug": "^4.3.2", + "@typescript-eslint/typescript-estree": "5.60.1", + "@typescript-eslint/utils": "5.60.1", + "debug": "^4.3.4", "tsutils": "^3.21.0" }, "dependencies": { @@ -877,23 +2453,23 @@ } }, "@typescript-eslint/types": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", - "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", - "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0", - "debug": "^4.3.2", - "globby": "^11.0.4", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", + "debug": "^4.3.4", + "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.5", + "semver": "^7.3.7", "tsutils": "^3.21.0" }, "dependencies": { @@ -915,17 +2491,19 @@ } }, "@typescript-eslint/utils": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.22.0.tgz", - "integrity": "sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", + "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", "dev": true, "requires": { + "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/typescript-estree": "5.22.0", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" + "semver": "^7.3.7" }, "dependencies": { "eslint-scope": { @@ -947,13 +2525,13 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", - "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", - "eslint-visitor-keys": "^3.0.0" + "@typescript-eslint/types": "5.60.1", + "eslint-visitor-keys": "^3.3.0" } }, "@ungap/promise-all-settled": { @@ -962,16 +2540,20 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "@wessberg/stringutil": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@wessberg/stringutil/-/stringutil-1.0.19.tgz", + "integrity": "sha512-9AZHVXWlpN8Cn9k5BC/O0Dzb9E9xfEMXzYrNunwvkUTvuK7xgQPVRZpLo+jWCOZ5r8oBa8NIrHuPEu1hzbb6bg==" + }, "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.8", @@ -983,9 +2565,9 @@ } }, "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==" }, "acorn-globals": { "version": "6.0.0", @@ -1015,14 +2597,14 @@ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" }, "adm-zip": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz", - "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==" + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==" }, "after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + "integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==" }, "agent-base": { "version": "6.0.2", @@ -1033,9 +2615,9 @@ }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -1047,6 +2629,34 @@ } } }, + "agentkeepalive": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "optional": true, + "requires": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": 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==", + "optional": true + } + } + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -1091,9 +2701,9 @@ } }, "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -1114,59 +2724,172 @@ "requires": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" }, "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==", - "optional": true, + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.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 + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } }, - "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, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, "requires": { - "safe-buffer": "~5.2.0" + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" } } } }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "array-includes": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", - "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.7" - } - }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1174,15 +2897,280 @@ "dev": true }, "array.prototype.flat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", - "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "dependencies": { + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + } + } + }, + "array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", "es-shim-unscopables": "^1.0.0" + }, + "dependencies": { + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + } } }, "arraybuffer.slice": { @@ -1206,32 +3194,85 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", + "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.4.0", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", + "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.0", + "core-js-compat": "^3.30.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", + "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.0" + } }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==" }, "bail": { "version": "2.0.2", @@ -1246,7 +3287,7 @@ "base64-arraybuffer": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==" }, "base64-js": { "version": "1.5.1", @@ -1266,11 +3307,17 @@ "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "requires": { "tweetnacl": "^0.14.3" } }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true + }, "bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", @@ -1299,12 +3346,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "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==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1313,6 +3360,13 @@ "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": { @@ -1321,6 +3375,13 @@ "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==" + } } } } @@ -1333,7 +3394,7 @@ "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==" }, "body-parser": { "version": "1.20.1", @@ -1354,20 +3415,27 @@ "unpipe": "1.0.0" } }, + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "requires": { + "big-integer": "^1.6.44" + } + }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -1383,6 +3451,18 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserslist": { + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + } + }, "bson": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", @@ -1400,7 +3480,7 @@ "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "buffer-from": { "version": "1.1.2", @@ -1413,14 +3493,31 @@ "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, "builtins": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-4.1.0.tgz", - "integrity": "sha512-1bPRZQtmKaO6h7qV1YHXNtr6nCK28k0Zo95KM4dXfILcZZwoHJBN1m3lfLv9LPkcOZlrSr+J1bzMaZFO98Yq0w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", "dev": true, "requires": { "semver": "^7.0.0" } }, + "bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "requires": { + "run-applescript": "^5.0.0" + } + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1450,6 +3547,17 @@ "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "call-bind": { @@ -1472,15 +3580,21 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, + "caniuse-lite": { + "version": "1.0.30001512", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz", + "integrity": "sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "cassandra-driver": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.3.tgz", - "integrity": "sha512-npW670TXjTHrdb15LUFN01wssb9vvz6SuNYcppesoKcUXx3Q29nXVhRtnvsnkG0BaSnDGvCCR4udrzYLsbh+sg==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.4.tgz", + "integrity": "sha512-SksbIK0cZ2QZRx8ti7w+PnLqldyY+6kU2gRWFChwXFTtrD/ce8cQICDEHxyPwx+DeILwRnMrPf9cjUGizYw9Vg==", "requires": { "@types/long": "^4.0.0", "@types/node": ">=8", @@ -1545,6 +3659,34 @@ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "optional": true }, + "cjstoesm": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cjstoesm/-/cjstoesm-2.1.2.tgz", + "integrity": "sha512-zAbvVcY+HSxy4tTRfskV7DEK7l8nJTjpO9wuQumUu48JWBezqNgt7p9CFe3KACZJtHk8BJECSvl3ccV1qYT2RA==", + "requires": { + "@wessberg/stringutil": "^1.0.19", + "ansi-colors": "^4.1.3", + "commander": "^9.3.0", + "compatfactory": "^1.0.1", + "crosspath": "^2.0.0", + "fast-glob": "^3.2.11", + "helpertypes": "^0.0.18", + "reserved-words": "^0.1.2", + "resolve": "^1.22.0" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==" + } + } + }, "clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -1560,48 +3702,25 @@ "optional": true }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "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": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" } }, "cluster-key-slot": { @@ -1628,6 +3747,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "optional": true }, + "colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1646,10 +3771,24 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "compatfactory": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compatfactory/-/compatfactory-1.0.1.tgz", + "integrity": "sha512-hR9u0HSZTKDNNchPtMHg6myeNx0XO+av7UZIJPsi4rPALJBHi/W5Mbwi19hC/xm6y3JkYpxVYjTqnSGsU5X/iw==", + "requires": { + "helpertypes": "^0.0.18" + } + }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==" }, "component-emitter": { "version": "1.3.0", @@ -1659,17 +3798,45 @@ "component-inherit": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "concurrently": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz", + "integrity": "sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "supports-color": { + "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": "^4.0.0" + } + } + } }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "optional": true }, "content-disposition": { @@ -1678,19 +3845,18 @@ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "requires": { "safe-buffer": "5.2.1" - }, - "dependencies": { - "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==" - } } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "cookie": { "version": "0.4.1", @@ -1716,6 +3882,15 @@ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" }, + "core-js-compat": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", + "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "dev": true, + "requires": { + "browserslist": "^4.21.5" + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -1731,6 +3906,21 @@ "which": "^2.0.1" } }, + "crosspath": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crosspath/-/crosspath-2.0.0.tgz", + "integrity": "sha512-ju88BYCQ2uvjO2bR+SsgLSTwTSctU+6Vp2ePbKPgSCZyy4MWZxYsT738DlKVRE5utUjobjPRm1MkTYKJxCmpTA==", + "requires": { + "@types/node": "^17.0.36" + }, + "dependencies": { + "@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" + } + } + }, "cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -1754,21 +3944,46 @@ "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "requires": { "assert-plus": "^1.0.0" } }, "data-urls": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.1.tgz", - "integrity": "sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "requires": { - "abab": "^2.0.3", + "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^10.0.0" + "whatwg-url": "^11.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.21.0" } }, + "date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1784,14 +3999,37 @@ "dev": true }, "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "requires": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + } + }, + "default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "requires": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + } }, "define-lazy-prop": { "version": "2.0.0", @@ -1799,23 +4037,22 @@ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "object-keys": "^1.0.12" } }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, "denque": { @@ -1893,7 +4130,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -1913,46 +4150,28 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "ejs": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz", - "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", "requires": { "jake": "^10.8.5" } }, - "elasticsearch7": { - "version": "npm:@elastic/elasticsearch@7.17.0", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-7.17.0.tgz", - "integrity": "sha512-5QLPCjd0uLmLj1lSuKSThjNpq39f6NmlTy9ROLFwG5gjyTgpwSqufDeYG/Fm43Xs05uF7WcscoO7eguI3HuuYA==", + "elasticsearch8": { + "version": "npm:@elastic/elasticsearch@8.8.1", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.8.1.tgz", + "integrity": "sha512-ibArPKHEmak3jao7xts2gROATiwPQo9aOrWWdix5mJcX1gnjm/UeJBVO901ROmaxFVPKxVnjC9Op3gJYkqagjg==", "requires": { - "debug": "^4.3.1", - "hpagent": "^0.1.1", - "ms": "^2.1.3", - "secure-json-parse": "^2.4.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "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==" - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } + "@elastic/transport": "^8.3.2", + "tslib": "^2.4.0" } }, + "electron-to-chromium": { + "version": "1.4.450", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz", + "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1984,9 +4203,9 @@ } }, "engine.io": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", - "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.1.tgz", + "integrity": "sha512-dfs8EVg/i7QjFsXxn7cCRQ+Wai1G1TlEvHhdYEi80fxn5R1vZ2K661O6v/rezj1FP234SZ14r9CmJke99iYDGg==", "requires": { "accepts": "~1.3.4", "base64id": "2.0.0", @@ -2017,9 +4236,9 @@ } }, "engine.io-client": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", - "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.3.tgz", + "integrity": "sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==", "requires": { "component-emitter": "~1.3.0", "component-inherit": "0.0.3", @@ -2061,6 +4280,16 @@ "has-binary2": "~1.0.2" } }, + "enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -2074,9 +4303,9 @@ "optional": true }, "es-abstract": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", - "integrity": "sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", @@ -2084,48 +4313,172 @@ "get-intrinsic": "^1.1.1", "get-symbol-description": "^1.0.0", "has": "^1.0.3", - "has-symbols": "^1.0.3", + "has-symbols": "^1.0.2", "internal-slot": "^1.0.3", "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.1", "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", "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": { - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - } } }, "es-aggregate-error": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.8.tgz", - "integrity": "sha512-AKUb5MKLWMozPlFRHOKqWD7yta5uaEhH21qwtnf6FlKjNjTJOoqFi0/G14+FfSkIQhhu6X68Af4xgRC6y8qG4A==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.9.tgz", + "integrity": "sha512-fvnX40sb538wdU6r4s35cq4EY6Lr09Upj40BEVem4LEsuW8XgQep9yD5Q1U2KftokNp1rWODFJ2qwZSsAjFpbg==", "requires": { "define-properties": "^1.1.4", - "es-abstract": "^1.19.5", + "es-abstract": "^1.20.4", "function-bind": "^1.1.1", "functions-have-names": "^1.2.3", - "get-intrinsic": "^1.1.1", - "globalthis": "^1.0.2", + "get-intrinsic": "^1.1.3", + "globalthis": "^1.0.3", "has-property-descriptors": "^1.0.0" + }, + "dependencies": { + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + } + } + }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" } }, "es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -2152,64 +4505,67 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "requires": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1", "source-map": "~0.6.1" } }, "eslint": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", - "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.2", - "@humanwhocodes/config-array": "^0.9.2", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.6.0", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "text-table": "^0.2.0" }, "dependencies": { "ajv": { @@ -2224,46 +4580,6 @@ "uri-js": "^4.2.2" } }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "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 - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2273,117 +4589,49 @@ "ms": "2.1.2" } }, - "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 - }, - "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 - }, "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 - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "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" - } - }, - "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 - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "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" - } - }, - "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" - } } } }, "eslint-config-etherpad": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-3.0.13.tgz", - "integrity": "sha512-Bwt1gDxThlXhY6wan1fb3Jy9kI+yFGctp7+JX6Xs+BwbOdrB4qObgnLKdcLYPKPqv9c4xTSKo3C4BdhTkg7WtQ==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-3.0.15.tgz", + "integrity": "sha512-gdhxFlnkcFQhH1fTvJg1xQMmSoNAlYmGOEr8dQQFiO/AWxI7yBlrNVNlHOAyxfHx6c+KeXlLdofZslfL3su45w==", "dev": true, "requires": { - "@rushstack/eslint-patch": "^1.1.3", - "@typescript-eslint/eslint-plugin": "^5.22.0", - "@typescript-eslint/parser": "^5.22.0", - "eslint-import-resolver-typescript": "^2.7.1", - "eslint-plugin-cypress": "^2.12.1", + "@rushstack/eslint-patch": "^1.3.2", + "@typescript-eslint/eslint-plugin": "^5.60.1", + "@typescript-eslint/parser": "^5.60.1", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-cypress": "^2.13.3", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-mocha": "^10.0.4", - "eslint-plugin-n": "^15.2.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-mocha": "^10.1.0", + "eslint-plugin-n": "^15.7.0", "eslint-plugin-prefer-arrow": "^1.2.3", - "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-promise": "^6.1.1", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0" } }, "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", "dev": true, "requires": { "debug": "^3.2.7", - "resolve": "^1.20.0" + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" }, "dependencies": { "debug": { @@ -2404,16 +4652,19 @@ } }, "eslint-import-resolver-typescript": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", - "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz", + "integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==", "dev": true, "requires": { "debug": "^4.3.4", - "glob": "^7.2.0", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "get-tsconfig": "^4.5.0", + "globby": "^13.1.3", + "is-core-module": "^2.11.0", "is-glob": "^4.0.3", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "synckit": "^0.8.5" }, "dependencies": { "debug": { @@ -2425,22 +4676,40 @@ "ms": "2.1.2" } }, + "globby": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.1.tgz", + "integrity": "sha512-DPCBxctI7dN4EeIqjW2KGqgdcUMbrhJ9AzON+PlxCtvppWhubTLD4+a0GFxiym14ZvacUydTPjLPc2DlKz7EIg==", + "dev": true, + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.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 + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true } } }, "eslint-module-utils": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", - "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "requires": { - "debug": "^3.2.7", - "find-up": "^2.1.0" + "debug": "^3.2.7" }, "dependencies": { "debug": { @@ -2461,9 +4730,9 @@ } }, "eslint-plugin-cypress": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", - "integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.13.3.tgz", + "integrity": "sha512-nAPjZE5WopCsgJwl3vHm5iafpV+ZRO76Z9hMyRygWhmg5ODXDPd+9MaPl7kdJ2azj+sO87H3P1PRnggIrz848g==", "dev": true, "requires": { "globals": "^11.12.0" @@ -2512,29 +4781,48 @@ "requires": { "escape-string-regexp": "^1.0.5", "ignore": "^5.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + } } }, "eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", "dev": true, "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", "has": "^1.0.3", - "is-core-module": "^2.8.1", + "is-core-module": "^2.11.0", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", "tsconfig-paths": "^3.14.1" }, "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" + } + }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2543,41 +4831,45 @@ "requires": { "esutils": "^2.0.2" } + }, + "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 + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, "eslint-plugin-mocha": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.0.4.tgz", - "integrity": "sha512-8wzAeepVY027oBHz/TmBmUr7vhVqoC1KTFeDybFLhbaWKx+aQ7fJJVuUsqcUy+L+G+XvgQBJY+cbAf7hl5DF7Q==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.1.0.tgz", + "integrity": "sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==", "dev": true, "requires": { "eslint-utils": "^3.0.0", - "ramda": "^0.28.0" + "rambda": "^7.1.0" } }, "eslint-plugin-n": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.2.0.tgz", - "integrity": "sha512-lWLg++jGwC88GDGGBX3CMkk0GIWq0y41aH51lavWApOKcMQcYoL3Ayd0lEdtD3SnQtR+3qBvWQS3qGbR2BxRWg==", + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", "dev": true, "requires": { - "builtins": "^4.0.0", + "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", "eslint-utils": "^3.0.0", "ignore": "^5.1.1", - "is-core-module": "^2.3.0", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" } }, "eslint-plugin-prefer-arrow": { @@ -2587,9 +4879,9 @@ "dev": true }, "eslint-plugin-promise": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", - "integrity": "sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", "dev": true }, "eslint-plugin-you-dont-need-lodash-underscore": { @@ -2602,9 +4894,9 @@ } }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -2629,9 +4921,9 @@ } }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true }, "esm": { @@ -2641,14 +4933,14 @@ "optional": true }, "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", "dev": true, "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^3.3.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" } }, "esprima": { @@ -2657,9 +4949,9 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -2679,6 +4971,11 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2690,9 +4987,9 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "etherpad-cli-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-2.0.1.tgz", - "integrity": "sha512-cv7ep8NEkrebTIgWS/SBvpt6DhcMKSNu1zZNMFOWdoQkNRn3hVXZU8dedr4Xt5M1zBwPBSBTjisU436/TkEESA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/etherpad-cli-client/-/etherpad-cli-client-2.0.2.tgz", + "integrity": "sha512-yPsTD7cfGhnfd4fcrIW7Y8/ru+n93YpriP4Av83O35VYzMQVusgIaRWQ4xj+rbZkd5MBE+nmFaxIGkv0joJSSQ==", "dev": true, "requires": { "async": "^3.2.1", @@ -2732,12 +5029,6 @@ "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", @@ -2786,6 +5077,73 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, + "execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "dependencies": { + "human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } + } + }, "express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -2828,11 +5186,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "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==" } } }, @@ -2854,13 +5207,6 @@ "parseurl": "~1.3.3", "safe-buffer": "5.2.1", "uid-safe": "~2.1.5" - }, - "dependencies": { - "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==" - } } }, "extend": { @@ -2871,7 +5217,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "fast-deep-equal": { "version": "3.1.3", @@ -2879,10 +5225,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2895,7 +5240,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -2910,7 +5254,8 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "fast-safe-stringify": { "version": "2.1.1", @@ -2918,10 +5263,9 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "requires": { "reusify": "^1.0.4" } @@ -2936,21 +5280,13 @@ } }, "filelist": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.3.tgz", - "integrity": "sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "requires": { "minimatch": "^5.0.1" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -2965,7 +5301,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -2984,18 +5319,48 @@ "unpipe": "~1.0.0" } }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "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": "^2.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, "flat": { @@ -3015,20 +5380,27 @@ } }, "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "follow-redirects": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", - "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==" + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "form-data": { "version": "4.0.0", @@ -3061,6 +5433,23 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3068,8 +5457,25 @@ "optional": true, "requires": { "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3087,11 +5493,16 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } }, "functions-have-names": { "version": "1.2.3", @@ -3113,23 +5524,6 @@ "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "generic-pool": { @@ -3137,6 +5531,12 @@ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==" }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3144,15 +5544,22 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1" + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" } }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, "get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -3162,10 +5569,19 @@ "get-intrinsic": "^1.1.1" } }, + "get-tsconfig": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz", + "integrity": "sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "requires": { "assert-plus": "^1.0.0" } @@ -3193,18 +5609,18 @@ } }, "globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, "globalthis": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", - "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", "requires": { "define-properties": "^1.1.3" } @@ -3223,11 +5639,30 @@ "slash": "^3.0.0" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "optional": true + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "growl": { "version": "1.10.5", @@ -3238,7 +5673,7 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" }, "har-validator": { "version": "5.1.5", @@ -3276,9 +5711,9 @@ } }, "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" }, "has-binary2": { "version": "1.0.3", @@ -3291,14 +5726,14 @@ "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" } } }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==" }, "has-flag": { "version": "4.0.0", @@ -3313,10 +5748,15 @@ "get-intrinsic": "^1.1.1" } }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-tostringtag": { "version": "1.0.0", @@ -3329,7 +5769,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "optional": true }, "hast-util-embedded": { @@ -3412,15 +5852,20 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "helpertypes": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/helpertypes/-/helpertypes-0.0.18.tgz", + "integrity": "sha512-XRhfbSEmR+poXUC5/8AbmYNJb2riOT6qPzjGJZr0S9YedHiaY+/tzPYzWMUclYMEdCYo/1l8PDYrQFCj02v97w==" + }, "hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" }, "hpagent": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz", - "integrity": "sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==" }, "html-encoding-sniffer": { "version": "3.0.0", @@ -3491,9 +5936,9 @@ }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -3508,7 +5953,7 @@ "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -3516,18 +5961,18 @@ } }, "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "requires": { "agent-base": "6", "debug": "4" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -3539,15 +5984,36 @@ } } }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "optional": true, "requires": { "ms": "^2.0.0" } }, + "i18next": { + "version": "23.2.7", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.2.7.tgz", + "integrity": "sha512-EsbHHvF6b+p+B6Cht5fYWY7VE/WYOrqB1+DNwa1UpLTw6mG5g4tc8KCEjUUOSMUA2yqCsdYQP+PqVq5nBMtOtQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.22.5" + } + }, + "i18next-fs-backend": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.5.tgz", + "integrity": "sha512-7fgSH8nVhXSBYPHR/W3tEXXhcnwHwNiND4Dfx9knzPzdsWTUTL/TdDVV+DY0dL0asHKLbdoJaXS4LdVW6R8MVQ==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3562,9 +6028,15 @@ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, "immediate": { @@ -3597,7 +6069,7 @@ "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==" }, "infer-owner": { "version": "1.0.4", @@ -3640,6 +6112,16 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, "is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -3677,10 +6159,9 @@ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "requires": { "has": "^1.0.3" } @@ -3701,24 +6182,38 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "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==", - "optional": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "requires": { "is-extglob": "^2.1.1" } }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + }, + "dependencies": { + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + } + } + }, "is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -3733,13 +6228,12 @@ "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 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", "requires": { "has-tostringtag": "^1.0.0" } @@ -3749,11 +6243,26 @@ "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==" }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-plain-obj": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz", "integrity": "sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw==" }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, "is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3765,6 +6274,15 @@ "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=", "dev": true }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3775,12 +6293,15 @@ } }, "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "requires": { - "call-bind": "^1.0.2" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true }, "is-string": { "version": "1.0.7", @@ -3798,10 +6319,22 @@ "has-symbols": "^1.0.2" } }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "is-unicode-supported": { "version": "0.1.0", @@ -3828,27 +6361,34 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", "requires": { "async": "^3.2.3", "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" + "filelist": "^1.0.4", + "minimatch": "^3.1.2" } }, "js-cookie": { @@ -3856,6 +6396,17 @@ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" }, + "js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3865,14 +6416,14 @@ } }, "jsbi": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.5.tgz", - "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", + "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "jsdom": { "version": "19.0.0", @@ -3908,6 +6459,12 @@ "xml-name-validator": "^4.0.0" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, "json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -3921,13 +6478,13 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "json5": { "version": "1.0.2", @@ -3938,6 +6495,14 @@ "minimist": "^1.2.0" } }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsonminify": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.2.tgz", @@ -3955,6 +6520,33 @@ "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=", "dev": true }, + "jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "requires": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "dependencies": { + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -3999,6 +6591,12 @@ "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", @@ -4048,9 +6646,15 @@ } }, "kebab-case": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.1.tgz", - "integrity": "sha512-txPHx6nVLhv8PHGXIlAk0nYoh894SpAqGPXNvbg2hh8spvHXIah3+vT87DLoa59nKgC6scD3u3xAuRIgiMqbfQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", + "integrity": "sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "languages4translatewiki": { @@ -4059,12 +6663,13 @@ "integrity": "sha1-xDYgbgUtIUkLEQF6RNURj5Ih5ds=" }, "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "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.1.2", - "type-check": "~0.3.2" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, "lie": { @@ -4077,13 +6682,12 @@ } }, "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "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": "^2.0.0", - "path-exists": "^3.0.0" + "p-locate": "^5.0.0" } }, "lodash": { @@ -4096,10 +6700,16 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, "lodash.merge": { @@ -4115,79 +6725,39 @@ "requires": { "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": { - "version": "0.6.38", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", - "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", "requires": { - "readable-stream": "~1.0.2", - "semver": "~4.3.3" + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" }, "dependencies": { - "semver": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=" + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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==" } } }, "long": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", - "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" + "integrity": "sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ==" }, "lru-cache": { "version": "6.0.0", @@ -4197,11 +6767,19 @@ "yallist": "^4.0.0" } }, + "magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, "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" }, @@ -4209,8 +6787,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "optional": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -4244,17 +6821,6 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "optional": true }, - "agentkeepalive": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", - "optional": true, - "requires": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4275,6 +6841,15 @@ "debug": "4" } }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4308,22 +6883,26 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -4347,27 +6926,42 @@ "mime-db": "1.52.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + } } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true }, "minipass": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", - "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", - "optional": true, - "requires": { - "yallist": "^4.0.0" - } + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true }, "minipass-collect": { "version": "1.0.2", @@ -4376,6 +6970,17 @@ "optional": true, "requires": { "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "minipass-fetch": { @@ -4388,6 +6993,17 @@ "minipass": "^3.1.0", "minipass-sized": "^1.0.3", "minizlib": "^2.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "minipass-flush": { @@ -4397,6 +7013,17 @@ "optional": true, "requires": { "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "minipass-pipeline": { @@ -4406,6 +7033,17 @@ "optional": true, "requires": { "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "minipass-sized": { @@ -4415,6 +7053,17 @@ "optional": true, "requires": { "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "minizlib": { @@ -4425,6 +7074,17 @@ "requires": { "minipass": "^3.0.0", "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "mkdirp": { @@ -4465,6 +7125,27 @@ "yargs-unparser": "2.0.0" }, "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "cliui": { + "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": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -4482,37 +7163,6 @@ } } }, - "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 - }, - "find-up": { - "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": "^6.0.0", - "path-exists": "^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 - }, - "locate-path": { - "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": "^5.0.0" - } - }, "minimatch": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", @@ -4528,36 +7178,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "p-limit": { - "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": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "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": "^3.0.2" - } - }, - "path-exists": { - "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 - }, - "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": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -4566,6 +7186,27 @@ "requires": { "has-flag": "^4.0.0" } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true } } }, @@ -4584,9 +7225,9 @@ } }, "mongodb": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.3.tgz", - "integrity": "sha512-Psm+g3/wHXhjBEktkxXsFMZvd3nemI0r3IPsE0bU+4//PnvNWKkzhZcEsbPcYiWqe8XqXJJEg4Tgtr7Raw67Yw==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz", + "integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==", "requires": { "bl": "^2.2.1", "bson": "^1.1.4", @@ -4602,22 +7243,22 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mssql": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-8.1.0.tgz", - "integrity": "sha512-S7j4MoanTCLM09I+wMI9thTS2342mgxCpOQ9kpnFiG3P1NStuQMhPILLOgOt6hwMa/ctfTUKl7eJpB5XGPoe6A==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-9.1.1.tgz", + "integrity": "sha512-m0yTx9xzUtTvJpWJHqknUXUDPRnJXZYOOFNygnNIXn1PBkLsC/rkXQdquObd+M0ZPlBhGC00Jg28zG0wCl7VWg==", "requires": { - "@tediousjs/connection-string": "^0.3.0", - "commander": "^9.1.0", + "@tediousjs/connection-string": "^0.4.1", + "commander": "^9.4.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", - "tedious": "^14.0.0" + "tedious": "^15.0.1" }, "dependencies": { "commander": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", - "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==" + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==" }, "debug": { "version": "4.3.4", @@ -4648,7 +7289,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "readable-stream": { "version": "2.3.7", @@ -4664,6 +7305,11 @@ "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", @@ -4674,6 +7320,16 @@ } } }, + "nano": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/nano/-/nano-10.1.2.tgz", + "integrity": "sha512-P3zSoD/sxAgDs/IE9eqpeAXqTdQ/gA9e9dnzaltr4A3WUo/n+eh66T873L+md5v8lXOutX/7dvcHFOO22f5hDw==", + "requires": { + "axios": "^1.2.2", + "node-abort-controller": "^3.0.1", + "qs": "^6.11.0" + } + }, "nanoid": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", @@ -4683,12 +7339,18 @@ "native-duplexpair": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", - "integrity": "sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A=" + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==" }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, "negotiator": { @@ -4697,18 +7359,47 @@ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", "dev": true, "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" }, "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -4721,14 +7412,21 @@ } }, "node-abort-controller": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz", - "integrity": "sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "optional": true }, "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "optional": true, "requires": { "whatwg-url": "^5.0.0" }, @@ -4736,17 +7434,20 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -4754,6 +7455,70 @@ } } }, + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + } + } + }, + "node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, "nodeify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nodeify/-/nodeify-1.0.1.tgz", @@ -4764,6 +7529,62 @@ "promise": "~1.3.0" } }, + "nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "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" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "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 + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -4780,9 +7601,9 @@ "dev": true }, "npm": { - "version": "6.14.16", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.16.tgz", - "integrity": "sha512-LMiLGYsVNJfVPlQg7v2NYjG7iRIapcLv+oMunlq7fkXVx0BATCjRu7XyWl0G+iuZzHy4CjtM32QB8ox8juTgaw==", + "version": "6.14.18", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.18.tgz", + "integrity": "sha512-p3SjqSchSuNQUqbJBgwdv0L3O6bKkaSfQrQzJsskNpNKLg0g37c5xTXFV0SqTlX9GWvoGxBELVJMRWq0J8oaLA==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -4791,9 +7612,9 @@ "aproba": "^2.0.0", "archy": "~1.0.0", "bin-links": "^1.1.8", - "bluebird": "^3.5.5", + "bluebird": "^3.7.2", "byte-size": "^5.0.1", - "cacache": "^12.0.3", + "cacache": "^12.0.4", "call-limit": "^1.1.1", "chownr": "^1.1.4", "ci-info": "^2.0.0", @@ -4801,19 +7622,19 @@ "cli-table3": "^0.5.1", "cmd-shim": "^3.0.3", "columnify": "~1.5.4", - "config-chain": "^1.1.12", + "config-chain": "^1.1.13", "debuglog": "*", "detect-indent": "~5.0.0", "detect-newline": "^2.1.0", - "dezalgo": "~1.0.3", + "dezalgo": "^1.0.4", "editor": "~1.0.0", - "figgy-pudding": "^3.5.1", + "figgy-pudding": "^3.5.2", "find-npm-prefix": "^1.0.2", "fs-vacuum": "~1.2.10", "fs-write-stream-atomic": "~1.0.10", "gentle-fs": "^2.3.1", - "glob": "^7.1.6", - "graceful-fs": "^4.2.4", + "glob": "^7.2.3", + "graceful-fs": "^4.2.10", "has-unicode": "~2.0.1", "hosted-git-info": "^2.8.9", "iferr": "^1.0.2", @@ -4823,7 +7644,7 @@ "inherits": "^2.0.4", "ini": "^1.3.8", "init-package-json": "^1.10.3", - "is-cidr": "^3.0.0", + "is-cidr": "^3.1.1", "json-parse-better-errors": "^1.0.2", "lazy-property": "~1.0.0", "libcipm": "^4.0.8", @@ -4834,7 +7655,7 @@ "libnpmsearch": "^2.0.2", "libnpmteam": "^1.0.2", "libnpx": "^10.2.4", - "lock-verify": "^2.1.0", + "lock-verify": "^2.2.2", "lockfile": "^1.0.4", "lodash._baseindexof": "*", "lodash._baseuniq": "~4.6.0", @@ -4848,11 +7669,11 @@ "lodash.uniq": "~4.5.0", "lodash.without": "~4.4.0", "lru-cache": "^5.1.1", - "meant": "^1.0.2", + "meant": "^1.0.3", "mississippi": "^3.0.0", - "mkdirp": "^0.5.5", + "mkdirp": "^0.5.6", "move-concurrently": "^1.0.1", - "node-gyp": "^5.1.0", + "node-gyp": "^5.1.1", "nopt": "^4.0.3", "normalize-package-data": "^2.5.0", "npm-audit-report": "^1.3.3", @@ -4873,19 +7694,19 @@ "path-is-inside": "~1.0.2", "promise-inflight": "~1.0.1", "qrcode-terminal": "^0.12.0", - "query-string": "^6.8.2", - "qw": "~1.0.1", + "query-string": "^6.14.1", + "qw": "^1.0.2", "read": "~1.0.7", "read-cmd-shim": "^1.0.5", "read-installed": "~4.0.3", - "read-package-json": "^2.1.1", + "read-package-json": "^2.1.2", "read-package-tree": "^5.3.1", "readable-stream": "^3.6.0", "readdir-scoped-modules": "^1.1.0", - "request": "^2.88.0", + "request": "^2.88.2", "retry": "^0.12.0", "rimraf": "^2.7.1", - "safe-buffer": "^5.1.2", + "safe-buffer": "^5.2.1", "semver": "^5.7.1", "sha": "^3.0.0", "slide": "~1.1.6", @@ -4901,7 +7722,7 @@ "unique-filename": "^1.1.1", "unpipe": "~1.0.0", "update-notifier": "^2.5.0", - "uuid": "^3.3.3", + "uuid": "^3.4.0", "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "~3.0.0", "which": "^1.3.1", @@ -4909,6 +7730,14 @@ "write-file-atomic": "^2.4.3" }, "dependencies": { + "@iarna/cli": { + "version": "2.1.0", + "bundled": true, + "requires": { + "glob": "^7.1.2", + "signal-exit": "^3.0.2" + } + }, "JSONStream": { "version": "1.3.5", "bundled": true, @@ -4988,6 +7817,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -4995,6 +7830,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -5004,7 +7845,7 @@ "bundled": true }, "asn1": { - "version": "0.2.4", + "version": "0.2.6", "bundled": true, "requires": { "safer-buffer": "~2.1.0" @@ -5023,17 +7864,16 @@ "bundled": true }, "aws4": { - "version": "1.8.0", + "version": "1.11.0", "bundled": true }, "balanced-match": { - "version": "1.0.0", + "version": "1.0.2", "bundled": true }, "bcrypt-pbkdf": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "tweetnacl": "^0.14.3" } @@ -5051,7 +7891,7 @@ } }, "bluebird": { - "version": "3.5.5", + "version": "3.7.2", "bundled": true }, "boxen": { @@ -5092,7 +7932,7 @@ "bundled": true }, "cacache": { - "version": "12.0.3", + "version": "12.0.4", "bundled": true, "requires": { "bluebird": "^3.5.5", @@ -5183,7 +8023,7 @@ }, "dependencies": { "ansi-regex": { - "version": "4.1.0", + "version": "4.1.1", "bundled": true }, "is-fullwidth-code-point": { @@ -5249,7 +8089,7 @@ } }, "combined-stream": { - "version": "1.0.6", + "version": "1.0.8", "bundled": true, "requires": { "delayed-stream": "~1.0.0" @@ -5280,6 +8120,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -5287,12 +8133,18 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } }, "config-chain": { - "version": "1.1.12", + "version": "1.1.13", "bundled": true, "requires": { "ini": "^1.3.4", @@ -5408,7 +8260,7 @@ "bundled": true }, "decode-uri-component": { - "version": "0.2.0", + "version": "0.2.2", "bundled": true }, "deep-extend": { @@ -5446,7 +8298,7 @@ "bundled": true }, "dezalgo": { - "version": "1.0.3", + "version": "1.0.4", "bundled": true, "requires": { "asap": "^2.0.0", @@ -5489,6 +8341,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -5496,6 +8354,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -5503,7 +8367,6 @@ "ecc-jsbn": { "version": "0.1.2", "bundled": true, - "optional": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -5532,7 +8395,7 @@ } }, "env-paths": { - "version": "2.2.0", + "version": "2.2.1", "bundled": true }, "err-code": { @@ -5613,7 +8476,11 @@ "bundled": true }, "figgy-pudding": { - "version": "3.5.1", + "version": "3.5.2", + "bundled": true + }, + "filter-obj": { + "version": "1.1.0", "bundled": true }, "find-npm-prefix": { @@ -5639,6 +8506,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -5646,6 +8519,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -5655,11 +8534,11 @@ "bundled": true }, "form-data": { - "version": "2.3.2", + "version": "2.3.3", "bundled": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "1.0.6", + "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, @@ -5682,6 +8561,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -5689,6 +8574,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -5744,6 +8635,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -5751,6 +8648,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -5842,15 +8745,24 @@ } }, "glob": { - "version": "7.1.6", + "version": "7.2.3", "bundled": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "global-dirs": { @@ -5884,7 +8796,7 @@ } }, "graceful-fs": { - "version": "4.2.4", + "version": "4.2.10", "bundled": true }, "har-schema": { @@ -6064,7 +8976,7 @@ } }, "is-cidr": { - "version": "3.0.0", + "version": "3.1.1", "bundled": true, "requires": { "cidr-regex": "^2.0.10" @@ -6148,13 +9060,16 @@ }, "jsbn": { "version": "0.1.1", - "bundled": true, - "optional": true + "bundled": true }, "json-parse-better-errors": { "version": "1.0.2", "bundled": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "bundled": true + }, "json-schema": { "version": "0.4.0", "bundled": true @@ -6358,9 +9273,10 @@ } }, "lock-verify": { - "version": "2.1.0", + "version": "2.2.2", "bundled": true, "requires": { + "@iarna/cli": "^2.1.0", "npm-package-arg": "^6.1.0", "semver": "^5.4.1" } @@ -6467,7 +9383,7 @@ } }, "meant": { - "version": "1.0.2", + "version": "1.0.3", "bundled": true }, "mime-db": { @@ -6481,6 +9397,17 @@ "mime-db": "~1.35.0" } }, + "minimatch": { + "version": "3.1.2", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "bundled": true + }, "minizlib": { "version": "1.3.3", "bundled": true, @@ -6515,10 +9442,10 @@ } }, "mkdirp": { - "version": "0.5.5", + "version": "0.5.6", "bundled": true, "requires": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" } }, "move-concurrently": { @@ -6557,7 +9484,7 @@ } }, "node-gyp": { - "version": "5.1.0", + "version": "5.1.1", "bundled": true, "requires": { "env-paths": "^2.2.0", @@ -6856,6 +9783,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -6863,6 +9796,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -6948,7 +9887,7 @@ "bundled": true }, "psl": { - "version": "1.1.29", + "version": "1.9.0", "bundled": true }, "pump": { @@ -6978,30 +9917,26 @@ } } }, - "punycode": { - "version": "1.4.1", - "bundled": true - }, "qrcode-terminal": { "version": "0.12.0", "bundled": true }, "qs": { "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + "bundled": true }, "query-string": { - "version": "6.8.2", + "version": "6.14.1", "bundled": true, "requires": { "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "qw": { - "version": "1.0.1", + "version": "1.0.2", "bundled": true }, "rc": { @@ -7042,12 +9977,11 @@ } }, "read-package-json": { - "version": "2.1.1", + "version": "2.1.2", "bundled": true, "requires": { "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "normalize-package-data": "^2.0.0", "npm-normalize-package-bin": "^1.0.0" } @@ -7096,7 +10030,7 @@ } }, "request": { - "version": "2.88.0", + "version": "2.88.2", "bundled": true, "requires": { "aws-sign2": "~0.7.0", @@ -7106,7 +10040,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -7116,7 +10050,7 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } @@ -7158,7 +10092,7 @@ } }, "safe-buffer": { - "version": "5.1.2", + "version": "5.2.1", "bundled": true }, "safer-buffer": { @@ -7304,7 +10238,7 @@ "bundled": true }, "sshpk": { - "version": "1.14.2", + "version": "1.17.0", "bundled": true, "requires": { "asn1": "~0.2.3", @@ -7352,6 +10286,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -7359,6 +10299,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -7500,6 +10446,12 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } }, "string_decoder": { @@ -7507,6 +10459,12 @@ "bundled": true, "requires": { "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "bundled": true + } } } } @@ -7520,11 +10478,17 @@ "bundled": true }, "tough-cookie": { - "version": "2.4.3", + "version": "2.5.0", "bundled": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "bundled": true + } } }, "tunnel-agent": { @@ -7536,8 +10500,7 @@ }, "tweetnacl": { "version": "0.14.5", - "bundled": true, - "optional": true + "bundled": true }, "typedarray": { "version": "0.0.6", @@ -7597,7 +10560,7 @@ } }, "uri-js": { - "version": "4.4.0", + "version": "4.4.1", "bundled": true, "requires": { "punycode": "^2.1.0" @@ -7632,7 +10595,7 @@ } }, "uuid": { - "version": "3.3.3", + "version": "3.4.0", "bundled": true }, "validate-npm-package-license": { @@ -7719,7 +10682,7 @@ }, "dependencies": { "ansi-regex": { - "version": "4.1.0", + "version": "4.1.1", "bundled": true }, "is-fullwidth-code-point": { @@ -7865,6 +10828,15 @@ } } }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -7878,9 +10850,9 @@ } }, "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.6.tgz", + "integrity": "sha512-vSZ4miHQ4FojLjmz2+ux4B0/XA16jfwt/LBzIUftDpRd8tujHFkXjMyLwjS08fIZCzesj2z7gJukOKJwqebJAQ==" }, "oauth-sign": { "version": "0.9.0", @@ -7890,13 +10862,13 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "optional": true }, "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, "object-keys": { "version": "1.1.1", @@ -7915,14 +10887,146 @@ } }, "object.values": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", - "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "dependencies": { + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + } } }, "observable-fns": { @@ -7946,15 +11050,24 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "requires": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -8026,34 +11139,35 @@ } }, "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "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" + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" } }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "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": "^1.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "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": "^1.1.0" + "p-limit": "^3.0.2" } }, "p-map": { @@ -8066,9 +11180,9 @@ } }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "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": { @@ -8112,9 +11226,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": { @@ -8146,26 +11260,33 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "pg": { - "version": "8.7.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", - "integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==", + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.1.tgz", + "integrity": "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.5.0", - "pg-pool": "^3.5.1", - "pg-protocol": "^1.5.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.1", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", "pg-types": "^2.1.0", "pgpass": "1.x" } }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, "pg-connection-string": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", - "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, "pg-int8": { "version": "1.0.1", @@ -8173,14 +11294,14 @@ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, "pg-pool": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz", - "integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==" + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==" }, "pg-protocol": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", - "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" }, "pg-types": { "version": "2.2.0", @@ -8202,12 +11323,83 @@ "split2": "^4.1.0" } }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.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==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true + } + } + }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -8216,7 +11408,7 @@ "postgres-bytea": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" }, "postgres-date": { "version": "1.0.7", @@ -8232,14 +11424,10 @@ } }, "prelude-ls": { - "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", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true }, "process-nextick-args": { "version": "2.0.1", @@ -8291,14 +11479,20 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, "qs": { "version": "6.11.0", @@ -8308,16 +11502,20 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, - "ramda": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz", - "integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==", + "rambda": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", + "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", "dev": true }, "random-bytes": { @@ -8356,14 +11554,14 @@ } }, "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, "readdirp": { @@ -8375,12 +11573,107 @@ "picomatch": "^2.2.1" } }, + "redis": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.7.tgz", + "integrity": "sha512-KrkuNJNpCwRm5vFJh0tteMxW8SaUzkm5fBH7eL5hd/D0fAkzvapxbfGPP/r+4JAXdQuX7nebsBkBqA2RHB7Usw==", + "requires": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.8", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.3", + "@redis/time-series": "1.0.4" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "dependencies": { + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + } + } + }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, "rehype": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz", @@ -8487,7 +11780,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, "require-from-string": { @@ -8495,78 +11788,265 @@ "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==" }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "reserved-words": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", + "integrity": "sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==" + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, + "rethinkdb": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/rethinkdb/-/rethinkdb-2.4.2.tgz", + "integrity": "sha512-6DzwqEpFc8cqesAdo07a845oBRxLiHvWzopTKBo/uY2ypGWIsJQFJk3wjRDtSEhczxJqLS0jnf37rwgzYAw8NQ==", + "requires": { + "bluebird": ">= 2.3.2 < 3" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "rollup-plugin-copy": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz", + "integrity": "sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==", + "dev": true, + "requires": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "dependencies": { + "globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } + }, + "is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true + } + } + }, + "rollup-plugin-typescript2": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.35.0.tgz", + "integrity": "sha512-szcIO9hPUx3PhQl91u4pfNAH2EKbtrXaES+m163xQVE5O1CC0ea6YZV/5woiDDW3CR9jF2CszPrKN+AFiND0bg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^4.1.2", + "find-cache-dir": "^3.3.2", + "fs-extra": "^10.0.0", + "semver": "^7.3.7", + "tslib": "^2.4.0" + }, + "dependencies": { + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "execa": "^5.0.0" }, "dependencies": { - "is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "requires": { - "has": "^1.0.3" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" } } } }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "rethinkdb": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/rethinkdb/-/rethinkdb-2.4.2.tgz", - "integrity": "sha512-6DzwqEpFc8cqesAdo07a845oBRxLiHvWzopTKBo/uY2ypGWIsJQFJk3wjRDtSEhczxJqLS0jnf37rwgzYAw8NQ==", - "requires": { - "bluebird": ">= 2.3.2 < 3" - } - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "optional": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "requires": { "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, "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==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } }, "safer-buffer": { "version": "2.1.2", @@ -8582,11 +12062,6 @@ "sparse-bitfield": "^3.0.3" } }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "saxes": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", @@ -8614,20 +12089,12 @@ "jszip": "^3.10.1", "tmp": "^0.2.1", "ws": ">=8.13.0" - }, - "dependencies": { - "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true - } } }, "semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "requires": { "lru-cache": "^6.0.0" } @@ -8682,13 +12149,13 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "optional": true }, "set-cookie-parser": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", - "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", "dev": true }, "setimmediate": { @@ -8702,6 +12169,15 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8715,6 +12191,12 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -8728,13 +12210,12 @@ "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "optional": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "simple-git": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz", - "integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==", + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.1.tgz", + "integrity": "sha512-Ck+rcjVaE1HotraRAS8u/+xgTvToTuoMkT9/l9lvuP5jftwnYUp6DwuJzsKErHgfyRk8IB8pqGHWEbM3tLgV1w==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", @@ -8756,6 +12237,23 @@ } } }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, "sinon": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.2.tgz", @@ -8768,23 +12266,6 @@ "diff": "^5.0.0", "nise": "^5.1.1", "supports-color": "^7.2.0" - }, - "dependencies": { - "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" - } - } } }, "slash": { @@ -8800,15 +12281,15 @@ "optional": true }, "socket.io": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz", - "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.5.0.tgz", + "integrity": "sha512-gGunfS0od3VpwDBpGwVkzSZx6Aqo9uOcf1afJj2cKnKFAoyl16fvhpsUhmUFd4Ldbvl5JvRQed6eQw6oQp6n8w==", "requires": { "debug": "~4.1.0", - "engine.io": "~3.5.0", + "engine.io": "~3.6.0", "has-binary2": "~1.0.2", "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.4.0", + "socket.io-client": "2.5.0", "socket.io-parser": "~3.4.0" }, "dependencies": { @@ -8833,9 +12314,9 @@ "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" }, "socket.io-client": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", - "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.5.0.tgz", + "integrity": "sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==", "requires": { "backo2": "1.0.2", "component-bind": "1.0.0", @@ -8861,7 +12342,7 @@ "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" }, "socket.io-parser": { "version": "3.3.3", @@ -8970,12 +12451,18 @@ "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", "optional": true, "requires": { "memory-pager": "^1.0.2" } }, + "spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "split-grid": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/split-grid/-/split-grid-1.0.11.tgz", @@ -8983,19 +12470,31 @@ "dev": true }, "split2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", - "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, "sprintf-js": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" }, + "sqlite3": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", + "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", + "optional": true, + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^4.2.0", + "node-gyp": "8.x", + "tar": "^6.1.11" + } + }, "sqlstring": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==" }, "sshpk": { "version": "1.17.0", @@ -9020,6 +12519,17 @@ "optional": true, "requires": { "minipass": "^3.1.1" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + } } }, "statuses": { @@ -9032,11 +12542,40 @@ "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" }, + "streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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==" + } + } + }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -9046,20 +12585,150 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, "requires": { "ansi-regex": "^5.0.1" } } } }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "dependencies": { + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + } + } + }, "string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", @@ -9079,9 +12748,13 @@ } }, "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "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" + } }, "stringify-entities": { "version": "4.0.2", @@ -9092,10 +12765,30 @@ "character-entities-legacy": "^3.0.0" } }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, "strip-json-comments": { @@ -9129,26 +12822,6 @@ "ms": "2.1.2" } }, - "dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", - "requires": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - } - }, "mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -9158,22 +12831,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -9211,15 +12868,31 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", "optional": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -9231,28 +12904,28 @@ "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==" }, "tedious": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-14.5.0.tgz", - "integrity": "sha512-Mr/ku6J0yku9MvWKO7e//awwI52122jS5AYRz/VOI2jZZawv84iHPKF/FnHBoIEKlRjzahrtevfpNktw/eBAEw==", - "requires": { - "@azure/identity": "^2.0.1", - "@azure/keyvault-keys": "^4.3.0", - "@js-joda/core": "^4.0.0", - "@types/es-aggregate-error": "^1.0.2", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-15.1.3.tgz", + "integrity": "sha512-166EpRm5qknwhEisjZqz/mF7k14fXKJYHRg6XiAXVovd/YkyHJ3SG4Ppy89caPaNFfRr7PVYe+s4dAvKaCMFvw==", + "requires": { + "@azure/identity": "^2.0.4", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.2.0", "bl": "^5.0.0", - "es-aggregate-error": "^1.0.7", + "es-aggregate-error": "^1.0.8", "iconv-lite": "^0.6.3", - "jsbi": "^3.2.1", + "js-md4": "^0.3.2", + "jsbi": "^4.3.0", "native-duplexpair": "^1.0.0", - "node-abort-controller": "^3.0.0", + "node-abort-controller": "^3.0.1", "punycode": "^2.1.0", "sprintf-js": "^1.1.2" }, "dependencies": { "bl": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.0.0.tgz", - "integrity": "sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", "requires": { "buffer": "^6.0.3", "inherits": "^2.0.4", @@ -9273,20 +12946,15 @@ "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==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "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", @@ -9298,12 +12966,12 @@ } }, "terser": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.5.tgz", - "integrity": "sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz", + "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==", "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" } @@ -9311,7 +12979,7 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "threads": { @@ -9355,6 +13023,12 @@ "resolved": "https://registry.npmjs.org/tinycon/-/tinycon-0.6.8.tgz", "integrity": "sha1-59oiPj7gy/nbeWP6M1aZuyF3enM=" }, + "titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -9367,25 +13041,51 @@ "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + "integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "requires": { "is-number": "^7.0.0" } }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, "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==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" } }, "tr46": { @@ -9396,27 +13096,33 @@ "punycode": "^2.1.1" } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "trough": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/trough/-/trough-2.0.2.tgz", "integrity": "sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w==" }, "tsconfig-paths": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", - "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", "dev": true, "requires": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" }, "tsutils": { "version": "3.21.0", @@ -9435,15 +13141,10 @@ } } }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "requires": { "safe-buffer": "^5.0.1" } @@ -9451,14 +13152,15 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "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.1.2" + "prelude-ls": "^1.2.1" } }, "type-detect": { @@ -9482,6 +13184,16 @@ "mime-types": "~2.1.24" } }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, "typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -9489,171 +13201,270 @@ "dev": true }, "ueberdb2": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-4.0.1.tgz", - "integrity": "sha512-wo377quP2M/g3j7LoTMMVVX7ED3gj1sk8bkFR1plNEh/TLEgumdjfJl5KmpkXfIj2j6VYmQY4yEhwszpyRRRFg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-4.1.5.tgz", + "integrity": "sha512-nXnOjXaORcj6zDsvv3YzohSn46yUuBRZen5+Y7temBf/zaxkOx/3gtR2/nG/A67FhVxZIHychxQ+mwsVbAyLBA==", "requires": { - "async": "^3.2.3", - "cassandra-driver": "^4.6.3", + "async": "^3.2.4", + "cassandra-driver": "^4.6.4", "dirty": "^1.1.3", - "elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0", + "elasticsearch8": "npm:@elastic/elasticsearch@^8.8.1", + "eslint-plugin-import": "^2.26.0", "mongodb": "^3.7.3", - "mssql": "^8.1.0", + "mssql": "^9.1.1", "mysql": "2.18.1", - "nano": "^10.0.0", - "pg": "^8.7.3", - "redis": "^4.1.0", + "nano": "^10.1.2", + "pg": "^8.11.1", + "redis": "^4.6.7", "rethinkdb": "^2.4.2", - "simple-git": "^3.7.1", - "sqlite3": "^5.0.6" + "semver": "^7.5.3", + "simple-git": "^3.19.1", + "sqlite3": "^5.1.6" }, "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true + "array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + } }, - "are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "optional": true, + "array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" } }, - "axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "ms": "^2.1.1" } }, - "gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "optional": true, + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, - "nano": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/nano/-/nano-10.1.2.tgz", - "integrity": "sha512-P3zSoD/sxAgDs/IE9eqpeAXqTdQ/gA9e9dnzaltr4A3WUo/n+eh66T873L+md5v8lXOutX/7dvcHFOO22f5hDw==", + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "requires": { - "axios": "^1.2.2", - "node-abort-controller": "^3.0.1", - "qs": "^6.11.0" + "esutils": "^2.0.2" } }, - "node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "optional": true + "es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "requires": { + "debug": "^3.2.7" + } + }, + "eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } }, - "node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "optional": true, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" } }, - "npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "optional": true, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", "requires": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" + "call-bind": "^1.0.2" } }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "minimist": "^1.2.0" } }, - "redis": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.7.tgz", - "integrity": "sha512-KrkuNJNpCwRm5vFJh0tteMxW8SaUzkm5fBH7eL5hd/D0fAkzvapxbfGPP/r+4JAXdQuX7nebsBkBqA2RHB7Usw==", + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "requires": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.5.8", - "@redis/graph": "1.1.0", - "@redis/json": "1.0.4", - "@redis/search": "1.1.3", - "@redis/time-series": "1.0.4" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.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 + "object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } }, - "sqlite3": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", - "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", - "optional": true, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", "requires": { - "@mapbox/node-pre-gyp": "^1.0.0", - "node-addon-api": "^4.2.0", - "node-gyp": "8.x", - "tar": "^6.1.11" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, - "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, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", "requires": { - "safe-buffer": "~5.2.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, + "tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", "requires": { - "ansi-regex": "^5.0.1" + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" } } } @@ -9667,28 +13478,63 @@ } }, "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", "which-boxed-primitive": "^1.0.2" - }, - "dependencies": { - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - } } }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "underscore": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==" }, + "undici": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", + "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", + "requires": { + "busboy": "^1.6.0" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, "unified": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.1.tgz", @@ -9735,9 +13581,9 @@ } }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" }, "unorm": { "version": "1.6.0", @@ -9749,6 +13595,22 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -9757,10 +13619,19 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "utils-merge": { "version": "1.0.1", @@ -9772,12 +13643,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -9786,7 +13651,7 @@ "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -9796,7 +13661,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" } } }, @@ -9907,6 +13772,19 @@ "is-symbol": "^1.0.3" } }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -9916,11 +13794,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "word-wrap": { - "version": "1.2.3", - "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.2.0", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", @@ -9936,75 +13809,17 @@ "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "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": "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": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } } }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==" + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==" }, "wtfnode": { "version": "0.9.1", @@ -10016,11 +13831,6 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -10048,58 +13858,24 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "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": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } + "yargs-parser": "^21.1.1" } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, "yargs-unparser": { @@ -10125,7 +13901,7 @@ "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + "integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==" }, "yocto-queue": { "version": "0.1.0", diff --git a/src/package.json b/src/package.json index ca9ef5a8a4e..03fe6f5c02f 100644 --- a/src/package.json +++ b/src/package.json @@ -30,7 +30,9 @@ } ], "dependencies": { + "@rollup/plugin-json": "^6.0.0", "async": "^3.2.2", + "cjstoesm": "^2.1.2", "clean-css": "^5.3.2", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", @@ -45,14 +47,14 @@ "formidable": "^2.1.2", "http-errors": "^2.0.0", "js-cookie": "^3.0.5", + "npm": "^6.14.18", "jsdom": "^19.0.0", "jsonminify": "0.4.2", "languages4translatewiki": "0.1.3", "lodash.clonedeep": "4.5.0", - "log4js": "0.6.38", + "log4js": "^6.9.1", "measured-core": "^2.0.0", "mime-types": "^2.1.35", - "npm": "^6.14.15", "openapi-backend": "^5.9.2", "proxy-addr": "^2.0.7", "rate-limiter-flexible": "^2.4.1", @@ -60,7 +62,7 @@ "rehype-minify-whitespace": "^5.0.1", "request": "2.88.2", "resolve": "1.22.2", - "security": "1.0.0", + "security": "^1.0.0", "semver": "^7.5.2", "socket.io": "^2.4.1", "superagent": "^8.0.9", @@ -78,19 +80,36 @@ "etherpad-lite": "node/server.js" }, "devDependencies": { + "@babel/cli": "^7.22.5", + "@babel/core": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "@babel/register": "^7.22.5", + "@types/cross-spawn": "^6.0.2", + "@types/express": "4.17.17", + "@types/jquery": "^3.5.16", + "@types/js-cookie": "^3.0.3", + "@types/node": "^20.3.1", + "@types/underscore": "^1.11.5", + "concurrently": "^8.2.0", "eslint": "^8.14.0", "eslint-config-etherpad": "^3.0.13", "etherpad-cli-client": "^2.0.1", + "i18next": "^23.2.3", + "i18next-fs-backend": "^2.1.5", "mocha": "^9.2.2", "mocha-froth": "^0.2.10", "nodeify": "^1.0.1", + "nodemon": "^2.0.22", "openapi-schema-validation": "^0.4.2", "selenium-webdriver": "^4.10.0", "set-cookie-parser": "^2.4.8", "sinon": "^13.0.2", "split-grid": "^1.0.11", "supertest": "^6.3.3", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "rollup-plugin-typescript2": "^0.35.0", + "rollup-plugin-copy": "^3.4.0", + "@rollup/plugin-commonjs": "^25.0.2" }, "engines": { "node": ">=14.15.0", @@ -102,8 +121,10 @@ }, "scripts": { "lint": "eslint .", - "test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", - "test-container": "mocha --timeout 5000 tests/container/specs/api" + "test": "mocha --require @babel/register --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", + "test-container": "mocha --timeout 5000 tests/container/specs/api", + "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/server.js\"", + "build": "npx rollup -c rollup.config.js" }, "version": "1.9.0", "license": "Apache-2.0" diff --git a/src/rollup.config.js b/src/rollup.config.js new file mode 100644 index 00000000000..aeef504a3c5 --- /dev/null +++ b/src/rollup.config.js @@ -0,0 +1,32 @@ +const typescript = require('rollup-plugin-typescript2'); +const copy = require('rollup-plugin-copy'); +const glob = require('glob'); +const json = require('@rollup/plugin-json') +const commonJS = require('@rollup/plugin-commonjs') + + +module.exports = { + input: './node/server.ts', // Matches all TypeScript files in the 'src' directory and its subdirectories + output: { + preserveModules: true, + dir: './dist', + format: 'cjs', + }, + plugins: [ + json(), + typescript({ + tsconfig: 'tsconfig.json', + }), + commonJS(), + copy({ + targets: [ + {src:'./package.json', dest:'./dist'}, + { src: './LICENSE', dest: './dist' }, + { src: './src/locales/*', dest: './dist/locales' }, + { src: './src/static/css/*', dest: './dist/static/css' }, + { src: './src/templates', dest: './dist/templates' }, + {src:'./ep.json', dest:'./dist'}, + ] + }) + ], +}; \ No newline at end of file diff --git a/src/static/favicon.ico b/src/static/favicon.ico index c33f2e614c6..e69de29bb2d 100644 Binary files a/src/static/favicon.ico and b/src/static/favicon.ico differ diff --git a/src/static/font/Aleygreya-ExtraBold.woff b/src/static/font/Aleygreya-ExtraBold.woff index b1e0423bbb9..e69de29bb2d 100644 Binary files a/src/static/font/Aleygreya-ExtraBold.woff and b/src/static/font/Aleygreya-ExtraBold.woff differ diff --git a/src/static/font/Aleygreya-ExtraBold.woff2 b/src/static/font/Aleygreya-ExtraBold.woff2 index f66acbaf8be..e69de29bb2d 100644 Binary files a/src/static/font/Aleygreya-ExtraBold.woff2 and b/src/static/font/Aleygreya-ExtraBold.woff2 differ diff --git a/src/static/font/Aleygreya-Medium.woff b/src/static/font/Aleygreya-Medium.woff index 0403d5714c2..e69de29bb2d 100644 Binary files a/src/static/font/Aleygreya-Medium.woff and b/src/static/font/Aleygreya-Medium.woff differ diff --git a/src/static/font/Aleygreya-Medium.woff2 b/src/static/font/Aleygreya-Medium.woff2 index 5056666ba77..e69de29bb2d 100644 Binary files a/src/static/font/Aleygreya-Medium.woff2 and b/src/static/font/Aleygreya-Medium.woff2 differ diff --git a/src/static/font/Montserrat-Light.otf b/src/static/font/Montserrat-Light.otf index f2f0e2df089..e69de29bb2d 100644 Binary files a/src/static/font/Montserrat-Light.otf and b/src/static/font/Montserrat-Light.otf differ diff --git a/src/static/font/Montserrat-Regular.otf b/src/static/font/Montserrat-Regular.otf index f61d57ec133..e69de29bb2d 100644 Binary files a/src/static/font/Montserrat-Regular.otf and b/src/static/font/Montserrat-Regular.otf differ diff --git a/src/static/font/Quicksand-Bold.ttf b/src/static/font/Quicksand-Bold.ttf index 49326cda826..e69de29bb2d 100644 Binary files a/src/static/font/Quicksand-Bold.ttf and b/src/static/font/Quicksand-Bold.ttf differ diff --git a/src/static/font/Quicksand-Medium.ttf b/src/static/font/Quicksand-Medium.ttf index 7dc8c2745dd..e69de29bb2d 100644 Binary files a/src/static/font/Quicksand-Medium.ttf and b/src/static/font/Quicksand-Medium.ttf differ diff --git a/src/static/font/Quicksand-Regular.ttf b/src/static/font/Quicksand-Regular.ttf index 9fdce17db50..e69de29bb2d 100644 Binary files a/src/static/font/Quicksand-Regular.ttf and b/src/static/font/Quicksand-Regular.ttf differ diff --git a/src/static/font/Roboto-Bold.ttf b/src/static/font/Roboto-Bold.ttf index d998cf5b468..e69de29bb2d 100644 Binary files a/src/static/font/Roboto-Bold.ttf and b/src/static/font/Roboto-Bold.ttf differ diff --git a/src/static/font/Roboto-Regular.ttf b/src/static/font/Roboto-Regular.ttf index 2b6392ffe87..e69de29bb2d 100644 Binary files a/src/static/font/Roboto-Regular.ttf and b/src/static/font/Roboto-Regular.ttf differ diff --git a/src/static/font/RobotoMono-Bold.ttf b/src/static/font/RobotoMono-Bold.ttf index 07ef607d50c..e69de29bb2d 100644 Binary files a/src/static/font/RobotoMono-Bold.ttf and b/src/static/font/RobotoMono-Bold.ttf differ diff --git a/src/static/font/RobotoMono-Regular.ttf b/src/static/font/RobotoMono-Regular.ttf index b158a334eb3..e69de29bb2d 100644 Binary files a/src/static/font/RobotoMono-Regular.ttf and b/src/static/font/RobotoMono-Regular.ttf differ diff --git a/src/static/font/fontawesome-etherpad.eot b/src/static/font/fontawesome-etherpad.eot index 2f2b4e8be37..e69de29bb2d 100644 Binary files a/src/static/font/fontawesome-etherpad.eot and b/src/static/font/fontawesome-etherpad.eot differ diff --git a/src/static/font/fontawesome-etherpad.ttf b/src/static/font/fontawesome-etherpad.ttf index 120d72500e6..e69de29bb2d 100644 Binary files a/src/static/font/fontawesome-etherpad.ttf and b/src/static/font/fontawesome-etherpad.ttf differ diff --git a/src/static/font/fontawesome-etherpad.woff b/src/static/font/fontawesome-etherpad.woff index 1d21170939a..e69de29bb2d 100644 Binary files a/src/static/font/fontawesome-etherpad.woff and b/src/static/font/fontawesome-etherpad.woff differ diff --git a/src/static/font/fontawesome-etherpad.woff2 b/src/static/font/fontawesome-etherpad.woff2 index 72eb1da718d..e69de29bb2d 100644 Binary files a/src/static/font/fontawesome-etherpad.woff2 and b/src/static/font/fontawesome-etherpad.woff2 differ diff --git a/src/static/font/opendyslexic.otf b/src/static/font/opendyslexic.otf index 1a7c9d411b2..e69de29bb2d 100644 Binary files a/src/static/font/opendyslexic.otf and b/src/static/font/opendyslexic.otf differ diff --git a/src/static/img/brand.svg b/src/static/img/brand.svg old mode 100755 new mode 100644 diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js deleted file mode 100644 index f508af64111..00000000000 --- a/src/static/js/AttributeManager.js +++ /dev/null @@ -1,382 +0,0 @@ -'use strict'; - -const AttributeMap = require('./AttributeMap'); -const Changeset = require('./Changeset'); -const ChangesetUtils = require('./ChangesetUtils'); -const attributes = require('./attributes'); -const _ = require('./underscore'); - -const lineMarkerAttribute = 'lmkr'; - -// Some of these attributes are kept for compatibility purposes. -// Not sure if we need all of them -const DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; - -// If one of these attributes are set to the first character of a -// line it is considered as a line attribute marker i.e. attributes -// set on this marker are applied to the whole line. -// The list attribute is only maintained for compatibility reasons -const lineAttributes = [lineMarkerAttribute, 'list']; - -/* - The Attribute manager builds changesets based on a document - representation for setting and removing range or line-based attributes. - - @param rep the document representation to be used - @param applyChangesetCallback this callback will be called - once a changeset has been built. - - - A document representation contains - - an array `alines` containing 1 attributes string for each line - - an Attribute pool `apool` - - a SkipList `lines` containing the text lines of the document. -*/ - -const AttributeManager = function (rep, applyChangesetCallback) { - this.rep = rep; - this.applyChangesetCallback = applyChangesetCallback; - this.author = ''; - - // If the first char in a line has one of the following attributes - // it will be considered as a line marker -}; - -AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES; -AttributeManager.lineAttributes = lineAttributes; - -AttributeManager.prototype = _(AttributeManager.prototype).extend({ - - applyChangeset(changeset) { - if (!this.applyChangesetCallback) return changeset; - - const cs = changeset.toString(); - if (!Changeset.isIdentity(cs)) { - this.applyChangesetCallback(cs); - } - - return changeset; - }, - - /* - Sets attributes on a range - @param start [row, col] tuple pointing to the start of the range - @param end [row, col] tuple pointing to the end of the range - @param attribs: an array of attributes - */ - setAttributesOnRange(start, end, attribs) { - if (start[0] < 0) throw new RangeError('selection start line number is negative'); - if (start[1] < 0) throw new RangeError('selection start column number is negative'); - if (end[0] < 0) throw new RangeError('selection end line number is negative'); - if (end[1] < 0) throw new RangeError('selection end column number is negative'); - if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) { - throw new RangeError('selection ends before it starts'); - } - - // instead of applying the attributes to the whole range at once, we need to apply them - // line by line, to be able to disregard the "*" used as line marker. For more details, - // see https://github.com/ether/etherpad-lite/issues/2772 - let allChangesets; - for (let row = start[0]; row <= end[0]; row++) { - const [startCol, endCol] = this._findRowRange(row, start, end); - const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); - - // compose changesets of all rows into a single changeset - // as the range might not be continuous - // due to the presence of line markers on the rows - if (allChangesets) { - allChangesets = Changeset.compose( - allChangesets.toString(), rowChangeset.toString(), this.rep.apool); - } else { - allChangesets = rowChangeset; - } - } - - return this.applyChangeset(allChangesets); - }, - - _findRowRange(row, start, end) { - if (row < start[0] || row > end[0]) throw new RangeError(`line ${row} not in selection`); - if (row >= this.rep.lines.length()) throw new RangeError(`selected line ${row} does not exist`); - - // Subtract 1 for the end-of-line '\n' (it is never selected). - const lineLength = - this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1; - const markerWidth = this.lineHasMarker(row) ? 1 : 0; - if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`); - - 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'); - - 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'); - - return [startCol, endCol]; - }, - - /** - * Sets attributes on a range, by line - * @param row the row where range is - * @param startCol column where range starts - * @param endCol column where range ends (one past the last selected column) - * @param attribs an array of attributes - */ - _setAttributesOnRangeByLine(row, startCol, endCol, attribs) { - const builder = Changeset.builder(this.rep.lines.totalWidth()); - ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]); - ChangesetUtils.buildKeepRange( - this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool); - return builder; - }, - - /* - Returns if the line already has a line marker - @param lineNum: the number of the line - */ - lineHasMarker(lineNum) { - return lineAttributes.find( - (attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined; - }, - - /* - Gets a specified attribute on a line - @param lineNum: the number of the line to set the attribute for - @param attributeKey: the name of the attribute to get, e.g. list - */ - getAttributeOnLine(lineNum, attributeName) { - // get `attributeName` attribute of first char of line - const aline = this.rep.alines[lineNum]; - if (!aline) return ''; - const [op] = Changeset.deserializeOps(aline); - if (op == null) return ''; - return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || ''; - }, - - /* - Gets all attributes on a line - @param lineNum: the number of the line to get the attribute for - */ - getAttributesOnLine(lineNum) { - // get attributes of first char of line - const aline = this.rep.alines[lineNum]; - if (!aline) return []; - const [op] = Changeset.deserializeOps(aline); - if (op == null) return []; - return [...attributes.attribsFromString(op.attribs, this.rep.apool)]; - }, - - /* - Gets a given attribute on a selection - @param attributeName - @param prevChar - returns true or false if an attribute is visible in range - */ - getAttributeOnSelection(attributeName, prevChar) { - const rep = this.rep; - if (!(rep.selStart && rep.selEnd)) return; - // If we're looking for the caret attribute not the selection - // has the user already got a selection or is this purely a caret location? - const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); - if (isNotSelection) { - if (prevChar) { - // If it's not the start of the line - if (rep.selStart[1] !== 0) { - rep.selStart[1]--; - } - } - } - - const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); - const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); - const hasIt = (attribs) => withItRegex.test(attribs); - - const rangeHasAttrib = (selStart, selEnd) => { - // if range is collapsed -> no attribs in range - if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false; - - if (selStart[0] !== selEnd[0]) { // -> More than one line selected - // from selStart to the end of the first line - let hasAttrib = rangeHasAttrib( - selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); - - // for all lines in between - for (let n = selStart[0] + 1; n < selEnd[0]; n++) { - hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); - } - - // for the last, potentially partial, line - hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); - - return hasAttrib; - } - - // Logic tells us we now have a range on a single line - - const lineNum = selStart[0]; - const start = selStart[1]; - const end = selEnd[1]; - let hasAttrib = true; - - let indexIntoLine = 0; - for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { - const opStartInLine = indexIntoLine; - const opEndInLine = opStartInLine + op.chars; - if (!hasIt(op.attribs)) { - // does op overlap selection? - if (!(opEndInLine <= start || opStartInLine >= end)) { - // since it's overlapping but hasn't got the attrib -> range hasn't got it - hasAttrib = false; - break; - } - } - indexIntoLine = opEndInLine; - } - - return hasAttrib; - }; - return rangeHasAttrib(rep.selStart, rep.selEnd); - }, - - /* - Gets all attributes at a position containing line number and column - @param lineNumber starting with zero - @param column starting with zero - returns a list of attributes in the format - [ ["key","value"], ["key","value"], ... ] - */ - getAttributesOnPosition(lineNumber, column) { - // get all attributes of the line - const aline = this.rep.alines[lineNumber]; - - if (!aline) { - return []; - } - - // we need to sum up how much characters each operations take until the wanted position - let currentPointer = 0; - - for (const currentOperation of Changeset.deserializeOps(aline)) { - currentPointer += currentOperation.chars; - if (currentPointer <= column) continue; - return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)]; - } - return []; - }, - - /* - Gets all attributes at caret position - if the user selected a range, the start of the selection is taken - returns a list of attributes in the format - [ ["key","value"], ["key","value"], ... ] - */ - getAttributesOnCaret() { - return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]); - }, - - /* - Sets a specified attribute on a line - @param lineNum: the number of the line to set the attribute for - @param attributeKey: the name of the attribute to set, e.g. list - @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) - - */ - setAttributeOnLine(lineNum, attributeName, attributeValue) { - let loc = [0, 0]; - const builder = Changeset.builder(this.rep.lines.totalWidth()); - const hasMarker = this.lineHasMarker(lineNum); - - ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); - - if (hasMarker) { - ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [ - [attributeName, attributeValue], - ], this.rep.apool); - } else { - // add a line marker - builder.insert('*', [ - ['author', this.author], - ['insertorder', 'first'], - [lineMarkerAttribute, '1'], - [attributeName, attributeValue], - ], this.rep.apool); - } - - return this.applyChangeset(builder); - }, - - /** - * Removes a specified attribute on a line - * @param lineNum the number of the affected line - * @param attributeName the name of the attribute to remove, e.g. list - * @param attributeValue if given only attributes with equal value will be removed - */ - removeAttributeOnLine(lineNum, attributeName, attributeValue) { - const builder = Changeset.builder(this.rep.lines.totalWidth()); - const hasMarker = this.lineHasMarker(lineNum); - let found = false; - - const attribs = this.getAttributesOnLine(lineNum).map((attrib) => { - if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) { - found = true; - return [attrib[0], '']; - } else if (attrib[0] === 'author') { - // update last author to make changes to line attributes on this line - return [attrib[0], this.author]; - } - return attrib; - }); - - if (!found) { - return; - } - - ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); - - const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1]) - .map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); - - // if we have marker and any of attributes don't need to have marker. we need delete it - if (hasMarker && !countAttribsWithMarker) { - ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]); - } else { - ChangesetUtils.buildKeepRange( - this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool); - } - - return this.applyChangeset(builder); - }, - - /* - Toggles a line attribute for the specified line number - If a line attribute with the specified name exists with any value it will be removed - Otherwise it will be set to the given value - @param lineNum: the number of the line to toggle the attribute for - @param attributeKey: the name of the attribute to toggle, e.g. list - @param attributeValue: the value to pass to the attribute (e.g. indention level) - */ - toggleAttributeOnLine(lineNum, attributeName, attributeValue) { - return this.getAttributeOnLine(lineNum, attributeName) - ? this.removeAttributeOnLine(lineNum, attributeName) - : this.setAttributeOnLine(lineNum, attributeName, attributeValue); - }, - - hasAttributeOnSelectionOrCaretPosition(attributeName) { - const hasSelection = ( - (this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1]) - ); - let hasAttrib; - if (hasSelection) { - hasAttrib = this.getAttributeOnSelection(attributeName); - } else { - const attributesOnCaretPosition = this.getAttributesOnCaret(); - const allAttribs = [].concat(...attributesOnCaretPosition); // flatten - hasAttrib = allAttribs.includes(attributeName); - } - return hasAttrib; - }, -}); - -module.exports = AttributeManager; diff --git a/src/static/js/AttributeManager.ts b/src/static/js/AttributeManager.ts new file mode 100644 index 00000000000..67a71c0ee6a --- /dev/null +++ b/src/static/js/AttributeManager.ts @@ -0,0 +1,346 @@ +import AttributeMap from "./AttributeMap.js"; +import * as Changeset from "./Changeset.js"; +import * as ChangesetUtils from "./ChangesetUtils.js"; +import * as attributes from "./attributes.js"; +import _ from "underscore"; +export { map, each, identity} from "underscore"; + +'use strict'; +const lineMarkerAttribute = 'lmkr'; +// Some of these attributes are kept for compatibility purposes. +// Not sure if we need all of them +const DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; +// If one of these attributes are set to the first character of a +// line it is considered as a line attribute marker i.e. attributes +// set on this marker are applied to the whole line. +// The list attribute is only maintained for compatibility reasons +const lineAttributes = [lineMarkerAttribute, 'list']; +/* + The Attribute manager builds changesets based on a document + representation for setting and removing range or line-based attributes. + + @param rep the document representation to be used + @param applyChangesetCallback this callback will be called + once a changeset has been built. + + + A document representation contains + - an array `alines` containing 1 attributes string for each line + - an Attribute pool `apool` + - a SkipList `lines` containing the text lines of the document. +*/ +const AttributeManager = function (rep, applyChangesetCallback) { + this.rep = rep; + this.applyChangesetCallback = applyChangesetCallback; + this.author = ''; + // If the first char in a line has one of the following attributes + // it will be considered as a line marker +}; +AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES; +AttributeManager.lineAttributes = lineAttributes; +AttributeManager.prototype = _(AttributeManager.prototype).extend({ + applyChangeset(changeset) { + if (!this.applyChangesetCallback) + return changeset; + const cs = changeset.toString(); + if (!Changeset.isIdentity(cs)) { + this.applyChangesetCallback(cs); + } + return changeset; + }, + /* + Sets attributes on a range + @param start [row, col] tuple pointing to the start of the range + @param end [row, col] tuple pointing to the end of the range + @param attribs: an array of attributes + */ + setAttributesOnRange(start, end, attribs) { + if (start[0] < 0) + throw new RangeError('selection start line number is negative'); + if (start[1] < 0) + throw new RangeError('selection start column number is negative'); + if (end[0] < 0) + throw new RangeError('selection end line number is negative'); + if (end[1] < 0) + throw new RangeError('selection end column number is negative'); + if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) { + throw new RangeError('selection ends before it starts'); + } + // instead of applying the attributes to the whole range at once, we need to apply them + // line by line, to be able to disregard the "*" used as line marker. For more details, + // see https://github.com/ether/etherpad-lite/issues/2772 + let allChangesets; + for (let row = start[0]; row <= end[0]; row++) { + const [startCol, endCol] = this._findRowRange(row, start, end); + const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); + // compose changesets of all rows into a single changeset + // as the range might not be continuous + // due to the presence of line markers on the rows + if (allChangesets) { + allChangesets = Changeset.compose(allChangesets.toString(), rowChangeset.toString(), this.rep.apool); + } + else { + allChangesets = rowChangeset; + } + } + return this.applyChangeset(allChangesets); + }, + _findRowRange(row, start, end) { + if (row < start[0] || row > end[0]) + throw new RangeError(`line ${row} not in selection`); + if (row >= this.rep.lines.length()) + throw new RangeError(`selected line ${row} does not exist`); + // Subtract 1 for the end-of-line '\n' (it is never selected). + const lineLength = this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1; + const markerWidth = this.lineHasMarker(row) ? 1 : 0; + if (lineLength - markerWidth < 0) + throw new Error(`line ${row} has negative length`); + 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'); + 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'); + return [startCol, endCol]; + }, + /** + * Sets attributes on a range, by line + * @param row the row where range is + * @param startCol column where range starts + * @param endCol column where range ends (one past the last selected column) + * @param attribs an array of attributes + */ + _setAttributesOnRangeByLine(row, startCol, endCol, attribs) { + const builder = Changeset.builder(this.rep.lines.totalWidth()); + ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]); + ChangesetUtils.buildKeepRange(this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool); + return builder; + }, + /* + Returns if the line already has a line marker + @param lineNum: the number of the line + */ + lineHasMarker(lineNum) { + return lineAttributes.find((attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined; + }, + /* + Gets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to get, e.g. list + */ + getAttributeOnLine(lineNum, attributeName) { + // get `attributeName` attribute of first char of line + const aline = this.rep.alines[lineNum]; + if (!aline) + return ''; + const [op] = Changeset.deserializeOps(aline); + if (op == null) + return ''; + return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || ''; + }, + /* + Gets all attributes on a line + @param lineNum: the number of the line to get the attribute for + */ + getAttributesOnLine(lineNum) { + // get attributes of first char of line + const aline = this.rep.alines[lineNum]; + if (!aline) + return []; + const [op] = Changeset.deserializeOps(aline); + if (op == null) + return []; + return [...attributes.attribsFromString(op.attribs, this.rep.apool)]; + }, + /* + Gets a given attribute on a selection + @param attributeName + @param prevChar + returns true or false if an attribute is visible in range + */ + getAttributeOnSelection(attributeName, prevChar) { + const rep = this.rep; + if (!(rep.selStart && rep.selEnd)) + return; + // If we're looking for the caret attribute not the selection + // has the user already got a selection or is this purely a caret location? + const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); + if (isNotSelection) { + if (prevChar) { + // If it's not the start of the line + if (rep.selStart[1] !== 0) { + rep.selStart[1]--; + } + } + } + const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); + const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); + const hasIt = (attribs) => withItRegex.test(attribs); + const rangeHasAttrib = (selStart, selEnd) => { + // if range is collapsed -> no attribs in range + if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) + return false; + if (selStart[0] !== selEnd[0]) { // -> More than one line selected + // from selStart to the end of the first line + let hasAttrib = rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); + // for all lines in between + for (let n = selStart[0] + 1; n < selEnd[0]; n++) { + hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); + } + // for the last, potentially partial, line + hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); + return hasAttrib; + } + // Logic tells us we now have a range on a single line + const lineNum = selStart[0]; + const start = selStart[1]; + const end = selEnd[1]; + let hasAttrib = true; + let indexIntoLine = 0; + for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { + const opStartInLine = indexIntoLine; + const opEndInLine = opStartInLine + op.chars; + if (!hasIt(op.attribs)) { + // does op overlap selection? + if (!(opEndInLine <= start || opStartInLine >= end)) { + // since it's overlapping but hasn't got the attrib -> range hasn't got it + hasAttrib = false; + break; + } + } + indexIntoLine = opEndInLine; + } + return hasAttrib; + }; + return rangeHasAttrib(rep.selStart, rep.selEnd); + }, + /* + Gets all attributes at a position containing line number and column + @param lineNumber starting with zero + @param column starting with zero + returns a list of attributes in the format + [ ["key","value"], ["key","value"], ... ] + */ + getAttributesOnPosition(lineNumber, column) { + // get all attributes of the line + const aline = this.rep.alines[lineNumber]; + if (!aline) { + return []; + } + // we need to sum up how much characters each operations take until the wanted position + let currentPointer = 0; + for (const currentOperation of Changeset.deserializeOps(aline)) { + currentPointer += currentOperation.chars; + if (currentPointer <= column) + continue; + return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)]; + } + return []; + }, + /* + Gets all attributes at caret position + if the user selected a range, the start of the selection is taken + returns a list of attributes in the format + [ ["key","value"], ["key","value"], ... ] + */ + getAttributesOnCaret() { + return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]); + }, + /* + Sets a specified attribute on a line + @param lineNum: the number of the line to set the attribute for + @param attributeKey: the name of the attribute to set, e.g. list + @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) + + */ + setAttributeOnLine(lineNum, attributeName, attributeValue) { + let loc = [0, 0]; + const builder = Changeset.builder(this.rep.lines.totalWidth()); + const hasMarker = this.lineHasMarker(lineNum); + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); + if (hasMarker) { + ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [ + [attributeName, attributeValue], + ], this.rep.apool); + } + else { + // add a line marker + builder.insert('*', [ + ['author', this.author], + ['insertorder', 'first'], + [lineMarkerAttribute, '1'], + [attributeName, attributeValue], + ], this.rep.apool); + } + return this.applyChangeset(builder); + }, + /** + * Removes a specified attribute on a line + * @param lineNum the number of the affected line + * @param attributeName the name of the attribute to remove, e.g. list + * @param attributeValue if given only attributes with equal value will be removed + */ + removeAttributeOnLine(lineNum, attributeName, attributeValue) { + const builder = Changeset.builder(this.rep.lines.totalWidth()); + const hasMarker = this.lineHasMarker(lineNum); + let found = false; + const attribs = this.getAttributesOnLine(lineNum).map((attrib) => { + if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) { + found = true; + return [attrib[0], '']; + } + else if (attrib[0] === 'author') { + // update last author to make changes to line attributes on this line + return [attrib[0], this.author]; + } + return attrib; + }); + if (!found) { + return; + } + ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); + const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1]) + .map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); + // if we have marker and any of attributes don't need to have marker. we need delete it + if (hasMarker && !countAttribsWithMarker) { + ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]); + } + else { + ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool); + } + return this.applyChangeset(builder); + }, + /* + Toggles a line attribute for the specified line number + If a line attribute with the specified name exists with any value it will be removed + Otherwise it will be set to the given value + @param lineNum: the number of the line to toggle the attribute for + @param attributeKey: the name of the attribute to toggle, e.g. list + @param attributeValue: the value to pass to the attribute (e.g. indention level) + */ + toggleAttributeOnLine(lineNum, attributeName, attributeValue) { + return this.getAttributeOnLine(lineNum, attributeName) + ? this.removeAttributeOnLine(lineNum, attributeName) + : this.setAttributeOnLine(lineNum, attributeName, attributeValue); + }, + hasAttributeOnSelectionOrCaretPosition(attributeName) { + const hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])); + let hasAttrib; + if (hasSelection) { + hasAttrib = this.getAttributeOnSelection(attributeName); + } + else { + const attributesOnCaretPosition = this.getAttributesOnCaret(); + const allAttribs = [].concat(...attributesOnCaretPosition); // flatten + hasAttrib = allAttribs.includes(attributeName); + } + return hasAttrib; + }, +}); +export default AttributeManager; diff --git a/src/static/js/AttributeMap.js b/src/static/js/AttributeMap.js deleted file mode 100644 index 55640eb8bc6..00000000000 --- a/src/static/js/AttributeMap.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; - -const attributes = require('./attributes'); - -/** - * A `[key, value]` pair of strings describing a text attribute. - * - * @typedef {[string, string]} Attribute - */ - -/** - * A concatenated sequence of zero or more attribute identifiers, each one represented by an - * asterisk followed by a base-36 encoded attribute number. - * - * Examples: '', '*0', '*3*j*z*1q' - * - * @typedef {string} AttributeString - */ - -/** - * Convenience class to convert an Op's attribute string to/from a Map of key, value pairs. - */ -class AttributeMap extends Map { - /** - * Converts an attribute string into an AttributeMap. - * - * @param {AttributeString} str - The attribute string to convert into an AttributeMap. - * @param {AttributePool} pool - Attribute pool. - * @returns {AttributeMap} - */ - static fromString(str, pool) { - return new AttributeMap(pool).updateFromString(str); - } - - /** - * @param {AttributePool} pool - Attribute pool. - */ - constructor(pool) { - super(); - /** @public */ - this.pool = pool; - } - - /** - * @param {string} k - Attribute name. - * @param {string} v - Attribute value. - * @returns {AttributeMap} `this` (for chaining). - */ - set(k, v) { - k = k == null ? '' : String(k); - v = v == null ? '' : String(v); - this.pool.putAttrib([k, v]); - return super.set(k, v); - } - - toString() { - return attributes.attribsToString(attributes.sort([...this]), this.pool); - } - - /** - * @param {Iterable} entries - [key, value] pairs to insert into this map. - * @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the - * key is removed from this map (if present). - * @returns {AttributeMap} `this` (for chaining). - */ - update(entries, emptyValueIsDelete = false) { - for (let [k, v] of entries) { - k = k == null ? '' : String(k); - v = v == null ? '' : String(v); - if (!v && emptyValueIsDelete) { - this.delete(k); - } else { - this.set(k, v); - } - } - return this; - } - - /** - * @param {AttributeString} str - The attribute string identifying the attributes to insert into - * this map. - * @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the - * key is removed from this map (if present). - * @returns {AttributeMap} `this` (for chaining). - */ - updateFromString(str, emptyValueIsDelete = false) { - return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete); - } -} - -module.exports = AttributeMap; diff --git a/src/static/js/AttributeMap.ts b/src/static/js/AttributeMap.ts new file mode 100644 index 00000000000..ae39f9a05b9 --- /dev/null +++ b/src/static/js/AttributeMap.ts @@ -0,0 +1,83 @@ +import * as attributes from "./attributes.js"; +'use strict'; +/** + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute + */ +/** + * A concatenated sequence of zero or more attribute identifiers, each one represented by an + * asterisk followed by a base-36 encoded attribute number. + * + * Examples: '', '*0', '*3*j*z*1q' + * + * @typedef {string} AttributeString + */ +/** + * Convenience class to convert an Op's attribute string to/from a Map of key, value pairs. + */ +class AttributeMap extends Map { + pool: any; + /** + * Converts an attribute string into an AttributeMap. + * + * @param {AttributeString} str - The attribute string to convert into an AttributeMap. + * @param {AttributePool} pool - Attribute pool. + * @returns {AttributeMap} + */ + static fromString(str, pool) { + return new AttributeMap(pool).updateFromString(str); + } + /** + * @param {AttributePool} pool - Attribute pool. + */ + constructor(pool) { + super(); + /** @public */ + this.pool = pool; + } + /** + * @param {string} k - Attribute name. + * @param {string} v - Attribute value. + * @returns {AttributeMap} `this` (for chaining). + */ + set(k, v) { + k = k == null ? '' : String(k); + v = v == null ? '' : String(v); + this.pool.putAttrib([k, v]); + return super.set(k, v); + } + toString() { + return attributes.attribsToString(attributes.sort([...this]), this.pool); + } + /** + * @param {Iterable} entries - [key, value] pairs to insert into this map. + * @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the + * key is removed from this map (if present). + * @returns {AttributeMap} `this` (for chaining). + */ + update(entries, emptyValueIsDelete = false) { + for (let [k, v] of entries) { + k = k == null ? '' : String(k); + v = v == null ? '' : String(v); + if (!v && emptyValueIsDelete) { + this.delete(k); + } + else { + this.set(k, v); + } + } + return this; + } + /** + * @param {AttributeString} str - The attribute string identifying the attributes to insert into + * this map. + * @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the + * key is removed from this map (if present). + * @returns {AttributeMap} `this` (for chaining). + */ + updateFromString(str, emptyValueIsDelete = false) { + return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete); + } +} +export default AttributeMap; diff --git a/src/static/js/AttributePool.js b/src/static/js/AttributePool.ts similarity index 98% rename from src/static/js/AttributePool.js rename to src/static/js/AttributePool.ts index ccdd2eb35ca..3f7691db345 100644 --- a/src/static/js/AttributePool.js +++ b/src/static/js/AttributePool.ts @@ -54,7 +54,10 @@ * There is one attribute pool per pad, and it includes every current and historical attribute used * in the pad. */ -class AttributePool { +export class AttributePool { + numToAttrib: {}; + private attribToNum: {}; + private nextNum: number; constructor() { /** * Maps an attribute identifier to the attribute's `[key, value]` string pair. diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js deleted file mode 100644 index 53b3f2c8f09..00000000000 --- a/src/static/js/Changeset.js +++ /dev/null @@ -1,2404 +0,0 @@ -'use strict'; - -/* - * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * 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 AttributeMap = require('./AttributeMap'); -const AttributePool = require('./AttributePool'); -const attributes = require('./attributes'); -const {padutils} = require('./pad_utils'); - -/** - * A `[key, value]` pair of strings describing a text attribute. - * - * @typedef {[string, string]} Attribute - */ - -/** - * A concatenated sequence of zero or more attribute identifiers, each one represented by an - * asterisk followed by a base-36 encoded attribute number. - * - * Examples: '', '*0', '*3*j*z*1q' - * - * @typedef {string} AttributeString - */ - -/** - * This method is called whenever there is an error in the sync process. - * - * @param {string} msg - Just some message - */ -const error = (msg) => { - const e = new Error(msg); - e.easysync = true; - throw e; -}; - -/** - * 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} msg - error message to include in the exception - * @type {(b: boolean, msg: string) => asserts b} - */ -const assert = (b, msg) => { - if (!b) error(`Failed assertion: ${msg}`); -}; - -/** - * 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 {number} num - number - * @returns {string} string - */ -exports.numToString = (num) => num.toString(36).toLowerCase(); - -/** - * An operation to apply to a shared document. - */ -class Op { - /** - * @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property. - */ - constructor(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. - * - * @type {(''|'='|'+'|'-')} - * @public - */ - this.opcode = opcode; - - /** - * The number of characters to keep, insert, or delete. - * - * @type {number} - * @public - */ - this.chars = 0; - - /** - * The number of characters among the `chars` characters that are newlines. If non-zero, the - * last character must be a newline. - * - * @type {number} - * @public - */ - this.lines = 0; - - /** - * 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 attribute pool. - * - * 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. - * - * This is the empty string for remove ('-') operations. - * - * @type {string} - * @public - */ - this.attribs = ''; - } - - toString() { - if (!this.opcode) throw new TypeError('null op'); - if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string'); - const l = this.lines ? `|${exports.numToString(this.lines)}` : ''; - return this.attribs + l + this.opcode + exports.numToString(this.chars); - } -} -exports.Op = Op; - -/** - * 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 {string} cs - String representation of the Changeset - * @returns {number} newLen property - */ -exports.newLen = (cs) => exports.unpack(cs).newLen; - -/** - * Parses a string of serialized changeset operations. - * - * @param {string} ops - Serialized changeset operations. - * @yields {Op} - * @returns {Generator} - */ -exports.deserializeOps = function* (ops) { - // TODO: Migrate to String.prototype.matchAll() once there is enough browser support. - const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; - let match; - while ((match = regex.exec(ops)) != null) { - if (match[5] === '$') return; // Start of the insert operation character bank. - if (match[5] != null) error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`); - const op = new Op(match[3]); - op.lines = exports.parseNum(match[2] || '0'); - op.chars = exports.parseNum(match[4]); - op.attribs = match[1]; - yield op; - } -}; - -/** - * Iterator over a changeset's operations. - * - * Note: This class does NOT implement the ECMAScript iterable or iterator protocols. - * - * @deprecated Use `deserializeOps` instead. - */ -class OpIter { - /** - * @param {string} ops - String encoding the change operations to iterate over. - */ - constructor(ops) { - this._gen = exports.deserializeOps(ops); - this._next = this._gen.next(); - } - - /** - * @returns {boolean} Whether there are any remaining operations. - */ - hasNext() { - return !this._next.done; - } - - /** - * Returns the next operation object and advances the iterator. - * - * Note: This does NOT implement the ECMAScript iterator protocol. - * - * @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value. - * @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are - * no more operations. - */ - next(opOut = new Op()) { - if (this.hasNext()) { - copyOp(this._next.value, opOut); - this._next = this._gen.next(); - } else { - clearOp(opOut); - } - return opOut; - } -} - -/** - * Creates an iterator which decodes string changeset operations. - * - * @deprecated Use `deserializeOps` instead. - * @param {string} opsStr - String encoding of the change operations to perform. - * @returns {OpIter} Operator iterator object. - */ -exports.opIterator = (opsStr) => { - padutils.warnDeprecated( - 'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead'); - return new OpIter(opsStr); -}; - -/** - * Cleans an Op object. - * - * @param {Op} op - object to clear - */ -const clearOp = (op) => { - op.opcode = ''; - op.chars = 0; - op.lines = 0; - op.attribs = ''; -}; - -/** - * Creates a new Op object - * - * @deprecated Use the `Op` class instead. - * @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator. - * @returns {Op} - */ -exports.newOp = (optOpcode) => { - padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead'); - return new Op(optOpcode); -}; - -/** - * Copies op1 to op2 - * - * @param {Op} op1 - src Op - * @param {Op} [op2] - dest Op. If not given, a new Op is used. - * @returns {Op} `op2` - */ -const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1); - -/** - * 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 - - */ - -/** - * Generates operations from the given text and attributes. - * - * @param {('-'|'+'|'=')} opcode - The operator to use. - * @param {string} text - The text to remove/add/keep. - * @param {(Iterable|AttributeString)} [attribs] - The attributes to insert into the pool - * (if necessary) and encode. If an attribute string, no checking is performed to ensure that - * the attributes exist in the pool, are in the canonical order, and contain no duplicate keys. - * If this is an iterable of attributes, `pool` must be non-null. - * @param {?AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of - * attributes, ignored if `attribs` is an attribute string. - * @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 = new Op(opcode); - op.attribs = typeof attribs === 'string' - ? attribs : new AttributeMap(pool).update(attribs || [], opcode === '+').toString(); - 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 - * 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) => { - const unpacked = exports.unpack(cs); - const oldLen = unpacked.oldLen; - const newLen = unpacked.newLen; - const ops = unpacked.ops; - let charBank = unpacked.charBank; - - const assem = exports.smartOpAssembler(); - let oldPos = 0; - let calcNewLen = 0; - for (const o of exports.deserializeOps(ops)) { - switch (o.opcode) { - case '=': - oldPos += o.chars; - calcNewLen += o.chars; - break; - case '-': - oldPos += o.chars; - assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`); - break; - case '+': - { - assert(charBank.length >= o.chars, 'Invalid changeset: not enough chars in charBank'); - const chars = charBank.slice(0, o.chars); - const nlines = (chars.match(/\n/g) || []).length; - assert(nlines === o.lines, - 'Invalid changeset: number of newlines in insert op does not match the charBank'); - assert(o.lines === 0 || chars.endsWith('\n'), - 'Invalid changeset: multiline insert op does not end with a newline'); - charBank = charBank.slice(o.chars); - calcNewLen += o.chars; - assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`); - break; - } - default: - assert(false, `Invalid changeset: Unknown opcode: ${JSON.stringify(o.opcode)}`); - } - assem.append(o); - } - calcNewLen += oldLen - oldPos; - assert(calcNewLen === newLen, 'Invalid changeset: claimed length does not match actual length'); - assert(charBank === '', 'Invalid changeset: excess characters in the charBank'); - assem.endDocument(); - const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), unpacked.charBank); - assert(normalized === cs, 'Invalid changeset: not in canonical form'); - return cs; -}; - -/** - * @returns {SmartOpAssembler} - */ -exports.smartOpAssembler = () => { - const minusAssem = exports.mergingOpAssembler(); - const plusAssem = exports.mergingOpAssembler(); - const keepAssem = exports.mergingOpAssembler(); - const assem = exports.stringAssembler(); - let lastOpcode = ''; - let lengthChange = 0; - - const flushKeeps = () => { - assem.append(keepAssem.toString()); - keepAssem.clear(); - }; - - const flushPlusMinus = () => { - assem.append(minusAssem.toString()); - minusAssem.clear(); - assem.append(plusAssem.toString()); - plusAssem.clear(); - }; - - const append = (op) => { - if (!op.opcode) return; - if (!op.chars) return; - - if (op.opcode === '-') { - if (lastOpcode === '=') { - flushKeeps(); - } - minusAssem.append(op); - lengthChange -= op.chars; - } else if (op.opcode === '+') { - if (lastOpcode === '=') { - flushKeeps(); - } - plusAssem.append(op); - lengthChange += op.chars; - } else if (op.opcode === '=') { - if (lastOpcode !== '=') { - flushPlusMinus(); - } - keepAssem.append(op); - } - lastOpcode = op.opcode; - }; - - /** - * 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|Iterable)} attribs - The attributes to apply to the operations. - * @param {?AttributePool} pool - Attribute pool. Only required if `attribs` is an iterable of - * attribute key, value pairs. - */ - const appendOpWithText = (opcode, text, attribs, pool) => { - padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' + - 'use opsFromText() instead.'); - for (const op of opsFromText(opcode, text, attribs, pool)) append(op); - }; - - const toString = () => { - flushPlusMinus(); - flushKeeps(); - return assem.toString(); - }; - - const clear = () => { - minusAssem.clear(); - plusAssem.clear(); - keepAssem.clear(); - assem.clear(); - lengthChange = 0; - }; - - const endDocument = () => { - keepAssem.endDocument(); - }; - - const getLengthChange = () => lengthChange; - - return { - append, - toString, - clear, - endDocument, - appendOpWithText, - getLengthChange, - }; -}; - -/** - * @returns {MergingOpAssembler} - */ -exports.mergingOpAssembler = () => { - const assem = exports.opAssembler(); - const bufOp = new Op(); - - // If we get, for example, insertions [xxx\n,yyy], those don't merge, - // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. - // This variable stores the length of yyy and any other newline-less - // ops immediately after it. - let bufOpAdditionalCharsAfterNewline = 0; - - /** - * @param {boolean} [isEndDocument] - */ - const flush = (isEndDocument) => { - 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); - bufOpAdditionalCharsAfterNewline = 0; - } - } - bufOp.opcode = ''; - }; - - const append = (op) => { - 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 { - // append in-line text to multi-line bufOp - bufOpAdditionalCharsAfterNewline += op.chars; - } - } else { - flush(); - copyOp(op, bufOp); - } - }; - - const endDocument = () => { - flush(true); - }; - - const toString = () => { - flush(); - return assem.toString(); - }; - - const clear = () => { - assem.clear(); - clearOp(bufOp); - }; - return { - append, - toString, - clear, - endDocument, - }; -}; - -/** - * @returns {OpAssembler} - */ -exports.opAssembler = () => { - let serialized = ''; - - /** - * @param {Op} op - Operation to add. Ownership remains with the caller. - */ - const append = (op) => { - assert(op instanceof Op, 'argument must be an instance of Op'); - serialized += op.toString(); - }; - - const toString = () => serialized; - - const clear = () => { - serialized = ''; - }; - return { - append, - toString, - clear, - }; -}; - -/** - * A custom made String Iterator - * - * @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; - // newLines is the number of \n between curIndex and str.length - let newLines = str.split('\n').length - 1; - const getnewLines = () => newLines; - - const assertRemaining = (n) => { - assert(n <= remaining(), `!(${n} <= ${remaining()})`); - }; - - const take = (n) => { - assertRemaining(n); - const s = str.substr(curIndex, n); - newLines -= s.split('\n').length - 1; - curIndex += n; - return s; - }; - - const peek = (n) => { - assertRemaining(n); - const s = str.substr(curIndex, n); - return s; - }; - - const skip = (n) => { - assertRemaining(n); - curIndex += n; - }; - - const remaining = () => str.length - curIndex; - return { - take, - skip, - remaining, - peek, - newlines: getnewLines, - }; -}; - -/** - * A custom made StringBuffer - * - * @typedef {object} StringAssembler - * @property {Function} append - - * @property {Function} toString - - */ - -/** - * @returns {StringAssembler} - */ -exports.stringAssembler = () => ({ - _str: '', - clear() { this._str = ''; }, - /** - * @param {string} x - - */ - append(x) { this._str += String(x); }, - toString() { return this._str; }, -}); - -/** - * @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. - * - * 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. - */ -class TextLinesMutator { - /** - * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). - */ - constructor(lines) { - this._lines = lines; - /** - * this._curSplice holds values that will be passed as arguments to this._lines.splice() to - * insert, delete, or change lines: - * - this._curSplice[0] is an index into the this._lines array. - * - this._curSplice[1] is the number of lines that will be removed from the this._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[]?]} - */ - this._curSplice = [0, 0]; - this._inSplice = false; - // position in lines after curSplice is applied: - this._curLine = 0; - this._curCol = 0; - // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && - // curLine >= curSplice[0] - // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then - // curCol == 0 - } - - /** - * Get a line from `lines` at given index. - * - * @param {number} idx - an index - * @returns {string} - */ - _linesGet(idx) { - if ('get' in this._lines) { - return this._lines.get(idx); - } else { - return this._lines[idx]; - } - } - - /** - * Return a slice from `lines`. - * - * @param {number} start - the start index - * @param {number} end - the end index - * @returns {string[]} - */ - _linesSlice(start, end) { - // can be unimplemented if removeLines's return value not needed - if (this._lines.slice) { - return this._lines.slice(start, end); - } else { - return []; - } - } - - /** - * Return the length of `lines`. - * - * @returns {number} - */ - _linesLength() { - if (typeof this._lines.length === 'number') { - return this._lines.length; - } else { - return this._lines.length(); - } - } - - /** - * Starts a new splice. - */ - _enterSplice() { - this._curSplice[0] = this._curLine; - this._curSplice[1] = 0; - // TODO(doc) when is this the case? - // check all enterSplice calls and changes to curCol - if (this._curCol > 0) this._putCurLineInSplice(); - this._inSplice = true; - } - - /** - * Changes the lines array according to the values in curSplice and resets curSplice. Called via - * close or TODO(doc). - */ - _leaveSplice() { - this._lines.splice(...this._curSplice); - this._curSplice.length = 2; - this._curSplice[0] = this._curSplice[1] = 0; - this._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 or inserting). - * - * @returns {boolean} true if curLine is in splice - */ - _isCurLineInSplice() { - // The value of `this._curSplice[1]` does not matter when determining the return value because - // `this._curLine` refers to the line number *after* the splice is applied (so after those lines - // are deleted). - return this._curLine - this._curSplice[0] < this._curSplice.length - 2; - } - - /** - * Incorporates current line into the splice and marks its old position to be deleted. - * - * @returns {number} the index of the added line in curSplice - */ - _putCurLineInSplice() { - if (!this._isCurLineInSplice()) { - this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1])); - this._curSplice[1]++; - } - // TODO should be the same as this._curSplice.length - 1 - return 2 + this._curLine - this._curSplice[0]; - } - - /** - * It will skip some newlines by putting them into the splice. - * - * @param {number} L - - * @param {boolean} includeInSplice - Indicates that attributes are present. - */ - skipLines(L, includeInSplice) { - if (!L) return; - if (includeInSplice) { - if (!this._inSplice) this._enterSplice(); - // TODO(doc) should this count the number of characters that are skipped to check? - for (let i = 0; i < L; i++) { - this._curCol = 0; - this._putCurLineInSplice(); - this._curLine++; - } - } else { - if (this._inSplice) { - if (L > 1) { - // TODO(doc) figure out why single lines are incorporated into splice instead of ignored - this._leaveSplice(); - } else { - this._putCurLineInSplice(); - } - } - this._curLine += L; - this._curCol = 0; - } - // tests case foo in remove(), which isn't otherwise covered in current impl - } - - /** - * 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 - */ - skip(N, L, includeInSplice) { - if (!N) return; - if (L) { - this.skipLines(L, includeInSplice); - } else { - if (includeInSplice && !this._inSplice) this._enterSplice(); - if (this._inSplice) { - // although the line is put into splice curLine is not increased, because - // only some chars are skipped, not the whole line - this._putCurLineInSplice(); - } - this._curCol += N; - } - } - - /** - * Remove whole lines from lines array. - * - * @param {number} L - number of lines to remove - * @returns {string} - */ - removeLines(L) { - if (!L) return ''; - if (!this._inSplice) this._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 = this._curSplice[0] + this._curSplice[1]; - return this._linesSlice(m, m + k).join(''); - }; - - let removed = ''; - if (this._isCurLineInSplice()) { - if (this._curCol === 0) { - removed = this._curSplice[this._curSplice.length - 1]; - this._curSplice.length--; - removed += nextKLinesText(L - 1); - this._curSplice[1] += L - 1; - } else { - removed = nextKLinesText(L - 1); - this._curSplice[1] += L - 1; - const sline = this._curSplice.length - 1; - removed = this._curSplice[sline].substring(this._curCol) + removed; - this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + - this._linesGet(this._curSplice[0] + this._curSplice[1]); - this._curSplice[1] += 1; - } - } else { - removed = nextKLinesText(L); - this._curSplice[1] += L; - } - return removed; - } - - /** - * Remove text from lines array. - * - * @param {number} N - characters to delete - * @param {number} L - lines to delete - * @returns {string} - */ - remove(N, L) { - if (!N) return ''; - if (L) return this.removeLines(L); - if (!this._inSplice) this._enterSplice(); - // although the line is put into splice, curLine is not increased, because - // only some chars are removed not the whole line - const sline = this._putCurLineInSplice(); - const removed = this._curSplice[sline].substring(this._curCol, this._curCol + N); - this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + - this._curSplice[sline].substring(this._curCol + N); - return removed; - } - - /** - * Inserts text into lines array. - * - * @param {string} text - the text to insert - * @param {number} L - number of newlines in text - */ - insert(text, L) { - if (!text) return; - if (!this._inSplice) this._enterSplice(); - if (L) { - const newLines = exports.splitTextLines(text); - if (this._isCurLineInSplice()) { - const sline = this._curSplice.length - 1; - /** @type {string} */ - const theLine = this._curSplice[sline]; - const lineCol = this._curCol; - // Insert the chars up to `curCol` and the first new line. - this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; - this._curLine++; - newLines.splice(0, 1); - // insert the remaining new lines - this._curSplice.push(...newLines); - this._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) - this._curSplice.push(theLine.substring(lineCol)); - this._curCol = 0; // TODO(doc) why is this not set to the length of last line? - } else { - this._curSplice.push(...newLines); - this._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 = this._putCurLineInSplice(); - if (!this._curSplice[sline]) { - const err = new Error( - 'curSplice[sline] not populated, actual curSplice contents is ' + - `${JSON.stringify(this._curSplice)}. Possibly related to ` + - 'https://github.com/ether/etherpad-lite/issues/2802'); - console.error(err.stack || err.toString()); - } - this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + text + - this._curSplice[sline].substring(this._curCol); - this._curCol += text.length; - } - } - - /** - * Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`. - * - * @returns {boolean} indicates if there are lines left - */ - hasMore() { - let docLines = this._linesLength(); - if (this._inSplice) { - docLines += this._curSplice.length - 2 - this._curSplice[1]; - } - return this._curLine < docLines; - } - - /** - * Closes the splice - */ - close() { - if (this._inSplice) this._leaveSplice(); - } -} - -/** - * Apply operations to other operations. - * - * @param {string} in1 - first Op string - * @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 - * `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 - * 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` 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) => { - const ops1 = exports.deserializeOps(in1); - const ops2 = exports.deserializeOps(in2); - let next1 = ops1.next(); - let next2 = ops2.next(); - const assem = exports.smartOpAssembler(); - while (!next1.done || !next2.done) { - if (!next1.done && !next1.value.opcode) next1 = ops1.next(); - if (!next2.done && !next2.value.opcode) next2 = ops2.next(); - if (next1.value == null) next1.value = new Op(); - if (next2.value == null) next2.value = new Op(); - if (!next1.value.opcode && !next2.value.opcode) break; - const opOut = func(next1.value, next2.value); - if (opOut && opOut.opcode) assem.append(opOut); - } - assem.endDocument(); - return assem.toString(); -}; - -/** - * Parses an encoded changeset. - * - * @param {string} cs - The encoded changeset. - * @returns {Changeset} - */ -exports.unpack = (cs) => { - const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; - const headerMatch = headerRegex.exec(cs); - if ((!headerMatch) || (!headerMatch[0])) error(`Not a changeset: ${cs}`); - const oldLen = exports.parseNum(headerMatch[1]); - const changeSign = (headerMatch[2] === '>') ? 1 : -1; - const changeMag = exports.parseNum(headerMatch[3]); - const newLen = oldLen + changeSign * changeMag; - const opsStart = headerMatch[0].length; - let opsEnd = cs.indexOf('$'); - if (opsEnd < 0) opsEnd = cs.length; - return { - oldLen, - newLen, - ops: cs.substring(opsStart, opsEnd), - charBank: cs.substring(opsEnd + 1), - }; -}; - -/** - * 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; - const lenDiffStr = (lenDiff >= 0 ? `>${exports.numToString(lenDiff)}` - : `<${exports.numToString(-lenDiff)}`); - const a = []; - a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank); - return a.join(''); -}; - -/** - * 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); - assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); - const bankIter = exports.stringIterator(unpacked.charBank); - const strIter = exports.stringIterator(str); - const assem = exports.stringAssembler(); - for (const op of exports.deserializeOps(unpacked.ops)) { - switch (op.opcode) { - case '+': - // op is + and op.lines 0: no newlines must be in op.chars - // op is + and op.lines >0: op.chars must include op.lines newlines - if (op.lines !== bankIter.peek(op.chars).split('\n').length - 1) { - throw new Error(`newline count is wrong in op +; cs:${cs} and text:${str}`); - } - assem.append(bankIter.take(op.chars)); - break; - case '-': - // op is - and op.lines 0: no newlines must be in the deleted string - // op is - and op.lines >0: op.lines newlines must be in the deleted string - if (op.lines !== strIter.peek(op.chars).split('\n').length - 1) { - throw new Error(`newline count is wrong in op -; cs:${cs} and text:${str}`); - } - strIter.skip(op.chars); - break; - case '=': - // op is = and op.lines 0: no newlines must be in the copied string - // op is = and op.lines >0: op.lines newlines must be in the copied string - if (op.lines !== strIter.peek(op.chars).split('\n').length - 1) { - throw new Error(`newline count is wrong in op =; cs:${cs} and text:${str}`); - } - assem.append(strIter.take(op.chars)); - break; - } - } - assem.append(strIter.take(strIter.remaining())); - return assem.toString(); -}; - -/** - * 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); - const bankIter = exports.stringIterator(unpacked.charBank); - const mut = new TextLinesMutator(lines); - for (const op of exports.deserializeOps(unpacked.ops)) { - switch (op.opcode) { - case '+': - mut.insert(bankIter.take(op.chars), op.lines); - break; - case '-': - mut.remove(op.chars, op.lines); - break; - case '=': - mut.skip(op.chars, op.lines, (!!op.attribs)); - break; - } - } - mut.close(); -}; - -/** - * Composes two attribute strings (see below) into one. - * - * @param {AttributeString} att1 - first attribute string - * @param {AttributeString} 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. - // Sometimes attribute (key,value) pairs are treated as attribute presence - // information, while other times they are treated as operations that - // mutate a set of attributes, and this affects whether an empty value - // is a deletion or a change. - // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result - // ([], [(bold, )], true) -> [(bold, )] - // ([], [(bold, )], false) -> [] - // ([], [(bold, true)], true) -> [(bold, true)] - // ([], [(bold, true)], false) -> [(bold, true)] - // ([(bold, true)], [(bold, )], true) -> [(bold, )] - // ([(bold, true)], [(bold, )], false) -> [] - // pool can be null if att2 has no attributes. - if ((!att1) && resultIsMutation) { - // In the case of a mutation (i.e. composing two exportss), - // an att2 composed with an empy att1 is just att2. If att1 - // is part of an attribution string, then att2 may remove - // attributes that are already gone, so don't do this optimization. - return att2; - } - if (!att2) return att1; - return AttributeMap.fromString(att1, pool).updateFromString(att2, !resultIsMutation).toString(); -}; - -/** - * 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 {AttributePool} pool - Can be null if definitely not needed. - * @returns {Op} The result of applying `csOp` to `attOp`. - */ -const slicerZipperFunc = (attOp, csOp, pool) => { - const opOut = new Op(); - if (!attOp.opcode) { - copyOp(csOp, opOut); - csOp.opcode = ''; - } else if (!csOp.opcode) { - copyOp(attOp, opOut); - attOp.opcode = ''; - } else if (attOp.opcode === '-') { - copyOp(attOp, opOut); - attOp.opcode = ''; - } else if (csOp.opcode === '+') { - 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. - '=': '+', - }, - '=': { - '-': '-', - '=': '=', - }, - }[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; -}; - -/** - * Applies a Changeset to the attribs string of a AText. - * - * @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); - return applyZip(astr, unpacked.ops, (op1, op2) => slicerZipperFunc(op1, op2, pool)); -}; - -/** - * Applies a changeset to an array of attribute lines. - * - * @param {string} cs - The encoded changeset. - * @param {Array} lines - Attribute lines. Modified in place. - * @param {AttributePool} pool - Attribute pool. - */ -exports.mutateAttributionLines = (cs, lines, pool) => { - const unpacked = exports.unpack(cs); - const csOps = exports.deserializeOps(unpacked.ops); - let csOpsNext = csOps.next(); - const csBank = unpacked.charBank; - let csBankIndex = 0; - // treat the attribution lines as text lines, mutating a line at a time - const mut = new TextLinesMutator(lines); - - /** - * The Ops in the current line from `lines`. - * - * @type {?Generator} - */ - let lineOps = null; - let lineOpsNext = null; - - const lineOpsHasNext = () => lineOpsNext && !lineOpsNext.done; - /** - * Returns false if we are on the last attribute line in `lines` and there is no additional op in - * that line. - * - * @returns {boolean} True if there are more ops to go through. - */ - const isNextMutOp = () => lineOpsHasNext() || mut.hasMore(); - - /** - * @returns {Op} The next Op from `lineIter`. If there are no more Ops, `lineIter` is reset to - * iterate over the next line, which is consumed from `mut`. If there are no more lines, - * returns a null Op. - */ - const nextMutOp = () => { - if (!lineOpsHasNext() && mut.hasMore()) { - // There are more attribute lines in `lines` to do AND either we just started so `lineIter` is - // still null or there are no more ops in current `lineIter`. - const line = mut.removeLines(1); - lineOps = exports.deserializeOps(line); - lineOpsNext = lineOps.next(); - } - if (!lineOpsHasNext()) return new Op(); // No more ops and no more lines. - const op = lineOpsNext.value; - lineOpsNext = lineOps.next(); - return op; - }; - let lineAssem = null; - - /** - * Appends an op to `lineAssem`. In case `lineAssem` includes one single newline, adds it to the - * `lines` mutator. - */ - const outputMutOp = (op) => { - if (!lineAssem) { - lineAssem = exports.mergingOpAssembler(); - } - lineAssem.append(op); - 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; - }; - - let csOp = new Op(); - let attOp = new Op(); - while (csOp.opcode || !csOpsNext.done || attOp.opcode || isNextMutOp()) { - if (!csOp.opcode && !csOpsNext.done) { - // coOp done, but more ops in cs. - csOp = csOpsNext.value; - csOpsNext = csOps.next(); - } - if (!csOp.opcode && !attOp.opcode && !lineAssem && !lineOpsHasNext()) { - break; // done - } else if (csOp.opcode === '=' && csOp.lines > 0 && !csOp.attribs && !attOp.opcode && - !lineAssem && !lineOpsHasNext()) { - // Skip multiple lines without attributes; this is what makes small changes not order of the - // document size. - mut.skipLines(csOp.lines); - csOp.opcode = ''; - } else if (csOp.opcode === '+') { - const opOut = copyOp(csOp); - if (csOp.lines > 1) { - // Copy the first line from `csOp` to `opOut`. - const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; - csOp.chars -= firstLineLen; - csOp.lines--; - opOut.lines = 1; - opOut.chars = firstLineLen; - } else { - // Either one or no newlines in '+' `csOp`, copy to `opOut` and reset `csOp`. - csOp.opcode = ''; - } - outputMutOp(opOut); - csBankIndex += opOut.chars; - } else { - if (!attOp.opcode && isNextMutOp()) attOp = nextMutOp(); - const opOut = slicerZipperFunc(attOp, csOp, pool); - if (opOut.opcode) outputMutOp(opOut); - } - } - - assert(!lineAssem, `line assembler not finished:${cs}`); - mut.close(); -}; - -/** - * Joins several Attribution lines. - * - * @param {string[]} theAlines - collection of Attribution lines - * @returns {string} joined Attribution lines - */ -exports.joinAttributionLines = (theAlines) => { - const assem = exports.mergingOpAssembler(); - for (const aline of theAlines) { - for (const op of exports.deserializeOps(aline)) assem.append(op); - } - return assem.toString(); -}; - -exports.splitAttributionLines = (attrOps, text) => { - const assem = exports.mergingOpAssembler(); - const lines = []; - let pos = 0; - - const appendOp = (op) => { - assem.append(op); - if (op.lines > 0) { - lines.push(assem.toString()); - assem.clear(); - } - pos += op.chars; - }; - - for (const op of exports.deserializeOps(attrOps)) { - let numChars = op.chars; - let numLines = op.lines; - while (numLines > 1) { - const newlineEnd = text.indexOf('\n', pos) + 1; - assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); - op.chars = newlineEnd - pos; - op.lines = 1; - appendOp(op); - numChars -= op.chars; - numLines -= op.lines; - } - if (numLines === 1) { - op.chars = numChars; - op.lines = 1; - } - appendOp(op); - } - - return lines; -}; - -/** - * 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 {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); - 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) => { - const op1code = op1.opcode; - const op2code = op2.opcode; - if (op1code === '+' && op2code === '-') { - bankIter1.skip(Math.min(op1.chars, op2.chars)); - } - const opOut = slicerZipperFunc(op1, op2, pool); - if (opOut.opcode === '+') { - if (op2code === '+') { - bankAssem.append(bankIter2.take(opOut.chars)); - } else { - bankAssem.append(bankIter1.take(opOut.chars)); - } - } - return opOut; - }); - - return exports.pack(len1, len3, newOps, bankAssem.toString()); -}; - -/** - * 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 {Attribute} attribPair - `[key, value]` pair of strings. - * @param {AttributePool} pool - Attribute pool - * @returns {Function} - */ -exports.attributeTester = (attribPair, pool) => { - const never = (attribs) => false; - if (!pool) return never; - const attribNum = pool.putAttrib(attribPair, true); - if (attribNum < 0) return never; - const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); - return (attribs) => re.test(attribs); -}; - -/** - * 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 {string} orig - Original text. - * @param {number} start - Index into `orig` where characters should be removed and inserted. - * @param {number} ndel - Number of characters to delete at `start`. - * @param {string} ins - Text to insert at `start` (after deleting `ndel` characters). - * @param {string} [attribs] - Optional attributes to apply to the inserted text. - * @param {AttributePool} [pool] - Attribute pool. - * @returns {string} - */ -exports.makeSplice = (orig, start, ndel, ins, attribs, pool) => { - if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`); - if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`); - if (start > orig.length) start = orig.length; - if (ndel > orig.length - start) ndel = orig.length - start; - const deleted = orig.substring(start, start + ndel); - const assem = exports.smartOpAssembler(); - const ops = (function* () { - yield* opsFromText('=', orig.substring(0, start)); - yield* opsFromText('-', deleted); - yield* opsFromText('+', ins, attribs, pool); - })(); - for (const op of ops) assem.append(op); - assem.endDocument(); - return exports.pack(orig.length, orig.length + ins.length - ndel, assem.toString(), ins); -}; - -/** - * 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][]} - */ -const toSplices = (cs) => { - const unpacked = exports.unpack(cs); - /** @type {[number, number, string][]} */ - const splices = []; - - let oldPos = 0; - const charIter = exports.stringIterator(unpacked.charBank); - let inSplice = false; - for (const op of exports.deserializeOps(unpacked.ops)) { - if (op.opcode === '=') { - oldPos += op.chars; - inSplice = false; - } else { - if (!inSplice) { - splices.push([oldPos, oldPos, '']); - inSplice = true; - } - if (op.opcode === '-') { - oldPos += op.chars; - splices[splices.length - 1][1] += op.chars; - } else if (op.opcode === '+') { - splices[splices.length - 1][2] += charIter.take(op.chars); - } - } - } - - return splices; -}; - -/** - * @param {string} cs - - * @param {number} startChar - - * @param {number} endChar - - * @param {number} insertionsAfter - - * @returns {[number, number]} - */ -exports.characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { - let newStartChar = startChar; - let newEndChar = endChar; - let lengthChangeSoFar = 0; - for (const splice of toSplices(cs)) { - const spliceStart = splice[0] + lengthChangeSoFar; - const spliceEnd = splice[1] + lengthChangeSoFar; - const newTextLength = splice[2].length; - const thisLengthChange = newTextLength - (spliceEnd - spliceStart); - - if (spliceStart <= newStartChar && spliceEnd >= newEndChar) { - // splice fully replaces/deletes range - // (also case that handles insertion at a collapsed selection) - if (insertionsAfter) { - newStartChar = newEndChar = spliceStart; - } else { - newStartChar = newEndChar = spliceStart + newTextLength; - } - } else if (spliceEnd <= newStartChar) { - // splice is before range - newStartChar += thisLengthChange; - newEndChar += thisLengthChange; - } else if (spliceStart >= newEndChar) { - // splice is after range - } else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) { - // splice is inside range - newEndChar += thisLengthChange; - } else if (spliceEnd < newEndChar) { - // splice overlaps beginning of range - newStartChar = spliceStart + newTextLength; - newEndChar += thisLengthChange; - } else { - // splice overlaps end of range - newEndChar = spliceStart; - } - - lengthChangeSoFar += thisLengthChange; - } - - return [newStartChar, newEndChar]; -}; - -/** - * 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 - let dollarPos = cs.indexOf('$'); - if (dollarPos < 0) { - dollarPos = cs.length; - } - const upToDollar = cs.substring(0, dollarPos); - const fromDollar = cs.substring(dollarPos); - // order of attribs stays the same - return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { - const oldNum = exports.parseNum(a); - 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; -}; - -/** - * Create an attribution inserting a text. - * - * @param {string} text - text to insert - * @returns {string} - */ -exports.makeAttribution = (text) => { - const assem = exports.smartOpAssembler(); - for (const op of opsFromText('+', text)) assem.append(op); - return assem.toString(); -}; - -/** - * Iterates over attributes in exports, attribution string, or attribs property of an op and runs - * function func on them. - * - * @deprecated Use `attributes.decodeAttribString()` instead. - * @param {string} cs - changeset - * @param {Function} func - function to call - */ -exports.eachAttribNumber = (cs, func) => { - padutils.warnDeprecated( - 'Changeset.eachAttribNumber() is deprecated; use attributes.decodeAttribString() instead'); - let dollarPos = cs.indexOf('$'); - if (dollarPos < 0) { - dollarPos = cs.length; - } - const upToDollar = cs.substring(0, dollarPos); - - // WARNING: The following cannot be replaced with a call to `attributes.decodeAttribString()` - // because that function only works on attribute strings, not serialized operations or changesets. - upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { - func(exports.parseNum(a)); - return ''; - }); -}; - -/** - * 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. - * - * @param {string} cs - - * @param {Function} func - - * @returns {string} - */ -exports.mapAttribNumbers = (cs, func) => { - let dollarPos = cs.indexOf('$'); - if (dollarPos < 0) { - dollarPos = cs.length; - } - const upToDollar = cs.substring(0, dollarPos); - - const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => { - const n = func(exports.parseNum(a)); - if (n === true) { - return s; - } else if ((typeof n) === 'number') { - return `*${exports.numToString(n)}`; - } else { - return ''; - } - }); - - return newUpToDollar + cs.substring(dollarPos); -}; - -/** - * 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, - attribs: (attribs || exports.makeAttribution(text)), -}); - -/** - * 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), - attribs: exports.applyToAttribution(cs, atext.attribs, pool), -}); - -/** - * Clones a AText structure. - * - * @param {AText} atext - - * @returns {AText} - */ -exports.cloneAText = (atext) => { - if (!atext) error('atext is null'); - return { - text: atext.text, - attribs: atext.attribs, - }; -}; - -/** - * Copies a AText structure from atext1 to atext2. - * - * @param {AText} atext1 - - * @param {AText} atext2 - - */ -exports.copyAText = (atext1, atext2) => { - atext2.text = atext1.text; - atext2.attribs = atext1.attribs; -}; - -/** - * Convert AText to a series of operations. Strips final newline. - * - * @param {AText} atext - The AText to convert. - * @yields {Op} - * @returns {Generator} - */ -exports.opsFromAText = function* (atext) { - // intentionally skips last newline char of atext - let lastOp = null; - for (const op of exports.deserializeOps(atext.attribs)) { - if (lastOp != null) yield lastOp; - lastOp = op; - } - 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); - yield copyOp(lastOp); - lastOp.lines = 0; - lastOp.chars = lastLineLength; - } - if (lastOp.chars) yield lastOp; -}; - -/** - * Append the set of operations from atext to an assembler. - * - * @deprecated Use `opsFromAText` instead. - * @param {AText} atext - - * @param assem - Assembler like SmartOpAssembler TODO add desc - */ -exports.appendATextToAssembler = (atext, assem) => { - padutils.warnDeprecated( - 'Changeset.appendATextToAssembler() is deprecated; use Changeset.opsFromAText() instead'); - for (const op of exports.opsFromAText(atext)) assem.append(op); -}; - -/** - * 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(); - const newCs = exports.moveOpsToNewPool(cs, pool, newPool); - return { - translated: newCs, - pool: newPool, - }; -}; - -/** - * Checks if a changeset s the identity changeset. - * - * @param {string} cs - - * @returns {boolean} - */ -exports.isIdentity = (cs) => { - const unpacked = exports.unpack(cs); - return unpacked.ops === '' && unpacked.oldLen === unpacked.newLen; -}; - -/** - * @deprecated Use an AttributeMap instead. - */ -const attribsAttributeValue = (attribs, key, pool) => { - if (!attribs) return ''; - for (const [k, v] of attributes.attribsFromString(attribs, pool)) { - if (k === key) return v; - } - return ''; -}; - -/** - * Returns all the values of attributes with a certain key in an Op attribs string. - * - * @deprecated Use an AttributeMap instead. - * @param {Op} op - Op - * @param {string} key - string to search for - * @param {AttributePool} pool - attribute pool - * @returns {string} - */ -exports.opAttributeValue = (op, key, pool) => { - padutils.warnDeprecated( - 'Changeset.opAttributeValue() is deprecated; use an AttributeMap instead'); - return attribsAttributeValue(op.attribs, key, pool); -}; - -/** - * Returns all the values of attributes with a certain key in an attribs string. - * - * @deprecated Use an AttributeMap instead. - * @param {AttributeString} attribs - Attribute string - * @param {string} key - string to search for - * @param {AttributePool} pool - attribute pool - * @returns {string} - */ -exports.attribsAttributeValue = (attribs, key, pool) => { - padutils.warnDeprecated( - 'Changeset.attribsAttributeValue() is deprecated; use an AttributeMap instead'); - return attribsAttributeValue(attribs, key, pool); -}; - -/** - * 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(); - const o = new Op(); - const charBank = exports.stringAssembler(); - - const self = { - /** - * @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 = '='; - o.attribs = typeof attribs === 'string' - ? attribs : new AttributeMap(pool).update(attribs || []).toString(); - o.chars = N; - o.lines = (L || 0); - 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) => { - for (const op of opsFromText('=', text, attribs, pool)) assem.append(op); - 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) => { - for (const op of opsFromText('+', text, attribs, pool)) assem.append(op); - 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 = ''; - o.chars = N; - o.lines = (L || 0); - assem.append(o); - return self; - }, - - toString: () => { - assem.endDocument(); - const newLen = oldLen + assem.getLengthChange(); - return exports.pack(oldLen, newLen, assem.toString(), charBank.toString()); - }, - }; - - return self; -}; - -/** - * Constructs an attribute string from a sequence of attributes. - * - * @deprecated Use `AttributeMap.prototype.toString()` or `attributes.attribsToString()` instead. - * @param {string} opcode - The opcode for the Op that will get the resulting attribute string. - * @param {?(Iterable|AttributeString)} attribs - The attributes to insert into the pool - * (if necessary) and encode. If an attribute string, no checking is performed to ensure that - * the attributes exist in the pool, are in the canonical order, and contain no duplicate keys. - * If this is an iterable of attributes, `pool` must be non-null. - * @param {AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of attributes, - * ignored if `attribs` is an attribute string. - * @returns {AttributeString} - */ -exports.makeAttribsString = (opcode, attribs, pool) => { - padutils.warnDeprecated( - 'Changeset.makeAttribsString() is deprecated; ' + - 'use AttributeMap.prototype.toString() or attributes.attribsToString() instead'); - if (!attribs || !['=', '+'].includes(opcode)) return ''; - if (typeof attribs === 'string') return attribs; - return new AttributeMap(pool).update(attribs, opcode === '+').toString(); -}; - -/** - * Like "substring" but on a single-line attribution string. - */ -exports.subattribution = (astr, start, optEnd) => { - const attOps = exports.deserializeOps(astr); - let attOpsNext = attOps.next(); - const assem = exports.smartOpAssembler(); - let attOp = new Op(); - const csOp = new Op(); - - const doCsOp = () => { - if (!csOp.chars) return; - while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) { - if (!attOp.opcode) { - attOp = attOpsNext.value; - attOpsNext = attOps.next(); - } - if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && - attOp.lines > 0 && csOp.lines <= 0) { - csOp.lines++; - } - const opOut = slicerZipperFunc(attOp, csOp, null); - if (opOut.opcode) assem.append(opOut); - } - }; - - csOp.opcode = '-'; - csOp.chars = start; - - doCsOp(); - - if (optEnd === undefined) { - if (attOp.opcode) { - assem.append(attOp); - } - while (!attOpsNext.done) { - assem.append(attOpsNext.value); - attOpsNext = attOps.next(); - } - } else { - csOp.opcode = '='; - csOp.chars = optEnd - start; - doCsOp(); - } - - return assem.toString(); -}; - -exports.inverse = (cs, lines, alines, pool) => { - // lines and alines are what the exports is meant to apply to. - // They may be arrays or objects with .get(i) and .length methods. - // They include final newlines on lines. - - const linesGet = (idx) => { - if (lines.get) { - return lines.get(idx); - } else { - return lines[idx]; - } - }; - - /** - * @param {number} idx - - * @returns {string} - */ - const alinesGet = (idx) => { - if (alines.get) { - return alines.get(idx); - } else { - return alines[idx]; - } - }; - - let curLine = 0; - let curChar = 0; - let curLineOps = null; - let curLineOpsNext = null; - let curLineOpsLine; - let curLineNextOp = new Op('+'); - - const unpacked = exports.unpack(cs); - const builder = exports.builder(unpacked.newLen); - - const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { - if (!curLineOps || curLineOpsLine !== curLine) { - curLineOps = exports.deserializeOps(alinesGet(curLine)); - curLineOpsNext = curLineOps.next(); - curLineOpsLine = curLine; - let indexIntoLine = 0; - while (!curLineOpsNext.done) { - curLineNextOp = curLineOpsNext.value; - curLineOpsNext = curLineOps.next(); - if (indexIntoLine + curLineNextOp.chars >= curChar) { - curLineNextOp.chars -= (curChar - indexIntoLine); - break; - } - indexIntoLine += curLineNextOp.chars; - } - } - - while (numChars > 0) { - if (!curLineNextOp.chars && curLineOpsNext.done) { - curLine++; - curChar = 0; - curLineOpsLine = curLine; - curLineNextOp.chars = 0; - curLineOps = exports.deserializeOps(alinesGet(curLine)); - curLineOpsNext = curLineOps.next(); - } - if (!curLineNextOp.chars) { - if (curLineOpsNext.done) { - curLineNextOp = new Op(); - } else { - curLineNextOp = curLineOpsNext.value; - curLineOpsNext = curLineOps.next(); - } - } - const charsToUse = Math.min(numChars, curLineNextOp.chars); - func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && - curLineNextOp.lines > 0); - numChars -= charsToUse; - curLineNextOp.chars -= charsToUse; - curChar += charsToUse; - } - - if (!curLineNextOp.chars && curLineOpsNext.done) { - curLine++; - curChar = 0; - } - }; - - const skip = (N, L) => { - if (L) { - curLine += L; - curChar = 0; - } else if (curLineOps && curLineOpsLine === curLine) { - consumeAttribRuns(N, () => {}); - } else { - curChar += N; - } - }; - - const nextText = (numChars) => { - let len = 0; - const assem = exports.stringAssembler(); - const firstString = linesGet(curLine).substring(curChar); - len += firstString.length; - assem.append(firstString); - - let lineNum = curLine + 1; - while (len < numChars) { - const nextString = linesGet(lineNum); - len += nextString.length; - assem.append(nextString); - lineNum++; - } - - return assem.toString().substring(0, numChars); - }; - - const cachedStrFunc = (func) => { - const cache = {}; - return (s) => { - if (!cache[s]) { - cache[s] = func(s); - } - return cache[s]; - }; - }; - - for (const csOp of exports.deserializeOps(unpacked.ops)) { - if (csOp.opcode === '=') { - if (csOp.attribs) { - const attribs = AttributeMap.fromString(csOp.attribs, pool); - const undoBackToAttribs = cachedStrFunc((oldAttribsStr) => { - const oldAttribs = AttributeMap.fromString(oldAttribsStr, pool); - const backAttribs = new AttributeMap(pool); - for (const [key, value] of attribs) { - const oldValue = oldAttribs.get(key) || ''; - if (oldValue !== value) backAttribs.set(key, oldValue); - } - // TODO: backAttribs does not restore removed attributes (it is missing attributes that - // are in oldAttribs but not in attribs). I don't know if that is intentional. - return backAttribs.toString(); - }); - consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { - builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); - }); - } else { - skip(csOp.chars, csOp.lines); - builder.keep(csOp.chars, csOp.lines); - } - } else if (csOp.opcode === '+') { - builder.remove(csOp.chars, csOp.lines); - } else if (csOp.opcode === '-') { - const textBank = nextText(csOp.chars); - let textBankIndex = 0; - consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { - builder.insert(textBank.substr(textBankIndex, len), attribs); - textBankIndex += len; - }); - } - } - - return exports.checkRep(builder.toString()); -}; - -// %CLIENT FILE ENDS HERE% -exports.follow = (cs1, cs2, reverseInsertOrder, pool) => { - const unpacked1 = exports.unpack(cs1); - const unpacked2 = exports.unpack(cs2); - const len1 = unpacked1.oldLen; - const len2 = unpacked2.oldLen; - assert(len1 === len2, 'mismatched follow - cannot transform cs1 on top of cs2'); - const chars1 = exports.stringIterator(unpacked1.charBank); - const chars2 = exports.stringIterator(unpacked2.charBank); - - const oldLen = unpacked1.newLen; - let oldPos = 0; - let newLen = 0; - - const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); - - const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => { - const opOut = new Op(); - if (op1.opcode === '+' || op2.opcode === '+') { - let whichToDo; - if (op2.opcode !== '+') { - whichToDo = 1; - } else if (op1.opcode !== '+') { - whichToDo = 2; - } else { - // both + - const firstChar1 = chars1.peek(1); - const firstChar2 = chars2.peek(1); - const insertFirst1 = hasInsertFirst(op1.attribs); - const insertFirst2 = hasInsertFirst(op2.attribs); - if (insertFirst1 && !insertFirst2) { - whichToDo = 1; - } else if (insertFirst2 && !insertFirst1) { - whichToDo = 2; - } else if (firstChar1 === '\n' && firstChar2 !== '\n') { - // insert string that doesn't start with a newline first so as not to break up lines - whichToDo = 2; - } else if (firstChar1 !== '\n' && firstChar2 === '\n') { - whichToDo = 1; - } else if (reverseInsertOrder) { - // break symmetry: - whichToDo = 2; - } else { - whichToDo = 1; - } - } - if (whichToDo === 1) { - chars1.skip(op1.chars); - opOut.opcode = '='; - opOut.lines = op1.lines; - opOut.chars = op1.chars; - opOut.attribs = ''; - op1.opcode = ''; - } else { - // whichToDo == 2 - chars2.skip(op2.chars); - copyOp(op2, opOut); - op2.opcode = ''; - } - } else if (op1.opcode === '-') { - if (!op2.opcode) { - op1.opcode = ''; - } else if (op1.chars <= op2.chars) { - op2.chars -= op1.chars; - op2.lines -= op1.lines; - op1.opcode = ''; - if (!op2.chars) { - op2.opcode = ''; - } - } else { - op1.chars -= op2.chars; - op1.lines -= op2.lines; - op2.opcode = ''; - } - } else if (op2.opcode === '-') { - copyOp(op2, opOut); - if (!op1.opcode) { - op2.opcode = ''; - } else if (op2.chars <= op1.chars) { - // delete part or all of a keep - op1.chars -= op2.chars; - op1.lines -= op2.lines; - op2.opcode = ''; - if (!op1.chars) { - op1.opcode = ''; - } - } else { - // delete all of a keep, and keep going - opOut.lines = op1.lines; - opOut.chars = op1.chars; - op2.lines -= op1.lines; - op2.chars -= op1.chars; - op1.opcode = ''; - } - } else if (!op1.opcode) { - 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. - // copyOp(op1, opOut); - op1.opcode = ''; - } else { - // both keeps - opOut.opcode = '='; - opOut.attribs = followAttributes(op1.attribs, op2.attribs, pool); - if (op1.chars <= op2.chars) { - opOut.chars = op1.chars; - opOut.lines = op1.lines; - op2.chars -= op1.chars; - op2.lines -= op1.lines; - op1.opcode = ''; - if (!op2.chars) { - op2.opcode = ''; - } - } else { - opOut.chars = op2.chars; - opOut.lines = op2.lines; - op1.chars -= op2.chars; - op1.lines -= op2.lines; - op2.opcode = ''; - } - } - switch (opOut.opcode) { - case '=': - oldPos += opOut.chars; - newLen += opOut.chars; - break; - case '-': - oldPos += opOut.chars; - break; - case '+': - newLen += opOut.chars; - break; - } - return opOut; - }); - newLen += oldLen - oldPos; - - return exports.pack(oldLen, newLen, newOps, unpacked2.charBank); -}; - -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 - // both attribute sets are taken. This operation is the "follow", - // so a set of changes is produced that can be applied to att1 - // to produce the merged set. - if ((!att2) || (!pool)) return ''; - if (!att1) return att2; - const atts = new Map(); - att2.replace(/\*([0-9a-z]+)/g, (_, a) => { - const [key, val] = pool.getAttrib(exports.parseNum(a)); - atts.set(key, val); - return ''; - }); - att1.replace(/\*([0-9a-z]+)/g, (_, a) => { - 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 - const buf = exports.stringAssembler(); - for (const att of atts) { - buf.append('*'); - buf.append(exports.numToString(pool.putAttrib(att))); - } - return buf.toString(); -}; - -exports.exportedForTestingOnly = { - TextLinesMutator, - followAttributes, - toSplices, -}; diff --git a/src/static/js/Changeset.ts b/src/static/js/Changeset.ts new file mode 100644 index 00000000000..db29738abcc --- /dev/null +++ b/src/static/js/Changeset.ts @@ -0,0 +1,1976 @@ +import AttributeMap from "./AttributeMap.js"; +import {AttributePool} from "./AttributePool"; +import * as attributes from "./attributes.js"; +import { padutils } from "./pad_utils.js"; +import {CustomError} from "../../node/utils/customError"; +'use strict'; +/** + * A `[key, value]` pair of strings describing a text attribute. + * + * @typedef {[string, string]} Attribute + */ +/** + * A concatenated sequence of zero or more attribute identifiers, each one represented by an + * asterisk followed by a base-36 encoded attribute number. + * + * Examples: '', '*0', '*3*j*z*1q' + * + * @typedef {string} AttributeString + */ +/** + * This method is called whenever there is an error in the sync process. + * + * @param {string} msg - Just some message + */ +const error = (msg) => { + const e = new CustomError(msg); + e.easysync = true; + throw e; +}; +/** + * 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} msg - error message to include in the exception + * @type {(b: boolean, msg: string) => asserts b} + */ +const assert = (b, msg) => { + if (!b) + error(`Failed assertion: ${msg}`); +}; +/** + * An operation to apply to a shared document. + */ +class Op { + chars: number; + opcode: string; + lines: number; + attribs: string; + /** + * @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property. + */ + constructor(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. + * + * @type {(''|'='|'+'|'-')} + * @public + */ + this.opcode = opcode; + /** + * The number of characters to keep, insert, or delete. + * + * @type {number} + * @public + */ + this.chars = 0; + /** + * The number of characters among the `chars` characters that are newlines. If non-zero, the + * last character must be a newline. + * + * @type {number} + * @public + */ + this.lines = 0; + /** + * 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 attribute pool. + * + * 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. + * + * This is the empty string for remove ('-') operations. + * + * @type {string} + * @public + */ + this.attribs = ''; + } + toString() { + if (!this.opcode) + throw new TypeError('null op'); + if (typeof this.attribs !== 'string') + throw new TypeError('attribs must be a string'); + const l = this.lines ? `|${exports.numToString(this.lines)}` : ''; + return this.attribs + l + this.opcode + exports.numToString(this.chars); + } +} +/** + * Iterator over a changeset's operations. + * + * Note: This class does NOT implement the ECMAScript iterable or iterator protocols. + * + * @deprecated Use `deserializeOps` instead. + */ +class OpIter { + private _gen: any; + private _next: any; + /** + * @param {string} ops - String encoding the change operations to iterate over. + */ + constructor(ops) { + this._gen = exports.deserializeOps(ops); + this._next = this._gen.next(); + } + /** + * @returns {boolean} Whether there are any remaining operations. + */ + hasNext() { + return !this._next.done; + } + /** + * Returns the next operation object and advances the iterator. + * + * Note: This does NOT implement the ECMAScript iterator protocol. + * + * @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value. + * @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are + * no more operations. + */ + next(opOut = new Op()) { + if (this.hasNext()) { + copyOp(this._next.value, opOut); + this._next = this._gen.next(); + } + else { + clearOp(opOut); + } + return opOut; + } +} +/** + * Cleans an Op object. + * + * @param {Op} op - object to clear + */ +const clearOp = (op) => { + op.opcode = ''; + op.chars = 0; + op.lines = 0; + op.attribs = ''; +}; +/** + * Copies op1 to op2 + * + * @param {Op} op1 - src Op + * @param {Op} [op2] - dest Op. If not given, a new Op is used. + * @returns {Op} `op2` + */ +const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1); +/** + * 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 - + */ +/** + * Generates operations from the given text and attributes. + * + * @param {('-'|'+'|'=')} opcode - The operator to use. + * @param {string} text - The text to remove/add/keep. + * @param {(Iterable|AttributeString)} [attribs] - The attributes to insert into the pool + * (if necessary) and encode. If an attribute string, no checking is performed to ensure that + * the attributes exist in the pool, are in the canonical order, and contain no duplicate keys. + * If this is an iterable of attributes, `pool` must be non-null. + * @param {?AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of + * attributes, ignored if `attribs` is an attribute string. + * @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 = new Op(opcode); + op.attribs = typeof attribs === 'string' + ? attribs : new AttributeMap(pool).update(attribs || [], opcode === '+').toString(); + 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; + } +}; +/** + * @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. + * + * 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. + */ +class TextLinesMutator { + _lines: any; + // FIXME What is this for a type? + _curSplice: any[]; + _inSplice: boolean; + _curLine: number; + _curCol: number; + /** + * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). + */ + constructor(lines) { + this._lines = lines; + /** + * this._curSplice holds values that will be passed as arguments to this._lines.splice() to + * insert, delete, or change lines: + * - this._curSplice[0] is an index into the this._lines array. + * - this._curSplice[1] is the number of lines that will be removed from the this._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[]?]} + */ + this._curSplice = [0, 0]; + this._inSplice = false; + // position in lines after curSplice is applied: + this._curLine = 0; + this._curCol = 0; + // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && + // curLine >= curSplice[0] + // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then + // curCol == 0 + } + /** + * Get a line from `lines` at given index. + * + * @param {number} idx - an index + * @returns {string} + */ + _linesGet(idx) { + if ('get' in this._lines) { + return this._lines.get(idx); + } + else { + return this._lines[idx]; + } + } + /** + * Return a slice from `lines`. + * + * @param {number} start - the start index + * @param {number} end - the end index + * @returns {string[]} + */ + _linesSlice(start, end) { + // can be unimplemented if removeLines's return value not needed + if (this._lines.slice) { + return this._lines.slice(start, end); + } + else { + return []; + } + } + /** + * Return the length of `lines`. + * + * @returns {number} + */ + _linesLength() { + if (typeof this._lines.length === 'number') { + return this._lines.length; + } + else { + return this._lines.length(); + } + } + /** + * Starts a new splice. + */ + _enterSplice() { + this._curSplice[0] = this._curLine; + this._curSplice[1] = 0; + // TODO(doc) when is this the case? + // check all enterSplice calls and changes to curCol + if (this._curCol > 0) + this._putCurLineInSplice(); + this._inSplice = true; + } + /** + * Changes the lines array according to the values in curSplice and resets curSplice. Called via + * close or TODO(doc). + */ + _leaveSplice() { + this._lines.splice(...this._curSplice); + this._curSplice.length = 2; + this._curSplice[0] = this._curSplice[1] = 0; + this._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 or inserting). + * + * @returns {boolean} true if curLine is in splice + */ + _isCurLineInSplice() { + // The value of `this._curSplice[1]` does not matter when determining the return value because + // `this._curLine` refers to the line number *after* the splice is applied (so after those lines + // are deleted). + return this._curLine - this._curSplice[0] < this._curSplice.length - 2; + } + /** + * Incorporates current line into the splice and marks its old position to be deleted. + * + * @returns {number} the index of the added line in curSplice + */ + _putCurLineInSplice() { + if (!this._isCurLineInSplice()) { + this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1])); + this._curSplice[1]++; + } + // TODO should be the same as this._curSplice.length - 1 + return 2 + this._curLine - this._curSplice[0]; + } + /** + * It will skip some newlines by putting them into the splice. + * + * @param {number} L - + * @param {boolean} includeInSplice - Indicates that attributes are present. + */ + skipLines(L, includeInSplice?) { + if (!L) + return; + if (includeInSplice) { + if (!this._inSplice) + this._enterSplice(); + // TODO(doc) should this count the number of characters that are skipped to check? + for (let i = 0; i < L; i++) { + this._curCol = 0; + this._putCurLineInSplice(); + this._curLine++; + } + } + else { + if (this._inSplice) { + if (L > 1) { + // TODO(doc) figure out why single lines are incorporated into splice instead of ignored + this._leaveSplice(); + } + else { + this._putCurLineInSplice(); + } + } + this._curLine += L; + this._curCol = 0; + } + // tests case foo in remove(), which isn't otherwise covered in current impl + } + /** + * 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 + */ + skip(N, L, includeInSplice) { + if (!N) + return; + if (L) { + this.skipLines(L, includeInSplice); + } + else { + if (includeInSplice && !this._inSplice) + this._enterSplice(); + if (this._inSplice) { + // although the line is put into splice curLine is not increased, because + // only some chars are skipped, not the whole line + this._putCurLineInSplice(); + } + this._curCol += N; + } + } + /** + * Remove whole lines from lines array. + * + * @param {number} L - number of lines to remove + * @returns {string} + */ + removeLines(L) { + if (!L) + return ''; + if (!this._inSplice) + this._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 = this._curSplice[0] + this._curSplice[1]; + return this._linesSlice(m, m + k).join(''); + }; + let removed = ''; + if (this._isCurLineInSplice()) { + if (this._curCol === 0) { + removed = this._curSplice[this._curSplice.length - 1]; + this._curSplice.length--; + removed += nextKLinesText(L - 1); + this._curSplice[1] += L - 1; + } + else { + removed = nextKLinesText(L - 1); + this._curSplice[1] += L - 1; + const sline = this._curSplice.length - 1; + removed = this._curSplice[sline].substring(this._curCol) + removed; + this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + + this._linesGet(this._curSplice[0] + this._curSplice[1]); + this._curSplice[1] += 1; + } + } + else { + removed = nextKLinesText(L); + this._curSplice[1] += L; + } + return removed; + } + /** + * Remove text from lines array. + * + * @param {number} N - characters to delete + * @param {number} L - lines to delete + * @returns {string} + */ + remove(N, L) { + if (!N) + return ''; + if (L) + return this.removeLines(L); + if (!this._inSplice) + this._enterSplice(); + // although the line is put into splice, curLine is not increased, because + // only some chars are removed not the whole line + const sline = this._putCurLineInSplice(); + const removed = this._curSplice[sline].substring(this._curCol, this._curCol + N); + this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + + this._curSplice[sline].substring(this._curCol + N); + return removed; + } + /** + * Inserts text into lines array. + * + * @param {string} text - the text to insert + * @param {number} L - number of newlines in text + */ + insert(text, L) { + if (!text) + return; + if (!this._inSplice) + this._enterSplice(); + if (L) { + const newLines = exports.splitTextLines(text); + if (this._isCurLineInSplice()) { + const sline = this._curSplice.length - 1; + /** @type {string} */ + const theLine = this._curSplice[sline]; + const lineCol = this._curCol; + // Insert the chars up to `curCol` and the first new line. + this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; + this._curLine++; + newLines.splice(0, 1); + // insert the remaining new lines + this._curSplice.push(...newLines); + this._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) + this._curSplice.push(theLine.substring(lineCol)); + this._curCol = 0; // TODO(doc) why is this not set to the length of last line? + } + else { + this._curSplice.push(...newLines); + this._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 = this._putCurLineInSplice(); + if (!this._curSplice[sline]) { + const err = new Error('curSplice[sline] not populated, actual curSplice contents is ' + + `${JSON.stringify(this._curSplice)}. Possibly related to ` + + 'https://github.com/ether/etherpad-lite/issues/2802'); + console.error(err.stack || err.toString()); + } + this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + text + + this._curSplice[sline].substring(this._curCol); + this._curCol += text.length; + } + } + /** + * Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`. + * + * @returns {boolean} indicates if there are lines left + */ + hasMore() { + let docLines = this._linesLength(); + if (this._inSplice) { + docLines += this._curSplice.length - 2 - this._curSplice[1]; + } + return this._curLine < docLines; + } + /** + * Closes the splice + */ + close() { + if (this._inSplice) + this._leaveSplice(); + } +} +/** + * Apply operations to other operations. + * + * @param {string} in1 - first Op string + * @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 + * `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 + * 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` 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) => { + const ops1 = exports.deserializeOps(in1); + const ops2 = exports.deserializeOps(in2); + let next1 = ops1.next(); + let next2 = ops2.next(); + const assem = exports.smartOpAssembler(); + while (!next1.done || !next2.done) { + if (!next1.done && !next1.value.opcode) + next1 = ops1.next(); + if (!next2.done && !next2.value.opcode) + next2 = ops2.next(); + if (next1.value == null) + next1.value = new Op(); + if (next2.value == null) + next2.value = new Op(); + if (!next1.value.opcode && !next2.value.opcode) + break; + const opOut = func(next1.value, next2.value); + if (opOut && opOut.opcode) + assem.append(opOut); + } + assem.endDocument(); + return assem.toString(); +}; +/** + * 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 {AttributePool} pool - Can be null if definitely not needed. + * @returns {Op} The result of applying `csOp` to `attOp`. + */ +const slicerZipperFunc = (attOp, csOp, pool) => { + const opOut = new Op(); + if (!attOp.opcode) { + copyOp(csOp, opOut); + csOp.opcode = ''; + } + else if (!csOp.opcode) { + copyOp(attOp, opOut); + attOp.opcode = ''; + } + else if (attOp.opcode === '-') { + copyOp(attOp, opOut); + attOp.opcode = ''; + } + else if (csOp.opcode === '+') { + 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 = { + '+': { + '-': '', + '=': '+', + }, + '=': { + '-': '-', + '=': '=', + }, + }[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; +}; +/** + * 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][]} + */ +const toSplices = (cs) => { + const unpacked = exports.unpack(cs); + /** @type {[number, number, string][]} */ + const splices = []; + let oldPos = 0; + const charIter = exports.stringIterator(unpacked.charBank); + let inSplice = false; + for (const op of exports.deserializeOps(unpacked.ops)) { + if (op.opcode === '=') { + oldPos += op.chars; + inSplice = false; + } + else { + if (!inSplice) { + splices.push([oldPos, oldPos, '']); + inSplice = true; + } + if (op.opcode === '-') { + oldPos += op.chars; + splices[splices.length - 1][1] += op.chars; + } + else if (op.opcode === '+') { + splices[splices.length - 1][2] += charIter.take(op.chars); + } + } + } + return splices; +}; +/** + * @deprecated Use an AttributeMap instead. + */ +const attribsAttributeValue = (attribs, key, pool) => { + if (!attribs) + return ''; + for (const [k, v] of attributes.attribsFromString(attribs, pool)) { + if (k === key) + return v; + } + return ''; +}; +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 + // both attribute sets are taken. This operation is the "follow", + // so a set of changes is produced that can be applied to att1 + // to produce the merged set. + if ((!att2) || (!pool)) + return ''; + if (!att1) + return att2; + const atts = new Map(); + att2.replace(/\*([0-9a-z]+)/g, (_, a) => { + const [key, val] = pool.getAttrib(exports.parseNum(a)); + atts.set(key, val); + return ''; + }); + att1.replace(/\*([0-9a-z]+)/g, (_, a) => { + 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 + const buf = exports.stringAssembler(); + for (const att of atts) { + buf.append('*'); + buf.append(exports.numToString(pool.putAttrib(att))); + } + return buf.toString(); +}; +export const parseNum = (str) => parseInt(str, 36); +export const numToString = (num) => num.toString(36).toLowerCase(); +export const oldLen = (cs) => exports.unpack(cs).oldLen; +export const newLen = (cs) => exports.unpack(cs).newLen; +export const deserializeOps = function* (ops) { + // TODO: Migrate to String.prototype.matchAll() once there is enough browser support. + const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; + let match; + while ((match = regex.exec(ops)) != null) { + if (match[5] === '$') + return; // Start of the insert operation character bank. + if (match[5] != null) + error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`); + const op = new Op(match[3]); + op.lines = exports.parseNum(match[2] || '0'); + op.chars = exports.parseNum(match[4]); + op.attribs = match[1]; + yield op; + } +}; +export const opIterator = (opsStr) => { + padutils.warnDeprecated('Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead'); + return new OpIter(opsStr); +}; +export const newOp = (optOpcode) => { + padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead'); + return new Op(optOpcode); +}; +export const checkRep = (cs) => { + const unpacked = exports.unpack(cs); + const oldLen = unpacked.oldLen; + const newLen = unpacked.newLen; + const ops = unpacked.ops; + let charBank = unpacked.charBank; + const assem = exports.smartOpAssembler(); + let oldPos = 0; + let calcNewLen = 0; + for (const o of exports.deserializeOps(ops)) { + switch (o.opcode) { + case '=': + oldPos += o.chars; + calcNewLen += o.chars; + break; + case '-': + oldPos += o.chars; + assert(oldPos <= oldLen, `${oldPos} > ${oldLen} in ${cs}`); + break; + case '+': + { + assert(charBank.length >= o.chars, 'Invalid changeset: not enough chars in charBank'); + const chars = charBank.slice(0, o.chars); + const nlines = (chars.match(/\n/g) || []).length; + assert(nlines === o.lines, 'Invalid changeset: number of newlines in insert op does not match the charBank'); + assert(o.lines === 0 || chars.endsWith('\n'), 'Invalid changeset: multiline insert op does not end with a newline'); + charBank = charBank.slice(o.chars); + calcNewLen += o.chars; + assert(calcNewLen <= newLen, `${calcNewLen} > ${newLen} in ${cs}`); + break; + } + default: + assert(false, `Invalid changeset: Unknown opcode: ${JSON.stringify(o.opcode)}`); + } + assem.append(o); + } + calcNewLen += oldLen - oldPos; + assert(calcNewLen === newLen, 'Invalid changeset: claimed length does not match actual length'); + assert(charBank === '', 'Invalid changeset: excess characters in the charBank'); + assem.endDocument(); + const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), unpacked.charBank); + assert(normalized === cs, 'Invalid changeset: not in canonical form'); + return cs; +}; +export const smartOpAssembler = () => { + const minusAssem = exports.mergingOpAssembler(); + const plusAssem = exports.mergingOpAssembler(); + const keepAssem = exports.mergingOpAssembler(); + const assem = exports.stringAssembler(); + let lastOpcode = ''; + let lengthChange = 0; + const flushKeeps = () => { + assem.append(keepAssem.toString()); + keepAssem.clear(); + }; + const flushPlusMinus = () => { + assem.append(minusAssem.toString()); + minusAssem.clear(); + assem.append(plusAssem.toString()); + plusAssem.clear(); + }; + const append = (op) => { + if (!op.opcode) + return; + if (!op.chars) + return; + if (op.opcode === '-') { + if (lastOpcode === '=') { + flushKeeps(); + } + minusAssem.append(op); + lengthChange -= op.chars; + } + else if (op.opcode === '+') { + if (lastOpcode === '=') { + flushKeeps(); + } + plusAssem.append(op); + lengthChange += op.chars; + } + else if (op.opcode === '=') { + if (lastOpcode !== '=') { + flushPlusMinus(); + } + keepAssem.append(op); + } + lastOpcode = op.opcode; + }; + /** + * 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|Iterable)} attribs - The attributes to apply to the operations. + * @param {?AttributePool} pool - Attribute pool. Only required if `attribs` is an iterable of + * attribute key, value pairs. + */ + const appendOpWithText = (opcode, text, attribs, pool) => { + padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' + + 'use opsFromText() instead.'); + for (const op of opsFromText(opcode, text, attribs, pool)) + append(op); + }; + const toString = () => { + flushPlusMinus(); + flushKeeps(); + return assem.toString(); + }; + const clear = () => { + minusAssem.clear(); + plusAssem.clear(); + keepAssem.clear(); + assem.clear(); + lengthChange = 0; + }; + const endDocument = () => { + keepAssem.endDocument(); + }; + const getLengthChange = () => lengthChange; + return { + append, + toString, + clear, + endDocument, + appendOpWithText, + getLengthChange, + }; +}; +export const mergingOpAssembler = () => { + const assem = exports.opAssembler(); + const bufOp = new Op(); + // If we get, for example, insertions [xxx\n,yyy], those don't merge, + // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. + // This variable stores the length of yyy and any other newline-less + // ops immediately after it. + let bufOpAdditionalCharsAfterNewline = 0; + /** + * @param {boolean} [isEndDocument] + */ + const flush = (isEndDocument?: boolean) => { + 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); + bufOpAdditionalCharsAfterNewline = 0; + } + } + bufOp.opcode = ''; + }; + const append = (op) => { + 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 { + // append in-line text to multi-line bufOp + bufOpAdditionalCharsAfterNewline += op.chars; + } + } + else { + flush(); + copyOp(op, bufOp); + } + }; + const endDocument = () => { + flush(true); + }; + const toString = () => { + flush(); + return assem.toString(); + }; + const clear = () => { + assem.clear(); + clearOp(bufOp); + }; + return { + append, + toString, + clear, + endDocument, + }; +}; +export const opAssembler = () => { + let serialized = ''; + /** + * @param {Op} op - Operation to add. Ownership remains with the caller. + */ + const append = (op) => { + assert(op instanceof Op, 'argument must be an instance of Op'); + serialized += op.toString(); + }; + const toString = () => serialized; + const clear = () => { + serialized = ''; + }; + return { + append, + toString, + clear, + }; +}; +export const stringIterator = (str) => { + let curIndex = 0; + // newLines is the number of \n between curIndex and str.length + let newLines = str.split('\n').length - 1; + const getnewLines = () => newLines; + const assertRemaining = (n) => { + assert(n <= remaining(), `!(${n} <= ${remaining()})`); + }; + const take = (n) => { + assertRemaining(n); + const s = str.substr(curIndex, n); + newLines -= s.split('\n').length - 1; + curIndex += n; + return s; + }; + const peek = (n) => { + assertRemaining(n); + const s = str.substr(curIndex, n); + return s; + }; + const skip = (n) => { + assertRemaining(n); + curIndex += n; + }; + const remaining = () => str.length - curIndex; + return { + take, + skip, + remaining, + peek, + newlines: getnewLines, + }; +}; +export const stringAssembler = () => ({ + _str: '', + clear() { this._str = ''; }, + /** + * @param {string} x - + */ + append(x) { this._str += String(x); }, + toString() { return this._str; }, +}); +export const unpack = (cs) => { + const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; + const headerMatch = headerRegex.exec(cs); + if ((!headerMatch) || (!headerMatch[0])) + error(`Not a changeset: ${cs}`); + const oldLen = exports.parseNum(headerMatch[1]); + const changeSign = (headerMatch[2] === '>') ? 1 : -1; + const changeMag = exports.parseNum(headerMatch[3]); + const newLen = oldLen + changeSign * changeMag; + const opsStart = headerMatch[0].length; + let opsEnd = cs.indexOf('$'); + if (opsEnd < 0) + opsEnd = cs.length; + return { + oldLen, + newLen, + ops: cs.substring(opsStart, opsEnd), + charBank: cs.substring(opsEnd + 1), + }; +}; +export const pack = (oldLen, newLen, opsStr, bank) => { + const lenDiff = newLen - oldLen; + const lenDiffStr = (lenDiff >= 0 ? `>${exports.numToString(lenDiff)}` + : `<${exports.numToString(-lenDiff)}`); + const a = []; + a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank); + return a.join(''); +}; +export const applyToText = (cs, str) => { + const unpacked = exports.unpack(cs); + assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); + const bankIter = exports.stringIterator(unpacked.charBank); + const strIter = exports.stringIterator(str); + const assem = exports.stringAssembler(); + for (const op of exports.deserializeOps(unpacked.ops)) { + switch (op.opcode) { + case '+': + // op is + and op.lines 0: no newlines must be in op.chars + // op is + and op.lines >0: op.chars must include op.lines newlines + if (op.lines !== bankIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op +; cs:${cs} and text:${str}`); + } + assem.append(bankIter.take(op.chars)); + break; + case '-': + // op is - and op.lines 0: no newlines must be in the deleted string + // op is - and op.lines >0: op.lines newlines must be in the deleted string + if (op.lines !== strIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op -; cs:${cs} and text:${str}`); + } + strIter.skip(op.chars); + break; + case '=': + // op is = and op.lines 0: no newlines must be in the copied string + // op is = and op.lines >0: op.lines newlines must be in the copied string + if (op.lines !== strIter.peek(op.chars).split('\n').length - 1) { + throw new Error(`newline count is wrong in op =; cs:${cs} and text:${str}`); + } + assem.append(strIter.take(op.chars)); + break; + } + } + assem.append(strIter.take(strIter.remaining())); + return assem.toString(); +}; +export const mutateTextLines = (cs, lines) => { + const unpacked = exports.unpack(cs); + const bankIter = exports.stringIterator(unpacked.charBank); + const mut = new TextLinesMutator(lines); + for (const op of exports.deserializeOps(unpacked.ops)) { + switch (op.opcode) { + case '+': + mut.insert(bankIter.take(op.chars), op.lines); + break; + case '-': + mut.remove(op.chars, op.lines); + break; + case '=': + mut.skip(op.chars, op.lines, (!!op.attribs)); + break; + } + } + mut.close(); +}; +export const composeAttributes = (att1, att2, resultIsMutation, pool) => { + // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. + // Sometimes attribute (key,value) pairs are treated as attribute presence + // information, while other times they are treated as operations that + // mutate a set of attributes, and this affects whether an empty value + // is a deletion or a change. + // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result + // ([], [(bold, )], true) -> [(bold, )] + // ([], [(bold, )], false) -> [] + // ([], [(bold, true)], true) -> [(bold, true)] + // ([], [(bold, true)], false) -> [(bold, true)] + // ([(bold, true)], [(bold, )], true) -> [(bold, )] + // ([(bold, true)], [(bold, )], false) -> [] + // pool can be null if att2 has no attributes. + if ((!att1) && resultIsMutation) { + // In the case of a mutation (i.e. composing two exportss), + // an att2 composed with an empy att1 is just att2. If att1 + // is part of an attribution string, then att2 may remove + // attributes that are already gone, so don't do this optimization. + return att2; + } + if (!att2) + return att1; + return AttributeMap.fromString(att1, pool).updateFromString(att2, !resultIsMutation).toString(); +}; +export const applyToAttribution = (cs, astr, pool) => { + const unpacked = exports.unpack(cs); + return applyZip(astr, unpacked.ops, (op1, op2) => slicerZipperFunc(op1, op2, pool)); +}; +export const mutateAttributionLines = (cs, lines, pool) => { + const unpacked = exports.unpack(cs); + const csOps = exports.deserializeOps(unpacked.ops); + let csOpsNext = csOps.next(); + const csBank = unpacked.charBank; + let csBankIndex = 0; + // treat the attribution lines as text lines, mutating a line at a time + const mut = new TextLinesMutator(lines); + /** + * The Ops in the current line from `lines`. + * + * @type {?Generator} + */ + let lineOps = null; + let lineOpsNext = null; + const lineOpsHasNext = () => lineOpsNext && !lineOpsNext.done; + /** + * Returns false if we are on the last attribute line in `lines` and there is no additional op in + * that line. + * + * @returns {boolean} True if there are more ops to go through. + */ + const isNextMutOp = () => lineOpsHasNext() || mut.hasMore(); + /** + * @returns {Op} The next Op from `lineIter`. If there are no more Ops, `lineIter` is reset to + * iterate over the next line, which is consumed from `mut`. If there are no more lines, + * returns a null Op. + */ + const nextMutOp = () => { + if (!lineOpsHasNext() && mut.hasMore()) { + // There are more attribute lines in `lines` to do AND either we just started so `lineIter` is + // still null or there are no more ops in current `lineIter`. + const line = mut.removeLines(1); + lineOps = exports.deserializeOps(line); + lineOpsNext = lineOps.next(); + } + if (!lineOpsHasNext()) + return new Op(); // No more ops and no more lines. + const op = lineOpsNext.value; + lineOpsNext = lineOps.next(); + return op; + }; + let lineAssem = null; + /** + * Appends an op to `lineAssem`. In case `lineAssem` includes one single newline, adds it to the + * `lines` mutator. + */ + const outputMutOp = (op) => { + if (!lineAssem) { + lineAssem = exports.mergingOpAssembler(); + } + lineAssem.append(op); + 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; + }; + let csOp = new Op(); + let attOp = new Op(); + while (csOp.opcode || !csOpsNext.done || attOp.opcode || isNextMutOp()) { + if (!csOp.opcode && !csOpsNext.done) { + // coOp done, but more ops in cs. + csOp = csOpsNext.value; + csOpsNext = csOps.next(); + } + if (!csOp.opcode && !attOp.opcode && !lineAssem && !lineOpsHasNext()) { + break; // done + } + else if (csOp.opcode === '=' && csOp.lines > 0 && !csOp.attribs && !attOp.opcode && + !lineAssem && !lineOpsHasNext()) { + // Skip multiple lines without attributes; this is what makes small changes not order of the + // document size. + mut.skipLines(csOp.lines); + csOp.opcode = ''; + } + else if (csOp.opcode === '+') { + const opOut = copyOp(csOp); + if (csOp.lines > 1) { + // Copy the first line from `csOp` to `opOut`. + const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; + csOp.chars -= firstLineLen; + csOp.lines--; + opOut.lines = 1; + opOut.chars = firstLineLen; + } + else { + // Either one or no newlines in '+' `csOp`, copy to `opOut` and reset `csOp`. + csOp.opcode = ''; + } + outputMutOp(opOut); + csBankIndex += opOut.chars; + } + else { + if (!attOp.opcode && isNextMutOp()) + attOp = nextMutOp(); + const opOut = slicerZipperFunc(attOp, csOp, pool); + if (opOut.opcode) + outputMutOp(opOut); + } + } + assert(!lineAssem, `line assembler not finished:${cs}`); + mut.close(); +}; +export const joinAttributionLines = (theAlines) => { + const assem = exports.mergingOpAssembler(); + for (const aline of theAlines) { + for (const op of exports.deserializeOps(aline)) + assem.append(op); + } + return assem.toString(); +}; +export const splitAttributionLines = (attrOps, text) => { + const assem = exports.mergingOpAssembler(); + const lines = []; + let pos = 0; + const appendOp = (op) => { + assem.append(op); + if (op.lines > 0) { + lines.push(assem.toString()); + assem.clear(); + } + pos += op.chars; + }; + for (const op of exports.deserializeOps(attrOps)) { + let numChars = op.chars; + let numLines = op.lines; + while (numLines > 1) { + const newlineEnd = text.indexOf('\n', pos) + 1; + assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines'); + op.chars = newlineEnd - pos; + op.lines = 1; + appendOp(op); + numChars -= op.chars; + numLines -= op.lines; + } + if (numLines === 1) { + op.chars = numChars; + op.lines = 1; + } + appendOp(op); + } + return lines; +}; +export const splitTextLines = (text) => text.match(/[^\n]*(?:\n|[^\n]$)/g); +export const compose = (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) => { + const op1code = op1.opcode; + const op2code = op2.opcode; + if (op1code === '+' && op2code === '-') { + bankIter1.skip(Math.min(op1.chars, op2.chars)); + } + const opOut = slicerZipperFunc(op1, op2, pool); + if (opOut.opcode === '+') { + if (op2code === '+') { + bankAssem.append(bankIter2.take(opOut.chars)); + } + else { + bankAssem.append(bankIter1.take(opOut.chars)); + } + } + return opOut; + }); + return exports.pack(len1, len3, newOps, bankAssem.toString()); +}; +export const attributeTester = (attribPair, pool) => { + const never = (attribs) => false; + if (!pool) + return never; + const attribNum = pool.putAttrib(attribPair, true); + if (attribNum < 0) + return never; + const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`); + return (attribs) => re.test(attribs); +}; +export const identity = (N) => exports.pack(N, N, '', ''); +export const makeSplice = (orig, start, ndel, ins, attribs?, pool?) => { + if (start < 0) + throw new RangeError(`start index must be non-negative (is ${start})`); + if (ndel < 0) + throw new RangeError(`characters to delete must be non-negative (is ${ndel})`); + if (start > orig.length) + start = orig.length; + if (ndel > orig.length - start) + ndel = orig.length - start; + const deleted = orig.substring(start, start + ndel); + const assem = exports.smartOpAssembler(); + const ops = (function* () { + yield* opsFromText('=', orig.substring(0, start)); + yield* opsFromText('-', deleted); + yield* opsFromText('+', ins, attribs, pool); + })(); + for (const op of ops) + assem.append(op); + assem.endDocument(); + return exports.pack(orig.length, orig.length + ins.length - ndel, assem.toString(), ins); +}; +export const characterRangeFollow = (cs, startChar, endChar, insertionsAfter?) => { + let newStartChar = startChar; + let newEndChar = endChar; + let lengthChangeSoFar = 0; + for (const splice of toSplices(cs)) { + const spliceStart = splice[0] + lengthChangeSoFar; + const spliceEnd = splice[1] + lengthChangeSoFar; + const newTextLength = splice[2].length; + const thisLengthChange = newTextLength - (spliceEnd - spliceStart); + if (spliceStart <= newStartChar && spliceEnd >= newEndChar) { + // splice fully replaces/deletes range + // (also case that handles insertion at a collapsed selection) + if (insertionsAfter) { + newStartChar = newEndChar = spliceStart; + } + else { + newStartChar = newEndChar = spliceStart + newTextLength; + } + } + else if (spliceEnd <= newStartChar) { + // splice is before range + newStartChar += thisLengthChange; + newEndChar += thisLengthChange; + } + else if (spliceStart >= newEndChar) { + // splice is after range + } + else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) { + // splice is inside range + newEndChar += thisLengthChange; + } + else if (spliceEnd < newEndChar) { + // splice overlaps beginning of range + newStartChar = spliceStart + newTextLength; + newEndChar += thisLengthChange; + } + else { + // splice overlaps end of range + newEndChar = spliceStart; + } + lengthChangeSoFar += thisLengthChange; + } + return [newStartChar, newEndChar]; +}; +export const moveOpsToNewPool = (cs, oldPool, newPool) => { + // works on exports or attribution string + let dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + const upToDollar = cs.substring(0, dollarPos); + const fromDollar = cs.substring(dollarPos); + // order of attribs stays the same + return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { + const oldNum = exports.parseNum(a); + 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; +}; +export const makeAttribution = (text) => { + const assem = exports.smartOpAssembler(); + for (const op of opsFromText('+', text)) + assem.append(op); + return assem.toString(); +}; +export const eachAttribNumber = (cs, func) => { + padutils.warnDeprecated('Changeset.eachAttribNumber() is deprecated; use attributes.decodeAttribString() instead'); + let dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + const upToDollar = cs.substring(0, dollarPos); + // WARNING: The following cannot be replaced with a call to `attributes.decodeAttribString()` + // because that function only works on attribute strings, not serialized operations or changesets. + upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { + func(exports.parseNum(a)); + return ''; + }); +}; +export const filterAttribNumbers = (cs, filter) => exports.mapAttribNumbers(cs, filter); +export const mapAttribNumbers = (cs, func) => { + let dollarPos = cs.indexOf('$'); + if (dollarPos < 0) { + dollarPos = cs.length; + } + const upToDollar = cs.substring(0, dollarPos); + const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => { + const n = func(exports.parseNum(a)); + if (n === true) { + return s; + } + else if ((typeof n) === 'number') { + return `*${exports.numToString(n)}`; + } + else { + return ''; + } + }); + return newUpToDollar + cs.substring(dollarPos); +}; +export const makeAText = (text, attribs?) => ({ + text, + attribs: (attribs || exports.makeAttribution(text)), +}); +export const applyToAText = (cs, atext, pool) => ({ + text: exports.applyToText(cs, atext.text), + attribs: exports.applyToAttribution(cs, atext.attribs, pool), +}); +export const cloneAText = (atext) => { + if (!atext) + error('atext is null'); + return { + text: atext.text, + attribs: atext.attribs, + }; +}; +export const copyAText = (atext1, atext2) => { + atext2.text = atext1.text; + atext2.attribs = atext1.attribs; +}; +export const opsFromAText = function* (atext) { + // intentionally skips last newline char of atext + let lastOp = null; + for (const op of exports.deserializeOps(atext.attribs)) { + if (lastOp != null) + yield lastOp; + lastOp = op; + } + 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); + yield copyOp(lastOp); + lastOp.lines = 0; + lastOp.chars = lastLineLength; + } + if (lastOp.chars) + yield lastOp; +}; +export const appendATextToAssembler = (atext, assem) => { + padutils.warnDeprecated('Changeset.appendATextToAssembler() is deprecated; use Changeset.opsFromAText() instead'); + for (const op of exports.opsFromAText(atext)) + assem.append(op); +}; +export const prepareForWire = (cs, pool) => { + const newPool = new AttributePool(); + const newCs = exports.moveOpsToNewPool(cs, pool, newPool); + return { + translated: newCs, + pool: newPool, + }; +}; +export const isIdentity = (cs) => { + const unpacked = exports.unpack(cs); + return unpacked.ops === '' && unpacked.oldLen === unpacked.newLen; +}; +export const opAttributeValue = (op, key, pool) => { + padutils.warnDeprecated('Changeset.opAttributeValue() is deprecated; use an AttributeMap instead'); + return attribsAttributeValue(op.attribs, key, pool); +}; +const attribsAttributeValue$0 = (attribs, key, pool) => { + padutils.warnDeprecated('Changeset.attribsAttributeValue() is deprecated; use an AttributeMap instead'); + return attribsAttributeValue(attribs, key, pool); +}; +export const builder = (oldLen) => { + const assem = exports.smartOpAssembler(); + const o = new Op(); + const charBank = exports.stringAssembler(); + const self = { + /** + * @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 = '='; + o.attribs = typeof attribs === 'string' + ? attribs : new AttributeMap(pool).update(attribs || []).toString(); + o.chars = N; + o.lines = (L || 0); + 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?) => { + for (const op of opsFromText('=', text, attribs, pool)) + assem.append(op); + 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?) => { + for (const op of opsFromText('+', text, attribs, pool)) + assem.append(op); + 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 = ''; + o.chars = N; + o.lines = (L || 0); + assem.append(o); + return self; + }, + toString: () => { + assem.endDocument(); + const newLen = oldLen + assem.getLengthChange(); + return exports.pack(oldLen, newLen, assem.toString(), charBank.toString()); + }, + }; + return self; +}; +export const makeAttribsString = (opcode, attribs, pool) => { + padutils.warnDeprecated('Changeset.makeAttribsString() is deprecated; ' + + 'use AttributeMap.prototype.toString() or attributes.attribsToString() instead'); + if (!attribs || !['=', '+'].includes(opcode)) + return ''; + if (typeof attribs === 'string') + return attribs; + return new AttributeMap(pool).update(attribs, opcode === '+').toString(); +}; +export const subattribution = (astr, start, optEnd?) => { + const attOps = exports.deserializeOps(astr); + let attOpsNext = attOps.next(); + const assem = exports.smartOpAssembler(); + let attOp = new Op(); + const csOp = new Op(); + const doCsOp = () => { + if (!csOp.chars) + return; + while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) { + if (!attOp.opcode) { + attOp = attOpsNext.value; + attOpsNext = attOps.next(); + } + if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && + attOp.lines > 0 && csOp.lines <= 0) { + csOp.lines++; + } + const opOut = slicerZipperFunc(attOp, csOp, null); + if (opOut.opcode) + assem.append(opOut); + } + }; + csOp.opcode = '-'; + csOp.chars = start; + doCsOp(); + if (optEnd === undefined) { + if (attOp.opcode) { + assem.append(attOp); + } + while (!attOpsNext.done) { + assem.append(attOpsNext.value); + attOpsNext = attOps.next(); + } + } + else { + csOp.opcode = '='; + csOp.chars = optEnd - start; + doCsOp(); + } + return assem.toString(); +}; +export const inverse = (cs, lines, alines, pool) => { + // lines and alines are what the exports is meant to apply to. + // They may be arrays or objects with .get(i) and .length methods. + // They include final newlines on lines. + const linesGet = (idx) => { + if (lines.get) { + return lines.get(idx); + } + else { + return lines[idx]; + } + }; + /** + * @param {number} idx - + * @returns {string} + */ + const alinesGet = (idx) => { + if (alines.get) { + return alines.get(idx); + } + else { + return alines[idx]; + } + }; + let curLine = 0; + let curChar = 0; + let curLineOps = null; + let curLineOpsNext = null; + let curLineOpsLine; + let curLineNextOp = new Op('+'); + const unpacked = exports.unpack(cs); + const builder = exports.builder(unpacked.newLen); + const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { + if (!curLineOps || curLineOpsLine !== curLine) { + curLineOps = exports.deserializeOps(alinesGet(curLine)); + curLineOpsNext = curLineOps.next(); + curLineOpsLine = curLine; + let indexIntoLine = 0; + while (!curLineOpsNext.done) { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); + if (indexIntoLine + curLineNextOp.chars >= curChar) { + curLineNextOp.chars -= (curChar - indexIntoLine); + break; + } + indexIntoLine += curLineNextOp.chars; + } + } + while (numChars > 0) { + if (!curLineNextOp.chars && curLineOpsNext.done) { + curLine++; + curChar = 0; + curLineOpsLine = curLine; + curLineNextOp.chars = 0; + curLineOps = exports.deserializeOps(alinesGet(curLine)); + curLineOpsNext = curLineOps.next(); + } + if (!curLineNextOp.chars) { + if (curLineOpsNext.done) { + curLineNextOp = new Op(); + } + else { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); + } + } + const charsToUse = Math.min(numChars, curLineNextOp.chars); + func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && + curLineNextOp.lines > 0); + numChars -= charsToUse; + curLineNextOp.chars -= charsToUse; + curChar += charsToUse; + } + if (!curLineNextOp.chars && curLineOpsNext.done) { + curLine++; + curChar = 0; + } + }; + const skip = (N, L) => { + if (L) { + curLine += L; + curChar = 0; + } + else if (curLineOps && curLineOpsLine === curLine) { + consumeAttribRuns(N, () => { }); + } + else { + curChar += N; + } + }; + const nextText = (numChars) => { + let len = 0; + const assem = exports.stringAssembler(); + const firstString = linesGet(curLine).substring(curChar); + len += firstString.length; + assem.append(firstString); + let lineNum = curLine + 1; + while (len < numChars) { + const nextString = linesGet(lineNum); + len += nextString.length; + assem.append(nextString); + lineNum++; + } + return assem.toString().substring(0, numChars); + }; + const cachedStrFunc = (func) => { + const cache = {}; + return (s) => { + if (!cache[s]) { + cache[s] = func(s); + } + return cache[s]; + }; + }; + for (const csOp of exports.deserializeOps(unpacked.ops)) { + if (csOp.opcode === '=') { + if (csOp.attribs) { + const attribs = AttributeMap.fromString(csOp.attribs, pool); + const undoBackToAttribs = cachedStrFunc((oldAttribsStr) => { + const oldAttribs = AttributeMap.fromString(oldAttribsStr, pool); + const backAttribs = new AttributeMap(pool); + for (const [key, value] of attribs) { + const oldValue = oldAttribs.get(key) || ''; + if (oldValue !== value) + backAttribs.set(key, oldValue); + } + // TODO: backAttribs does not restore removed attributes (it is missing attributes that + // are in oldAttribs but not in attribs). I don't know if that is intentional. + return backAttribs.toString(); + }); + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { + builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); + }); + } + else { + skip(csOp.chars, csOp.lines); + builder.keep(csOp.chars, csOp.lines); + } + } + else if (csOp.opcode === '+') { + builder.remove(csOp.chars, csOp.lines); + } + else if (csOp.opcode === '-') { + const textBank = nextText(csOp.chars); + let textBankIndex = 0; + consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { + builder.insert(textBank.substr(textBankIndex, len), attribs); + textBankIndex += len; + }); + } + } + return exports.checkRep(builder.toString()); +}; +export const follow = (cs1, cs2, reverseInsertOrder, pool) => { + const unpacked1 = exports.unpack(cs1); + const unpacked2 = exports.unpack(cs2); + const len1 = unpacked1.oldLen; + const len2 = unpacked2.oldLen; + assert(len1 === len2, 'mismatched follow - cannot transform cs1 on top of cs2'); + const chars1 = exports.stringIterator(unpacked1.charBank); + const chars2 = exports.stringIterator(unpacked2.charBank); + const oldLen = unpacked1.newLen; + let oldPos = 0; + let newLen = 0; + const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool); + const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => { + const opOut = new Op(); + if (op1.opcode === '+' || op2.opcode === '+') { + let whichToDo; + if (op2.opcode !== '+') { + whichToDo = 1; + } + else if (op1.opcode !== '+') { + whichToDo = 2; + } + else { + // both + + const firstChar1 = chars1.peek(1); + const firstChar2 = chars2.peek(1); + const insertFirst1 = hasInsertFirst(op1.attribs); + const insertFirst2 = hasInsertFirst(op2.attribs); + if (insertFirst1 && !insertFirst2) { + whichToDo = 1; + } + else if (insertFirst2 && !insertFirst1) { + whichToDo = 2; + } + else if (firstChar1 === '\n' && firstChar2 !== '\n') { + // insert string that doesn't start with a newline first so as not to break up lines + whichToDo = 2; + } + else if (firstChar1 !== '\n' && firstChar2 === '\n') { + whichToDo = 1; + } + else if (reverseInsertOrder) { + // break symmetry: + whichToDo = 2; + } + else { + whichToDo = 1; + } + } + if (whichToDo === 1) { + chars1.skip(op1.chars); + opOut.opcode = '='; + opOut.lines = op1.lines; + opOut.chars = op1.chars; + opOut.attribs = ''; + op1.opcode = ''; + } + else { + // whichToDo == 2 + chars2.skip(op2.chars); + copyOp(op2, opOut); + op2.opcode = ''; + } + } + else if (op1.opcode === '-') { + if (!op2.opcode) { + op1.opcode = ''; + } + else if (op1.chars <= op2.chars) { + op2.chars -= op1.chars; + op2.lines -= op1.lines; + op1.opcode = ''; + if (!op2.chars) { + op2.opcode = ''; + } + } + else { + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; + } + } + else if (op2.opcode === '-') { + copyOp(op2, opOut); + if (!op1.opcode) { + op2.opcode = ''; + } + else if (op2.chars <= op1.chars) { + // delete part or all of a keep + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; + if (!op1.chars) { + op1.opcode = ''; + } + } + else { + // delete all of a keep, and keep going + opOut.lines = op1.lines; + opOut.chars = op1.chars; + op2.lines -= op1.lines; + op2.chars -= op1.chars; + op1.opcode = ''; + } + } + else if (!op1.opcode) { + 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. + // copyOp(op1, opOut); + op1.opcode = ''; + } + else { + // both keeps + opOut.opcode = '='; + opOut.attribs = followAttributes(op1.attribs, op2.attribs, pool); + if (op1.chars <= op2.chars) { + opOut.chars = op1.chars; + opOut.lines = op1.lines; + op2.chars -= op1.chars; + op2.lines -= op1.lines; + op1.opcode = ''; + if (!op2.chars) { + op2.opcode = ''; + } + } + else { + opOut.chars = op2.chars; + opOut.lines = op2.lines; + op1.chars -= op2.chars; + op1.lines -= op2.lines; + op2.opcode = ''; + } + } + switch (opOut.opcode) { + case '=': + oldPos += opOut.chars; + newLen += opOut.chars; + break; + case '-': + oldPos += opOut.chars; + break; + case '+': + newLen += opOut.chars; + break; + } + return opOut; + }); + newLen += oldLen - oldPos; + return exports.pack(oldLen, newLen, newOps, unpacked2.charBank); +}; +export const exportedForTestingOnly = { + TextLinesMutator, + followAttributes, + toSplices, +}; +export { Op }; +export { attribsAttributeValue$0 as attribsAttributeValue }; diff --git a/src/static/js/ChangesetUtils.js b/src/static/js/ChangesetUtils.js deleted file mode 100644 index ef2be2ebe07..00000000000 --- a/src/static/js/ChangesetUtils.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -/** - * This module contains several helper Functions to build Changesets - * based on a SkipList - */ - -/** - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -exports.buildRemoveRange = (rep, builder, start, end) => { - const startLineOffset = rep.lines.offsetOfIndex(start[0]); - const endLineOffset = rep.lines.offsetOfIndex(end[0]); - - if (end[0] > start[0]) { - builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); - builder.remove(end[1]); - } else { - builder.remove(end[1] - start[1]); - } -}; - -exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => { - const startLineOffset = rep.lines.offsetOfIndex(start[0]); - const endLineOffset = rep.lines.offsetOfIndex(end[0]); - - if (end[0] > start[0]) { - builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); - builder.keep(end[1], 0, attribs, pool); - } else { - builder.keep(end[1] - start[1], 0, attribs, pool); - } -}; - -exports.buildKeepToStartOfRange = (rep, builder, start) => { - const startLineOffset = rep.lines.offsetOfIndex(start[0]); - - builder.keep(startLineOffset, start[0]); - builder.keep(start[1]); -}; diff --git a/src/static/js/ChangesetUtils.ts b/src/static/js/ChangesetUtils.ts new file mode 100644 index 00000000000..a527f1e74f0 --- /dev/null +++ b/src/static/js/ChangesetUtils.ts @@ -0,0 +1,28 @@ +'use strict'; +export const buildRemoveRange = (rep, builder, start, end) => { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); + const endLineOffset = rep.lines.offsetOfIndex(end[0]); + if (end[0] > start[0]) { + builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); + builder.remove(end[1]); + } + else { + builder.remove(end[1] - start[1]); + } +}; +export const buildKeepRange = (rep, builder, start, end, attribs?, pool?) => { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); + const endLineOffset = rep.lines.offsetOfIndex(end[0]); + if (end[0] > start[0]) { + builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); + builder.keep(end[1], 0, attribs, pool); + } + else { + builder.keep(end[1] - start[1], 0, attribs, pool); + } +}; +export const buildKeepToStartOfRange = (rep, builder, start) => { + const startLineOffset = rep.lines.offsetOfIndex(start[0]); + builder.keep(startLineOffset, start[0]); + builder.keep(start[1]); +}; diff --git a/src/static/js/ChatMessage.js b/src/static/js/ChatMessage.js deleted file mode 100644 index a627f88f9f3..00000000000 --- a/src/static/js/ChatMessage.js +++ /dev/null @@ -1,98 +0,0 @@ -'use strict'; - -const {padutils: {warnDeprecated}} = require('./pad_utils'); - -/** - * 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) { - // The userId property was renamed to authorId, and userName was renamed to displayName. Accept - // the old names in case the db record was written by an older version of Etherpad. - obj = Object.assign({}, obj); // Don't mutate the caller's object. - if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId; - delete obj.userId; - if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName; - delete obj.userName; - return Object.assign(new ChatMessage(), obj); - } - - /** - * @param {?string} [text] - Initial value of the `text` property. - * @param {?string} [authorId] - Initial value of the `authorId` property. - * @param {?number} [time] - Initial value of the `time` property. - */ - constructor(text = null, authorId = 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.authorId = authorId; - - /** - * The message's timestamp, as milliseconds since epoch. - * - * @type {?number} - */ - this.time = time; - - /** - * The user's display name. - * - * @type {?string} - */ - this.displayName = null; - } - - /** - * Alias of `authorId`, for compatibility with old plugins. - * - * @deprecated Use `authorId` instead. - * @type {string} - */ - get userId() { - warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); - return this.authorId; - } - set userId(val) { - warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); - this.authorId = val; - } - - /** - * Alias of `displayName`, for compatibility with old plugins. - * - * @deprecated Use `displayName` instead. - * @type {string} - */ - get userName() { - warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); - return this.displayName; - } - set userName(val) { - warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); - 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() { - const {authorId, displayName, ...obj} = this; - obj.userId = authorId; - obj.userName = displayName; - return obj; - } -} - -module.exports = ChatMessage; diff --git a/src/static/js/ChatMessage.ts b/src/static/js/ChatMessage.ts new file mode 100644 index 00000000000..3873e00004a --- /dev/null +++ b/src/static/js/ChatMessage.ts @@ -0,0 +1,96 @@ +import { padutils } from "./pad_utils.js"; +'use strict'; +const { padutils: { warnDeprecated } } = { padutils }; +/** + * 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 { + private text: any; + authorId: any; + private time: any; + displayName: null; + static fromObject(obj) { + // The userId property was renamed to authorId, and userName was renamed to displayName. Accept + // the old names in case the db record was written by an older version of Etherpad. + obj = Object.assign({}, obj); // Don't mutate the caller's object. + if ('userId' in obj && !('authorId' in obj)) + obj.authorId = obj.userId; + delete obj.userId; + if ('userName' in obj && !('displayName' in obj)) + obj.displayName = obj.userName; + delete obj.userName; + return Object.assign(new ChatMessage(), obj); + } + /** + * @param {?string} [text] - Initial value of the `text` property. + * @param {?string} [authorId] - Initial value of the `authorId` property. + * @param {?number} [time] - Initial value of the `time` property. + */ + constructor(text = null, authorId = 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.authorId = authorId; + /** + * The message's timestamp, as milliseconds since epoch. + * + * @type {?number} + */ + this.time = time; + /** + * The user's display name. + * + * @type {?string} + */ + this.displayName = null; + } + /** + * Alias of `authorId`, for compatibility with old plugins. + * + * @deprecated Use `authorId` instead. + * @type {string} + */ + get userId() { + warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); + return this.authorId; + } + set userId(val) { + warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); + this.authorId = val; + } + /** + * Alias of `displayName`, for compatibility with old plugins. + * + * @deprecated Use `displayName` instead. + * @type {string} + */ + get userName() { + warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); + return this.displayName; + } + set userName(val) { + warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); + 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() { + const { authorId, displayName, ...obj } = this; + let objExtendable = obj as any + objExtendable.userId = authorId; + objExtendable.userName = displayName; + return objExtendable; + } +} +export default ChatMessage; diff --git a/src/static/js/ace.js b/src/static/js/ace.ts similarity index 81% rename from src/static/js/ace.js rename to src/static/js/ace.ts index b0a0425702e..310171dd41d 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.ts @@ -1,49 +1,28 @@ +import * as hooks from "./pluginfw/hooks.js"; +import { makeCSSManager as makeCSSManager$0 } from "./cssmanager.js"; +import * as pluginUtils from "./pluginfw/shared.js"; +import {Ace2EditorInfo, AceDocType} from "../module/Ace2EditorInfo"; +import {clientVars} from "../../node/handler/PadMessageHandler"; +import {CustomElementWithSheet, CustomWindow} from "../module/CustomWindow"; +import {required} from "../../node/eejs"; 'use strict'; -/** - * This code is mostly from the old Etherpad. Please help us to comment this code. - * This helps other people to understand this code better and helps them to improve it. - * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED - */ - -/** - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// requires: top -// requires: undefined - -const hooks = require('./pluginfw/hooks'); -const makeCSSManager = require('./cssmanager').makeCSSManager; -const pluginUtils = require('./pluginfw/shared'); - -const debugLog = (...args) => {}; +const makeCSSManager = { makeCSSManager: makeCSSManager$0 }.makeCSSManager; +const debugLog = (...args) => { }; // The inner and outer iframe's locations are about:blank, so relative URLs are relative to that. // Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari // errors out unless given an absolute URL for a JavaScript-created element. const absUrl = (url) => new URL(url, window.location.href).href; - const eventFired = async (obj, event, cleanups = [], predicate = () => true) => { if (typeof cleanups === 'function') { predicate = cleanups; cleanups = []; } - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { let cleanup; const successCb = () => { - if (!predicate()) return; + if (!predicate()) + return; debugLog(`Ace2Editor.init() ${event} event on`, obj); cleanup(); resolve(); @@ -55,7 +34,7 @@ const eventFired = async (obj, event, cleanups = [], predicate = () => true) => reject(err); }; cleanup = () => { - cleanup = () => {}; + cleanup = () => { }; obj.removeEventListener(event, successCb); obj.removeEventListener('error', errorCb); }; @@ -64,14 +43,13 @@ const eventFired = async (obj, event, cleanups = [], predicate = () => true) => obj.addEventListener('error', errorCb); }); }; - // Resolves when the frame's document is ready to be mutated. Browsers seem to be quirky about // iframe ready events so this function throws the kitchen sink at the problem. Maybe one day we'll // find a concise general solution. const frameReady = async (frame) => { // Can't do `const doc = frame.contentDocument;` because Firefox seems to asynchronously replace // the document object after the frame is first created for some reason. ¯\_(ツ)_/¯ - const doc = () => frame.contentDocument; + const doc:AceDocType = () => frame.contentDocument; const cleanups = []; try { await Promise.race([ @@ -81,28 +59,27 @@ const frameReady = async (frame) => { eventFired(doc(), 'DOMContentLoaded', cleanups), eventFired(doc(), 'readystatechange', cleanups, () => doc.readyState === 'complete'), ]); - } finally { - for (const cleanup of cleanups) cleanup(); + } + finally { + for (const cleanup of cleanups) + cleanup(); } }; - const Ace2Editor = function () { - let info = {editor: this}; + let info:Ace2EditorInfo = { editor: this }; let loaded = false; - let actionsPendingInit = []; - const pendingInit = (func) => function (...args) { const action = () => func.apply(this, args); - if (loaded) return action(); + if (loaded) + return action(); actionsPendingInit.push(action); }; - const doActionsPendingInit = () => { - for (const fn of actionsPendingInit) fn(); + for (const fn of actionsPendingInit) + fn(); actionsPendingInit = []; }; - // The following functions (prefixed by 'ace_') are exposed by editor, but // execution is delayed until init is complete const aceFunctionsPendingInit = [ @@ -124,7 +101,6 @@ const Ace2Editor = function () { 'execCommand', 'replaceRange', ]; - for (const fnName of aceFunctionsPendingInit) { // Note: info[`ace_${fnName}`] does not exist yet, so it can't be passed directly to // pendingInit(). A simple wrapper is used to defer the info[`ace_${fnName}`] lookup until @@ -133,12 +109,9 @@ const Ace2Editor = function () { info[`ace_${fnName}`].apply(this, args); }); } - this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n'; - this.getInInternationalComposition = () => loaded ? info.ace_getInInternationalComposition() : null; - // prepareUserChangeset: // Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes // to the latest base text into a Changeset, which is returned (as a string if encodeAsString). @@ -148,7 +121,6 @@ const Ace2Editor = function () { // prepareUserChangeset will return an updated changeset that takes into account the latest user // changes, and modify the changeset to be applied by applyPreparedChangesetToBase accordingly. this.prepareUserChangeset = () => loaded ? info.ace_prepareUserChangeset() : null; - const addStyleTagsFor = (doc, files) => { for (const file of files) { const link = doc.createElement('link'); @@ -158,17 +130,14 @@ const Ace2Editor = function () { doc.head.appendChild(link); } }; - this.destroy = pendingInit(() => { info.ace_dispose(); info.frame.parentNode.removeChild(info.frame); info = null; // prevent IE 6 closure memory leaks }); - this.init = async function (containerId, initialCode) { debugLog('Ace2Editor.init()'); this.importText(initialCode); - const includedCSS = [ `../static/css/iframe_editor.css?v=${clientVars.randomVersionString}`, `../static/css/pad.css?v=${clientVars.randomVersionString}`, @@ -177,12 +146,9 @@ const Ace2Editor = function () { (p) => /\/\//.test(p) ? p : `../static/plugins/${p}`), `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`, ]; - const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== ''); - const outerFrame = document.createElement('iframe'); outerFrame.name = 'ace_outer'; - outerFrame.frameBorder = 0; // for IE outerFrame.title = 'Ether'; // Some browsers do strange things unless the iframe has a src or srcdoc property: // - Firefox replaces the frame's contentWindow.document object with a different object after @@ -197,25 +163,20 @@ const Ace2Editor = function () { info.frame = outerFrame; document.getElementById(containerId).appendChild(outerFrame); const outerWindow = outerFrame.contentWindow; - debugLog('Ace2Editor.init() waiting for outer frame'); await frameReady(outerFrame); debugLog('Ace2Editor.init() outer frame ready'); - // Firefox might replace the outerWindow.document object after iframe creation so this variable // is assigned after the Window's load event. const outerDocument = outerWindow.document; - // tag outerDocument.documentElement.classList.add('outer-editor', 'outerdoc', ...skinVariants); - // tag addStyleTagsFor(outerDocument, includedCSS); const outerStyle = outerDocument.createElement('style'); outerStyle.type = 'text/css'; outerStyle.title = 'dynamicsyntax'; outerDocument.head.appendChild(outerStyle); - // tag outerDocument.body.id = 'outerdocbody'; outerDocument.body.classList.add('outerdocbody', ...pluginUtils.clientPluginNames()); @@ -231,30 +192,24 @@ const Ace2Editor = function () { lineMetricsDiv.id = 'linemetricsdiv'; lineMetricsDiv.appendChild(outerDocument.createTextNode('x')); outerDocument.body.appendChild(lineMetricsDiv); - const innerFrame = outerDocument.createElement('iframe'); innerFrame.name = 'ace_inner'; innerFrame.title = 'pad'; innerFrame.scrolling = 'no'; - innerFrame.frameBorder = 0; - innerFrame.allowTransparency = true; // for IE + innerFrame.frameBorder = String(0); // The iframe MUST have a src or srcdoc property to avoid browser quirks. See the comment above // outerFrame.srcdoc. innerFrame.src = 'empty.html'; outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild); - const innerWindow = innerFrame.contentWindow; - + const innerWindow = innerFrame.contentWindow as unknown as CustomWindow; debugLog('Ace2Editor.init() waiting for inner frame'); await frameReady(innerFrame); debugLog('Ace2Editor.init() inner frame ready'); - // Firefox might replace the innerWindow.document object after iframe creation so this variable // is assigned after the Window's load event. const innerDocument = innerWindow.document; - // tag innerDocument.documentElement.classList.add('inner-editor', ...skinVariants); - // tag addStyleTagsFor(innerDocument, includedCSS); const requireKernel = innerDocument.createElement('script'); @@ -267,7 +222,7 @@ const Ace2Editor = function () { const script = innerDocument.createElement('script'); script.type = 'text/javascript'; script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` + - `?callback=require.define&v=${clientVars.randomVersionString}`); + `?callback=require.define&v=${clientVars.randomVersionString}`); innerDocument.head.appendChild(script); } const innerStyle = innerDocument.createElement('style'); @@ -275,16 +230,13 @@ const Ace2Editor = function () { innerStyle.title = 'dynamicsyntax'; innerDocument.head.appendChild(innerStyle); const headLines = []; - hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines}); - innerDocument.head.appendChild( - innerDocument.createRange().createContextualFragment(headLines.join('\n'))); - + hooks.callAll('aceInitInnerdocbodyHead', { iframeHTML: headLines }); + innerDocument.head.appendChild(innerDocument.createRange().createContextualFragment(headLines.join('\n'))); // tag innerDocument.body.id = 'innerdocbody'; innerDocument.body.classList.add('innerdocbody'); innerDocument.body.setAttribute('spellcheck', 'false'); innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //   - debugLog('Ace2Editor.init() waiting for require kernel load'); await eventFired(requireKernel, 'load'); debugLog('Ace2Editor.init() require kernel loaded'); @@ -292,22 +244,18 @@ const Ace2Editor = function () { require.setRootURI(absUrl('../javascripts/src')); require.setLibraryURI(absUrl('../javascripts/lib')); require.setGlobalKeyPath('require'); - // intentially moved before requiring client_plugins to save a 307 - innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner'); - innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); + innerWindow.Ace2Inner = required('ep_etherpad-lite/static/js/ace2_inner'); + innerWindow.plugins = required('ep_etherpad-lite/static/js/pluginfw/client_plugins'); innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow); - - innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - + innerWindow.$ = innerWindow.jQuery = required('ep_etherpad-lite/static/js/rjquery').jQuery; debugLog('Ace2Editor.init() waiting for plugins'); - await new Promise((resolve, reject) => innerWindow.plugins.ensure( - (err) => err != null ? reject(err) : resolve())); + await new Promise((resolve, reject) => innerWindow.plugins.ensure((err) => err != null ? reject(err) : resolve())); debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); await innerWindow.Ace2Inner.init(info, { inner: makeCSSManager(innerStyle.sheet), outer: makeCSSManager(outerStyle.sheet), - parent: makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet), + parent: makeCSSManager((document.querySelector('style[title="dynamicsyntax"]') as unknown as CustomElementWithSheet).sheet), }); debugLog('Ace2Editor.init() Ace2Inner.init() returned'); loaded = true; @@ -315,5 +263,4 @@ const Ace2Editor = function () { debugLog('Ace2Editor.init() done'); }; }; - -exports.Ace2Editor = Ace2Editor; +export { Ace2Editor }; diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.ts similarity index 78% rename from src/static/js/ace2_common.js rename to src/static/js/ace2_common.ts index c1dab5cfd8b..683dace3ad6 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.ts @@ -22,11 +22,11 @@ * limitations under the License. */ -const isNodeText = (node) => (node.nodeType === 3); +export const isNodeText = (node) => (node.nodeType === 3); -const getAssoc = (obj, name) => obj[`_magicdom_${name}`]; +export const getAssoc = (obj, name) => obj[`_magicdom_${name}`]; -const setAssoc = (obj, name, value) => { +export const setAssoc = (obj, name, value) => { // note that in IE designMode, properties of a node can get // copied to new nodes that are spawned during editing; also, // properties representable in HTML text can survive copy-and-paste @@ -38,7 +38,7 @@ const setAssoc = (obj, name, value) => { // between false and true, a number between 0 and numItems inclusive. -const binarySearch = (numItems, func) => { +export const binarySearch = (numItems, func) => { if (numItems < 1) return 0; if (func(0)) return 0; if (!func(numItems - 1)) return numItems; @@ -52,17 +52,10 @@ const binarySearch = (numItems, func) => { return high; }; -const binarySearchInfinite = (expectedLength, func) => { +export const binarySearchInfinite = (expectedLength, func) => { let i = 0; while (!func(i)) i += expectedLength; return binarySearch(i, func); }; -const noop = () => {}; - -exports.isNodeText = isNodeText; -exports.getAssoc = getAssoc; -exports.setAssoc = setAssoc; -exports.binarySearch = binarySearch; -exports.binarySearchInfinite = binarySearchInfinite; -exports.noop = noop; +export const noop = () => {}; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js deleted file mode 100644 index a8b98ac16db..00000000000 --- a/src/static/js/ace2_inner.js +++ /dev/null @@ -1,3527 +0,0 @@ -'use strict'; - -/** - * Copyright 2009 Google Inc. - * Copyright 2020 John McLear - The Etherpad Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -let documentAttributeManager; - -const AttributeMap = require('./AttributeMap'); -const browser = require('./vendors/browser'); -const padutils = require('./pad_utils').padutils; -const Ace2Common = require('./ace2_common'); -const $ = require('./rjquery').$; - -const isNodeText = Ace2Common.isNodeText; -const getAssoc = Ace2Common.getAssoc; -const setAssoc = Ace2Common.setAssoc; -const noop = Ace2Common.noop; -const hooks = require('./pluginfw/hooks'); - -function Ace2Inner(editorInfo, cssManagers) { - const makeChangesetTracker = require('./changesettracker').makeChangesetTracker; - const colorutils = require('./colorutils').colorutils; - const makeContentCollector = require('./contentcollector').makeContentCollector; - const domline = require('./domline').domline; - const AttribPool = require('./AttributePool'); - const Changeset = require('./Changeset'); - const ChangesetUtils = require('./ChangesetUtils'); - const linestylefilter = require('./linestylefilter').linestylefilter; - const SkipList = require('./skiplist'); - const undoModule = require('./undomodule').undoModule; - const AttributeManager = require('./AttributeManager'); - const Scroll = require('./scroll'); - const DEBUG = false; - - const THE_TAB = ' '; // 4 - const MAX_LIST_LEVEL = 16; - - const FORMATTING_STYLES = ['bold', 'italic', 'underline', 'strikethrough']; - const SELECT_BUTTON_CLASS = 'selected'; - - let thisAuthor = ''; - - let disposed = false; - - const focus = () => { - window.focus(); - }; - - const outerWin = window.parent; - const outerDoc = outerWin.document; - 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(sideDivInner.children.length)); - lineDiv.appendChild(lineSpan); - }; - appendNewSideDivLine(); - - const scroll = Scroll.init(outerWin); - - let outsideKeyDown = noop; - let outsideKeyPress = (e) => true; - let outsideNotifyDirty = noop; - - /** - * Document representation. - */ - const rep = { - /** - * 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(), - /** - * 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, - /** - * 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! - */ - selFocusAtStart: false, - alltext: '', - alines: [], - apool: new AttribPool(), - }; - - // lines, alltext, alines, and DOM are set up in init() - if (undoModule.enabled) { - undoModule.apool = rep.apool; - } - - let isEditable = true; - let doesWrap = true; - let hasLineNumbers = true; - let isStyled = true; - - let console = (DEBUG && window.console); - - if (!window.console) { - const names = [ - 'log', - 'debug', - 'info', - 'warn', - 'error', - 'assert', - 'dir', - 'dirxml', - 'group', - 'groupEnd', - 'time', - 'timeEnd', - 'count', - 'trace', - 'profile', - 'profileEnd', - ]; - console = {}; - for (const name of names) console[name] = noop; - } - - const scheduler = parent; // hack for opera required - - const performDocumentReplaceRange = (start, end, newText) => { - if (start === undefined) start = rep.selStart; - if (end === undefined) end = rep.selEnd; - - // start[0]: <--- start[1] --->CCCCCCCCCCC\n - // CCCCCCCCCCCCCCCCCCCC\n - // CCCC\n - // end[0]: -------\n - const builder = Changeset.builder(rep.lines.totalWidth()); - ChangesetUtils.buildKeepToStartOfRange(rep, builder, start); - ChangesetUtils.buildRemoveRange(rep, builder, start, end); - builder.insert(newText, [ - ['author', thisAuthor], - ], rep.apool); - const cs = builder.toString(); - - performDocumentApplyChangeset(cs); - }; - - const changesetTracker = makeChangesetTracker(scheduler, rep.apool, { - withCallbacks: (operationName, f) => { - inCallStackIfNecessary(operationName, () => { - fastIncorp(1); - f( - { - setDocumentAttributedText: (atext) => { - setDocAText(atext); - }, - applyChangesetToDocument: (changeset, preferInsertionAfterCaret) => { - const oldEventType = currentCallStack.editEvent.eventType; - currentCallStack.startNewEvent('nonundoable'); - - performDocumentApplyChangeset(changeset, preferInsertionAfterCaret); - - currentCallStack.startNewEvent(oldEventType); - }, - }); - }); - }, - }); - - const authorInfos = {}; // presence of key determines if author is present in doc - const getAuthorInfos = () => authorInfos; - editorInfo.ace_getAuthorInfos = getAuthorInfos; - - const setAuthorStyle = (author, info) => { - const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author)); - - const authorStyleSet = hooks.callAll('aceSetAuthorStyle', { - dynamicCSS: cssManagers.inner, - outerDynamicCSS: cssManagers.outer, - parentDynamicCSS: cssManagers.parent, - info, - author, - authorSelector, - }); - - // Prevent default behaviour if any hook says so - if (authorStyleSet.some((it) => it)) { - return; - } - - if (!info) { - cssManagers.inner.removeSelectorStyle(authorSelector); - cssManagers.parent.removeSelectorStyle(authorSelector); - } else if (info.bgcolor) { - let bgcolor = info.bgcolor; - if ((typeof info.fade) === 'number') { - bgcolor = fadeColor(bgcolor, info.fade); - } - const textColor = - colorutils.textColorFromBackgroundColor(bgcolor, parent.parent.clientVars.skinName); - const styles = [ - cssManagers.inner.selectorStyle(authorSelector), - cssManagers.parent.selectorStyle(authorSelector), - ]; - for (const style of styles) { - style.backgroundColor = bgcolor; - style.color = textColor; - style['padding-top'] = '3px'; - style['padding-bottom'] = '4px'; - } - } - }; - - const setAuthorInfo = (author, info) => { - if (!author) return; // author ID not set for some reason - if ((typeof author) !== 'string') { - // Potentially caused by: https://github.com/ether/etherpad-lite/issues/2802"); - throw new Error(`setAuthorInfo: author (${author}) is not a string`); - } - if (!info) { - delete authorInfos[author]; - } else { - authorInfos[author] = info; - } - setAuthorStyle(author, info); - }; - - const getAuthorClassName = (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => { - if (c === '.') return '-'; - return `z${c.charCodeAt(0)}z`; - })}`; - - const className2Author = (className) => { - if (className.substring(0, 7) === 'author-') { - return className.substring(7).replace(/[a-y0-9]+|-|z.+?z/g, (cc) => { - if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') { - return String.fromCharCode(Number(cc.slice(1, -1))); - } else { - return cc; - } - }); - } - return null; - }; - - const getAuthorColorClassSelector = (oneClassName) => `.authorColors .${oneClassName}`; - - const fadeColor = (colorCSS, fadeFrac) => { - let color = colorutils.css2triple(colorCSS); - color = colorutils.blend(color, [1, 1, 1], fadeFrac); - return colorutils.triple2css(color); - }; - - editorInfo.ace_getRep = () => rep; - - editorInfo.ace_getAuthor = () => thisAuthor; - - const _nonScrollableEditEvents = { - applyChangesToBase: 1, - }; - - for (const eventType of hooks.callAll('aceRegisterNonScrollableEditEvents')) { - _nonScrollableEditEvents[eventType] = 1; - } - - const isScrollableEditEvent = (eventType) => !_nonScrollableEditEvents[eventType]; - - let currentCallStack = null; - - const inCallStack = (type, action) => { - if (disposed) return; - - const newEditEvent = (eventType) => ({ - eventType, - backset: null, - }); - - const submitOldEvent = (evt) => { - if (rep.selStart && rep.selEnd) { - const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; - const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; - evt.selStart = selStartChar; - evt.selEnd = selEndChar; - evt.selFocusAtStart = rep.selFocusAtStart; - } - if (undoModule.enabled) { - let undoWorked = false; - try { - if (isPadLoading(evt.eventType)) { - undoModule.clearHistory(); - } else if (evt.eventType === 'nonundoable') { - if (evt.changeset) { - undoModule.reportExternalChange(evt.changeset); - } - } else { - undoModule.reportEvent(evt); - } - undoWorked = true; - } finally { - if (!undoWorked) { - undoModule.enabled = false; // for safety - } - } - } - }; - - const startNewEvent = (eventType, dontSubmitOld) => { - const oldEvent = currentCallStack.editEvent; - if (!dontSubmitOld) { - submitOldEvent(oldEvent); - } - currentCallStack.editEvent = newEditEvent(eventType); - return oldEvent; - }; - - currentCallStack = { - type, - docTextChanged: false, - selectionAffected: false, - userChangedSelection: false, - domClean: false, - isUserChange: false, - // is this a "user change" type of call-stack - repChanged: false, - editEvent: newEditEvent(type), - startNewEvent, - }; - let cleanExit = false; - let result; - try { - result = action(); - - hooks.callAll('aceEditEvent', { - callstack: currentCallStack, - editorInfo, - rep, - documentAttributeManager, - }); - - cleanExit = true; - } finally { - const cs = currentCallStack; - if (cleanExit) { - submitOldEvent(cs.editEvent); - if (cs.domClean && cs.type !== 'setup') { - if (cs.selectionAffected) { - updateBrowserSelectionFromRep(); - } - if ((cs.docTextChanged || cs.userChangedSelection) && isScrollableEditEvent(cs.type)) { - scrollSelectionIntoView(); - } - if (cs.docTextChanged && cs.type.indexOf('importText') < 0) { - outsideNotifyDirty(); - } - } - } else if (currentCallStack.type === 'idleWorkTimer') { - idleWorkTimer.atLeast(1000); - } - currentCallStack = null; - } - return result; - }; - editorInfo.ace_inCallStack = inCallStack; - - const inCallStackIfNecessary = (type, action) => { - if (!currentCallStack) { - inCallStack(type, action); - } else { - action(); - } - }; - editorInfo.ace_inCallStackIfNecessary = inCallStackIfNecessary; - - const dispose = () => { - disposed = true; - if (idleWorkTimer) idleWorkTimer.never(); - teardown(); - }; - - const setWraps = (newVal) => { - doesWrap = newVal; - document.body.classList.toggle('doesWrap', doesWrap); - scheduler.setTimeout(() => { - inCallStackIfNecessary('setWraps', () => { - fastIncorp(7); - recreateDOM(); - fixView(); - }); - }, 0); - }; - - const setStyled = (newVal) => { - const oldVal = isStyled; - isStyled = !!newVal; - - if (newVal !== oldVal) { - if (!newVal) { - // clear styles - inCallStackIfNecessary('setStyled', () => { - fastIncorp(12); - const clearStyles = []; - for (const k of Object.keys(STYLE_ATTRIBS)) { - clearStyles.push([k, '']); - } - performDocumentApplyAttributesToCharRange(0, rep.alltext.length, clearStyles); - }); - } - } - }; - - const setTextFace = (face) => { - document.body.style.fontFamily = face; - lineMetricsDiv.style.fontFamily = face; - }; - - const recreateDOM = () => { - // precond: normalized - recolorLinesInRange(0, rep.alltext.length); - }; - - const setEditable = (newVal) => { - isEditable = newVal; - document.body.contentEditable = isEditable ? 'true' : 'false'; - document.body.classList.toggle('static', !isEditable); - }; - - const enforceEditability = () => setEditable(isEditable); - - const importText = (text, undoable, dontProcess) => { - let lines; - if (dontProcess) { - if (text.charAt(text.length - 1) !== '\n') { - throw new Error('new raw text must end with newline'); - } - if (/[\r\t\xa0]/.exec(text)) { - throw new Error('new raw text must not contain CR, tab, or nbsp'); - } - lines = text.substring(0, text.length - 1).split('\n'); - } else { - lines = text.split('\n').map(textify); - } - let newText = '\n'; - if (lines.length > 0) { - newText = `${lines.join('\n')}\n`; - } - - inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { - setDocText(newText); - }); - - if (dontProcess && rep.alltext !== text) { - throw new Error('mismatch error setting raw text in importText'); - } - }; - - const importAText = (atext, apoolJsonObj, undoable) => { - atext = Changeset.cloneAText(atext); - if (apoolJsonObj) { - const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj); - atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool); - } - inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => { - setDocAText(atext); - }); - }; - - const setDocAText = (atext) => { - if (atext.text === '') { - /* - * The server is fine with atext.text being an empty string, but the front - * end is not, and crashes. - * - * It is not clear if this is a problem in the server or in the client - * code, and this is a client-side hack fix. The underlying problem needs - * to be investigated. - * - * See for reference: - * - https://github.com/ether/etherpad-lite/issues/3861 - */ - atext.text = '\n'; - } - - fastIncorp(8); - - const oldLen = rep.lines.totalWidth(); - const numLines = rep.lines.length(); - const upToLastLine = rep.lines.offsetOfIndex(numLines - 1); - const lastLineLength = rep.lines.atIndex(numLines - 1).text.length; - const assem = Changeset.smartOpAssembler(); - const o = new Changeset.Op('-'); - o.chars = upToLastLine; - o.lines = numLines - 1; - assem.append(o); - o.chars = lastLineLength; - o.lines = 0; - assem.append(o); - for (const op of Changeset.opsFromAText(atext)) assem.append(op); - const newLen = oldLen + assem.getLengthChange(); - const changeset = Changeset.checkRep( - Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1))); - performDocumentApplyChangeset(changeset); - - performSelectionChange( - [0, rep.lines.atIndex(0).lineMarker], [0, rep.lines.atIndex(0).lineMarker]); - - idleWorkTimer.atMost(100); - - if (rep.alltext !== atext.text) { - throw new Error('mismatch error setting raw text in setDocAText'); - } - }; - - const setDocText = (text) => { - setDocAText(Changeset.makeAText(text)); - }; - - const getDocText = () => { - const alltext = rep.alltext; - let len = alltext.length; - if (len > 0) len--; // final extra newline - return alltext.substring(0, len); - }; - - const exportText = () => { - if (currentCallStack && !currentCallStack.domClean) { - inCallStackIfNecessary('exportText', () => { - fastIncorp(2); - }); - } - return getDocText(); - }; - - const editorChangedSize = () => fixView(); - - const setOnKeyPress = (handler) => { - outsideKeyPress = handler; - }; - - const setOnKeyDown = (handler) => { - outsideKeyDown = handler; - }; - - const setNotifyDirty = (handler) => { - outsideNotifyDirty = handler; - }; - - const CMDS = { - clearauthorship: (prompt) => { - if ((!(rep.selStart && rep.selEnd)) || isCaret()) { - if (prompt) { - prompt(); - } else { - performDocumentApplyAttributesToCharRange(0, rep.alltext.length, [ - ['author', ''], - ]); - } - } else { - setAttributeOnSelection('author', ''); - } - }, - }; - - const execCommand = (cmd, ...args) => { - cmd = cmd.toLowerCase(); - if (CMDS[cmd]) { - inCallStackIfNecessary(cmd, () => { - fastIncorp(9); - CMDS[cmd](...args); - }); - } - }; - - const replaceRange = (start, end, text) => { - inCallStackIfNecessary('replaceRange', () => { - fastIncorp(9); - performDocumentReplaceRange(start, end, text); - }); - }; - - editorInfo.ace_callWithAce = (fn, callStack, normalize) => { - let wrapper = () => fn(editorInfo); - - if (normalize !== undefined) { - const wrapper1 = wrapper; - wrapper = () => { - editorInfo.ace_fastIncorp(9); - wrapper1(); - }; - } - - if (callStack !== undefined) { - return editorInfo.ace_inCallStack(callStack, wrapper); - } else { - return wrapper(); - } - }; - - /** - * This methed exposes a setter for some ace properties - * @param key the name of the parameter - * @param value the value to set to - */ - editorInfo.ace_setProperty = (key, value) => { - // These properties are exposed - const setters = { - wraps: setWraps, - 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); - fixView(); - }, - userauthor: (value) => { - thisAuthor = String(value); - documentAttributeManager.author = thisAuthor; - }, - styled: setStyled, - textface: setTextFace, - rtlistrue: (value) => { - document.body.classList.toggle('rtl', value); - document.body.classList.toggle('ltr', !value); - document.documentElement.dir = value ? 'rtl' : 'ltr'; - }, - }; - - const setter = setters[key.toLowerCase()]; - - // check if setter is present - if (setter !== undefined) { - setter(value); - } - }; - - editorInfo.ace_setBaseText = (txt) => { - changesetTracker.setBaseText(txt); - }; - editorInfo.ace_setBaseAttributedText = (atxt, apoolJsonObj) => { - changesetTracker.setBaseAttributedText(atxt, apoolJsonObj); - }; - editorInfo.ace_applyChangesToBase = (c, optAuthor, apoolJsonObj) => { - changesetTracker.applyChangesToBase(c, optAuthor, apoolJsonObj); - }; - editorInfo.ace_prepareUserChangeset = () => changesetTracker.prepareUserChangeset(); - editorInfo.ace_applyPreparedChangesetToBase = () => { - changesetTracker.applyPreparedChangesetToBase(); - }; - editorInfo.ace_setUserChangeNotificationCallback = (f) => { - changesetTracker.setUserChangeNotificationCallback(f); - }; - editorInfo.ace_setAuthorInfo = (author, info) => { - setAuthorInfo(author, info); - }; - - editorInfo.ace_getDocument = () => document; - - const now = () => Date.now(); - - const newTimeLimit = (ms) => { - const startTime = now(); - let exceededAlready = false; - let printedTrace = false; - const isTimeUp = () => { - if (exceededAlready) { - if ((!printedTrace)) { - printedTrace = true; - } - return true; - } - const elapsed = now() - startTime; - if (elapsed > ms) { - exceededAlready = true; - return true; - } else { - return false; - } - }; - - isTimeUp.elapsed = () => now() - startTime; - return isTimeUp; - }; - - - const makeIdleAction = (func) => { - let scheduledTimeout = null; - let scheduledTime = 0; - - const unschedule = () => { - if (scheduledTimeout) { - scheduler.clearTimeout(scheduledTimeout); - scheduledTimeout = null; - } - }; - - const reschedule = (time) => { - unschedule(); - scheduledTime = time; - let delay = time - now(); - if (delay < 0) delay = 0; - scheduledTimeout = scheduler.setTimeout(callback, delay); - }; - - const callback = () => { - scheduledTimeout = null; - // func may reschedule the action - func(); - }; - - return { - atMost: (ms) => { - const latestTime = now() + ms; - if ((!scheduledTimeout) || scheduledTime > latestTime) { - reschedule(latestTime); - } - }, - // atLeast(ms) will schedule the action if not scheduled yet. - // In other words, "infinity" is replaced by ms, even though - // it is technically larger. - atLeast: (ms) => { - const earliestTime = now() + ms; - if ((!scheduledTimeout) || scheduledTime < earliestTime) { - reschedule(earliestTime); - } - }, - never: () => { - unschedule(); - }, - }; - }; - - const fastIncorp = (n) => { - // normalize but don't do any lexing or anything - incorporateUserChanges(); - }; - editorInfo.ace_fastIncorp = fastIncorp; - - const idleWorkTimer = makeIdleAction(() => { - if (inInternationalComposition) { - // don't do idle input incorporation during international input composition - idleWorkTimer.atLeast(500); - return; - } - - inCallStackIfNecessary('idleWorkTimer', () => { - const isTimeUp = newTimeLimit(250); - - let finishedImportantWork = false; - let finishedWork = false; - - try { - incorporateUserChanges(); - - if (isTimeUp()) return; - - updateLineNumbers(); // update line numbers if any time left - if (isTimeUp()) return; - finishedImportantWork = true; - finishedWork = true; - } finally { - if (finishedWork) { - idleWorkTimer.atMost(1000); - } else if (finishedImportantWork) { - // if we've finished highlighting the view area, - // more highlighting could be counter-productive, - // e.g. if the user just opened a triple-quote and will soon close it. - idleWorkTimer.atMost(500); - } else { - let timeToWait = Math.round(isTimeUp.elapsed() / 2); - if (timeToWait < 100) timeToWait = 100; - idleWorkTimer.atMost(timeToWait); - } - } - }); - }); - - let _nextId = 1; - - const uniqueId = (n) => { - // not actually guaranteed to be unique, e.g. if user copy-pastes - // nodes with ids - const nid = n.id; - if (nid) return nid; - return (n.id = `magicdomid${_nextId++}`); - }; - - - const recolorLinesInRange = (startChar, endChar) => { - if (endChar <= startChar) return; - if (startChar < 0 || startChar >= rep.lines.totalWidth()) return; - let lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary - let lineStart = rep.lines.offsetOfEntry(lineEntry); - let lineIndex = rep.lines.indexOfEntry(lineEntry); - let selectionNeedsResetting = false; - let firstLine = null; - - // tokenFunc function; accesses current value of lineEntry and curDocChar, - // also mutates curDocChar - const tokenFunc = (tokenText, tokenClass) => { - lineEntry.domInfo.appendSpan(tokenText, tokenClass); - }; - - while (lineEntry && lineStart < endChar) { - const lineEnd = lineStart + lineEntry.width; - lineEntry.domInfo.clearSpans(); - getSpansForLine(lineEntry, tokenFunc, lineStart); - lineEntry.domInfo.finishUpdate(); - - markNodeClean(lineEntry.lineNode); - - if (rep.selStart && rep.selStart[0] === lineIndex || - rep.selEnd && rep.selEnd[0] === lineIndex) { - selectionNeedsResetting = true; - } - - if (firstLine == null) firstLine = lineIndex; - lineStart = lineEnd; - lineEntry = rep.lines.next(lineEntry); - lineIndex++; - } - if (selectionNeedsResetting) { - currentCallStack.selectionAffected = true; - } - }; - - // like getSpansForRange, but for a line, and the func takes (text,class) - // instead of (width,class); excludes the trailing '\n' from - // consideration by func - - - const getSpansForLine = (lineEntry, textAndClassFunc, lineEntryOffsetHint) => { - let lineEntryOffset = lineEntryOffsetHint; - if ((typeof lineEntryOffset) !== 'number') { - lineEntryOffset = rep.lines.offsetOfEntry(lineEntry); - } - const text = lineEntry.text; - if (text.length === 0) { - // allow getLineStyleFilter to set line-div styles - const func = linestylefilter.getLineStyleFilter( - 0, '', textAndClassFunc, rep.apool); - func('', ''); - } else { - let filteredFunc = linestylefilter.getFilterStack(text, textAndClassFunc, browser); - const lineNum = rep.lines.indexOfEntry(lineEntry); - const aline = rep.alines[lineNum]; - filteredFunc = linestylefilter.getLineStyleFilter( - text.length, aline, filteredFunc, rep.apool); - filteredFunc(text, ''); - } - }; - - let observedChanges; - - const clearObservedChanges = () => { - observedChanges = { - cleanNodesNearChanges: {}, - }; - }; - clearObservedChanges(); - - const getCleanNodeByKey = (key) => { - let n = document.getElementById(key); - // copying and pasting can lead to duplicate ids - while (n && isNodeDirty(n)) { - n.id = ''; - n = document.getElementById(key); - } - return n; - }; - - const observeChangesAroundNode = (node) => { - // Around this top-level DOM node, look for changes to the document - // (from how it looks in our representation) and record them in a way - // that can be used to "normalize" the document (apply the changes to our - // representation, and put the DOM in a canonical form). - let cleanNode; - let hasAdjacentDirtyness; - if (!isNodeDirty(node)) { - cleanNode = node; - const prevSib = cleanNode.previousSibling; - const nextSib = cleanNode.nextSibling; - hasAdjacentDirtyness = ((prevSib && isNodeDirty(prevSib)) || - (nextSib && isNodeDirty(nextSib))); - } else { - // node is dirty, look for clean node above - let upNode = node.previousSibling; - while (upNode && isNodeDirty(upNode)) { - upNode = upNode.previousSibling; - } - if (upNode) { - cleanNode = upNode; - } else { - let downNode = node.nextSibling; - while (downNode && isNodeDirty(downNode)) { - downNode = downNode.nextSibling; - } - if (downNode) { - cleanNode = downNode; - } - } - if (!cleanNode) { - // Couldn't find any adjacent clean nodes! - // Since top and bottom of doc is dirty, the dirty area will be detected. - return; - } - hasAdjacentDirtyness = true; - } - - if (hasAdjacentDirtyness) { - // previous or next line is dirty - observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true; - } else { - // next and prev lines are clean (if they exist) - const lineKey = uniqueId(cleanNode); - const prevSib = cleanNode.previousSibling; - const nextSib = cleanNode.nextSibling; - const actualPrevKey = ((prevSib && uniqueId(prevSib)) || null); - const actualNextKey = ((nextSib && uniqueId(nextSib)) || null); - const repPrevEntry = rep.lines.prev(rep.lines.atKey(lineKey)); - const repNextEntry = rep.lines.next(rep.lines.atKey(lineKey)); - const repPrevKey = ((repPrevEntry && repPrevEntry.key) || null); - const repNextKey = ((repNextEntry && repNextEntry.key) || null); - if (actualPrevKey !== repPrevKey || actualNextKey !== repNextKey) { - observedChanges.cleanNodesNearChanges[`$${uniqueId(cleanNode)}`] = true; - } - } - }; - - const observeChangesAroundSelection = () => { - if (currentCallStack.observedSelection) return; - currentCallStack.observedSelection = true; - - const selection = getSelection(); - - if (selection) { - const node1 = topLevel(selection.startPoint.node); - const node2 = topLevel(selection.endPoint.node); - if (node1) observeChangesAroundNode(node1); - if (node2 && node1 !== node2) { - observeChangesAroundNode(node2); - } - } - }; - - const observeSuspiciousNodes = () => { - // 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 elts = document.body.getElementsByTagName('style'); - for (const elt of elts) { - const n = topLevel(elt); - if (n && n.parentNode === document.body) { - observeChangesAroundNode(n); - } - } - } - }; - - const incorporateUserChanges = () => { - if (currentCallStack.domClean) return false; - - currentCallStack.isUserChange = true; - - if (DEBUG && window.DONT_INCORP || window.DEBUG_DONT_INCORP) return false; - - // returns true if dom changes were made - if (!document.body.firstChild) { - document.body.innerHTML = '
'; - } - - observeChangesAroundSelection(); - observeSuspiciousNodes(); - let dirtyRanges = getDirtyRanges(); - let dirtyRangesCheckOut = true; - let j = 0; - let a, b; - let scrollToTheLeftNeeded = false; - - while (j < dirtyRanges.length) { - a = dirtyRanges[j][0]; - b = dirtyRanges[j][1]; - if (!((a === 0 || getCleanNodeByKey(rep.lines.atIndex(a - 1).key)) && - (b === rep.lines.length() || getCleanNodeByKey(rep.lines.atIndex(b).key)))) { - dirtyRangesCheckOut = false; - break; - } - j++; - } - if (!dirtyRangesCheckOut) { - for (const bodyNode of document.body.childNodes) { - if ((bodyNode.tagName) && ((!bodyNode.id) || (!rep.lines.containsKey(bodyNode.id)))) { - observeChangesAroundNode(bodyNode); - } - } - dirtyRanges = getDirtyRanges(); - } - - clearObservedChanges(); - - const selection = getSelection(); - - let selStart, selEnd; // each one, if truthy, has [line,char] needed to set selection - let i = 0; - const splicesToDo = []; - let netNumLinesChangeSoFar = 0; - const toDeleteAtEnd = []; - const domInsertsNeeded = []; // each entry is [nodeToInsertAfter, [info1, info2, ...]] - while (i < dirtyRanges.length) { - const range = dirtyRanges[i]; - a = range[0]; - b = range[1]; - 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()) && document.body.lastChild) || - getCleanNodeByKey(rep.lines.atIndex(b).key).previousSibling); - - lastDirtyNode = (lastDirtyNode && isNodeDirty(lastDirtyNode) && lastDirtyNode); - if (firstDirtyNode && lastDirtyNode) { - const cc = makeContentCollector(isStyled, browser, rep.apool, className2Author); - cc.notifySelection(selection); - const dirtyNodes = []; - for (let n = firstDirtyNode; n && - !(n.previousSibling && n.previousSibling === lastDirtyNode); - n = n.nextSibling) { - cc.collectContent(n); - dirtyNodes.push(n); - } - cc.notifyNextNode(lastDirtyNode.nextSibling); - let lines = cc.getLines(); - if ((lines.length <= 1 || lines[lines.length - 1] !== '') && lastDirtyNode.nextSibling) { - // dirty region doesn't currently end a line, even taking the following node - // (or lack of node) into account, so include the following clean node. - // It could be SPAN or a DIV; basically this is any case where the contentCollector - // decides it isn't done. - // Note that this clean node might need to be there for the next dirty range. - b++; - const cleanLine = lastDirtyNode.nextSibling; - cc.collectContent(cleanLine); - toDeleteAtEnd.push(cleanLine); - cc.notifyNextNode(cleanLine.nextSibling); - } - - const ccData = cc.finish(); - const ss = ccData.selStart; - const se = ccData.selEnd; - lines = ccData.lines; - const lineAttribs = ccData.lineAttribs; - const linesWrapped = ccData.linesWrapped; - - if (linesWrapped > 0) { - // Chrome decides in its infinite wisdom that it's okay to put the browser's visisble - // window in the middle of the span. An outcome of this is that the first chars of the - // string are no longer visible to the user.. Yay chrome.. Move the browser's visible area - // to the left hand side of the span. Firefox isn't quite so bad, but it's still pretty - // quirky. - scrollToTheLeftNeeded = true; - } - - if (ss[0] >= 0) selStart = [ss[0] + a + netNumLinesChangeSoFar, ss[1]]; - if (se[0] >= 0) selEnd = [se[0] + a + netNumLinesChangeSoFar, se[1]]; - - const entries = []; - const nodeToAddAfter = lastDirtyNode; - const lineNodeInfos = []; - for (const lineString of lines) { - const newEntry = createDomLineEntry(lineString); - entries.push(newEntry); - lineNodeInfos.push(newEntry.domInfo); - } - domInsertsNeeded.push([nodeToAddAfter, lineNodeInfos]); - for (const n of dirtyNodes) toDeleteAtEnd.push(n); - const spliceHints = {}; - if (selStart) spliceHints.selStart = selStart; - if (selEnd) spliceHints.selEnd = selEnd; - splicesToDo.push([a + netNumLinesChangeSoFar, b - a, entries, lineAttribs, spliceHints]); - netNumLinesChangeSoFar += (lines.length - (b - a)); - } else if (b > a) { - splicesToDo.push([a + netNumLinesChangeSoFar, b - a, [], []]); - } - i++; - } - - const domChanges = (splicesToDo.length > 0); - - 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) { - $('#innerdocbody').scrollLeft(0); - } - - // if the nodes that define the selection weren't encountered during - // content collection, figure out where those nodes are now. - if (selection && !selStart) { - const selStartFromHook = hooks.callAll('aceStartLineAndCharForPoint', { - callstack: currentCallStack, - editorInfo, - rep, - root: document.body, - point: selection.startPoint, - documentAttributeManager, - }); - selStart = (selStartFromHook == null || selStartFromHook.length === 0) - ? getLineAndCharForPoint(selection.startPoint) : selStartFromHook; - } - if (selection && !selEnd) { - const selEndFromHook = hooks.callAll('aceEndLineAndCharForPoint', { - callstack: currentCallStack, - editorInfo, - rep, - root: document.body, - point: selection.endPoint, - documentAttributeManager, - }); - selEnd = (selEndFromHook == null || - selEndFromHook.length === 0) - ? getLineAndCharForPoint(selection.endPoint) : selEndFromHook; - } - - // selection from content collection can, in various ways, extend past final - // BR in firefox DOM, so cap the line - const numLines = rep.lines.length(); - if (selStart && selStart[0] >= numLines) { - selStart[0] = numLines - 1; - selStart[1] = rep.lines.atIndex(selStart[0]).text.length; - } - if (selEnd && selEnd[0] >= numLines) { - selEnd[0] = numLines - 1; - selEnd[1] = rep.lines.atIndex(selEnd[0]).text.length; - } - - // update rep if we have a new selection - // NOTE: IE loses the selection when you click stuff in e.g. the - // editbar, so removing the selection when it's lost is not a good - // idea. - if (selection) repSelectionChange(selStart, selEnd, selection && selection.focusAtStart); - // update browser selection - if (selection && (domChanges || isCaret())) { - // if no DOM changes (not this case), want to treat range selection delicately, - // e.g. in IE not lose which end of the selection is the focus/anchor; - // on the other hand, we may have just noticed a press of PageUp/PageDown - currentCallStack.selectionAffected = true; - } - - currentCallStack.domClean = true; - - fixView(); - - return domChanges; - }; - - const STYLE_ATTRIBS = { - bold: true, - italic: true, - underline: true, - strikethrough: true, - list: true, - }; - - const isStyleAttribute = (aname) => !!STYLE_ATTRIBS[aname]; - - const isDefaultLineAttribute = - (aname) => AttributeManager.DEFAULT_LINE_ATTRIBUTES.indexOf(aname) !== -1; - - const insertDomLines = (nodeToAddAfter, infoStructs) => { - let lastEntry; - let lineStartOffset; - for (const info of infoStructs) { - const node = info.node; - const key = uniqueId(node); - let entry; - if (lastEntry) { - // optimization to avoid recalculation - const next = rep.lines.next(lastEntry); - if (next && next.key === key) { - entry = next; - lineStartOffset += lastEntry.width; - } - } - if (!entry) { - entry = rep.lines.atKey(key); - lineStartOffset = rep.lines.offsetOfKey(key); - } - lastEntry = entry; - getSpansForLine(entry, (tokenText, tokenClass) => { - info.appendSpan(tokenText, tokenClass); - }, lineStartOffset); - info.prepareForAdd(); - entry.lineMarker = info.lineMarker; - if (!nodeToAddAfter) { - document.body.insertBefore(node, document.body.firstChild); - } else { - document.body.insertBefore(node, nodeToAddAfter.nextSibling); - } - nodeToAddAfter = node; - info.notifyAdded(); - markNodeClean(node); - } - }; - - const isCaret = () => (rep.selStart && rep.selEnd && - rep.selStart[0] === rep.selEnd[0] && rep.selStart[1] === rep.selEnd[1]); - editorInfo.ace_isCaret = isCaret; - - // prereq: isCaret() - const caretLine = () => rep.selStart[0]; - - editorInfo.ace_caretLine = caretLine; - - const caretColumn = () => rep.selStart[1]; - - editorInfo.ace_caretColumn = caretColumn; - - const caretDocChar = () => rep.lines.offsetOfIndex(caretLine()) + caretColumn(); - - editorInfo.ace_caretDocChar = caretDocChar; - - const handleReturnIndentation = () => { - // on return, indent to level of previous line - if (isCaret() && caretColumn() === 0 && caretLine() > 0) { - const lineNum = caretLine(); - const thisLine = rep.lines.atIndex(lineNum); - const prevLine = rep.lines.prev(thisLine); - const prevLineText = prevLine.text; - let theIndent = /^ *(?:)/.exec(prevLineText)[0]; - const shouldIndent = parent.parent.clientVars.indentationOnNewLine; - if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) { - theIndent += THE_TAB; - } - const cs = Changeset.builder(rep.lines.totalWidth()).keep( - rep.lines.offsetOfIndex(lineNum), lineNum).insert( - theIndent, [ - ['author', thisAuthor], - ], rep.apool).toString(); - performDocumentApplyChangeset(cs); - performSelectionChange([lineNum, theIndent.length], [lineNum, theIndent.length]); - } - }; - - const getPointForLineAndChar = (lineAndChar) => { - const line = lineAndChar[0]; - let charsLeft = lineAndChar[1]; - const lineEntry = rep.lines.atIndex(line); - charsLeft -= lineEntry.lineMarker; - if (charsLeft < 0) { - charsLeft = 0; - } - const lineNode = lineEntry.lineNode; - let n = lineNode; - let after = false; - if (charsLeft === 0) { - return { - node: lineNode, - index: 0, - maxIndex: 1, - }; - } - while (!(n === lineNode && after)) { - if (after) { - if (n.nextSibling) { - n = n.nextSibling; - after = false; - } else { n = n.parentNode; } - } else if (isNodeText(n)) { - const len = n.nodeValue.length; - if (charsLeft <= len) { - return { - node: n, - index: charsLeft, - maxIndex: len, - }; - } - charsLeft -= len; - after = true; - } else if (n.firstChild) { n = n.firstChild; } else { after = true; } - } - return { - node: lineNode, - index: 1, - maxIndex: 1, - }; - }; - - const nodeText = (n) => n.textContent || n.nodeValue || ''; - - const getLineAndCharForPoint = (point) => { - // 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 === document.body) { - if (point.index === 0) { - return [0, 0]; - } else { - const N = rep.lines.length(); - const ln = rep.lines.atIndex(N - 1); - return [N - 1, ln.text.length]; - } - } else { - let n = point.node; - let col = 0; - // if this part fails, it probably means the selection node - // was dirty, and we didn't see it when collecting dirty nodes. - if (isNodeText(n)) { - col = point.index; - } else if (point.index > 0) { - col = nodeText(n).length; - } - let parNode, prevSib; - while ((parNode = n.parentNode) !== document.body) { - if ((prevSib = n.previousSibling)) { - n = prevSib; - col += nodeText(n).length; - } else { - n = parNode; - } - } - if (n.firstChild && isBlockElement(n.firstChild)) { - col += 1; // lineMarker - } - const lineEntry = rep.lines.atKey(n.id); - const lineNum = rep.lines.indexOfEntry(lineEntry); - return [lineNum, col]; - } - }; - editorInfo.ace_getLineAndCharForPoint = getLineAndCharForPoint; - - const createDomLineEntry = (lineString) => { - const info = doCreateDomLine(lineString.length > 0); - const newNode = info.node; - return { - key: uniqueId(newNode), - text: lineString, - lineNode: newNode, - domInfo: info, - lineMarker: 0, - }; - }; - - const performDocumentApplyChangeset = (changes, insertsAfterSelection) => { - const domAndRepSplice = (startLine, deleteCount, newLineStrings) => { - const keysToDelete = []; - if (deleteCount > 0) { - let entryToDelete = rep.lines.atIndex(startLine); - for (let i = 0; i < deleteCount; i++) { - keysToDelete.push(entryToDelete.key); - entryToDelete = rep.lines.next(entryToDelete); - } - } - - const lineEntries = newLineStrings.map(createDomLineEntry); - - doRepLineSplice(startLine, deleteCount, lineEntries); - - let nodeToAddAfter; - if (startLine > 0) { - nodeToAddAfter = getCleanNodeByKey(rep.lines.atIndex(startLine - 1).key); - } else { nodeToAddAfter = null; } - - insertDomLines(nodeToAddAfter, lineEntries.map((entry) => entry.domInfo)); - - for (const k of keysToDelete) { - const n = document.getElementById(k); - n.parentNode.removeChild(n); - } - - if ( - (rep.selStart && - rep.selStart[0] >= startLine && - rep.selStart[0] <= startLine + deleteCount) || - (rep.selEnd && rep.selEnd[0] >= startLine && rep.selEnd[0] <= startLine + deleteCount)) { - currentCallStack.selectionAffected = true; - } - }; - - doRepApplyChangeset(changes, insertsAfterSelection); - - let requiredSelectionSetting = null; - if (rep.selStart && rep.selEnd) { - const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1]; - const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1]; - const result = - Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection); - requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart]; - } - - const linesMutatee = { - splice: (start, numRemoved, ...args) => { - domAndRepSplice(start, numRemoved, args.map((s) => s.slice(0, -1))); - }, - get: (i) => `${rep.lines.atIndex(i).text}\n`, - length: () => rep.lines.length(), - }; - - Changeset.mutateTextLines(changes, linesMutatee); - - if (requiredSelectionSetting) { - performSelectionChange( - lineAndColumnFromChar(requiredSelectionSetting[0]), - lineAndColumnFromChar(requiredSelectionSetting[1]), - requiredSelectionSetting[2]); - } - }; - - const doRepApplyChangeset = (changes, insertsAfterSelection) => { - Changeset.checkRep(changes); - - if (Changeset.oldLen(changes) !== rep.alltext.length) { - const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`; - throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`); - } - - const editEvent = currentCallStack.editEvent; - if (editEvent.eventType === 'nonundoable') { - if (!editEvent.changeset) { - editEvent.changeset = changes; - } else { - editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool); - } - } 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); - - if (changesetTracker.isTracking()) { - changesetTracker.composeUserChangeset(changes); - } - }; - - /** - * Converts the position of a char (index in String) into a [row, col] tuple - */ - const lineAndColumnFromChar = (x) => { - const lineEntry = rep.lines.atOffset(x); - const lineStart = rep.lines.offsetOfEntry(lineEntry); - const lineNum = rep.lines.indexOfEntry(lineEntry); - return [lineNum, x - lineStart]; - }; - - const performDocumentReplaceCharRange = (startChar, endChar, newText) => { - if (startChar === endChar && newText.length === 0) { - return; - } - // Requires that the replacement preserve the property that the - // internal document text ends in a newline. Given this, we - // rewrite the splice so that it doesn't touch the very last - // char of the document. - if (endChar === rep.alltext.length) { - if (startChar === endChar) { - // an insert at end - startChar--; - endChar--; - newText = `\n${newText.substring(0, newText.length - 1)}`; - } else if (newText.length === 0) { - // a delete at end - startChar--; - endChar--; - } else { - // a replace at end - endChar--; - newText = newText.substring(0, newText.length - 1); - } - } - performDocumentReplaceRange( - lineAndColumnFromChar(startChar), lineAndColumnFromChar(endChar), newText); - }; - - const performDocumentApplyAttributesToCharRange = (start, end, attribs) => { - end = Math.min(end, rep.alltext.length - 1); - documentAttributeManager.setAttributesOnRange( - lineAndColumnFromChar(start), lineAndColumnFromChar(end), attribs); - }; - - editorInfo.ace_performDocumentApplyAttributesToCharRange = - performDocumentApplyAttributesToCharRange; - - const setAttributeOnSelection = (attributeName, attributeValue) => { - if (!(rep.selStart && rep.selEnd)) return; - - documentAttributeManager.setAttributesOnRange(rep.selStart, rep.selEnd, [ - [attributeName, attributeValue], - ]); - }; - editorInfo.ace_setAttributeOnSelection = setAttributeOnSelection; - - const getAttributeOnSelection = (attributeName, prevChar) => { - if (!(rep.selStart && rep.selEnd)) return; - const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); - if (isNotSelection) { - if (prevChar) { - // If it's not the start of the line - if (rep.selStart[1] !== 0) { - rep.selStart[1]--; - } - } - } - - const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); - const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); - const hasIt = (attribs) => withItRegex.test(attribs); - - const rangeHasAttrib = (selStart, selEnd) => { - // if range is collapsed -> no attribs in range - if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false; - - if (selStart[0] !== selEnd[0]) { // -> More than one line selected - let hasAttrib = true; - - // from selStart to the end of the first line - hasAttrib = hasAttrib && - rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); - - // for all lines in between - for (let n = selStart[0] + 1; n < selEnd[0]; n++) { - hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); - } - - // for the last, potentially partial, line - hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); - - return hasAttrib; - } - - // Logic tells us we now have a range on a single line - - const lineNum = selStart[0]; - const start = selStart[1]; - const end = selEnd[1]; - let hasAttrib = true; - - let indexIntoLine = 0; - for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { - const opStartInLine = indexIntoLine; - const opEndInLine = opStartInLine + op.chars; - if (!hasIt(op.attribs)) { - // does op overlap selection? - if (!(opEndInLine <= start || opStartInLine >= end)) { - // since it's overlapping but hasn't got the attrib -> range hasn't got it - hasAttrib = false; - break; - } - } - indexIntoLine = opEndInLine; - } - - return hasAttrib; - }; - return rangeHasAttrib(rep.selStart, rep.selEnd); - }; - - editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection; - - const toggleAttributeOnSelection = (attributeName) => { - if (!(rep.selStart && rep.selEnd)) return; - - let selectionAllHasIt = true; - const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); - const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); - - const hasIt = (attribs) => withItRegex.test(attribs); - - const selStartLine = rep.selStart[0]; - const selEndLine = rep.selEnd[0]; - for (let n = selStartLine; n <= selEndLine; n++) { - let indexIntoLine = 0; - let selectionStartInLine = 0; - if (documentAttributeManager.lineHasMarker(n)) { - selectionStartInLine = 1; // ignore "*" used as line marker - } - let selectionEndInLine = rep.lines.atIndex(n).text.length; // exclude newline - if (n === selStartLine) { - selectionStartInLine = rep.selStart[1]; - } - if (n === selEndLine) { - selectionEndInLine = rep.selEnd[1]; - } - for (const op of Changeset.deserializeOps(rep.alines[n])) { - const opStartInLine = indexIntoLine; - const opEndInLine = opStartInLine + op.chars; - if (!hasIt(op.attribs)) { - // does op overlap selection? - if (!(opEndInLine <= selectionStartInLine || opStartInLine >= selectionEndInLine)) { - selectionAllHasIt = false; - break; - } - } - indexIntoLine = opEndInLine; - } - if (!selectionAllHasIt) { - break; - } - } - - - const attributeValue = selectionAllHasIt ? '' : 'true'; - documentAttributeManager.setAttributesOnRange( - rep.selStart, rep.selEnd, [[attributeName, attributeValue]]); - if (attribIsFormattingStyle(attributeName)) { - updateStyleButtonState(attributeName, !selectionAllHasIt); // italic, bold, ... - } - }; - editorInfo.ace_toggleAttributeOnSelection = toggleAttributeOnSelection; - - const performDocumentReplaceSelection = (newText) => { - if (!(rep.selStart && rep.selEnd)) return; - performDocumentReplaceRange(rep.selStart, rep.selEnd, newText); - }; - - // 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) => { - for (const entry of newLineEntries) entry.width = entry.text.length + 1; - - const startOldChar = rep.lines.offsetOfIndex(startLine); - const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); - - rep.lines.splice(startLine, deleteCount, newLineEntries); - currentCallStack.docTextChanged = true; - currentCallStack.repChanged = true; - const newText = newLineEntries.map((e) => `${e.text}\n`).join(''); - - rep.alltext = rep.alltext.substring(0, startOldChar) + - newText + rep.alltext.substring(endOldChar, rep.alltext.length); - }; - - const doIncorpLineSplice = (startLine, deleteCount, newLineEntries, lineAttribs, hints) => { - const startOldChar = rep.lines.offsetOfIndex(startLine); - const endOldChar = rep.lines.offsetOfIndex(startLine + deleteCount); - - const oldRegionStart = rep.lines.offsetOfIndex(startLine); - - let selStartHintChar, selEndHintChar; - if (hints && hints.selStart) { - selStartHintChar = - rep.lines.offsetOfIndex(hints.selStart[0]) + hints.selStart[1] - oldRegionStart; - } - if (hints && hints.selEnd) { - selEndHintChar = rep.lines.offsetOfIndex(hints.selEnd[0]) + hints.selEnd[1] - oldRegionStart; - } - - const newText = newLineEntries.map((e) => `${e.text}\n`).join(''); - 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 commonStart = analysis[0]; - let commonEnd = analysis[1]; - let shortOldText = oldText.substring(commonStart, oldText.length - commonEnd); - let shortNewText = newText.substring(commonStart, newText.length - commonEnd); - let spliceStart = startOldChar + commonStart; - let spliceEnd = endOldChar - commonEnd; - let shiftFinalNewlineToBeforeNewText = false; - - // adjust the splice to not involve the final newline of the document; - // be very defensive - if (shortOldText.charAt(shortOldText.length - 1) === '\n' && - shortNewText.charAt(shortNewText.length - 1) === '\n') { - // replacing text that ends in newline with text that also ends in newline - // (still, after analysis, somehow) - shortOldText = shortOldText.slice(0, -1); - shortNewText = shortNewText.slice(0, -1); - spliceEnd--; - commonEnd++; - } - if (shortOldText.length === 0 && - spliceStart === rep.alltext.length && - shortNewText.length > 0) { - // inserting after final newline, bad - spliceStart--; - spliceEnd--; - shortNewText = `\n${shortNewText.slice(0, -1)}`; - shiftFinalNewlineToBeforeNewText = true; - } - if (spliceEnd === rep.alltext.length && - shortOldText.length > 0 && - shortNewText.length === 0) { - // deletion at end of rep.alltext - if (rep.alltext.charAt(spliceStart - 1) === '\n') { - // (if not then what the heck? it will definitely lead - // to a rep.alltext without a final newline) - spliceStart--; - spliceEnd--; - } - } - - if (!(shortOldText.length === 0 && shortNewText.length === 0)) { - const oldDocText = rep.alltext; - const oldLen = oldDocText.length; - - const spliceStartLine = rep.lines.indexOfOffset(spliceStart); - const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine); - - const startBuilder = () => { - const builder = Changeset.builder(oldLen); - builder.keep(spliceStartLineStart, spliceStartLine); - builder.keep(spliceStart - spliceStartLineStart); - return builder; - }; - - const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => { - let textIndex = 0; - const newTextStart = commonStart; - const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); - for (const op of Changeset.deserializeOps(attribs)) { - const nextIndex = textIndex + op.chars; - if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { - func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); - } - textIndex = nextIndex; - } - }; - - const justApplyStyles = (shortNewText === shortOldText); - let theChangeset; - - if (justApplyStyles) { - // create changeset that clears the incorporated styles on - // the existing text. we compose this with the - // changeset the applies the styles found in the DOM. - // This allows us to incorporate, e.g., Safari's native "unbold". - const incorpedAttribClearer = cachedStrFunc( - (oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => { - const k = rep.apool.getAttribKey(n); - if (isStyleAttribute(k)) { - return rep.apool.putAttrib([k, '']); - } - return false; - })); - - const builder1 = startBuilder(); - if (shiftFinalNewlineToBeforeNewText) { - builder1.keep(1, 1); - } - eachAttribRun(oldAttribs, (start, end, attribs) => { - builder1.keepText(newText.substring(start, end), incorpedAttribClearer(attribs)); - }); - const clearer = builder1.toString(); - - const builder2 = startBuilder(); - if (shiftFinalNewlineToBeforeNewText) { - builder2.keep(1, 1); - } - eachAttribRun(newAttribs, (start, end, attribs) => { - builder2.keepText(newText.substring(start, end), attribs); - }); - const styler = builder2.toString(); - - theChangeset = Changeset.compose(clearer, styler, rep.apool); - } else { - const builder = startBuilder(); - - const spliceEndLine = rep.lines.indexOfOffset(spliceEnd); - const spliceEndLineStart = rep.lines.offsetOfIndex(spliceEndLine); - if (spliceEndLineStart > spliceStart) { - builder.remove(spliceEndLineStart - spliceStart, spliceEndLine - spliceStartLine); - builder.remove(spliceEnd - spliceEndLineStart); - } else { - builder.remove(spliceEnd - spliceStart); - } - - let isNewTextMultiauthor = false; - const authorizer = cachedStrFunc((oldAtts) => { - const attribs = AttributeMap.fromString(oldAtts, rep.apool); - if (!isNewTextMultiauthor || !attribs.has('author')) attribs.set('author', thisAuthor); - return attribs.toString(); - }); - - let foundDomAuthor = ''; - eachAttribRun(newAttribs, (start, end, attribs) => { - const a = AttributeMap.fromString(attribs, rep.apool).get('author'); - if (a && a !== foundDomAuthor) { - if (!foundDomAuthor) { - foundDomAuthor = a; - } else { - isNewTextMultiauthor = true; // multiple authors in DOM! - } - } - }); - - if (shiftFinalNewlineToBeforeNewText) { - builder.insert('\n', authorizer('')); - } - - eachAttribRun(newAttribs, (start, end, attribs) => { - builder.insert(newText.substring(start, end), authorizer(attribs)); - }); - theChangeset = builder.toString(); - } - - doRepApplyChangeset(theChangeset); - } - - // do this no matter what, because we need to get the right - // line keys into the rep. - doRepLineSplice(startLine, deleteCount, newLineEntries); - }; - - const cachedStrFunc = (func) => { - const cache = {}; - return (s) => { - if (!cache[s]) { - cache[s] = func(s); - } - return cache[s]; - }; - }; - - const analyzeChange = ( - oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) => { - // we need to take into account both the styles attributes & attributes defined by - // the plugins, so basically we can ignore only the default line attribs used by - // Etherpad - const incorpedAttribFilter = (anum) => !isDefaultLineAttribute(rep.apool.getAttribKey(anum)); - - const attribRuns = (attribs) => { - const lengs = []; - const atts = []; - for (const op of Changeset.deserializeOps(attribs)) { - lengs.push(op.chars); - atts.push(op.attribs); - } - return [lengs, atts]; - }; - - const attribIterator = (runs, backward) => { - const lengs = runs[0]; - const atts = runs[1]; - let i = (backward ? lengs.length - 1 : 0); - let j = 0; - const next = () => { - while (j >= lengs[i]) { - if (backward) i--; - else i++; - j = 0; - } - const a = atts[i]; - j++; - return a; - }; - return next; - }; - - const oldLen = oldText.length; - const newLen = newText.length; - const minLen = Math.min(oldLen, newLen); - - const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter)); - const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter)); - - let commonStart = 0; - const oldStartIter = attribIterator(oldARuns, false); - const newStartIter = attribIterator(newARuns, false); - while (commonStart < minLen) { - if (oldText.charAt(commonStart) === newText.charAt(commonStart) && - oldStartIter() === newStartIter()) { - commonStart++; - } else { break; } - } - - let commonEnd = 0; - const oldEndIter = attribIterator(oldARuns, true); - const newEndIter = attribIterator(newARuns, true); - while (commonEnd < minLen) { - if (commonEnd === 0) { - // assume newline in common - oldEndIter(); - newEndIter(); - commonEnd++; - } else if ( - oldText.charAt(oldLen - 1 - commonEnd) === newText.charAt(newLen - 1 - commonEnd) && - oldEndIter() === newEndIter()) { - commonEnd++; - } else { break; } - } - - let hintedCommonEnd = -1; - if ((typeof optSelEndHint) === 'number') { - hintedCommonEnd = newLen - optSelEndHint; - } - - - if (commonStart + commonEnd > oldLen) { - // ambiguous insertion - const minCommonEnd = oldLen - commonStart; - const maxCommonEnd = commonEnd; - if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { - commonEnd = hintedCommonEnd; - } else { - commonEnd = minCommonEnd; - } - commonStart = oldLen - commonEnd; - } - if (commonStart + commonEnd > newLen) { - // ambiguous deletion - const minCommonEnd = newLen - commonStart; - const maxCommonEnd = commonEnd; - if (hintedCommonEnd >= minCommonEnd && hintedCommonEnd <= maxCommonEnd) { - commonEnd = hintedCommonEnd; - } else { - commonEnd = minCommonEnd; - } - commonStart = newLen - commonEnd; - } - - return [commonStart, commonEnd]; - }; - - const equalLineAndChars = (a, b) => { - if (!a) return !b; - if (!b) return !a; - return (a[0] === b[0] && a[1] === b[1]); - }; - - const performSelectionChange = (selectStart, selectEnd, focusAtStart) => { - if (repSelectionChange(selectStart, selectEnd, focusAtStart)) { - currentCallStack.selectionAffected = true; - } - }; - editorInfo.ace_performSelectionChange = performSelectionChange; - - // Change the abstract representation of the document to have a different selection. - // Should not rely on the line representation. Should not affect the DOM. - - - const repSelectionChange = (selectStart, selectEnd, focusAtStart) => { - focusAtStart = !!focusAtStart; - - const newSelFocusAtStart = (focusAtStart && ((!selectStart) || - (!selectEnd) || - (selectStart[0] !== selectEnd[0]) || - (selectStart[1] !== selectEnd[1]))); - - if ((!equalLineAndChars(rep.selStart, selectStart)) || - (!equalLineAndChars(rep.selEnd, selectEnd)) || - (rep.selFocusAtStart !== newSelFocusAtStart)) { - rep.selStart = selectStart; - rep.selEnd = selectEnd; - rep.selFocusAtStart = newSelFocusAtStart; - currentCallStack.repChanged = true; - - // select the formatting buttons when there is the style applied on selection - selectFormattingButtonIfLineHasStyleApplied(rep); - - hooks.callAll('aceSelectionChanged', { - rep, - callstack: currentCallStack, - documentAttributeManager, - }); - - // we scroll when user places the caret at the last line of the pad - // when this settings is enabled - const docTextChanged = currentCallStack.docTextChanged; - if (!docTextChanged) { - const isScrollableEvent = !isPadLoading(currentCallStack.type) && - isScrollableEditEvent(currentCallStack.type); - const innerHeight = getInnerHeight(); - scroll.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary( - rep, isScrollableEvent, innerHeight * 2); - } - - return true; - } - return false; - }; - - const isPadLoading = (t) => t === 'setup' || t === 'setBaseText' || t === 'importText'; - - const updateStyleButtonState = (attribName, hasStyleOnRepSelection) => { - const $formattingButton = parent.parent.$(`[data-key="${attribName}"]`).find('a'); - $formattingButton.toggleClass(SELECT_BUTTON_CLASS, hasStyleOnRepSelection); - }; - - const attribIsFormattingStyle = (attribName) => FORMATTING_STYLES.indexOf(attribName) !== -1; - - const selectFormattingButtonIfLineHasStyleApplied = (rep) => { - for (const style of FORMATTING_STYLES) { - const hasStyleOnRepSelection = - documentAttributeManager.hasAttributeOnSelectionOrCaretPosition(style); - updateStyleButtonState(style, hasStyleOnRepSelection); - } - }; - - const doCreateDomLine = - (nonEmpty) => domline.createDomLine(nonEmpty, doesWrap, browser, document); - - const textify = - (str) => str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); - - const _blockElems = { - div: 1, - p: 1, - pre: 1, - li: 1, - ol: 1, - ul: 1, - }; - - for (const element of hooks.callAll('aceRegisterBlockElements')) _blockElems[element] = 1; - - const isBlockElement = (n) => !!_blockElems[(n.tagName || '').toLowerCase()]; - editorInfo.ace_isBlockElement = isBlockElement; - - const getDirtyRanges = () => { - // based on observedChanges, return a list of ranges of original lines - // that need to be removed or replaced with new user content to incorporate - // the user's changes into the line representation. ranges may be zero-length, - // indicating inserted content. for example, [0,0] means content was inserted - // at the top of the document, while [3,4] means line 3 was deleted, modified, - // or replaced with one or more new lines of content. ranges do not touch. - - const cleanNodeForIndexCache = {}; - const N = rep.lines.length(); // old number of lines - - - const cleanNodeForIndex = (i) => { - // if line (i) in the un-updated line representation maps to a clean node - // in the document, return that node. - // if (i) is out of bounds, return true. else return false. - if (cleanNodeForIndexCache[i] === undefined) { - let result; - if (i < 0 || i >= N) { - result = true; // truthy, but no actual node - } else { - const key = rep.lines.atIndex(i).key; - result = (getCleanNodeByKey(key) || false); - } - cleanNodeForIndexCache[i] = result; - } - return cleanNodeForIndexCache[i]; - }; - const isConsecutiveCache = {}; - - const isConsecutive = (i) => { - if (isConsecutiveCache[i] === undefined) { - isConsecutiveCache[i] = (() => { - // returns whether line (i) and line (i-1), assumed to be map to clean DOM nodes, - // or document boundaries, are consecutive in the changed DOM - const a = cleanNodeForIndex(i - 1); - const b = cleanNodeForIndex(i); - if ((!a) || (!b)) return false; // violates precondition - 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; - return a.nextSibling === b; - })(); - } - return isConsecutiveCache[i]; - }; - - // returns whether line (i) in the un-updated representation maps to a clean node, - // or is outside the bounds of the document - const isClean = (i) => !!cleanNodeForIndex(i); - - // list of pairs, each representing a range of lines that is clean and consecutive - // in the changed DOM. lines (-1) and (N) are always clean, but may or may not - // be consecutive with lines in the document. pairs are in sorted order. - const cleanRanges = [ - [-1, N + 1], - ]; - - // returns index of cleanRange containing i, or -1 if none - const rangeForLine = (i) => { - 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) => { - // rng is index into cleanRanges, line is line number - // precond: line is in rng - const a = cleanRanges[rng][0]; - const b = cleanRanges[rng][1]; - if ((a + 1) === b) cleanRanges.splice(rng, 1); - else if (line === a) cleanRanges[rng][0]++; - else if (line === (b - 1)) cleanRanges[rng][1]--; - else cleanRanges.splice(rng, 1, [a, line], [line + 1, b]); - }; - - const splitRange = (rng, pt) => { - // precond: pt splits cleanRanges[rng] into two non-empty ranges - const a = cleanRanges[rng][0]; - const b = cleanRanges[rng][1]; - cleanRanges.splice(rng, 1, [a, pt], [pt, b]); - }; - - const correctedLines = {}; - - const correctlyAssignLine = (line) => { - if (correctedLines[line]) return true; - correctedLines[line] = true; - // "line" is an index of a line in the un-updated rep. - // returns whether line was already correctly assigned (i.e. correctly - // clean or dirty, according to cleanRanges, and if clean, correctly - // attached or not attached (i.e. in the same range as) the prev and next lines). - const rng = rangeForLine(line); - const lineClean = isClean(line); - if (rng < 0) { - if (lineClean) { - // somehow lost clean line - } - return true; - } - if (!lineClean) { - // a clean-range includes this dirty line, fix it - removeLineFromRange(rng, line); - return false; - } else { - // line is clean, but could be wrongly connected to a clean line - // above or below - const a = cleanRanges[rng][0]; - const b = cleanRanges[rng][1]; - let didSomething = false; - // we'll leave non-clean adjacent nodes in the clean range for the caller to - // detect and deal with. we deal with whether the range should be split - // just above or just below this line. - if (a < line && isClean(line - 1) && !isConsecutive(line)) { - splitRange(rng, line); - didSomething = true; - } - if (b > (line + 1) && isClean(line + 1) && !isConsecutive(line + 1)) { - splitRange(rng, line + 1); - didSomething = true; - } - return !didSomething; - } - }; - - const detectChangesAroundLine = (line, reqInARow) => { - // make sure cleanRanges is correct about line number "line" and the surrounding - // lines; only stops checking at end of document or after no changes need - // making for several consecutive lines. note that iteration is over old lines, - // so this operation takes time proportional to the number of old lines - // that are changed or missing, not the number of new lines inserted. - let correctInARow = 0; - let currentIndex = line; - while (correctInARow < reqInARow && currentIndex >= 0) { - if (correctlyAssignLine(currentIndex)) { - correctInARow++; - } else { correctInARow = 0; } - currentIndex--; - } - correctInARow = 0; - currentIndex = line; - while (correctInARow < reqInARow && currentIndex < N) { - if (correctlyAssignLine(currentIndex)) { - correctInARow++; - } else { correctInARow = 0; } - currentIndex++; - } - }; - - if (N === 0) { - if (!isConsecutive(0)) { - splitRange(0, 0); - } - } else { - detectChangesAroundLine(0, 1); - detectChangesAroundLine(N - 1, 1); - - 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); - } - } - } - - const dirtyRanges = []; - for (let r = 0; r < cleanRanges.length - 1; r++) { - dirtyRanges.push([cleanRanges[r][1], cleanRanges[r + 1][0]]); - } - - return dirtyRanges; - }; - - const markNodeClean = (n) => { - // clean nodes have knownHTML that matches their innerHTML - setAssoc(n, 'dirtiness', {nodeId: uniqueId(n), knownHTML: n.innerHTML}); - }; - - const isNodeDirty = (n) => { - if (n.parentNode !== document.body) return true; - const data = getAssoc(n, 'dirtiness'); - if (!data) return true; - if (n.id !== data.nodeId) return true; - if (n.innerHTML !== data.knownHTML) return true; - return false; - }; - - const handleClick = (evt) => { - inCallStackIfNecessary('handleClick', () => { - idleWorkTimer.atMost(200); - }); - - const isLink = (n) => (n.tagName || '').toLowerCase() === 'a' && n.href; - - // only want to catch left-click - if ((!evt.ctrlKey) && (evt.button !== 2) && (evt.button !== 3)) { - // find A tag with HREF - let n = evt.target; - while (n && n.parentNode && !isLink(n)) { - n = n.parentNode; - } - if (n && isLink(n)) { - try { - window.open(n.href, '_blank', 'noopener,noreferrer'); - } catch (e) { - // absorb "user canceled" error in IE for certain prompts - } - evt.preventDefault(); - } - } - - hideEditBarDropdowns(); - }; - - const hideEditBarDropdowns = () => { - window.parent.parent.padeditbar.toggleDropDown('none'); - }; - - const renumberList = (lineNum) => { - // 1-check we are in a list - let type = getLineListType(lineNum); - if (!type) { - return null; - } - type = /([a-z]+)[0-9]+/.exec(type); - if (type[1] === 'indent') { - return null; - } - - // 2-find the first line of the list - while (lineNum - 1 >= 0 && (type = getLineListType(lineNum - 1))) { - type = /([a-z]+)[0-9]+/.exec(type); - if (type[1] === 'indent') break; - lineNum--; - } - - // 3-renumber every list item of the same level from the beginning, level 1 - // IMPORTANT: never skip a level because there imbrication may be arbitrary - const builder = Changeset.builder(rep.lines.totalWidth()); - let loc = [0, 0]; - const applyNumberList = (line, level) => { - // init - let position = 1; - let curLevel = level; - let listType; - // loop over the lines - while ((listType = getLineListType(line))) { - // apply new num - listType = /([a-z]+)([0-9]+)/.exec(listType); - curLevel = Number(listType[2]); - if (isNaN(curLevel) || listType[0] === 'indent') { - return line; - } else if (curLevel === level) { - ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0])); - ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [ - ['start', position], - ], rep.apool); - - position++; - line++; - } else if (curLevel < level) { - return line;// back to parent - } else { - line = applyNumberList(line, level + 1);// recursive call - } - } - return line; - }; - - applyNumberList(lineNum, 1); - const cs = builder.toString(); - if (!Changeset.isIdentity(cs)) { - performDocumentApplyChangeset(cs); - } - - // 4-apply the modifications - }; - editorInfo.ace_renumberList = renumberList; - - const setLineListType = (lineNum, listType) => { - if (listType === '') { - documentAttributeManager.removeAttributeOnLine(lineNum, listAttributeName); - documentAttributeManager.removeAttributeOnLine(lineNum, 'start'); - } else { - documentAttributeManager.setAttributeOnLine(lineNum, listAttributeName, listType); - } - - // if the list has been removed, it is necessary to renumber - // starting from the *next* line because the list may have been - // separated. If it returns null, it means that the list was not cut, try - // from the current one. - if (renumberList(lineNum + 1) == null) { - renumberList(lineNum); - } - }; - - const doReturnKey = () => { - if (!(rep.selStart && rep.selEnd)) { - return; - } - - const lineNum = rep.selStart[0]; - let listType = getLineListType(lineNum); - - if (listType) { - const text = rep.lines.atIndex(lineNum).text; - listType = /([a-z]+)([0-9]+)/.exec(listType); - const type = listType[1]; - const level = Number(listType[2]); - - // detect empty list item; exclude indentation - if (text === '*' && type !== 'indent') { - // if not already on the highest level - if (level > 1) { - setLineListType(lineNum, type + (level - 1));// automatically decrease the level - } else { - setLineListType(lineNum, '');// remove the list - renumberList(lineNum + 1);// trigger renumbering of list that may be right after - } - } else if (lineNum + 1 <= rep.lines.length()) { - performDocumentReplaceSelection('\n'); - setLineListType(lineNum + 1, type + level); - } - } else { - performDocumentReplaceSelection('\n'); - handleReturnIndentation(); - } - }; - editorInfo.ace_doReturnKey = doReturnKey; - - 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) { - return false; - } - - const firstLine = rep.selStart[0]; - const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); - const mods = []; - for (let n = firstLine; n <= lastLine; n++) { - let listType = getLineListType(n); - let t = 'indent'; - let level = 0; - if (listType) { - listType = /([a-z]+)([0-9]+)/.exec(listType); - if (listType) { - t = listType[1]; - level = Number(listType[2]); - } - } - const newLevel = Math.max(0, Math.min(MAX_LIST_LEVEL, level + (isOut ? -1 : 1))); - if (level !== newLevel) { - mods.push([n, (newLevel > 0) ? t + newLevel : '']); - } - } - - for (const mod of mods) setLineListType(mod[0], mod[1]); - return true; - }; - editorInfo.ace_doIndentOutdent = doIndentOutdent; - - const doTabKey = (shiftDown) => { - if (!doIndentOutdent(shiftDown)) { - performDocumentReplaceSelection(THE_TAB); - } - }; - - const doDeleteKey = (optEvt) => { - const evt = optEvt || {}; - let handled = false; - if (rep.selStart) { - if (isCaret()) { - const lineNum = caretLine(); - const col = caretColumn(); - const lineEntry = rep.lines.atIndex(lineNum); - const lineText = lineEntry.text; - const lineMarker = lineEntry.lineMarker; - if (evt.metaKey && col > lineMarker) { - // cmd-backspace deletes to start of line (if not already at start) - performDocumentReplaceRange([lineNum, lineMarker], [lineNum, col], ''); - handled = true; - } else if (/^ +$/.exec(lineText.substring(lineMarker, col))) { - const col2 = col - lineMarker; - const tabSize = THE_TAB.length; - const toDelete = ((col2 - 1) % tabSize) + 1; - performDocumentReplaceRange([lineNum, col - toDelete], [lineNum, col], ''); - handled = true; - } - } - if (!handled) { - if (isCaret()) { - const theLine = caretLine(); - const lineEntry = rep.lines.atIndex(theLine); - if (caretColumn() <= lineEntry.lineMarker) { - // delete at beginning of line - const prevLineListType = (theLine > 0 ? getLineListType(theLine - 1) : ''); - const thisLineListType = getLineListType(theLine); - const prevLineEntry = (theLine > 0 && rep.lines.atIndex(theLine - 1)); - const prevLineBlank = (prevLineEntry && - prevLineEntry.text.length === prevLineEntry.lineMarker); - - const thisLineHasMarker = documentAttributeManager.lineHasMarker(theLine); - - if (thisLineListType) { - // this line is a list - if (prevLineBlank && !prevLineListType) { - // previous line is blank, remove it - performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); - } else { - // delistify - performDocumentReplaceRange([theLine, 0], [theLine, lineEntry.lineMarker], ''); - } - } 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], ''); - } else if (theLine > 0) { - // remove newline - performDocumentReplaceRange( - [theLine - 1, prevLineEntry.text.length], [theLine, 0], ''); - } - } else { - const docChar = caretDocChar(); - if (docChar > 0) { - if (evt.metaKey || evt.ctrlKey || evt.altKey) { - // delete as many unicode "letters or digits" in a row as possible; - // always delete one char, delete further even if that first char - // isn't actually a word char. - let deleteBackTo = docChar - 1; - while (deleteBackTo > lineEntry.lineMarker && - isWordChar(rep.alltext.charAt(deleteBackTo - 1))) { - deleteBackTo--; - } - performDocumentReplaceCharRange(deleteBackTo, docChar, ''); - } else { - // normal delete - performDocumentReplaceCharRange(docChar - 1, docChar, ''); - } - } - } - } else { - performDocumentReplaceSelection(''); - } - } - } - // if the list has been removed, it is necessary to renumber - // starting from the *next* line because the list may have been - // separated. If it returns null, it means that the list was not cut, try - // from the current one. - const line = caretLine(); - if (line !== -1 && renumberList(line + 1) == null) { - renumberList(line); - } - }; - - const isWordChar = (c) => padutils.wordCharRegex.test(c); - editorInfo.ace_isWordChar = isWordChar; - - const handleKeyEvent = (evt) => { - if (!isEditable) return; - 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. - // 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); - 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') { - // 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; - } - - 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; - - inCallStackIfNecessary('handleKeyEvent', function () { - if (type === 'keypress' || (isTypeForSpecialKey && keyCode === 13 /* return*/)) { - // in IE, special keys don't send keypress, the keydown does the action - if (!outsideKeyPress(evt)) { - evt.preventDefault(); - stopped = true; - } - } else if (evt.key === 'Dead') { - // If it's a dead key we don't want to do any Etherpad behavior. - stopped = true; - return true; - } else if (type === 'keydown') { - outsideKeyDown(evt); - } - let specialHandled = false; - if (!stopped) { - const specialHandledInHook = hooks.callAll('aceKeyEvent', { - callstack: currentCallStack, - editorInfo, - rep, - documentAttributeManager, - evt, - }); - - // if any hook returned true, set specialHandled with true - if (specialHandledInHook) { - specialHandled = specialHandledInHook.indexOf(true) !== -1; - } - - const padShortcutEnabled = parent.parent.clientVars.padShortcutEnabled; - 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.... - // Focus on the editbar. - // -- TODO: Move Focus back to previous state (we know it so we can use it) - const firstEditbarElement = parent.parent.$('#editbar') - .children('ul').first().children().first() - .children().first().children().first(); - $(this).blur(); - firstEditbarElement.focus(); - evt.preventDefault(); - } - 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 && 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]; - const apool = rep.apool; - - // TODO: support selection ranges - // TODO: Still work when authorship colors have been cleared - // TODO: i18n - // TODO: There appears to be a race condition or so. - const authorIds = new Set(); - if (alineAttrs) { - for (const op of Changeset.deserializeOps(alineAttrs)) { - const authorId = AttributeMap.fromString(op.attribs, apool).get('author'); - if (authorId) authorIds.add(authorId); - } - } - 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({ - title: 'Line Authors', - 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, - time: '4000', - }); - } - 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. - // -- - // we do deletes completely customly now: - // - allows consistent (and better) meta-delete behavior - // - normalizing and then allowing default behavior confused IE - // - probably eliminates a few minor quirks - fastIncorp(3); - evt.preventDefault(); - doDeleteKey(evt); - specialHandled = true; - } - 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); - evt.preventDefault(); - doReturnKey(); - scheduler.setTimeout(() => { - outerWin.scrollBy(-100, 0); - }, 0); - specialHandled = true; - } - if (!specialHandled && isTypeForSpecialKey && - keyCode === 27 && - padShortcutEnabled.esc) { - // prevent esc key; - // in mozilla versions 14-19 avoid reconnecting pad. - - fastIncorp(4); - evt.preventDefault(); - specialHandled = true; - - // close all gritters when the user hits escape key - parent.parent.$.gritter.removeAll(); - } - 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'}); - scheduler.setTimeout(() => { - parent.parent.$('#revisionlink').css({background: originalBackground}); - }, 1000); - /* The parent.parent part of this is BAD and I feel bad.. It may break something */ - parent.parent.pad.collabClient.sendMessage({type: 'SAVE_REVISION'}); - specialHandled = true; - } - if (!specialHandled && isTypeForSpecialKey && - // tab - keyCode === 9 && - !(evt.metaKey || evt.ctrlKey) && - padShortcutEnabled.tab) { - fastIncorp(5); - evt.preventDefault(); - doTabKey(evt.shiftKey); - specialHandled = true; - } - 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) { - doUndoRedo('redo'); - } else { - doUndoRedo('undo'); - } - specialHandled = true; - } - 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 && 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 && 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 && - // cmd-U (underline) - (evt.metaKey || evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'u' && - padShortcutEnabled.cmdU) { - fastIncorp(15); - evt.preventDefault(); - toggleAttributeOnSelection('underline'); - specialHandled = true; - } - 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 && 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 && 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 && 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 && isTypeForCmdKey && - // cmd-H (backspace) - (evt.ctrlKey) && String.fromCharCode(which).toLowerCase() === 'h' && - padShortcutEnabled.cmdH) { - fastIncorp(20); - evt.preventDefault(); - doDeleteKey(); - specialHandled = true; - } - 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) { - // This is required, browsers will try to do normal default behavior on - // page up / down and the default behavior SUCKS - evt.preventDefault(); - const oldVisibleLineRange = scroll.getVisibleLineRange(rep); - let topOffset = rep.selStart[0] - oldVisibleLineRange[0]; - if (topOffset < 0) { - topOffset = 0; - } - - const isPageDown = evt.which === 34; - const isPageUp = evt.which === 33; - - scheduler.setTimeout(() => { - // the visible lines IE 1,10 - const newVisibleLineRange = scroll.getVisibleLineRange(rep); - // total count of lines in pad IE 10 - const linesCount = rep.lines.length(); - // How many lines are in the viewport right now? - const numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0]; - - if (isPageUp && padShortcutEnabled.pageUp) { - // move to the bottom line +1 in the viewport (essentially skipping over a page) - rep.selEnd[0] -= numberOfLinesInViewport; - // move to the bottom line +1 in the viewport (essentially skipping over a page) - rep.selStart[0] -= numberOfLinesInViewport; - } - - // if we hit page down - if (isPageDown && padShortcutEnabled.pageDown) { - // If the new viewpoint position is actually further than where we are right now - if (rep.selEnd[0] >= oldVisibleLineRange[0]) { - // dont go further in the page down than what's visible IE go from 0 to 50 - // if 50 is visible on screen but dont go below that else we miss content - rep.selStart[0] = oldVisibleLineRange[1] - 1; - // dont go further in the page down than what's visible IE go from 0 to 50 - // if 50 is visible on screen but dont go below that else we miss content - rep.selEnd[0] = oldVisibleLineRange[1] - 1; - } - } - - // ensure min and max - if (rep.selEnd[0] < 0) { - rep.selEnd[0] = 0; - } - if (rep.selStart[0] < 0) { - rep.selStart[0] = 0; - } - if (rep.selEnd[0] >= linesCount) { - rep.selEnd[0] = linesCount - 1; - } - updateBrowserSelectionFromRep(); - // get the current caret selection, can't use rep. here because that only gives - // us the start position not the current - const myselection = document.getSelection(); - // get the carets selection offset in px IE 214 - let caretOffsetTop = myselection.focusNode.parentNode.offsetTop || - myselection.focusNode.offsetTop; - - // sometimes the first selection is -1 which causes problems - // (Especially with ep_page_view) - // so use focusNode.offsetTop value. - if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop; - // set the scrollY offset of the viewport on the document - scroll.setScrollY(caretOffsetTop); - }, 200); - } - } - - if (type === 'keydown') { - idleWorkTimer.atLeast(500); - } else if (type === 'keypress') { - // OPINION ASKED. What's going on here? :D - if (!specialHandled) { - idleWorkTimer.atMost(0); - } else { - idleWorkTimer.atLeast(500); - } - } else if (type === 'keyup') { - const wait = 0; - idleWorkTimer.atLeast(wait); - idleWorkTimer.atMost(wait); - } - - // Is part of multi-keystroke international character on Firefox Mac - const isFirefoxHalfCharacter = - (browser.firefox && evt.altKey && charCode === 0 && keyCode === 0); - - // Is part of multi-keystroke international character on Safari Mac - const isSafariHalfCharacter = - (browser.safari && evt.altKey && keyCode === 229); - - if (thisKeyDoesntTriggerNormalize || isFirefoxHalfCharacter || isSafariHalfCharacter) { - idleWorkTimer.atLeast(3000); // give user time to type - // if this is a keydown, e.g., the keyup shouldn't trigger a normalize - thisKeyDoesntTriggerNormalize = true; - } - - if (!specialHandled && !thisKeyDoesntTriggerNormalize && !inInternationalComposition && - type !== 'keyup') { - observeChangesAroundSelection(); - } - - if (type === 'keyup') { - thisKeyDoesntTriggerNormalize = false; - } - }); - }; - - let thisKeyDoesntTriggerNormalize = false; - - const doUndoRedo = (which) => { - // precond: normalized DOM - if (undoModule.enabled) { - let whichMethod; - if (which === 'undo') whichMethod = 'performUndo'; - if (which === 'redo') whichMethod = 'performRedo'; - if (whichMethod) { - const oldEventType = currentCallStack.editEvent.eventType; - currentCallStack.startNewEvent(which); - undoModule[whichMethod]((backset, selectionInfo) => { - if (backset) { - performDocumentApplyChangeset(backset); - } - if (selectionInfo) { - performSelectionChange( - lineAndColumnFromChar(selectionInfo.selStart), - lineAndColumnFromChar(selectionInfo.selEnd), - selectionInfo.selFocusAtStart); - } - const oldEvent = currentCallStack.startNewEvent(oldEventType, true); - return oldEvent; - }); - } - } - }; - editorInfo.ace_doUndoRedo = doUndoRedo; - - const setSelection = (selection) => { - const copyPoint = (pt) => ({ - node: pt.node, - index: pt.index, - maxIndex: pt.maxIndex, - }); - let isCollapsed; - - const pointToRangeBound = (pt) => { - const p = copyPoint(pt); - // Make sure Firefox cursor is deep enough; fixes cursor jumping when at top level, - // and also problem where cut/copy of a whole line selected with fake arrow-keys - // copies the next line too. - if (isCollapsed) { - const diveDeep = () => { - while (p.node.childNodes.length > 0) { - if (p.index === 0) { - p.node = p.node.firstChild; - p.maxIndex = nodeMaxIndex(p.node); - } else if (p.index === p.maxIndex) { - p.node = p.node.lastChild; - p.maxIndex = nodeMaxIndex(p.node); - p.index = p.maxIndex; - } else { break; } - } - }; - // now fix problem where cursor at end of text node at end of span-like element - // with background doesn't seem to show up... - if (isNodeText(p.node) && p.index === p.maxIndex) { - let n = p.node; - 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 !== 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); - p.index = 0; - diveDeep(); - } - } - // try to make sure insertion point is styled; - // also fixes other FF problems - if (!isNodeText(p.node)) { - diveDeep(); - } - } - if (isNodeText(p.node)) { - return { - container: p.node, - offset: p.index, - }; - } else { - // p.index in {0,1} - return { - container: p.node.parentNode, - offset: childIndex(p.node) + p.index, - }; - } - }; - const browserSelection = window.getSelection(); - if (browserSelection) { - browserSelection.removeAllRanges(); - if (selection) { - 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) { - // can handle "backwards"-oriented selection, shift-arrow-keys move start - // of selection - browserSelection.collapse(end.container, end.offset); - browserSelection.extend(start.container, start.offset); - } else { - const range = document.createRange(); - range.setStart(start.container, start.offset); - range.setEnd(end.container, end.offset); - browserSelection.removeAllRanges(); - browserSelection.addRange(range); - } - } - } - }; - - const updateBrowserSelectionFromRep = () => { - // requires normalized DOM! - const selStart = rep.selStart; - const selEnd = rep.selEnd; - - if (!(selStart && selEnd)) { - setSelection(null); - return; - } - - const selection = {}; - - const ss = [selStart[0], selStart[1]]; - selection.startPoint = getPointForLineAndChar(ss); - - const se = [selEnd[0], selEnd[1]]; - selection.endPoint = getPointForLineAndChar(se); - - selection.focusAtStart = !!rep.selFocusAtStart; - setSelection(selection); - }; - editorInfo.ace_updateBrowserSelectionFromRep = updateBrowserSelectionFromRep; - editorInfo.ace_focus = focus; - editorInfo.ace_importText = importText; - editorInfo.ace_importAText = importAText; - editorInfo.ace_exportText = exportText; - editorInfo.ace_editorChangedSize = editorChangedSize; - editorInfo.ace_setOnKeyPress = setOnKeyPress; - editorInfo.ace_setOnKeyDown = setOnKeyDown; - editorInfo.ace_setNotifyDirty = setNotifyDirty; - editorInfo.ace_dispose = dispose; - editorInfo.ace_setEditable = setEditable; - editorInfo.ace_execCommand = execCommand; - editorInfo.ace_replaceRange = replaceRange; - editorInfo.ace_getAuthorInfos = getAuthorInfos; - editorInfo.ace_performDocumentReplaceRange = performDocumentReplaceRange; - editorInfo.ace_performDocumentReplaceCharRange = performDocumentReplaceCharRange; - editorInfo.ace_setSelection = setSelection; - - const nodeMaxIndex = (nd) => { - if (isNodeText(nd)) return nd.nodeValue.length; - else return 1; - }; - - const getSelection = () => { - // returns null, or a structure containing startPoint and endPoint, - // each of which has node (a magicdom node), index, and maxIndex. If the node - // is a text node, maxIndex is the length of the text; else maxIndex is 1. - // index is between 0 and maxIndex, inclusive. - const browserSelection = window.getSelection(); - if (!browserSelection || browserSelection.type === 'None' || - browserSelection.rangeCount === 0) { - return null; - } - const range = browserSelection.getRangeAt(0); - - const isInBody = (n) => { - while (n && !(n.tagName && n.tagName.toLowerCase() === 'body')) { - n = n.parentNode; - } - return !!n; - }; - - const pointFromRangeBound = (container, offset) => { - if (!isInBody(container)) { - // command-click in Firefox selects whole document, HEAD and BODY! - return { - node: document.body, - index: 0, - maxIndex: 1, - }; - } - const n = container; - const childCount = n.childNodes.length; - if (isNodeText(n)) { - return { - node: n, - index: offset, - maxIndex: n.nodeValue.length, - }; - } else if (childCount === 0) { - return { - node: n, - index: 0, - maxIndex: 1, - }; - // treat point between two nodes as BEFORE the second (rather than after the first) - // if possible; this way point at end of a line block-element is treated as - // at beginning of next line - } else if (offset === childCount) { - const nd = n.childNodes.item(childCount - 1); - const max = nodeMaxIndex(nd); - return { - node: nd, - index: max, - maxIndex: max, - }; - } else { - const nd = n.childNodes.item(offset); - const max = nodeMaxIndex(nd); - return { - node: nd, - index: 0, - maxIndex: max, - }; - } - }; - const selection = { - startPoint: pointFromRangeBound(range.startContainer, range.startOffset), - endPoint: pointFromRangeBound(range.endContainer, range.endOffset), - focusAtStart: - (range.startContainer !== range.endContainer || range.startOffset !== range.endOffset) && - browserSelection.anchorNode && - browserSelection.anchorNode === range.endContainer && - browserSelection.anchorOffset === range.endOffset, - }; - - if (selection.startPoint.node.ownerDocument !== window.document) { - return null; - } - - return selection; - }; - - const childIndex = (n) => { - let idx = 0; - while (n.previousSibling) { - idx++; - n = n.previousSibling; - } - return idx; - }; - - const fixView = () => { - // calling this method repeatedly should be fast - if (getInnerWidth() === 0 || getInnerHeight() === 0) { - return; - } - - enforceEditability(); - - $(sideDiv).addClass('sidedivdelayed'); - }; - - const _teardownActions = []; - - const teardown = () => { for (const a of _teardownActions) a(); }; - - let inInternationalComposition = null; - editorInfo.ace_getInInternationalComposition = () => inInternationalComposition; - - const bindTheEventHandlers = () => { - $(document).on('keydown', handleKeyEvent); - $(document).on('keypress', handleKeyEvent); - $(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 - $(outerDoc).on('click', hideEditBarDropdowns); - - // If non-nullish, pasting on a link should be suppressed. - let suppressPasteOnLink = null; - - $(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 - // cursor. Users almost certainly do not want to paste when middle-clicking on a link, so - // tell the 'paste' event handler to suppress the paste. This is done by starting a - // short-lived timer that suppresses paste (when the target is a link) until either the - // paste event arrives or the timer fires. - // - // Why it is implemented this way: - // * Users want to be able to paste on a link via Ctrl-V, the Edit menu, or the context - // menu (https://github.com/ether/etherpad-lite/issues/2775) so we cannot simply - // suppress all paste actions when the target is a link. - // * Non-X11 systems do not paste when the user middle-clicks, so the paste suppression - // must be self-resetting. - // * On non-X11 systems, middle click should continue to open the link in a new tab. - // Suppressing the middle click here in the 'auxclick' handler (via e.preventDefault()) - // would break that behavior. - suppressPasteOnLink = scheduler.setTimeout(() => { suppressPasteOnLink = null; }, 0); - } - }); - - $(document.body).on('paste', (e) => { - if (suppressPasteOnLink != null && (e.target.a || e.target.localName === 'a')) { - scheduler.clearTimeout(suppressPasteOnLink); - suppressPasteOnLink = null; - e.preventDefault(); - return; - } - - // Call paste hook - hooks.callAll('acePaste', { - editorInfo, - rep, - documentAttributeManager, - e, - }); - }); - - // We reference document here, this is because if we don't this will expose a bug - // in Google Chrome. This bug will cause the last character on the last line to - // not fire an event when dropped into.. - $(document).on('drop', (e) => { - if (e.target.a || e.target.localName === 'a') { - e.preventDefault(); - } - - // Bug fix: when user drags some content and drop it far from its origin, we - // need to merge the changes into a single changeset. So mark origin with empty
', - wantHTML: 'empty

', - wantText: 'empty\n\n', - }, - 'indentedListsAreNotBullets': { - description: 'Indented lists are represented with tabs and without bullets', - input: '
  • indent
  • indent
', - wantHTML: '
  • indent
  • indent

', - wantText: '\tindent\n\tindent\n\n', - }, - 'lineWithMultipleSpaces': { - description: 'Multiple spaces should be collapsed', - input: 'Text with more than one space.
', - wantHTML: 'Text with more than one space.

', - wantText: 'Text with more than one space.\n\n', - }, - 'lineWithMultipleNonBreakingAndNormalSpaces': { - // XXX the HTML between "than" and "one" looks strange - description: 'non-breaking space should be preserved, but can be replaced when it', - input: 'Text with  more   than  one space.
', - wantHTML: 'Text with  more   than  one space.

', - wantText: 'Text with more than one space.\n\n', - }, - 'multiplenbsp': { - description: 'Multiple non-breaking space should be preserved', - input: '  
', - wantHTML: '  

', - wantText: ' \n\n', - }, - 'multipleNonBreakingSpaceBetweenWords': { - description: 'A normal space is always inserted before a word', - input: '  word1  word2   word3
', - wantHTML: '  word1  word2   word3

', - wantText: ' word1 word2 word3\n\n', - }, - 'nonBreakingSpacePreceededBySpaceBetweenWords': { - description: 'A non-breaking space preceded by a normal space', - input: '  word1  word2  word3
', - wantHTML: ' word1  word2  word3

', - wantText: ' word1 word2 word3\n\n', - }, - 'nonBreakingSpaceFollowededBySpaceBetweenWords': { - description: 'A non-breaking space followed by a normal space', - input: '  word1  word2  word3
', - wantHTML: '  word1  word2  word3

', - wantText: ' word1 word2 word3\n\n', - }, - 'spacesAfterNewline': { - description: 'Collapse spaces that follow a newline', - input: 'something
something
', - wantHTML: 'something
something

', - wantText: 'something\nsomething\n\n', - }, - 'spacesAfterNewlineP': { - description: 'Collapse spaces that follow a paragraph', - input: 'something

something
', - wantHTML: 'something

something

', - wantText: 'something\n\nsomething\n\n', - }, - 'spacesAtEndOfLine': { - description: 'Collapse spaces that preceed/follow a newline', - input: 'something
something
', - wantHTML: 'something
something

', - wantText: 'something\nsomething\n\n', - }, - 'spacesAtEndOfLineP': { - description: 'Collapse spaces that preceed/follow a paragraph', - input: 'something

something
', - wantHTML: 'something

something

', - wantText: 'something\n\nsomething\n\n', - }, - 'nonBreakingSpacesAfterNewlines': { - description: 'Don\'t collapse non-breaking spaces that follow a newline', - input: 'something
   something
', - wantHTML: 'something
   something

', - wantText: 'something\n something\n\n', - }, - 'nonBreakingSpacesAfterNewlinesP': { - description: 'Don\'t collapse non-breaking spaces that follow a paragraph', - input: 'something

   something
', - wantHTML: 'something

   something

', - wantText: 'something\n\n something\n\n', - }, - 'collapseSpacesInsideElements': { - description: 'Preserve only one space when multiple are present', - input: 'Need more space s !
', - wantHTML: 'Need more space s !

', - wantText: 'Need more space s !\n\n', - }, - 'collapseSpacesAcrossNewlines': { - description: 'Newlines and multiple spaces across newlines should be collapsed', - input: ` + 'malformed': { + input: '
  • wtf', + wantHTML: 'wtf

    ', + wantText: 'wtf\n\n', + disabled: true, + }, + 'nonelistiteminlist #3620': { + input: '
      test
    • FOO
    ', + wantHTML: '
      test
    • FOO

    ', + wantText: '\ttest\n\t* FOO\n\n', + disabled: true, + }, + 'whitespaceinlist #3620': { + input: '
    • FOO
    ', + wantHTML: '
    • FOO

    ', + wantText: '\t* FOO\n\n', + }, + 'prefixcorrectlinenumber': { + input: '
    1. should be 1
    2. should be 2
    ', + wantHTML: '
    1. should be 1
    2. should be 2

    ', + wantText: '\t1. should be 1\n\t2. should be 2\n\n', + }, + 'prefixcorrectlinenumbernested': { + input: '
    1. should be 1
      1. foo
    2. should be 2
    ', + wantHTML: '
    1. should be 1
      1. foo
    2. should be 2

    ', + wantText: '\t1. should be 1\n\t\t1.1. foo\n\t2. should be 2\n\n', + }, + /* + "prefixcorrectlinenumber when introduced none list item - currently not supported see #3450": { + input: '
    1. should be 1
    2. test
    3. should be 2
    ', + wantHTML: '
    1. should be 1
    2. test
    3. should be 2

    ', + wantText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n', + } + , + "newlinesshouldntresetlinenumber #2194": { + input: '
    1. should be 1
    2. test
    3. should be 2
    ', + wantHTML: '
    1. should be 1
    2. test
    3. should be 2

    ', + wantText: '\t1. should be 1\n\ttest\n\t2. should be 2\n\n', + } + */ + 'ignoreAnyTagsOutsideBody': { + description: 'Content outside body should be ignored', + input: 'titleempty
    ', + wantHTML: 'empty

    ', + wantText: 'empty\n\n', + }, + 'indentedListsAreNotBullets': { + description: 'Indented lists are represented with tabs and without bullets', + input: '
    • indent
    • indent
    ', + wantHTML: '
    • indent
    • indent

    ', + wantText: '\tindent\n\tindent\n\n', + }, + 'lineWithMultipleSpaces': { + description: 'Multiple spaces should be collapsed', + input: 'Text with more than one space.
    ', + wantHTML: 'Text with more than one space.

    ', + wantText: 'Text with more than one space.\n\n', + }, + 'lineWithMultipleNonBreakingAndNormalSpaces': { + // XXX the HTML between "than" and "one" looks strange + description: 'non-breaking space should be preserved, but can be replaced when it', + input: 'Text with  more   than  one space.
    ', + wantHTML: 'Text with  more   than  one space.

    ', + wantText: 'Text with more than one space.\n\n', + }, + 'multiplenbsp': { + description: 'Multiple non-breaking space should be preserved', + input: '  
    ', + wantHTML: '  

    ', + wantText: ' \n\n', + }, + 'multipleNonBreakingSpaceBetweenWords': { + description: 'A normal space is always inserted before a word', + input: '  word1  word2   word3
    ', + wantHTML: '  word1  word2   word3

    ', + wantText: ' word1 word2 word3\n\n', + }, + 'nonBreakingSpacePreceededBySpaceBetweenWords': { + description: 'A non-breaking space preceded by a normal space', + input: '  word1  word2  word3
    ', + wantHTML: ' word1  word2  word3

    ', + wantText: ' word1 word2 word3\n\n', + }, + 'nonBreakingSpaceFollowededBySpaceBetweenWords': { + description: 'A non-breaking space followed by a normal space', + input: '  word1  word2  word3
    ', + wantHTML: '  word1  word2  word3

    ', + wantText: ' word1 word2 word3\n\n', + }, + 'spacesAfterNewline': { + description: 'Collapse spaces that follow a newline', + input: 'something
    something
    ', + wantHTML: 'something
    something

    ', + wantText: 'something\nsomething\n\n', + }, + 'spacesAfterNewlineP': { + description: 'Collapse spaces that follow a paragraph', + input: 'something

    something
    ', + wantHTML: 'something

    something

    ', + wantText: 'something\n\nsomething\n\n', + }, + 'spacesAtEndOfLine': { + description: 'Collapse spaces that preceed/follow a newline', + input: 'something
    something
    ', + wantHTML: 'something
    something

    ', + wantText: 'something\nsomething\n\n', + }, + 'spacesAtEndOfLineP': { + description: 'Collapse spaces that preceed/follow a paragraph', + input: 'something

    something
    ', + wantHTML: 'something

    something

    ', + wantText: 'something\n\nsomething\n\n', + }, + 'nonBreakingSpacesAfterNewlines': { + description: 'Don\'t collapse non-breaking spaces that follow a newline', + input: 'something
       something
    ', + wantHTML: 'something
       something

    ', + wantText: 'something\n something\n\n', + }, + 'nonBreakingSpacesAfterNewlinesP': { + description: 'Don\'t collapse non-breaking spaces that follow a paragraph', + input: 'something

       something
    ', + wantHTML: 'something

       something

    ', + wantText: 'something\n\n something\n\n', + }, + 'collapseSpacesInsideElements': { + description: 'Preserve only one space when multiple are present', + input: 'Need more space s !
    ', + wantHTML: 'Need more space s !

    ', + wantText: 'Need more space s !\n\n', + }, + 'collapseSpacesAcrossNewlines': { + description: 'Newlines and multiple spaces across newlines should be collapsed', + input: ` Need more space s !
    `, - wantHTML: 'Need more space s !

    ', - wantText: 'Need more space s !\n\n', - }, - 'multipleNewLinesAtBeginning': { - description: 'Multiple new lines and paragraphs at the beginning should be preserved', - input: '

    first line

    second line
    ', - wantHTML: '



    first line

    second line

    ', - wantText: '\n\n\n\nfirst line\n\nsecond line\n\n', - }, - 'multiLineParagraph': { - description: 'A paragraph with multiple lines should not loose spaces when lines are combined', - input: ` + wantHTML: 'Need more space s !

    ', + wantText: 'Need more space s !\n\n', + }, + 'multipleNewLinesAtBeginning': { + description: 'Multiple new lines and paragraphs at the beginning should be preserved', + input: '

    first line

    second line
    ', + wantHTML: '



    first line

    second line

    ', + wantText: '\n\n\n\nfirst line\n\nsecond line\n\n', + }, + 'multiLineParagraph': { + description: 'A paragraph with multiple lines should not loose spaces when lines are combined', + input: `

    а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь

    `, - wantHTML: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь

    ', - wantText: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь\n\n', - }, - 'multiLineParagraphWithPre': { - // XXX why is there   before "in"? - description: 'lines in preformatted text should be kept intact', - input: ` + wantHTML: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь

    ', + wantText: 'а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь\n\n', + }, + 'multiLineParagraphWithPre': { + // XXX why is there   before "in"? + description: 'lines in preformatted text should be kept intact', + input: `

    а б в г ґ д е є ж з и і ї й к л м н о

    multiple
        lines
    @@ -188,97 +184,88 @@ const testImports = {
     

    п р с т у ф х ц ч ш щ ю я ь

    `, - wantHTML: 'а б в г ґ д е є ж з и і ї й к л м н о
    multiple
       lines
     in
          pre

    п р с т у ф х ц ч ш щ ю я ь

    ', - wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n', - }, - 'preIntroducesASpace': { - description: 'pre should be on a new line not preceded by a space', - input: `

    + wantHTML: 'а б в г ґ д е є ж з и і ї й к л м н о
    multiple
       lines
     in
          pre

    п р с т у ф х ц ч ш щ ю я ь

    ', + wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n', + }, + 'preIntroducesASpace': { + description: 'pre should be on a new line not preceded by a space', + input: `

    1

    preline
     

    `, - wantHTML: '1
    preline


    ', - wantText: '1\npreline\n\n\n', - }, - 'dontDeleteSpaceInsideElements': { - description: 'Preserve spaces inside elements', - input: 'Need more space s !
    ', - wantHTML: 'Need more space s !

    ', - wantText: 'Need more space s !\n\n', - }, - 'dontDeleteSpaceOutsideElements': { - description: 'Preserve spaces outside elements', - input: 'Need more space s !
    ', - wantHTML: 'Need more space s !

    ', - wantText: 'Need more space s !\n\n', - }, - 'dontDeleteSpaceAtEndOfElement': { - description: 'Preserve spaces at the end of an element', - input: 'Need more space s !
    ', - wantHTML: 'Need more space s !

    ', - wantText: 'Need more space s !\n\n', - }, - 'dontDeleteSpaceAtBeginOfElements': { - description: 'Preserve spaces at the start of an element', - input: 'Need more space s !
    ', - wantHTML: 'Need more space s !

    ', - wantText: 'Need more space s !\n\n', - }, + wantHTML: '1
    preline


    ', + wantText: '1\npreline\n\n\n', + }, + 'dontDeleteSpaceInsideElements': { + description: 'Preserve spaces inside elements', + input: 'Need more space s !
    ', + wantHTML: 'Need more space s !

    ', + wantText: 'Need more space s !\n\n', + }, + 'dontDeleteSpaceOutsideElements': { + description: 'Preserve spaces outside elements', + input: 'Need more space s !
    ', + wantHTML: 'Need more space s !

    ', + wantText: 'Need more space s !\n\n', + }, + 'dontDeleteSpaceAtEndOfElement': { + description: 'Preserve spaces at the end of an element', + input: 'Need more space s !
    ', + wantHTML: 'Need more space s !

    ', + wantText: 'Need more space s !\n\n', + }, + 'dontDeleteSpaceAtBeginOfElements': { + description: 'Preserve spaces at the start of an element', + input: 'Need more space s !
    ', + wantHTML: 'Need more space s !

    ', + wantText: 'Need more space s !\n\n', + }, }; - describe(__filename, function () { - this.timeout(1000); - - before(async function () { agent = await common.init(); }); - - Object.keys(testImports).forEach((testName) => { - describe(testName, function () { - const testPadId = makeid(); - const test = testImports[testName]; - if (test.disabled) { - return xit(`DISABLED: ${testName}`, function (done) { - done(); + this.timeout(1000); + before(async function () { agent = await common.init(); }); + Object.keys(testImports).forEach((testName) => { + describe(testName, function () { + const testPadId = makeid(); + const test = testImports[testName]; + if (test.disabled) { + return xit(`DISABLED: ${testName}`, function (done) { + done(); + }); + } + it('createPad', async function () { + const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + }); + it('setHTML', async function () { + const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` + + `&html=${encodeURIComponent(test.input)}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + }); + it('getHTML', async function () { + const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.html, test.wantHTML); + }); + it('getText', async function () { + const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.data.text, test.wantText); + }); }); - } - - it('createPad', async function () { - const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) - .expect(200) - .expect('Content-Type', /json/); - assert.equal(res.body.code, 0); - }); - - it('setHTML', async function () { - const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` + - `&html=${encodeURIComponent(test.input)}`) - .expect(200) - .expect('Content-Type', /json/); - assert.equal(res.body.code, 0); - }); - - it('getHTML', async function () { - const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) - .expect(200) - .expect('Content-Type', /json/); - assert.equal(res.body.data.html, test.wantHTML); - }); - - it('getText', async function () { - const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) - .expect(200) - .expect('Content-Type', /json/); - assert.equal(res.body.data.text, test.wantText); - }); }); - }); }); - function makeid() { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - for (let i = 0; i < 5; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 5; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; } diff --git a/src/tests/backend/specs/api/test.doc b/src/tests/backend/specs/api/test.doc index c92d46f2e74..e69de29bb2d 100644 Binary files a/src/tests/backend/specs/api/test.doc and b/src/tests/backend/specs/api/test.doc differ diff --git a/src/tests/backend/specs/api/test.docx b/src/tests/backend/specs/api/test.docx index 422bb4f037e..e69de29bb2d 100644 Binary files a/src/tests/backend/specs/api/test.docx and b/src/tests/backend/specs/api/test.docx differ diff --git a/src/tests/backend/specs/api/test.odt b/src/tests/backend/specs/api/test.odt index 8ab62a61271..e69de29bb2d 100644 Binary files a/src/tests/backend/specs/api/test.odt and b/src/tests/backend/specs/api/test.odt differ diff --git a/src/tests/backend/specs/api/test.pdf b/src/tests/backend/specs/api/test.pdf index 774c2ea70c5..e69de29bb2d 100644 Binary files a/src/tests/backend/specs/api/test.pdf and b/src/tests/backend/specs/api/test.pdf differ diff --git a/src/tests/backend/specs/caching_middleware.js b/src/tests/backend/specs/caching_middleware.js index ebfd65d9ff0..ca2377d9540 100644 --- a/src/tests/backend/specs/caching_middleware.js +++ b/src/tests/backend/specs/caching_middleware.js @@ -1,18 +1,10 @@ +import * as common from "../common.js"; +import assertLegacy from "../assert-legacy.js"; +import queryString from "querystring"; +import * as settings from "../../../node/utils/Settings.js"; 'use strict'; - -/** - * caching_middleware is responsible for serving everything under path `/javascripts/` - * That includes packages as defined in `src/node/utils/tar.json` and probably also plugin code - * - */ - -const common = require('../common'); -const assert = require('../assert-legacy').strict; -const queryString = require('querystring'); -const settings = require('../../../node/utils/Settings'); - +const assert = assertLegacy.strict; let agent; - /** * Hack! Returns true if the resource is not plaintext * The file should start with the callback method, so we need the @@ -23,95 +15,87 @@ let agent; * @returns {boolean} if it is plaintext */ const isPlaintextResponse = (fileContent, resource) => { - // callback=require.define&v=1234 - const query = (new URL(resource, 'http://localhost')).search.slice(1); - // require.define - const jsonp = queryString.parse(query).callback; - - // returns true if the first letters in fileContent equal the content of `jsonp` - return fileContent.substring(0, jsonp.length) === jsonp; + // callback=require.define&v=1234 + const query = (new URL(resource, 'http://localhost')).search.slice(1); + // require.define + const jsonp = queryString.parse(query).callback; + // returns true if the first letters in fileContent equal the content of `jsonp` + return fileContent.substring(0, jsonp.length) === jsonp; }; - /** * A hack to disable `superagent`'s auto unzip functionality * * @param {Request} request */ const disableAutoDeflate = (request) => { - request._shouldUnzip = () => false; + request._shouldUnzip = () => false; }; - describe(__filename, function () { - const backups = {}; - const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved - const packages = [ - '/javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define', - '/javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define', - '/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define', - '/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define', - ]; - - before(async function () { - agent = await common.init(); - backups.settings = {}; - backups.settings.minify = settings.minify; - }); - after(async function () { - Object.assign(settings, backups.settings); - }); - - for (const minify of [false, true]) { - context(`when minify is ${minify}`, function () { - before(async function () { - settings.minify = minify; - }); - - describe('gets packages uncompressed without Accept-Encoding gzip', function () { - for (const resource of packages) { - it(resource, async function () { - await agent.get(resource) - .set('Accept-Encoding', fantasyEncoding) - .use(disableAutoDeflate) - .expect(200) - .expect('Content-Type', /application\/javascript/) - .expect((res) => { - assert.equal(res.header['content-encoding'], undefined); - assert(isPlaintextResponse(res.text, resource)); - }); - }); - } - }); - - describe('gets packages compressed with Accept-Encoding gzip', function () { - for (const resource of packages) { - it(resource, async function () { - await agent.get(resource) - .set('Accept-Encoding', 'gzip') - .use(disableAutoDeflate) - .expect(200) - .expect('Content-Type', /application\/javascript/) - .expect('Content-Encoding', 'gzip') - .expect((res) => { - assert(!isPlaintextResponse(res.text, resource)); - }); - }); - } - }); - - it('does not cache content-encoding headers', async function () { - await agent.get(packages[0]) - .set('Accept-Encoding', fantasyEncoding) - .expect(200) - .expect((res) => assert.equal(res.header['content-encoding'], undefined)); - await agent.get(packages[0]) - .set('Accept-Encoding', 'gzip') - .expect(200) - .expect('Content-Encoding', 'gzip'); - await agent.get(packages[0]) - .set('Accept-Encoding', fantasyEncoding) - .expect(200) - .expect((res) => assert.equal(res.header['content-encoding'], undefined)); - }); + const backups = {}; + const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved + const packages = [ + '/javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define', + '/javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define', + '/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define', + '/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define', + ]; + before(async function () { + agent = await common.init(); + backups.settings = {}; + backups.settings.minify = settings.minify; + }); + after(async function () { + Object.assign(settings, backups.settings); }); - } + for (const minify of [false, true]) { + context(`when minify is ${minify}`, function () { + before(async function () { + settings.minify = minify; + }); + describe('gets packages uncompressed without Accept-Encoding gzip', function () { + for (const resource of packages) { + it(resource, async function () { + await agent.get(resource) + .set('Accept-Encoding', fantasyEncoding) + .use(disableAutoDeflate) + .expect(200) + .expect('Content-Type', /application\/javascript/) + .expect((res) => { + assert.equal(res.header['content-encoding'], undefined); + assert(isPlaintextResponse(res.text, resource)); + }); + }); + } + }); + describe('gets packages compressed with Accept-Encoding gzip', function () { + for (const resource of packages) { + it(resource, async function () { + await agent.get(resource) + .set('Accept-Encoding', 'gzip') + .use(disableAutoDeflate) + .expect(200) + .expect('Content-Type', /application\/javascript/) + .expect('Content-Encoding', 'gzip') + .expect((res) => { + assert(!isPlaintextResponse(res.text, resource)); + }); + }); + } + }); + it('does not cache content-encoding headers', async function () { + await agent.get(packages[0]) + .set('Accept-Encoding', fantasyEncoding) + .expect(200) + .expect((res) => assert.equal(res.header['content-encoding'], undefined)); + await agent.get(packages[0]) + .set('Accept-Encoding', 'gzip') + .expect(200) + .expect('Content-Encoding', 'gzip'); + await agent.get(packages[0]) + .set('Accept-Encoding', fantasyEncoding) + .expect(200) + .expect((res) => assert.equal(res.header['content-encoding'], undefined)); + }); + }); + } }); diff --git a/src/tests/backend/specs/chat.js b/src/tests/backend/specs/chat.js index aefa64183f6..1bf0c1114cb 100644 --- a/src/tests/backend/specs/chat.js +++ b/src/tests/backend/specs/chat.js @@ -1,160 +1,153 @@ +import ChatMessage from "../../../static/js/ChatMessage.js"; +import { Pad } from "../../../node/db/Pad.js"; +import assert$0 from "assert"; +import * as common from "../common.js"; +import * as padManager from "../../../node/db/PadManager.js"; +import * as pluginDefs from "../../../static/js/pluginfw/plugin_defs.js"; '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 assert = assert$0.strict; 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(); - }, + 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, - }); + socket.send({ + type: 'COLLABROOM', + component: 'pad', + data, + }); }; - -const sendChat = (socket, message) => sendMessage(socket, {type: 'CHAT_MESSAGE', message}); - +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}), - ]); + const padId = 'testChatPad'; + const hooksBackup = {}; + before(async function () { + for (const [name, defs] of Object.entries(pluginDefs.hooks)) { + if (defs == null) + continue; + hooksBackup[name] = defs; + } }); - - 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}), - ]); + 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(); + } }); - - it('padId', async function () { - await Promise.all([ - checkHook('chatNewMessage', (context) => { - assert.equal(context.padId, padId); - }), - sendChat(socket, {text: this.test.title}), - ]); + after(async function () { + Object.assign(pluginDefs.hooks, hooksBackup); + for (const name of Object.keys(pluginDefs.hooks)) { + if (hooksBackup[name] == null) + delete pluginDefs.hooks[name]; + } }); - - 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}), - ]); + 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 }), + ]); + }); }); - }); }); diff --git a/src/tests/backend/specs/contentcollector.js b/src/tests/backend/specs/contentcollector.js index a4696307edf..5e4ad92d946 100644 --- a/src/tests/backend/specs/contentcollector.js +++ b/src/tests/backend/specs/contentcollector.js @@ -1,284 +1,273 @@ +import AttributePool from "../../../static/js/AttributePool.js"; +import * as Changeset from "../../../static/js/Changeset.js"; +import assert$0 from "assert"; +import * as attributes from "../../../static/js/attributes.js"; +import * as contentcollector from "../../../static/js/contentcollector.js"; +import * as jsdom from "jsdom"; 'use strict'; - -/* - * While importexport tests target the `setHTML` API endpoint, which is nearly identical to what - * happens when a user manually imports a document via the UI, the contentcollector tests here don't - * use rehype to process the document. Rehype removes spaces and newĺines were applicable, so the - * expected results here can differ from importexport.js. - * - * If you add tests here, please also add them to importexport.js - */ - -const AttributePool = require('../../../static/js/AttributePool'); -const Changeset = require('../../../static/js/Changeset'); -const assert = require('assert').strict; -const attributes = require('../../../static/js/attributes'); -const contentcollector = require('../../../static/js/contentcollector'); -const jsdom = require('jsdom'); - +const assert = assert$0.strict; // All test case `wantAlines` values must only refer to attributes in this list so that the // attribute numbers do not change due to changes in pool insertion order. const knownAttribs = [ - ['insertorder', 'first'], - ['italic', 'true'], - ['list', 'bullet1'], - ['list', 'bullet2'], - ['list', 'number1'], - ['list', 'number2'], - ['lmkr', '1'], - ['start', '1'], - ['start', '2'], + ['insertorder', 'first'], + ['italic', 'true'], + ['list', 'bullet1'], + ['list', 'bullet2'], + ['list', 'number1'], + ['list', 'number2'], + ['lmkr', '1'], + ['start', '1'], + ['start', '2'], ]; - const testCases = [ - { - description: 'Simple', - html: '

    foo

    ', - wantAlines: ['+3'], - wantText: ['foo'], - }, - { - description: 'Line starts with asterisk', - html: '

    *foo

    ', - wantAlines: ['+4'], - wantText: ['*foo'], - }, - { - description: 'Complex nested Li', - html: '
    1. one
      1. 1.1
    2. two
    ', - wantAlines: [ - '*0*4*6*7+1+3', - '*0*5*6*8+1+3', - '*0*4*6*8+1+3', - ], - wantText: [ - '*one', '*1.1', '*two', - ], - }, - { - description: 'Complex list of different types', - html: '
    • one
    • two
    • 0
    • 1
    • 2
      • 3
      • 4
    1. item
      1. item1
      2. item2
    ', - wantAlines: [ - '*0*2*6+1+3', - '*0*2*6+1+3', - '*0*2*6+1+1', - '*0*2*6+1+1', - '*0*2*6+1+1', - '*0*3*6+1+1', - '*0*3*6+1+1', - '*0*4*6*7+1+4', - '*0*5*6*8+1+5', - '*0*5*6*8+1+5', - ], - wantText: [ - '*one', - '*two', - '*0', - '*1', - '*2', - '*3', - '*4', - '*item', - '*item1', - '*item2', - ], - }, - { - description: 'Tests if uls properly get attributes', - html: '
    • a
    • b
    div

    foo

    ', - wantAlines: [ - '*0*2*6+1+1', - '*0*2*6+1+1', - '+3', - '+3', - ], - wantText: ['*a', '*b', 'div', 'foo'], - }, - { - description: 'Tests if indented uls properly get attributes', - html: '
    • a
      • b
    • a

    foo

    ', - wantAlines: [ - '*0*2*6+1+1', - '*0*3*6+1+1', - '*0*2*6+1+1', - '+3', - ], - wantText: ['*a', '*b', '*a', 'foo'], - }, - { - description: 'Tests if ols properly get line numbers when in a normal OL', - html: '
    1. a
    2. b
    3. c

    test

    ', - wantAlines: [ - '*0*4*6*7+1+1', - '*0*4*6*7+1+1', - '*0*4*6*7+1+1', - '+4', - ], - wantText: ['*a', '*b', '*c', 'test'], - noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', - }, - { - description: 'A single completely empty line break within an ol should reset count if OL is closed off..', - html: '
    1. should be 1

    hello

    1. should be 1
    2. should be 2

    ', - wantAlines: [ - '*0*4*6*7+1+b', - '+5', - '*0*4*6*8+1+b', - '*0*4*6*8+1+b', - '', - ], - wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''], - noteToSelf: "Shouldn't include attribute marker in the

    line", - }, - { - description: 'A single

    should create a new line', - html: '

    ', - wantAlines: ['', ''], - wantText: ['', ''], - noteToSelf: '

    should create a line break but not break numbering', - }, - { - description: 'Tests if ols properly get line numbers when in a normal OL #2', - html: 'a
    1. b
      1. c
    notlist

    foo

    ', - wantAlines: [ - '+1', - '*0*4*6*7+1+1', - '*0*5*6*8+1+1', - '+7', - '+3', - ], - wantText: ['a', '*b', '*c', 'notlist', 'foo'], - noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', - }, - { - description: 'First item being an UL then subsequent being OL will fail', - html: '
    • a
      1. b
      2. c
    ', - wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'], - wantText: ['a', '*b', '*c'], - noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', - disabled: true, - }, - { - description: 'A single completely empty line break within an ol should NOT reset count', - html: '
    1. should be 1
    2. should be 2
    3. should be 3

    ', - wantAlines: [], - wantText: ['*should be 1', '*should be 2', '*should be 3'], - noteToSelf: "

    should create a line break but not break numbering -- This is what I can't get working!", - disabled: true, - }, - { - description: 'Content outside body should be ignored', - html: 'titleempty
    ', - wantAlines: ['+5'], - wantText: ['empty'], - }, - { - description: 'Multiple spaces should be preserved', - html: 'Text with more than one space.
    ', - wantAlines: ['+10'], - wantText: ['Text with more than one space.'], - }, - { - description: 'non-breaking and normal space should be preserved', - html: 'Text with  more   than  one space.
    ', - wantAlines: ['+10'], - wantText: ['Text with more than one space.'], - }, - { - description: 'Multiple nbsp should be preserved', - html: '  
    ', - wantAlines: ['+2'], - wantText: [' '], - }, - { - description: 'Multiple nbsp between words ', - html: '  word1  word2   word3
    ', - wantAlines: ['+m'], - wantText: [' word1 word2 word3'], - }, - { - description: 'A non-breaking space preceded by a normal space', - html: '  word1  word2  word3
    ', - wantAlines: ['+l'], - wantText: [' word1 word2 word3'], - }, - { - description: 'A non-breaking space followed by a normal space', - html: '  word1  word2  word3
    ', - wantAlines: ['+l'], - wantText: [' word1 word2 word3'], - }, - { - description: 'Don\'t collapse spaces that follow a newline', - html: 'something
    something
    ', - wantAlines: ['+9', '+m'], - wantText: ['something', ' something'], - }, - { - description: 'Don\'t collapse spaces that follow a empty paragraph', - html: 'something

    something
    ', - wantAlines: ['+9', '', '+m'], - wantText: ['something', '', ' something'], - }, - { - description: 'Don\'t collapse spaces that preceed/follow a newline', - html: 'something
    something
    ', - wantAlines: ['+l', '+m'], - wantText: ['something ', ' something'], - }, - { - description: 'Don\'t collapse spaces that preceed/follow a empty paragraph', - html: 'something

    something
    ', - wantAlines: ['+l', '', '+m'], - wantText: ['something ', '', ' something'], - }, - { - description: 'Don\'t collapse non-breaking spaces that follow a newline', - html: 'something
       something
    ', - wantAlines: ['+9', '+c'], - wantText: ['something', ' something'], - }, - { - description: 'Don\'t collapse non-breaking spaces that follow a paragraph', - html: 'something

       something
    ', - wantAlines: ['+9', '', '+c'], - wantText: ['something', '', ' something'], - }, - { - description: 'Preserve all spaces when multiple are present', - html: 'Need more space s !
    ', - wantAlines: ['+h*1+4+2'], - wantText: ['Need more space s !'], - }, - { - description: 'Newlines and multiple spaces across newlines should be preserved', - html: ` + { + description: 'Simple', + html: '

    foo

    ', + wantAlines: ['+3'], + wantText: ['foo'], + }, + { + description: 'Line starts with asterisk', + html: '

    *foo

    ', + wantAlines: ['+4'], + wantText: ['*foo'], + }, + { + description: 'Complex nested Li', + html: '
    1. one
      1. 1.1
    2. two
    ', + wantAlines: [ + '*0*4*6*7+1+3', + '*0*5*6*8+1+3', + '*0*4*6*8+1+3', + ], + wantText: [ + '*one', '*1.1', '*two', + ], + }, + { + description: 'Complex list of different types', + html: '
    • one
    • two
    • 0
    • 1
    • 2
      • 3
      • 4
    1. item
      1. item1
      2. item2
    ', + wantAlines: [ + '*0*2*6+1+3', + '*0*2*6+1+3', + '*0*2*6+1+1', + '*0*2*6+1+1', + '*0*2*6+1+1', + '*0*3*6+1+1', + '*0*3*6+1+1', + '*0*4*6*7+1+4', + '*0*5*6*8+1+5', + '*0*5*6*8+1+5', + ], + wantText: [ + '*one', + '*two', + '*0', + '*1', + '*2', + '*3', + '*4', + '*item', + '*item1', + '*item2', + ], + }, + { + description: 'Tests if uls properly get attributes', + html: '
    • a
    • b
    div

    foo

    ', + wantAlines: [ + '*0*2*6+1+1', + '*0*2*6+1+1', + '+3', + '+3', + ], + wantText: ['*a', '*b', 'div', 'foo'], + }, + { + description: 'Tests if indented uls properly get attributes', + html: '
    • a
      • b
    • a

    foo

    ', + wantAlines: [ + '*0*2*6+1+1', + '*0*3*6+1+1', + '*0*2*6+1+1', + '+3', + ], + wantText: ['*a', '*b', '*a', 'foo'], + }, + { + description: 'Tests if ols properly get line numbers when in a normal OL', + html: '
    1. a
    2. b
    3. c

    test

    ', + wantAlines: [ + '*0*4*6*7+1+1', + '*0*4*6*7+1+1', + '*0*4*6*7+1+1', + '+4', + ], + wantText: ['*a', '*b', '*c', 'test'], + noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', + }, + { + description: 'A single completely empty line break within an ol should reset count if OL is closed off..', + html: '
    1. should be 1

    hello

    1. should be 1
    2. should be 2

    ', + wantAlines: [ + '*0*4*6*7+1+b', + '+5', + '*0*4*6*8+1+b', + '*0*4*6*8+1+b', + '', + ], + wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''], + noteToSelf: "Shouldn't include attribute marker in the

    line", + }, + { + description: 'A single

    should create a new line', + html: '

    ', + wantAlines: ['', ''], + wantText: ['', ''], + noteToSelf: '

    should create a line break but not break numbering', + }, + { + description: 'Tests if ols properly get line numbers when in a normal OL #2', + html: 'a
    1. b
      1. c
    notlist

    foo

    ', + wantAlines: [ + '+1', + '*0*4*6*7+1+1', + '*0*5*6*8+1+1', + '+7', + '+3', + ], + wantText: ['a', '*b', '*c', 'notlist', 'foo'], + noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', + }, + { + description: 'First item being an UL then subsequent being OL will fail', + html: '
    • a
      1. b
      2. c
    ', + wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'], + wantText: ['a', '*b', '*c'], + noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?', + disabled: true, + }, + { + description: 'A single completely empty line break within an ol should NOT reset count', + html: '
    1. should be 1
    2. should be 2
    3. should be 3

    ', + wantAlines: [], + wantText: ['*should be 1', '*should be 2', '*should be 3'], + noteToSelf: "

    should create a line break but not break numbering -- This is what I can't get working!", + disabled: true, + }, + { + description: 'Content outside body should be ignored', + html: 'titleempty
    ', + wantAlines: ['+5'], + wantText: ['empty'], + }, + { + description: 'Multiple spaces should be preserved', + html: 'Text with more than one space.
    ', + wantAlines: ['+10'], + wantText: ['Text with more than one space.'], + }, + { + description: 'non-breaking and normal space should be preserved', + html: 'Text with  more   than  one space.
    ', + wantAlines: ['+10'], + wantText: ['Text with more than one space.'], + }, + { + description: 'Multiple nbsp should be preserved', + html: '  
    ', + wantAlines: ['+2'], + wantText: [' '], + }, + { + description: 'Multiple nbsp between words ', + html: '  word1  word2   word3
    ', + wantAlines: ['+m'], + wantText: [' word1 word2 word3'], + }, + { + description: 'A non-breaking space preceded by a normal space', + html: '  word1  word2  word3
    ', + wantAlines: ['+l'], + wantText: [' word1 word2 word3'], + }, + { + description: 'A non-breaking space followed by a normal space', + html: '  word1  word2  word3
    ', + wantAlines: ['+l'], + wantText: [' word1 word2 word3'], + }, + { + description: 'Don\'t collapse spaces that follow a newline', + html: 'something
    something
    ', + wantAlines: ['+9', '+m'], + wantText: ['something', ' something'], + }, + { + description: 'Don\'t collapse spaces that follow a empty paragraph', + html: 'something

    something
    ', + wantAlines: ['+9', '', '+m'], + wantText: ['something', '', ' something'], + }, + { + description: 'Don\'t collapse spaces that preceed/follow a newline', + html: 'something
    something
    ', + wantAlines: ['+l', '+m'], + wantText: ['something ', ' something'], + }, + { + description: 'Don\'t collapse spaces that preceed/follow a empty paragraph', + html: 'something

    something
    ', + wantAlines: ['+l', '', '+m'], + wantText: ['something ', '', ' something'], + }, + { + description: 'Don\'t collapse non-breaking spaces that follow a newline', + html: 'something
       something
    ', + wantAlines: ['+9', '+c'], + wantText: ['something', ' something'], + }, + { + description: 'Don\'t collapse non-breaking spaces that follow a paragraph', + html: 'something

       something
    ', + wantAlines: ['+9', '', '+c'], + wantText: ['something', '', ' something'], + }, + { + description: 'Preserve all spaces when multiple are present', + html: 'Need more space s !
    ', + wantAlines: ['+h*1+4+2'], + wantText: ['Need more space s !'], + }, + { + description: 'Newlines and multiple spaces across newlines should be preserved', + html: ` Need more space s !
    `, - wantAlines: ['+19*1+4+b'], - wantText: ['Need more space s !'], - }, - { - description: 'Multiple new lines at the beginning should be preserved', - html: '

    first line

    second line
    ', - wantAlines: ['', '', '', '', '+a', '', '+b'], - wantText: ['', '', '', '', 'first line', '', 'second line'], - }, - { - description: 'A paragraph with multiple lines should not loose spaces when lines are combined', - html: `

    + wantAlines: ['+19*1+4+b'], + wantText: ['Need more space s !'], + }, + { + description: 'Multiple new lines at the beginning should be preserved', + html: '

    first line

    second line
    ', + wantAlines: ['', '', '', '', '+a', '', '+b'], + wantText: ['', '', '', '', 'first line', '', 'second line'], + }, + { + description: 'A paragraph with multiple lines should not loose spaces when lines are combined', + html: `

    а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь

    `, - wantAlines: ['+1t'], - wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'], - }, - { - description: 'lines in preformatted text should be kept intact', - html: `

    + wantAlines: ['+1t'], + wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'], + }, + { + description: 'lines in preformatted text should be kept intact', + html: `

    а б в г ґ д е є ж з и і ї й к л м н о

    multiple
     lines
     in
    @@ -286,101 +275,98 @@ pre
     

    п р с т у ф х ц ч ш щ ю я ь

    `, - wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'], - wantText: [ - 'а б в г ґ д е є ж з и і ї й к л м н о', - 'multiple', - 'lines', - 'in', - 'pre', - 'п р с т у ф х ц ч ш щ ю я ь', - ], - }, - { - description: 'pre should be on a new line not preceded by a space', - html: `

    + wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'], + wantText: [ + 'а б в г ґ д е є ж з и і ї й к л м н о', + 'multiple', + 'lines', + 'in', + 'pre', + 'п р с т у ф х ц ч ш щ ю я ь', + ], + }, + { + description: 'pre should be on a new line not preceded by a space', + html: `

    1

    preline
     
    `, - wantAlines: ['+6', '+7'], - wantText: [' 1 ', 'preline'], - }, - { - description: 'Preserve spaces on the beginning and end of a element', - html: 'Need more space s !
    ', - wantAlines: ['+f*1+3+1'], - wantText: ['Need more space s !'], - }, - { - description: 'Preserve spaces outside elements', - html: 'Need more space s !
    ', - wantAlines: ['+g*1+1+2'], - wantText: ['Need more space s !'], - }, - { - description: 'Preserve spaces at the end of an element', - html: 'Need more space s !
    ', - wantAlines: ['+g*1+2+1'], - wantText: ['Need more space s !'], - }, - { - description: 'Preserve spaces at the start of an element', - html: 'Need more space s !
    ', - wantAlines: ['+f*1+2+2'], - wantText: ['Need more space s !'], - }, + wantAlines: ['+6', '+7'], + wantText: [' 1 ', 'preline'], + }, + { + description: 'Preserve spaces on the beginning and end of a element', + html: 'Need more space s !
    ', + wantAlines: ['+f*1+3+1'], + wantText: ['Need more space s !'], + }, + { + description: 'Preserve spaces outside elements', + html: 'Need more space s !
    ', + wantAlines: ['+g*1+1+2'], + wantText: ['Need more space s !'], + }, + { + description: 'Preserve spaces at the end of an element', + html: 'Need more space s !
    ', + wantAlines: ['+g*1+2+1'], + wantText: ['Need more space s !'], + }, + { + description: 'Preserve spaces at the start of an element', + html: 'Need more space s !
    ', + wantAlines: ['+f*1+2+2'], + wantText: ['Need more space s !'], + }, ]; - describe(__filename, function () { - for (const tc of testCases) { - describe(tc.description, function () { - let apool; - let result; - - before(async function () { - if (tc.disabled) return this.skip(); - const {window: {document}} = new jsdom.JSDOM(tc.html); - apool = new AttributePool(); - // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all - // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute - // numbers do not change if the attribute processing code changes.) - for (const attrib of knownAttribs) apool.putAttrib(attrib); - for (const aline of tc.wantAlines) { - for (const op of Changeset.deserializeOps(aline)) { - for (const n of attributes.decodeAttribString(op.attribs)) { - assert(n < knownAttribs.length); - } - } - } - const cc = contentcollector.makeContentCollector(true, null, apool); - cc.collectContent(document.body); - result = cc.finish(); - }); - - it('text matches', async function () { - assert.deepEqual(result.lines, tc.wantText); - }); - - it('alines match', async function () { - assert.deepEqual(result.lineAttribs, tc.wantAlines); - }); - - it('attributes are sorted in canonical order', async function () { - const gotAttribs = []; - const wantAttribs = []; - for (const aline of result.lineAttribs) { - const gotAlineAttribs = []; - gotAttribs.push(gotAlineAttribs); - const wantAlineAttribs = []; - wantAttribs.push(wantAlineAttribs); - for (const op of Changeset.deserializeOps(aline)) { - const gotOpAttribs = [...attributes.attribsFromString(op.attribs, apool)]; - gotAlineAttribs.push(gotOpAttribs); - wantAlineAttribs.push(attributes.sort([...gotOpAttribs])); - } - } - assert.deepEqual(gotAttribs, wantAttribs); - }); - }); - } + for (const tc of testCases) { + describe(tc.description, function () { + let apool; + let result; + before(async function () { + if (tc.disabled) + return this.skip(); + const { window: { document } } = new jsdom.JSDOM(tc.html); + apool = new AttributePool(); + // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all + // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute + // numbers do not change if the attribute processing code changes.) + for (const attrib of knownAttribs) + apool.putAttrib(attrib); + for (const aline of tc.wantAlines) { + for (const op of Changeset.deserializeOps(aline)) { + for (const n of attributes.decodeAttribString(op.attribs)) { + assert(n < knownAttribs.length); + } + } + } + const cc = contentcollector.makeContentCollector(true, null, apool); + cc.collectContent(document.body); + result = cc.finish(); + }); + it('text matches', async function () { + assert.deepEqual(result.lines, tc.wantText); + }); + it('alines match', async function () { + assert.deepEqual(result.lineAttribs, tc.wantAlines); + }); + it('attributes are sorted in canonical order', async function () { + const gotAttribs = []; + const wantAttribs = []; + for (const aline of result.lineAttribs) { + const gotAlineAttribs = []; + gotAttribs.push(gotAlineAttribs); + const wantAlineAttribs = []; + wantAttribs.push(wantAlineAttribs); + for (const op of Changeset.deserializeOps(aline)) { + const gotOpAttribs = [...attributes.attribsFromString(op.attribs, apool)]; + gotAlineAttribs.push(gotOpAttribs); + wantAlineAttribs.push(attributes.sort([...gotOpAttribs])); + } + } + assert.deepEqual(gotAttribs, wantAttribs); + }); + }); + } }); diff --git a/src/tests/backend/specs/export.js b/src/tests/backend/specs/export.js index d2fcde131dc..ea4a50be035 100644 --- a/src/tests/backend/specs/export.js +++ b/src/tests/backend/specs/export.js @@ -1,26 +1,21 @@ +import * as common from "../common.js"; +import * as padManager from "../../../node/db/PadManager.js"; +import * as settings from "../../../node/utils/Settings.js"; 'use strict'; - -const common = require('../common'); -const padManager = require('../../../node/db/PadManager'); -const settings = require('../../../node/utils/Settings'); - describe(__filename, function () { - let agent; - const settingsBackup = {}; - - before(async function () { - agent = await common.init(); - settingsBackup.soffice = settings.soffice; - await padManager.getPad('testExportPad', 'test content'); - }); - - after(async function () { - Object.assign(settings, settingsBackup); - }); - - it('returns 500 on export error', async function () { - settings.soffice = 'false'; // '/bin/false' doesn't work on Windows - await agent.get('/p/testExportPad/export/doc') - .expect(500); - }); + let agent; + const settingsBackup = {}; + before(async function () { + agent = await common.init(); + settingsBackup.soffice = settings.soffice; + await padManager.getPad('testExportPad', 'test content'); + }); + after(async function () { + Object.assign(settings, settingsBackup); + }); + it('returns 500 on export error', async function () { + settings.soffice = 'false'; // '/bin/false' doesn't work on Windows + await agent.get('/p/testExportPad/export/doc') + .expect(500); + }); }); diff --git a/src/tests/backend/specs/favicon-test-custom.png b/src/tests/backend/specs/favicon-test-custom.png index 9c6532c9610..e69de29bb2d 100644 Binary files a/src/tests/backend/specs/favicon-test-custom.png and b/src/tests/backend/specs/favicon-test-custom.png differ diff --git a/src/tests/backend/specs/favicon-test-skin.png b/src/tests/backend/specs/favicon-test-skin.png index 87bdadbbb32..e69de29bb2d 100644 Binary files a/src/tests/backend/specs/favicon-test-skin.png and b/src/tests/backend/specs/favicon-test-skin.png differ diff --git a/src/tests/backend/specs/favicon.js b/src/tests/backend/specs/favicon.js index a5e3095dee8..c69dfb3af82 100644 --- a/src/tests/backend/specs/favicon.js +++ b/src/tests/backend/specs/favicon.js @@ -1,91 +1,83 @@ +import assert$0 from "assert"; +import * as common from "../common.js"; +import fs from "fs"; +import path from "path"; +import * as settings from "../../../node/utils/Settings.js"; +import superagent from "superagent"; 'use strict'; - -const assert = require('assert').strict; -const common = require('../common'); -const fs = require('fs'); +const assert = assert$0.strict; const fsp = fs.promises; -const path = require('path'); -const settings = require('../../../node/utils/Settings'); -const superagent = require('superagent'); - describe(__filename, function () { - let agent; - let backupSettings; - let skinDir; - let wantCustomIcon; - let wantDefaultIcon; - let wantSkinIcon; - - before(async function () { - agent = await common.init(); - wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png')); - wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico')); - wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png')); - }); - - beforeEach(async function () { - backupSettings = {...settings}; - skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-')); - settings.skinName = path.basename(skinDir); - }); - - afterEach(async function () { - delete settings.favicon; - delete settings.skinName; - Object.assign(settings, backupSettings); - try { - // TODO: The {recursive: true} option wasn't added to fsp.rmdir() until Node.js v12.10.0 so we - // can't rely on it until support for Node.js v10 is dropped. - await fsp.unlink(path.join(skinDir, 'favicon.ico')); - await fsp.rmdir(skinDir, {recursive: true}); - } catch (err) { /* intentionally ignored */ } - }); - - it('uses custom favicon if set (relative pathname)', async function () { - settings.favicon = - path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png')); - assert(!path.isAbsolute(settings.favicon)); - const {body: gotIcon} = await agent.get('/favicon.ico') - .accept('png').buffer(true).parse(superagent.parse.image) - .expect(200); - assert(gotIcon.equals(wantCustomIcon)); - }); - - it('uses custom favicon if set (absolute pathname)', async function () { - settings.favicon = path.join(__dirname, 'favicon-test-custom.png'); - assert(path.isAbsolute(settings.favicon)); - const {body: gotIcon} = await agent.get('/favicon.ico') - .accept('png').buffer(true).parse(superagent.parse.image) - .expect(200); - assert(gotIcon.equals(wantCustomIcon)); - }); - - it('falls back if custom favicon is missing', async function () { - // The previous default for settings.favicon was 'favicon.ico', so many users will continue to - // have that in their settings.json for a long time. There is unlikely to be a favicon at - // path.resolve(settings.root, 'favicon.ico'), so this test ensures that 'favicon.ico' won't be - // a problem for those users. - settings.favicon = 'favicon.ico'; - const {body: gotIcon} = await agent.get('/favicon.ico') - .accept('png').buffer(true).parse(superagent.parse.image) - .expect(200); - assert(gotIcon.equals(wantDefaultIcon)); - }); - - it('uses skin favicon if present', async function () { - await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon); - settings.favicon = null; - const {body: gotIcon} = await agent.get('/favicon.ico') - .accept('png').buffer(true).parse(superagent.parse.image) - .expect(200); - assert(gotIcon.equals(wantSkinIcon)); - }); - - it('falls back to default favicon', async function () { - settings.favicon = null; - const {body: gotIcon} = await agent.get('/favicon.ico') - .accept('png').buffer(true).parse(superagent.parse.image) - .expect(200); - assert(gotIcon.equals(wantDefaultIcon)); - }); + let agent; + let backupSettings; + let skinDir; + let wantCustomIcon; + let wantDefaultIcon; + let wantSkinIcon; + before(async function () { + agent = await common.init(); + wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png')); + wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico')); + wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png')); + }); + beforeEach(async function () { + backupSettings = { ...settings }; + skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-')); + settings.skinName = path.basename(skinDir); + }); + afterEach(async function () { + delete settings.favicon; + delete settings.skinName; + Object.assign(settings, backupSettings); + try { + // TODO: The {recursive: true} option wasn't added to fsp.rmdir() until Node.js v12.10.0 so we + // can't rely on it until support for Node.js v10 is dropped. + await fsp.unlink(path.join(skinDir, 'favicon.ico')); + await fsp.rmdir(skinDir, { recursive: true }); + } + catch (err) { /* intentionally ignored */ } + }); + it('uses custom favicon if set (relative pathname)', async function () { + settings.favicon = + path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png')); + assert(!path.isAbsolute(settings.favicon)); + const { body: gotIcon } = await agent.get('/favicon.ico') + .accept('png').buffer(true).parse(superagent.parse.image) + .expect(200); + assert(gotIcon.equals(wantCustomIcon)); + }); + it('uses custom favicon if set (absolute pathname)', async function () { + settings.favicon = path.join(__dirname, 'favicon-test-custom.png'); + assert(path.isAbsolute(settings.favicon)); + const { body: gotIcon } = await agent.get('/favicon.ico') + .accept('png').buffer(true).parse(superagent.parse.image) + .expect(200); + assert(gotIcon.equals(wantCustomIcon)); + }); + it('falls back if custom favicon is missing', async function () { + // The previous default for settings.favicon was 'favicon.ico', so many users will continue to + // have that in their settings.json for a long time. There is unlikely to be a favicon at + // path.resolve(settings.root, 'favicon.ico'), so this test ensures that 'favicon.ico' won't be + // a problem for those users. + settings.favicon = 'favicon.ico'; + const { body: gotIcon } = await agent.get('/favicon.ico') + .accept('png').buffer(true).parse(superagent.parse.image) + .expect(200); + assert(gotIcon.equals(wantDefaultIcon)); + }); + it('uses skin favicon if present', async function () { + await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon); + settings.favicon = null; + const { body: gotIcon } = await agent.get('/favicon.ico') + .accept('png').buffer(true).parse(superagent.parse.image) + .expect(200); + assert(gotIcon.equals(wantSkinIcon)); + }); + it('falls back to default favicon', async function () { + settings.favicon = null; + const { body: gotIcon } = await agent.get('/favicon.ico') + .accept('png').buffer(true).parse(superagent.parse.image) + .expect(200); + assert(gotIcon.equals(wantDefaultIcon)); + }); }); diff --git a/src/tests/backend/specs/health.js b/src/tests/backend/specs/health.js index 0090aedbb90..bc640af239e 100644 --- a/src/tests/backend/specs/health.js +++ b/src/tests/backend/specs/health.js @@ -1,56 +1,48 @@ +import assert$0 from "assert"; +import * as common from "../common.js"; +import * as settings from "../../../node/utils/Settings.js"; +import superagent from "superagent"; 'use strict'; - -const assert = require('assert').strict; -const common = require('../common'); -const settings = require('../../../node/utils/Settings'); -const superagent = require('superagent'); - +const assert = assert$0.strict; describe(__filename, function () { - let agent; - const backup = {}; - - const getHealth = () => agent.get('/health') - .accept('application/health+json') - .buffer(true) - .parse(superagent.parse['application/json']) - .expect(200) - .expect((res) => assert.equal(res.type, 'application/health+json')); - - before(async function () { - agent = await common.init(); - }); - - beforeEach(async function () { - backup.settings = {}; - for (const setting of ['requireAuthentication', 'requireAuthorization']) { - backup.settings[setting] = settings[setting]; - } - }); - - afterEach(async function () { - Object.assign(settings, backup.settings); - }); - - it('/health works', async function () { - const res = await getHealth(); - assert.equal(res.body.status, 'pass'); - assert.equal(res.body.releaseId, settings.getEpVersion()); - }); - - it('auth is not required', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await getHealth(); - assert.equal(res.body.status, 'pass'); - }); - - // We actually want to test that no express-session state is created, but that is difficult to do - // without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a - // cookie means that no express-session state was created (how would express-session look up the - // session state if no ID was returned to the client?). - it('no cookie is returned', async function () { - const res = await getHealth(); - const cookie = res.headers['set-cookie']; - assert(cookie == null, `unexpected Set-Cookie: ${cookie}`); - }); + let agent; + const backup = {}; + const getHealth = () => agent.get('/health') + .accept('application/health+json') + .buffer(true) + .parse(superagent.parse['application/json']) + .expect(200) + .expect((res) => assert.equal(res.type, 'application/health+json')); + before(async function () { + agent = await common.init(); + }); + beforeEach(async function () { + backup.settings = {}; + for (const setting of ['requireAuthentication', 'requireAuthorization']) { + backup.settings[setting] = settings[setting]; + } + }); + afterEach(async function () { + Object.assign(settings, backup.settings); + }); + it('/health works', async function () { + const res = await getHealth(); + assert.equal(res.body.status, 'pass'); + assert.equal(res.body.releaseId, settings.getEpVersion()); + }); + it('auth is not required', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await getHealth(); + assert.equal(res.body.status, 'pass'); + }); + // We actually want to test that no express-session state is created, but that is difficult to do + // without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a + // cookie means that no express-session state was created (how would express-session look up the + // session state if no ID was returned to the client?). + it('no cookie is returned', async function () { + const res = await getHealth(); + const cookie = res.headers['set-cookie']; + assert(cookie == null, `unexpected Set-Cookie: ${cookie}`); + }); }); diff --git a/src/tests/backend/specs/hooks.js b/src/tests/backend/specs/hooks.js index 3120911aea2..e8972943a7a 100644 --- a/src/tests/backend/specs/hooks.js +++ b/src/tests/backend/specs/hooks.js @@ -1,1209 +1,1106 @@ +import assertLegacy from "../assert-legacy.js"; +import * as hooks from "../../../static/js/pluginfw/hooks.js"; +import * as plugins from "../../../static/js/pluginfw/plugin_defs.js"; +import sinon from "sinon"; 'use strict'; - -const assert = require('../assert-legacy').strict; -const hooks = require('../../../static/js/pluginfw/hooks'); -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const sinon = require('sinon'); - +const assert = assertLegacy.strict; describe(__filename, function () { - const hookName = 'testHook'; - const hookFnName = 'testPluginFileName:testHookFunctionName'; - let testHooks; // Convenience shorthand for plugins.hooks[hookName]. - let hook; // Convenience shorthand for plugins.hooks[hookName][0]. - - beforeEach(async function () { - // Make sure these are not already set so that we don't accidentally step on someone else's - // toes: - assert(plugins.hooks[hookName] == null); - assert(hooks.deprecationNotices[hookName] == null); - assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null); - - // Many of the tests only need a single registered hook function. Set that up here to reduce - // boilerplate. - hook = makeHook(); - plugins.hooks[hookName] = [hook]; - testHooks = plugins.hooks[hookName]; - }); - - afterEach(async function () { - sinon.restore(); - delete plugins.hooks[hookName]; - delete hooks.deprecationNotices[hookName]; - delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName]; - }); - - const makeHook = (ret) => ({ - hook_name: hookName, - // Many tests will likely want to change this. Unfortunately, we can't use a convenience - // wrapper like `(...args) => hookFn(..args)` because the hooks look at Function.length and - // change behavior depending on the number of parameters. - hook_fn: (hn, ctx, cb) => cb(ret), - hook_fn_name: hookFnName, - part: {plugin: 'testPluginName'}, - }); - - // Hook functions that should work for both synchronous and asynchronous hooks. - const supportedSyncHookFunctions = [ - { - name: 'return non-Promise value, with callback parameter', - fn: (hn, ctx, cb) => 'val', - want: 'val', - syncOk: true, - }, - { - name: 'return non-Promise value, without callback parameter', - fn: (hn, ctx) => 'val', - want: 'val', - syncOk: true, - }, - { - name: 'return undefined, without callback parameter', - fn: (hn, ctx) => {}, - want: undefined, - syncOk: true, - }, - { - name: 'pass non-Promise value to callback', - fn: (hn, ctx, cb) => { cb('val'); }, - want: 'val', - syncOk: true, - }, - { - name: 'pass undefined to callback', - fn: (hn, ctx, cb) => { cb(); }, - want: undefined, - syncOk: true, - }, - { - name: 'return the value returned from the callback', - fn: (hn, ctx, cb) => cb('val'), - want: 'val', - syncOk: true, - }, - { - name: 'throw', - fn: (hn, ctx, cb) => { throw new Error('test exception'); }, - wantErr: 'test exception', - syncOk: true, - }, - ]; - - describe('callHookFnSync', function () { - const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand. - - describe('basic behavior', function () { - it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; - callHookFnSync(hook); - }); - - it('passes context', async function () { - for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; - callHookFnSync(hook, val); - } - }); - - it('returns the value provided to the callback', async function () { - for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; - assert.equal(callHookFnSync(hook, val), val); - } - }); - - it('returns the value returned by the hook function', async function () { - for (const val of ['value', null, undefined]) { - // Must not have the cb parameter otherwise returning undefined will error. - hook.hook_fn = (hn, ctx) => ctx; - assert.equal(callHookFnSync(hook, val), val); - } - }); - - it('does not catch exceptions', async function () { - hook.hook_fn = () => { throw new Error('test exception'); }; - assert.throws(() => callHookFnSync(hook), {message: 'test exception'}); - }); - - it('callback returns undefined', async function () { - hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; - callHookFnSync(hook); - }); - - it('checks for deprecation', async function () { - sinon.stub(console, 'warn'); - hooks.deprecationNotices[hookName] = 'test deprecation'; - callHookFnSync(hook); - assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); - assert.equal(console.warn.callCount, 1); - assert.match(console.warn.getCall(0).args[0], /test deprecation/); - }); + const hookName = 'testHook'; + const hookFnName = 'testPluginFileName:testHookFunctionName'; + let testHooks; // Convenience shorthand for plugins.hooks[hookName]. + let hook; // Convenience shorthand for plugins.hooks[hookName][0]. + beforeEach(async function () { + // Make sure these are not already set so that we don't accidentally step on someone else's + // toes: + assert(plugins.hooks[hookName] == null); + assert(hooks.deprecationNotices[hookName] == null); + assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null); + // Many of the tests only need a single registered hook function. Set that up here to reduce + // boilerplate. + hook = makeHook(); + plugins.hooks[hookName] = [hook]; + testHooks = plugins.hooks[hookName]; }); - - describe('supported hook function styles', function () { - for (const tc of supportedSyncHookFunctions) { - it(tc.name, async function () { - sinon.stub(console, 'warn'); - sinon.stub(console, 'error'); - hook.hook_fn = tc.fn; - const call = () => callHookFnSync(hook); - if (tc.wantErr) { - assert.throws(call, {message: tc.wantErr}); - } else { - assert.equal(call(), tc.want); - } - assert.equal(console.warn.callCount, 0); - assert.equal(console.error.callCount, 0); - }); - } + afterEach(async function () { + sinon.restore(); + delete plugins.hooks[hookName]; + delete hooks.deprecationNotices[hookName]; + delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName]; }); - - describe('bad hook function behavior (other than double settle)', function () { - const promise1 = Promise.resolve('val1'); - const promise2 = Promise.resolve('val2'); - - const testCases = [ + const makeHook = (ret) => ({ + hook_name: hookName, + // Many tests will likely want to change this. Unfortunately, we can't use a convenience + // wrapper like `(...args) => hookFn(..args)` because the hooks look at Function.length and + // change behavior depending on the number of parameters. + hook_fn: (hn, ctx, cb) => cb(ret), + hook_fn_name: hookFnName, + part: { plugin: 'testPluginName' }, + }); + // Hook functions that should work for both synchronous and asynchronous hooks. + const supportedSyncHookFunctions = [ { - name: 'never settles -> buggy hook detected', - // Note that returning undefined without calling the callback is permitted if the function - // has 2 or fewer parameters, so this test function must have 3 parameters. - fn: (hn, ctx, cb) => {}, - wantVal: undefined, - wantError: /UNSETTLED FUNCTION BUG/, + name: 'return non-Promise value, with callback parameter', + fn: (hn, ctx, cb) => 'val', + want: 'val', + syncOk: true, }, { - name: 'returns a Promise -> buggy hook detected', - fn: () => promise1, - wantVal: promise1, - wantError: /PROHIBITED PROMISE BUG/, + name: 'return non-Promise value, without callback parameter', + fn: (hn, ctx) => 'val', + want: 'val', + syncOk: true, }, { - name: 'passes a Promise to cb -> buggy hook detected', - fn: (hn, ctx, cb) => cb(promise2), - wantVal: promise2, - wantError: /PROHIBITED PROMISE BUG/, + name: 'return undefined, without callback parameter', + fn: (hn, ctx) => { }, + want: undefined, + syncOk: true, }, - ]; - - for (const tc of testCases) { - it(tc.name, async function () { - sinon.stub(console, 'error'); - hook.hook_fn = tc.fn; - assert.equal(callHookFnSync(hook), tc.wantVal); - assert.equal(console.error.callCount, tc.wantError ? 1 : 0); - if (tc.wantError) assert.match(console.error.getCall(0).args[0], tc.wantError); - }); - } - }); - - // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second - // time, or call the callback and then return a value.) - describe('bad hook function behavior (double settle)', function () { - beforeEach(async function () { - sinon.stub(console, 'error'); - }); - - // Each item in this array codifies a way to settle a synchronous hook function. Each of the - // test cases below combines two of these behaviors in a single hook function and confirms - // that callHookFnSync both (1) returns the result of the first settle attempt, and - // (2) detects the second settle attempt. - const behaviors = [ { - name: 'throw', - fn: (cb, err, val) => { throw err; }, - rejects: true, + name: 'pass non-Promise value to callback', + fn: (hn, ctx, cb) => { cb('val'); }, + want: 'val', + syncOk: true, }, { - name: 'return value', - fn: (cb, err, val) => val, + name: 'pass undefined to callback', + fn: (hn, ctx, cb) => { cb(); }, + want: undefined, + syncOk: true, }, { - name: 'immediately call cb(value)', - fn: (cb, err, val) => cb(val), + name: 'return the value returned from the callback', + fn: (hn, ctx, cb) => cb('val'), + want: 'val', + syncOk: true, }, { - name: 'defer call to cb(value)', - fn: (cb, err, val) => { process.nextTick(cb, val); }, - async: true, + name: 'throw', + fn: (hn, ctx, cb) => { throw new Error('test exception'); }, + wantErr: 'test exception', + syncOk: true, }, - ]; - - for (const step1 of behaviors) { - // There can't be a second step if the first step is to return or throw. - if (step1.name.startsWith('return ') || step1.name === 'throw') continue; - for (const step2 of behaviors) { - // If step1 and step2 are both async then there would be three settle attempts (first an - // erroneous unsettled return, then async step 1, then async step 2). Handling triple - // settle would complicate the tests, and it is sufficient to test only double settles. - if (step1.async && step2.async) continue; - - it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { - 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); - }; - - // Temporarily remove unhandled error listeners so that the errors we expect to see - // don't trigger a test failure (or terminate node). - const events = ['uncaughtException', 'unhandledRejection']; - const listenerBackups = {}; - for (const event of events) { - listenerBackups[event] = process.rawListeners(event); - process.removeAllListeners(event); - } - - // We should see an asynchronous error (either an unhandled Promise rejection or an - // uncaught exception) if and only if one of the two steps was asynchronous or there was - // a throw (in which case the double settle is deferred so that the caller sees the - // original error). - const wantAsyncErr = step1.async || step2.async || step2.rejects; - let tempListener; - let asyncErr; - try { - const seenErrPromise = new Promise((resolve) => { - tempListener = (err) => { - assert.equal(asyncErr, undefined); - asyncErr = err; - resolve(); - }; - if (!wantAsyncErr) resolve(); - }); - events.forEach((event) => process.on(event, tempListener)); - const call = () => callHookFnSync(hook, {ret1: 'val1', ret2: 'val2'}); - if (step2.rejects) { - assert.throws(call, {message: 'val2'}); - } else if (!step1.async && !step2.async) { - assert.throws(call, {message: /DOUBLE SETTLE BUG/}); - } else { - assert.equal(call(), step1.async ? 'val2' : 'val1'); - } - await seenErrPromise; - } finally { - // Restore the original listeners. - for (const event of events) { - process.off(event, tempListener); - for (const listener of listenerBackups[event]) { - process.on(event, listener); + ]; + describe('callHookFnSync', function () { + const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand. + describe('basic behavior', function () { + it('passes hook name', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + callHookFnSync(hook); + }); + it('passes context', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + callHookFnSync(hook, val); + } + }); + it('returns the value provided to the callback', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + assert.equal(callHookFnSync(hook, val), val); } - } + }); + it('returns the value returned by the hook function', async function () { + for (const val of ['value', null, undefined]) { + // Must not have the cb parameter otherwise returning undefined will error. + hook.hook_fn = (hn, ctx) => ctx; + assert.equal(callHookFnSync(hook, val), val); + } + }); + it('does not catch exceptions', async function () { + hook.hook_fn = () => { throw new Error('test exception'); }; + assert.throws(() => callHookFnSync(hook), { message: 'test exception' }); + }); + it('callback returns undefined', async function () { + hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + callHookFnSync(hook); + }); + it('checks for deprecation', async function () { + sinon.stub(console, 'warn'); + hooks.deprecationNotices[hookName] = 'test deprecation'; + callHookFnSync(hook); + assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); + assert.equal(console.warn.callCount, 1); + assert.match(console.warn.getCall(0).args[0], /test deprecation/); + }); + }); + describe('supported hook function styles', function () { + for (const tc of supportedSyncHookFunctions) { + it(tc.name, async function () { + sinon.stub(console, 'warn'); + sinon.stub(console, 'error'); + hook.hook_fn = tc.fn; + const call = () => callHookFnSync(hook); + if (tc.wantErr) { + assert.throws(call, { message: tc.wantErr }); + } + else { + assert.equal(call(), tc.want); + } + assert.equal(console.warn.callCount, 0); + assert.equal(console.error.callCount, 0); + }); } - assert.equal(console.error.callCount, 1); - assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); - if (wantAsyncErr) { - assert(asyncErr instanceof Error); - assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); + }); + describe('bad hook function behavior (other than double settle)', function () { + const promise1 = Promise.resolve('val1'); + const promise2 = Promise.resolve('val2'); + const testCases = [ + { + name: 'never settles -> buggy hook detected', + // Note that returning undefined without calling the callback is permitted if the function + // has 2 or fewer parameters, so this test function must have 3 parameters. + fn: (hn, ctx, cb) => { }, + wantVal: undefined, + wantError: /UNSETTLED FUNCTION BUG/, + }, + { + name: 'returns a Promise -> buggy hook detected', + fn: () => promise1, + wantVal: promise1, + wantError: /PROHIBITED PROMISE BUG/, + }, + { + name: 'passes a Promise to cb -> buggy hook detected', + fn: (hn, ctx, cb) => cb(promise2), + wantVal: promise2, + wantError: /PROHIBITED PROMISE BUG/, + }, + ]; + for (const tc of testCases) { + it(tc.name, async function () { + sinon.stub(console, 'error'); + hook.hook_fn = tc.fn; + assert.equal(callHookFnSync(hook), tc.wantVal); + assert.equal(console.error.callCount, tc.wantError ? 1 : 0); + if (tc.wantError) + assert.match(console.error.getCall(0).args[0], tc.wantError); + }); } - }); - - // This next test is the same as the above test, except the second settle attempt is for - // the same outcome. The two outcomes can't be the same if one step throws and the other - // doesn't, so skip those cases. - if (step1.rejects !== step2.rejects) continue; - - it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { - const err = new Error('val'); - hook.hook_fn = (hn, ctx, cb) => { - step1.fn(cb, err, 'val'); - return step2.fn(cb, err, 'val'); - }; - - const errorLogged = new Promise((resolve) => console.error.callsFake(resolve)); - const call = () => callHookFnSync(hook); - if (step2.rejects) { - assert.throws(call, {message: 'val'}); - } else { - assert.equal(call(), 'val'); + }); + // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second + // time, or call the callback and then return a value.) + describe('bad hook function behavior (double settle)', function () { + beforeEach(async function () { + sinon.stub(console, 'error'); + }); + // Each item in this array codifies a way to settle a synchronous hook function. Each of the + // test cases below combines two of these behaviors in a single hook function and confirms + // that callHookFnSync both (1) returns the result of the first settle attempt, and + // (2) detects the second settle attempt. + const behaviors = [ + { + name: 'throw', + fn: (cb, err, val) => { throw err; }, + rejects: true, + }, + { + name: 'return value', + fn: (cb, err, val) => val, + }, + { + name: 'immediately call cb(value)', + fn: (cb, err, val) => cb(val), + }, + { + name: 'defer call to cb(value)', + fn: (cb, err, val) => { process.nextTick(cb, val); }, + async: true, + }, + ]; + for (const step1 of behaviors) { + // There can't be a second step if the first step is to return or throw. + if (step1.name.startsWith('return ') || step1.name === 'throw') + continue; + for (const step2 of behaviors) { + // If step1 and step2 are both async then there would be three settle attempts (first an + // erroneous unsettled return, then async step 1, then async step 2). Handling triple + // settle would complicate the tests, and it is sufficient to test only double settles. + if (step1.async && step2.async) + continue; + it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { + 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); + }; + // Temporarily remove unhandled error listeners so that the errors we expect to see + // don't trigger a test failure (or terminate node). + const events = ['uncaughtException', 'unhandledRejection']; + const listenerBackups = {}; + for (const event of events) { + listenerBackups[event] = process.rawListeners(event); + process.removeAllListeners(event); + } + // We should see an asynchronous error (either an unhandled Promise rejection or an + // uncaught exception) if and only if one of the two steps was asynchronous or there was + // a throw (in which case the double settle is deferred so that the caller sees the + // original error). + const wantAsyncErr = step1.async || step2.async || step2.rejects; + let tempListener; + let asyncErr; + try { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err) => { + assert.equal(asyncErr, undefined); + asyncErr = err; + resolve(); + }; + if (!wantAsyncErr) + resolve(); + }); + events.forEach((event) => process.on(event, tempListener)); + const call = () => callHookFnSync(hook, { ret1: 'val1', ret2: 'val2' }); + if (step2.rejects) { + assert.throws(call, { message: 'val2' }); + } + else if (!step1.async && !step2.async) { + assert.throws(call, { message: /DOUBLE SETTLE BUG/ }); + } + else { + assert.equal(call(), step1.async ? 'val2' : 'val1'); + } + await seenErrPromise; + } + finally { + // Restore the original listeners. + for (const event of events) { + process.off(event, tempListener); + for (const listener of listenerBackups[event]) { + process.on(event, listener); + } + } + } + assert.equal(console.error.callCount, 1); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + if (wantAsyncErr) { + assert(asyncErr instanceof Error); + assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); + } + }); + // This next test is the same as the above test, except the second settle attempt is for + // the same outcome. The two outcomes can't be the same if one step throws and the other + // doesn't, so skip those cases. + if (step1.rejects !== step2.rejects) + continue; + it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { + const err = new Error('val'); + hook.hook_fn = (hn, ctx, cb) => { + step1.fn(cb, err, 'val'); + return step2.fn(cb, err, 'val'); + }; + const errorLogged = new Promise((resolve) => console.error.callsFake(resolve)); + const call = () => callHookFnSync(hook); + if (step2.rejects) { + assert.throws(call, { message: 'val' }); + } + else { + assert.equal(call(), 'val'); + } + await errorLogged; + assert.equal(console.error.callCount, 1); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + }); + } } - await errorLogged; - assert.equal(console.error.callCount, 1); - assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); - }); - } - } - }); - }); - - describe('hooks.callAll', function () { - describe('basic behavior', function () { - it('calls all in order', async function () { - 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 () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; - hooks.callAll(hookName); - }); - - it('undefined context -> {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; - hooks.callAll(hookName); - }); - - it('null context -> {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; - hooks.callAll(hookName, null); - }); - - it('context unmodified', async function () { - const wantContext = {}; - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; - hooks.callAll(hookName, wantContext); - }); - }); - - describe('result processing', function () { - it('no registered hooks (undefined) -> []', async function () { - delete plugins.hooks.testHook; - assert.deepEqual(hooks.callAll(hookName), []); - }); - - it('no registered hooks (empty list) -> []', async function () { - testHooks.length = 0; - assert.deepEqual(hooks.callAll(hookName), []); - }); - - it('flattens one level', async function () { - 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 () { - testHooks.length = 0; - testHooks.push(makeHook(), makeHook([2]), makeHook([[3]])); - assert.deepEqual(hooks.callAll(hookName), [2, [3]]); - }); - - it('preserves null', async function () { - testHooks.length = 0; - testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]])); - assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]); - }); - - it('all undefined -> []', async function () { - testHooks.length = 0; - testHooks.push(makeHook(), makeHook()); - assert.deepEqual(hooks.callAll(hookName), []); - }); - }); - }); - - describe('hooks.callFirst', function () { - it('no registered hooks (undefined) -> []', async function () { - delete plugins.hooks.testHook; - assert.deepEqual(hooks.callFirst(hookName), []); - }); - - it('no registered hooks (empty list) -> []', async function () { - testHooks.length = 0; - assert.deepEqual(hooks.callFirst(hookName), []); - }); - - it('passes hook name => {}', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; - hooks.callFirst(hookName); - }); - - it('undefined context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; - hooks.callFirst(hookName); - }); - - it('null context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; - hooks.callFirst(hookName, null); - }); - - it('context unmodified', async function () { - 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 () { - const gotCalls = []; - testHooks.length = 0; - for (let i = 0; i < 3; i++) { - const hook = makeHook(); - hook.hook_fn = () => { gotCalls.push(i); }; - testHooks.push(hook); - } - assert.deepEqual(hooks.callFirst(hookName), []); - assert.deepEqual(gotCalls, [0, 1, 2]); - }); - - it('stops when predicate is satisfied', async function () { - 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 () { - 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 () { - testHooks.length = 0; - testHooks.push(makeHook([]), makeHook('val1')); - assert.deepEqual(hooks.callFirst(hookName), ['val1']); - }); - - it('null satisifes the predicate', async function () { - testHooks.length = 0; - testHooks.push(makeHook(null), makeHook('val1')); - assert.deepEqual(hooks.callFirst(hookName), [null]); - }); - - it('non-empty arrays are returned unmodified', async function () { - const want = ['val1']; - testHooks.length = 0; - testHooks.push(makeHook(want), makeHook(['val2'])); - assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual! - }); - - it('value can be passed via callback', async function () { - const want = {}; - hook.hook_fn = (hn, ctx, cb) => { cb(want); }; - const got = hooks.callFirst(hookName); - assert.deepEqual(got, [want]); - assert.equal(got[0], want); // Note: *NOT* deepEqual! + }); }); - }); - - describe('callHookFnAsync', function () { - const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand. - - describe('basic behavior', function () { - it('passes hook name', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; - await callHookFnAsync(hook); - }); - - it('passes context', async function () { - for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; - await callHookFnAsync(hook, val); - } - }); - - it('returns the value provided to the callback', async function () { - for (const val of ['value', null, undefined]) { - hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; - assert.equal(await callHookFnAsync(hook, val), val); - assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); - } - }); - - it('returns the value returned by the hook function', async function () { - 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; - assert.equal(await callHookFnAsync(hook, val), val); - assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); - } - }); - - it('rejects if it throws an exception', async function () { - 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 () { - 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 () { - 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 () { - hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; - await callHookFnAsync(hook); - }); - - it('checks for deprecation', async function () { - sinon.stub(console, 'warn'); - hooks.deprecationNotices[hookName] = 'test deprecation'; - await callHookFnAsync(hook); - assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); - assert.equal(console.warn.callCount, 1); - assert.match(console.warn.getCall(0).args[0], /test deprecation/); - }); + describe('hooks.callAll', function () { + describe('basic behavior', function () { + it('calls all in order', async function () { + 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 () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hooks.callAll(hookName); + }); + it('undefined context -> {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callAll(hookName); + }); + it('null context -> {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callAll(hookName, null); + }); + it('context unmodified', async function () { + const wantContext = {}; + hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hooks.callAll(hookName, wantContext); + }); + }); + describe('result processing', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks.testHook; + assert.deepEqual(hooks.callAll(hookName), []); + }); + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(hooks.callAll(hookName), []); + }); + it('flattens one level', async function () { + 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 () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook([2]), makeHook([[3]])); + assert.deepEqual(hooks.callAll(hookName), [2, [3]]); + }); + it('preserves null', async function () { + testHooks.length = 0; + testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]])); + assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]); + }); + it('all undefined -> []', async function () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook()); + assert.deepEqual(hooks.callAll(hookName), []); + }); + }); }); - - describe('supported hook function styles', function () { - const supportedHookFunctions = supportedSyncHookFunctions.concat([ - { - name: 'legacy async cb', - fn: (hn, ctx, cb) => { process.nextTick(cb, 'val'); }, - want: 'val', - }, - // Already resolved Promises: - { - name: 'return resolved Promise, with callback parameter', - fn: (hn, ctx, cb) => Promise.resolve('val'), - want: 'val', - }, - { - name: 'return resolved Promise, without callback parameter', - fn: (hn, ctx) => Promise.resolve('val'), - want: 'val', - }, - { - name: 'pass resolved Promise to callback', - fn: (hn, ctx, cb) => { cb(Promise.resolve('val')); }, - want: 'val', - }, - // Not yet resolved Promises: - { - name: 'return unresolved Promise, with callback parameter', - fn: (hn, ctx, cb) => new Promise((resolve) => process.nextTick(resolve, 'val')), - want: 'val', - }, - { - name: 'return unresolved Promise, without callback parameter', - fn: (hn, ctx) => new Promise((resolve) => process.nextTick(resolve, 'val')), - want: 'val', - }, - { - name: 'pass unresolved Promise to callback', - fn: (hn, ctx, cb) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); }, - want: 'val', - }, - // Already rejected Promises: - { - name: 'return rejected Promise, with callback parameter', - fn: (hn, ctx, cb) => Promise.reject(new Error('test rejection')), - wantErr: 'test rejection', - }, - { - name: 'return rejected Promise, without callback parameter', - fn: (hn, ctx) => Promise.reject(new Error('test rejection')), - wantErr: 'test rejection', - }, - { - name: 'pass rejected Promise to callback', - fn: (hn, ctx, cb) => { cb(Promise.reject(new Error('test rejection'))); }, - wantErr: 'test rejection', - }, - // Not yet rejected Promises: - { - name: 'return unrejected Promise, with callback parameter', - fn: (hn, ctx, cb) => new Promise((resolve, reject) => { - process.nextTick(reject, new Error('test rejection')); - }), - wantErr: 'test rejection', - }, - { - name: 'return unrejected Promise, without callback parameter', - fn: (hn, ctx) => new Promise((resolve, reject) => { - process.nextTick(reject, new Error('test rejection')); - }), - wantErr: 'test rejection', - }, - { - name: 'pass unrejected Promise to callback', - fn: (hn, ctx, cb) => { - cb(new Promise((resolve, reject) => { - process.nextTick(reject, new Error('test rejection')); - })); - }, - wantErr: 'test rejection', - }, - ]); - - for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) { - it(tc.name, async function () { - sinon.stub(console, 'warn'); - sinon.stub(console, 'error'); - hook.hook_fn = tc.fn; - const p = callHookFnAsync(hook); - if (tc.wantErr) { - await assert.rejects(p, {message: tc.wantErr}); - } else { - assert.equal(await p, tc.want); - } - assert.equal(console.warn.callCount, 0); - assert.equal(console.error.callCount, 0); + describe('hooks.callFirst', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks.testHook; + assert.deepEqual(hooks.callFirst(hookName), []); + }); + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(hooks.callFirst(hookName), []); + }); + it('passes hook name => {}', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hooks.callFirst(hookName); + }); + it('undefined context => {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callFirst(hookName); + }); + it('null context => {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callFirst(hookName, null); + }); + it('context unmodified', async function () { + 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 () { + const gotCalls = []; + testHooks.length = 0; + for (let i = 0; i < 3; i++) { + const hook = makeHook(); + hook.hook_fn = () => { gotCalls.push(i); }; + testHooks.push(hook); + } + assert.deepEqual(hooks.callFirst(hookName), []); + assert.deepEqual(gotCalls, [0, 1, 2]); + }); + it('stops when predicate is satisfied', async function () { + 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 () { + 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 () { + testHooks.length = 0; + testHooks.push(makeHook([]), makeHook('val1')); + assert.deepEqual(hooks.callFirst(hookName), ['val1']); + }); + it('null satisifes the predicate', async function () { + testHooks.length = 0; + testHooks.push(makeHook(null), makeHook('val1')); + assert.deepEqual(hooks.callFirst(hookName), [null]); + }); + it('non-empty arrays are returned unmodified', async function () { + const want = ['val1']; + testHooks.length = 0; + testHooks.push(makeHook(want), makeHook(['val2'])); + assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual! + }); + it('value can be passed via callback', async function () { + const want = {}; + hook.hook_fn = (hn, ctx, cb) => { cb(want); }; + const got = hooks.callFirst(hookName); + assert.deepEqual(got, [want]); + assert.equal(got[0], want); // Note: *NOT* deepEqual! }); - } }); - - // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second - // time, or call the callback and then return a value.) - describe('bad hook function behavior (double settle)', function () { - beforeEach(async function () { - sinon.stub(console, 'error'); - }); - - // Each item in this array codifies a way to settle an asynchronous hook function. Each of the - // test cases below combines two of these behaviors in a single hook function and confirms - // that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2) - // detects the second settle attempt. - // - // The 'when' property specifies the relative time that two behaviors will cause the hook - // function to settle: - // * If behavior1.when <= behavior2.when and behavior1 is called before behavior2 then - // behavior1 will settle the hook function before behavior2. - // * Otherwise, behavior2 will settle the hook function before behavior1. - const behaviors = [ - { - name: 'throw', - fn: (cb, err, val) => { throw err; }, - rejects: true, - when: 0, - }, - { - name: 'return value', - fn: (cb, err, val) => val, - // This behavior has a later relative settle time vs. the 'throw' behavior because 'throw' - // immediately settles the hook function, whereas the 'return value' case is settled by a - // .then() function attached to a Promise. EcmaScript guarantees that a .then() function - // attached to a Promise is enqueued on the event loop (not executed immediately) when the - // Promise settles. - when: 1, - }, - { - name: 'immediately call cb(value)', - fn: (cb, err, val) => cb(val), - // This behavior has the same relative time as the 'return value' case because it too is - // settled by a .then() function attached to a Promise. - when: 1, - }, - { - name: 'return resolvedPromise', - fn: (cb, err, val) => Promise.resolve(val), - // This behavior has the same relative time as the 'return value' case because the return - // value is wrapped in a Promise via Promise.resolve(). The EcmaScript standard guarantees - // that Promise.resolve(Promise.resolve(value)) is equivalent to Promise.resolve(value), - // so returning an already resolved Promise vs. returning a non-Promise value are - // equivalent. - when: 1, - }, - { - name: 'immediately call cb(resolvedPromise)', - fn: (cb, err, val) => cb(Promise.resolve(val)), - when: 1, - }, - { - name: 'return rejectedPromise', - fn: (cb, err, val) => Promise.reject(err), - rejects: true, - when: 1, - }, - { - name: 'immediately call cb(rejectedPromise)', - fn: (cb, err, val) => cb(Promise.reject(err)), - rejects: true, - when: 1, - }, - { - name: 'return unresolvedPromise', - fn: (cb, err, val) => new Promise((resolve) => process.nextTick(resolve, val)), - when: 2, - }, - { - name: 'immediately call cb(unresolvedPromise)', - fn: (cb, err, val) => cb(new Promise((resolve) => process.nextTick(resolve, val))), - when: 2, - }, - { - name: 'return unrejectedPromise', - fn: (cb, err, val) => new Promise((resolve, reject) => process.nextTick(reject, err)), - rejects: true, - when: 2, - }, - { - name: 'immediately call cb(unrejectedPromise)', - fn: (cb, err, val) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))), - rejects: true, - when: 2, - }, - { - name: 'defer call to cb(value)', - fn: (cb, err, val) => { process.nextTick(cb, val); }, - when: 2, - }, - { - name: 'defer call to cb(resolvedPromise)', - fn: (cb, err, val) => { process.nextTick(cb, Promise.resolve(val)); }, - when: 2, - }, - { - name: 'defer call to cb(rejectedPromise)', - fn: (cb, err, val) => { process.nextTick(cb, Promise.reject(err)); }, - rejects: true, - when: 2, - }, - { - name: 'defer call to cb(unresolvedPromise)', - fn: (cb, err, val) => { - process.nextTick(() => { - cb(new Promise((resolve) => process.nextTick(resolve, val))); + describe('callHookFnAsync', function () { + const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand. + describe('basic behavior', function () { + it('passes hook name', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + await callHookFnAsync(hook); }); - }, - when: 3, - }, - { - name: 'defer call cb(unrejectedPromise)', - fn: (cb, err, val) => { - process.nextTick(() => { - cb(new Promise((resolve, reject) => process.nextTick(reject, err))); + it('passes context', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; + await callHookFnAsync(hook, val); + } }); - }, - rejects: true, - when: 3, - }, - ]; - - for (const step1 of behaviors) { - // There can't be a second step if the first step is to return or throw. - 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 () { - 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); - }; - - // Temporarily remove unhandled Promise rejection listeners so that the unhandled - // rejections we expect to see don't trigger a test failure (or terminate node). - const event = 'unhandledRejection'; - const listenersBackup = process.rawListeners(event); - process.removeAllListeners(event); - - let tempListener; - let asyncErr; - try { - const seenErrPromise = new Promise((resolve) => { - tempListener = (err) => { - assert.equal(asyncErr, undefined); - asyncErr = err; - resolve(); - }; - }); - process.on(event, tempListener); - const step1Wins = step1.when <= step2.when; - const winningStep = step1Wins ? step1 : step2; - const winningVal = step1Wins ? 'val1' : 'val2'; - const p = callHookFnAsync(hook, {ret1: 'val1', ret2: 'val2'}); - if (winningStep.rejects) { - await assert.rejects(p, {message: winningVal}); - } else { - assert.equal(await p, winningVal); - } - await seenErrPromise; - } finally { - // Restore the original listeners. - process.off(event, tempListener); - for (const listener of listenersBackup) { - process.on(event, listener); - } + it('returns the value provided to the callback', async function () { + for (const val of ['value', null, undefined]) { + hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; + assert.equal(await callHookFnAsync(hook, val), val); + assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); + } + }); + it('returns the value returned by the hook function', async function () { + 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; + assert.equal(await callHookFnAsync(hook, val), val); + assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); + } + }); + it('rejects if it throws an exception', async function () { + 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 () { + 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 () { + 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 () { + hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; + await callHookFnAsync(hook); + }); + it('checks for deprecation', async function () { + sinon.stub(console, 'warn'); + hooks.deprecationNotices[hookName] = 'test deprecation'; + await callHookFnAsync(hook); + assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true); + assert.equal(console.warn.callCount, 1); + assert.match(console.warn.getCall(0).args[0], /test deprecation/); + }); + }); + describe('supported hook function styles', function () { + const supportedHookFunctions = supportedSyncHookFunctions.concat([ + { + name: 'legacy async cb', + fn: (hn, ctx, cb) => { process.nextTick(cb, 'val'); }, + want: 'val', + }, + // Already resolved Promises: + { + name: 'return resolved Promise, with callback parameter', + fn: (hn, ctx, cb) => Promise.resolve('val'), + want: 'val', + }, + { + name: 'return resolved Promise, without callback parameter', + fn: (hn, ctx) => Promise.resolve('val'), + want: 'val', + }, + { + name: 'pass resolved Promise to callback', + fn: (hn, ctx, cb) => { cb(Promise.resolve('val')); }, + want: 'val', + }, + // Not yet resolved Promises: + { + name: 'return unresolved Promise, with callback parameter', + fn: (hn, ctx, cb) => new Promise((resolve) => process.nextTick(resolve, 'val')), + want: 'val', + }, + { + name: 'return unresolved Promise, without callback parameter', + fn: (hn, ctx) => new Promise((resolve) => process.nextTick(resolve, 'val')), + want: 'val', + }, + { + name: 'pass unresolved Promise to callback', + fn: (hn, ctx, cb) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); }, + want: 'val', + }, + // Already rejected Promises: + { + name: 'return rejected Promise, with callback parameter', + fn: (hn, ctx, cb) => Promise.reject(new Error('test rejection')), + wantErr: 'test rejection', + }, + { + name: 'return rejected Promise, without callback parameter', + fn: (hn, ctx) => Promise.reject(new Error('test rejection')), + wantErr: 'test rejection', + }, + { + name: 'pass rejected Promise to callback', + fn: (hn, ctx, cb) => { cb(Promise.reject(new Error('test rejection'))); }, + wantErr: 'test rejection', + }, + // Not yet rejected Promises: + { + name: 'return unrejected Promise, with callback parameter', + fn: (hn, ctx, cb) => new Promise((resolve, reject) => { + process.nextTick(reject, new Error('test rejection')); + }), + wantErr: 'test rejection', + }, + { + name: 'return unrejected Promise, without callback parameter', + fn: (hn, ctx) => new Promise((resolve, reject) => { + process.nextTick(reject, new Error('test rejection')); + }), + wantErr: 'test rejection', + }, + { + name: 'pass unrejected Promise to callback', + fn: (hn, ctx, cb) => { + cb(new Promise((resolve, reject) => { + process.nextTick(reject, new Error('test rejection')); + })); + }, + wantErr: 'test rejection', + }, + ]); + for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) { + it(tc.name, async function () { + sinon.stub(console, 'warn'); + sinon.stub(console, 'error'); + hook.hook_fn = tc.fn; + const p = callHookFnAsync(hook); + if (tc.wantErr) { + await assert.rejects(p, { message: tc.wantErr }); + } + else { + assert.equal(await p, tc.want); + } + assert.equal(console.warn.callCount, 0); + assert.equal(console.error.callCount, 0); + }); } - assert.equal(console.error.callCount, 1, - `Got errors:\n${ - console.error.getCalls().map((call) => call.args[0]).join('\n')}`); - assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); - assert(asyncErr instanceof Error); - assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); - }); - - // This next test is the same as the above test, except the second settle attempt is for - // the same outcome. The two outcomes can't be the same if one step rejects and the other - // doesn't, so skip those cases. - if (step1.rejects !== step2.rejects) continue; - - it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { - const err = new Error('val'); - hook.hook_fn = (hn, ctx, cb) => { - step1.fn(cb, err, 'val'); - return step2.fn(cb, err, 'val'); - }; - const winningStep = (step1.when <= step2.when) ? step1 : step2; - const errorLogged = new Promise((resolve) => console.error.callsFake(resolve)); - const p = callHookFnAsync(hook); - if (winningStep.rejects) { - await assert.rejects(p, {message: 'val'}); - } else { - assert.equal(await p, 'val'); + }); + // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second + // time, or call the callback and then return a value.) + describe('bad hook function behavior (double settle)', function () { + beforeEach(async function () { + sinon.stub(console, 'error'); + }); + // Each item in this array codifies a way to settle an asynchronous hook function. Each of the + // test cases below combines two of these behaviors in a single hook function and confirms + // that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2) + // detects the second settle attempt. + // + // The 'when' property specifies the relative time that two behaviors will cause the hook + // function to settle: + // * If behavior1.when <= behavior2.when and behavior1 is called before behavior2 then + // behavior1 will settle the hook function before behavior2. + // * Otherwise, behavior2 will settle the hook function before behavior1. + const behaviors = [ + { + name: 'throw', + fn: (cb, err, val) => { throw err; }, + rejects: true, + when: 0, + }, + { + name: 'return value', + fn: (cb, err, val) => val, + // This behavior has a later relative settle time vs. the 'throw' behavior because 'throw' + // immediately settles the hook function, whereas the 'return value' case is settled by a + // .then() function attached to a Promise. EcmaScript guarantees that a .then() function + // attached to a Promise is enqueued on the event loop (not executed immediately) when the + // Promise settles. + when: 1, + }, + { + name: 'immediately call cb(value)', + fn: (cb, err, val) => cb(val), + // This behavior has the same relative time as the 'return value' case because it too is + // settled by a .then() function attached to a Promise. + when: 1, + }, + { + name: 'return resolvedPromise', + fn: (cb, err, val) => Promise.resolve(val), + // This behavior has the same relative time as the 'return value' case because the return + // value is wrapped in a Promise via Promise.resolve(). The EcmaScript standard guarantees + // that Promise.resolve(Promise.resolve(value)) is equivalent to Promise.resolve(value), + // so returning an already resolved Promise vs. returning a non-Promise value are + // equivalent. + when: 1, + }, + { + name: 'immediately call cb(resolvedPromise)', + fn: (cb, err, val) => cb(Promise.resolve(val)), + when: 1, + }, + { + name: 'return rejectedPromise', + fn: (cb, err, val) => Promise.reject(err), + rejects: true, + when: 1, + }, + { + name: 'immediately call cb(rejectedPromise)', + fn: (cb, err, val) => cb(Promise.reject(err)), + rejects: true, + when: 1, + }, + { + name: 'return unresolvedPromise', + fn: (cb, err, val) => new Promise((resolve) => process.nextTick(resolve, val)), + when: 2, + }, + { + name: 'immediately call cb(unresolvedPromise)', + fn: (cb, err, val) => cb(new Promise((resolve) => process.nextTick(resolve, val))), + when: 2, + }, + { + name: 'return unrejectedPromise', + fn: (cb, err, val) => new Promise((resolve, reject) => process.nextTick(reject, err)), + rejects: true, + when: 2, + }, + { + name: 'immediately call cb(unrejectedPromise)', + fn: (cb, err, val) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))), + rejects: true, + when: 2, + }, + { + name: 'defer call to cb(value)', + fn: (cb, err, val) => { process.nextTick(cb, val); }, + when: 2, + }, + { + name: 'defer call to cb(resolvedPromise)', + fn: (cb, err, val) => { process.nextTick(cb, Promise.resolve(val)); }, + when: 2, + }, + { + name: 'defer call to cb(rejectedPromise)', + fn: (cb, err, val) => { process.nextTick(cb, Promise.reject(err)); }, + rejects: true, + when: 2, + }, + { + name: 'defer call to cb(unresolvedPromise)', + fn: (cb, err, val) => { + process.nextTick(() => { + cb(new Promise((resolve) => process.nextTick(resolve, val))); + }); + }, + when: 3, + }, + { + name: 'defer call cb(unrejectedPromise)', + fn: (cb, err, val) => { + process.nextTick(() => { + cb(new Promise((resolve, reject) => process.nextTick(reject, err))); + }); + }, + rejects: true, + when: 3, + }, + ]; + for (const step1 of behaviors) { + // There can't be a second step if the first step is to return or throw. + 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 () { + 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); + }; + // Temporarily remove unhandled Promise rejection listeners so that the unhandled + // rejections we expect to see don't trigger a test failure (or terminate node). + const event = 'unhandledRejection'; + const listenersBackup = process.rawListeners(event); + process.removeAllListeners(event); + let tempListener; + let asyncErr; + try { + const seenErrPromise = new Promise((resolve) => { + tempListener = (err) => { + assert.equal(asyncErr, undefined); + asyncErr = err; + resolve(); + }; + }); + process.on(event, tempListener); + const step1Wins = step1.when <= step2.when; + const winningStep = step1Wins ? step1 : step2; + const winningVal = step1Wins ? 'val1' : 'val2'; + const p = callHookFnAsync(hook, { ret1: 'val1', ret2: 'val2' }); + if (winningStep.rejects) { + await assert.rejects(p, { message: winningVal }); + } + else { + assert.equal(await p, winningVal); + } + await seenErrPromise; + } + finally { + // Restore the original listeners. + process.off(event, tempListener); + for (const listener of listenersBackup) { + process.on(event, listener); + } + } + assert.equal(console.error.callCount, 1, `Got errors:\n${console.error.getCalls().map((call) => call.args[0]).join('\n')}`); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + assert(asyncErr instanceof Error); + assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); + }); + // This next test is the same as the above test, except the second settle attempt is for + // the same outcome. The two outcomes can't be the same if one step rejects and the other + // doesn't, so skip those cases. + if (step1.rejects !== step2.rejects) + continue; + it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { + const err = new Error('val'); + hook.hook_fn = (hn, ctx, cb) => { + step1.fn(cb, err, 'val'); + return step2.fn(cb, err, 'val'); + }; + const winningStep = (step1.when <= step2.when) ? step1 : step2; + const errorLogged = new Promise((resolve) => console.error.callsFake(resolve)); + const p = callHookFnAsync(hook); + if (winningStep.rejects) { + await assert.rejects(p, { message: 'val' }); + } + else { + assert.equal(await p, 'val'); + } + await errorLogged; + assert.equal(console.error.callCount, 1); + assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); + }); + } } - await errorLogged; - assert.equal(console.error.callCount, 1); - assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); - }); - } - } - }); - }); - - describe('hooks.aCallAll', function () { - describe('basic behavior', function () { - it('calls all asynchronously, returns values in order', async function () { - testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it. - let nextIndex = 0; - const hookPromises = []; - const hookStarted = []; - const hookFinished = []; - const makeHook = () => { - const i = nextIndex++; - const entry = {}; - hookStarted[i] = false; - hookFinished[i] = false; - hookPromises[i] = entry; - entry.promise = new Promise((resolve) => { - entry.resolve = () => { - hookFinished[i] = true; - resolve(i); - }; - }); - return {hook_fn: () => { - hookStarted[i] = true; - return entry.promise; - }}; - }; - testHooks.push(makeHook(), makeHook()); - const p = hooks.aCallAll(hookName); - assert.deepEqual(hookStarted, [true, true]); - assert.deepEqual(hookFinished, [false, false]); - hookPromises[1].resolve(); - await hookPromises[1].promise; - assert.deepEqual(hookFinished, [false, true]); - hookPromises[0].resolve(); - assert.deepEqual(await p, [0, 1]); - }); - - it('passes hook name', async function () { - hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; - await hooks.aCallAll(hookName); - }); - - it('undefined context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; - await hooks.aCallAll(hookName); - }); - - it('null context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; - await hooks.aCallAll(hookName, null); - }); - - it('context unmodified', async function () { - const wantContext = {}; - hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; - await hooks.aCallAll(hookName, wantContext); - }); + }); }); - - describe('aCallAll callback', function () { - it('exception in callback rejects', async function () { - const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); }); - await assert.rejects(p, {message: 'test exception'}); - }); - - it('propagates error on exception', async function () { - hook.hook_fn = () => { throw new Error('test exception'); }; - await hooks.aCallAll(hookName, {}, (err) => { - assert(err instanceof Error); - assert.equal(err.message, 'test exception'); + describe('hooks.aCallAll', function () { + describe('basic behavior', function () { + it('calls all asynchronously, returns values in order', async function () { + testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it. + let nextIndex = 0; + const hookPromises = []; + const hookStarted = []; + const hookFinished = []; + const makeHook = () => { + const i = nextIndex++; + const entry = {}; + hookStarted[i] = false; + hookFinished[i] = false; + hookPromises[i] = entry; + entry.promise = new Promise((resolve) => { + entry.resolve = () => { + hookFinished[i] = true; + resolve(i); + }; + }); + return { hook_fn: () => { + hookStarted[i] = true; + return entry.promise; + } }; + }; + testHooks.push(makeHook(), makeHook()); + const p = hooks.aCallAll(hookName); + assert.deepEqual(hookStarted, [true, true]); + assert.deepEqual(hookFinished, [false, false]); + hookPromises[1].resolve(); + await hookPromises[1].promise; + assert.deepEqual(hookFinished, [false, true]); + hookPromises[0].resolve(); + assert.deepEqual(await p, [0, 1]); + }); + it('passes hook name', async function () { + hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; + await hooks.aCallAll(hookName); + }); + it('undefined context -> {}', async function () { + hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.aCallAll(hookName); + }); + it('null context -> {}', async function () { + hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.aCallAll(hookName, null); + }); + it('context unmodified', async function () { + const wantContext = {}; + hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; + await hooks.aCallAll(hookName, wantContext); + }); }); - }); - - it('propagages null error on success', async function () { - await hooks.aCallAll(hookName, {}, (err) => { - assert(err == null, `got non-null error: ${err}`); + describe('aCallAll callback', function () { + it('exception in callback rejects', async function () { + const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); }); + await assert.rejects(p, { message: 'test exception' }); + }); + it('propagates error on exception', async function () { + hook.hook_fn = () => { throw new Error('test exception'); }; + await hooks.aCallAll(hookName, {}, (err) => { + assert(err instanceof Error); + assert.equal(err.message, 'test exception'); + }); + }); + it('propagages null error on success', async function () { + await hooks.aCallAll(hookName, {}, (err) => { + assert(err == null, `got non-null error: ${err}`); + }); + }); + it('propagages results on success', async function () { + hook.hook_fn = () => 'val'; + await hooks.aCallAll(hookName, {}, (err, results) => { + assert.deepEqual(results, ['val']); + }); + }); + it('returns callback return value', async function () { + assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val'); + }); }); - }); - - it('propagages results on success', async function () { - hook.hook_fn = () => 'val'; - await hooks.aCallAll(hookName, {}, (err, results) => { - assert.deepEqual(results, ['val']); + describe('result processing', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks[hookName]; + assert.deepEqual(await hooks.aCallAll(hookName), []); + }); + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(await hooks.aCallAll(hookName), []); + }); + it('flattens one level', async function () { + 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 () { + 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 () { + 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 () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook(Promise.resolve())); + assert.deepEqual(await hooks.aCallAll(hookName), []); + }); }); - }); - - it('returns callback return value', async function () { - assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val'); - }); - }); - - describe('result processing', function () { - it('no registered hooks (undefined) -> []', async function () { - delete plugins.hooks[hookName]; - assert.deepEqual(await hooks.aCallAll(hookName), []); - }); - - it('no registered hooks (empty list) -> []', async function () { - testHooks.length = 0; - assert.deepEqual(await hooks.aCallAll(hookName), []); - }); - - it('flattens one level', async function () { - 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 () { - 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 () { - 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 () { - testHooks.length = 0; - testHooks.push(makeHook(), makeHook(Promise.resolve())); - assert.deepEqual(await hooks.aCallAll(hookName), []); - }); }); - }); - - describe('hooks.callAllSerial', function () { - describe('basic behavior', function () { - it('calls all asynchronously, serially, in order', async function () { - const gotCalls = []; - testHooks.length = 0; - for (let i = 0; i < 3; i++) { - const hook = makeHook(); - hook.hook_fn = async () => { - gotCalls.push(i); - // Check gotCalls asynchronously to ensure that the next hook function does not start - // executing before this hook function has resolved. - return await new Promise((resolve) => { - setImmediate(() => { - assert.deepEqual(gotCalls, [...Array(i + 1).keys()]); - resolve(i); - }); + describe('hooks.callAllSerial', function () { + describe('basic behavior', function () { + it('calls all asynchronously, serially, in order', async function () { + const gotCalls = []; + testHooks.length = 0; + for (let i = 0; i < 3; i++) { + const hook = makeHook(); + hook.hook_fn = async () => { + gotCalls.push(i); + // Check gotCalls asynchronously to ensure that the next hook function does not start + // executing before this hook function has resolved. + return await new Promise((resolve) => { + setImmediate(() => { + assert.deepEqual(gotCalls, [...Array(i + 1).keys()]); + resolve(i); + }); + }); + }; + testHooks.push(hook); + } + assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]); + assert.deepEqual(gotCalls, [0, 1, 2]); }); - }; - testHooks.push(hook); - } - assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]); - assert.deepEqual(gotCalls, [0, 1, 2]); - }); - - it('passes hook name', async function () { - hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; - await hooks.callAllSerial(hookName); - }); - - it('undefined context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; - await hooks.callAllSerial(hookName); - }); - - it('null context -> {}', async function () { - hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; - await hooks.callAllSerial(hookName, null); - }); - - it('context unmodified', async function () { - const wantContext = {}; - hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; - await hooks.callAllSerial(hookName, wantContext); - }); - }); - - describe('result processing', function () { - it('no registered hooks (undefined) -> []', async function () { - delete plugins.hooks[hookName]; - assert.deepEqual(await hooks.callAllSerial(hookName), []); - }); - - it('no registered hooks (empty list) -> []', async function () { - testHooks.length = 0; - assert.deepEqual(await hooks.callAllSerial(hookName), []); - }); - - it('flattens one level', async function () { - 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 () { - 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 () { - 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 () { - testHooks.length = 0; - testHooks.push(makeHook(), makeHook(Promise.resolve())); - assert.deepEqual(await hooks.callAllSerial(hookName), []); - }); - }); - }); - - describe('hooks.aCallFirst', function () { - it('no registered hooks (undefined) -> []', async function () { - delete plugins.hooks.testHook; - assert.deepEqual(await hooks.aCallFirst(hookName), []); - }); - - it('no registered hooks (empty list) -> []', async function () { - testHooks.length = 0; - assert.deepEqual(await hooks.aCallFirst(hookName), []); - }); - - it('passes hook name => {}', async function () { - hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; - await hooks.aCallFirst(hookName); - }); - - it('undefined context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; - await hooks.aCallFirst(hookName); - }); - - it('null context => {}', async function () { - hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; - await hooks.aCallFirst(hookName, null); - }); - - it('context unmodified', async function () { - 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 () { - const gotCalls = []; - testHooks.length = 0; - for (let i = 0; i < 3; i++) { - const hook = makeHook(); - hook.hook_fn = () => { gotCalls.push(i); }; - testHooks.push(hook); - } - assert.deepEqual(await hooks.aCallFirst(hookName), []); - assert.deepEqual(gotCalls, [0, 1, 2]); - }); - - it('calls hook functions serially', async function () { - const gotCalls = []; - testHooks.length = 0; - for (let i = 0; i < 3; i++) { - const hook = makeHook(); - hook.hook_fn = async () => { - gotCalls.push(i); - // Check gotCalls asynchronously to ensure that the next hook function does not start - // executing before this hook function has resolved. - return await new Promise((resolve) => { - setImmediate(() => { - assert.deepEqual(gotCalls, [...Array(i + 1).keys()]); - resolve(); + it('passes hook name', async function () { + hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; + await hooks.callAllSerial(hookName); }); - }); - }; - testHooks.push(hook); - } - assert.deepEqual(await hooks.aCallFirst(hookName), []); - assert.deepEqual(gotCalls, [0, 1, 2]); - }); - - it('default predicate: stops when satisfied', async function () { - 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 () { - 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 () { - testHooks.length = 0; - testHooks.push(makeHook([]), makeHook('val1')); - assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); - }); - - it('default predicate: null satisifes', async function () { - 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 () { - testHooks.length = 0; - testHooks.push(makeHook(0), makeHook(1), makeHook(2)); - let got = 0; - await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; }); - assert.equal(got, 3); - }); - - it('custom predicate: boolean false/true continues/stops iteration', async function () { - testHooks.length = 0; - testHooks.push(makeHook(1), makeHook(2), makeHook(3)); - let nCall = 0; - const predicate = (val) => { - assert.deepEqual(val, [++nCall]); - return nCall === 2; - }; - assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]); - assert.equal(nCall, 2); - }); - - it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () { - testHooks.length = 0; - testHooks.push(makeHook(1), makeHook(2), makeHook(3)); - let nCall = 0; - const predicate = (val) => { - assert.deepEqual(val, [++nCall]); - return nCall === 2 ? {} : null; - }; - assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]); - assert.equal(nCall, 2); - }); - - it('custom predicate: array value passed unmodified to predicate', async function () { - const want = [0]; - hook.hook_fn = () => want; - const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! - await hooks.aCallFirst(hookName, null, null, predicate); - }); - - it('custom predicate: normalized value passed to predicate (undefined)', async function () { - const predicate = (got) => { assert.deepEqual(got, []); }; - await hooks.aCallFirst(hookName, null, null, predicate); - }); - - it('custom predicate: normalized value passed to predicate (null)', async function () { - 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 () { - const want = ['val1']; - testHooks.length = 0; - testHooks.push(makeHook(want), makeHook(['val2'])); - assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual! + it('undefined context -> {}', async function () { + hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.callAllSerial(hookName); + }); + it('null context -> {}', async function () { + hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.callAllSerial(hookName, null); + }); + it('context unmodified', async function () { + const wantContext = {}; + hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; + await hooks.callAllSerial(hookName, wantContext); + }); + }); + describe('result processing', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks[hookName]; + assert.deepEqual(await hooks.callAllSerial(hookName), []); + }); + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(await hooks.callAllSerial(hookName), []); + }); + it('flattens one level', async function () { + 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 () { + 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 () { + 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 () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook(Promise.resolve())); + assert.deepEqual(await hooks.callAllSerial(hookName), []); + }); + }); }); - - it('value can be passed via callback', async function () { - const want = {}; - hook.hook_fn = (hn, ctx, cb) => { cb(want); }; - const got = await hooks.aCallFirst(hookName); - assert.deepEqual(got, [want]); - assert.equal(got[0], want); // Note: *NOT* deepEqual! + describe('hooks.aCallFirst', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks.testHook; + assert.deepEqual(await hooks.aCallFirst(hookName), []); + }); + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(await hooks.aCallFirst(hookName), []); + }); + it('passes hook name => {}', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + await hooks.aCallFirst(hookName); + }); + it('undefined context => {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.aCallFirst(hookName); + }); + it('null context => {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + await hooks.aCallFirst(hookName, null); + }); + it('context unmodified', async function () { + 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 () { + const gotCalls = []; + testHooks.length = 0; + for (let i = 0; i < 3; i++) { + const hook = makeHook(); + hook.hook_fn = () => { gotCalls.push(i); }; + testHooks.push(hook); + } + assert.deepEqual(await hooks.aCallFirst(hookName), []); + assert.deepEqual(gotCalls, [0, 1, 2]); + }); + it('calls hook functions serially', async function () { + const gotCalls = []; + testHooks.length = 0; + for (let i = 0; i < 3; i++) { + const hook = makeHook(); + hook.hook_fn = async () => { + gotCalls.push(i); + // Check gotCalls asynchronously to ensure that the next hook function does not start + // executing before this hook function has resolved. + return await new Promise((resolve) => { + setImmediate(() => { + assert.deepEqual(gotCalls, [...Array(i + 1).keys()]); + resolve(); + }); + }); + }; + testHooks.push(hook); + } + assert.deepEqual(await hooks.aCallFirst(hookName), []); + assert.deepEqual(gotCalls, [0, 1, 2]); + }); + it('default predicate: stops when satisfied', async function () { + 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 () { + 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 () { + testHooks.length = 0; + testHooks.push(makeHook([]), makeHook('val1')); + assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); + }); + it('default predicate: null satisifes', async function () { + 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 () { + testHooks.length = 0; + testHooks.push(makeHook(0), makeHook(1), makeHook(2)); + let got = 0; + await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; }); + assert.equal(got, 3); + }); + it('custom predicate: boolean false/true continues/stops iteration', async function () { + testHooks.length = 0; + testHooks.push(makeHook(1), makeHook(2), makeHook(3)); + let nCall = 0; + const predicate = (val) => { + assert.deepEqual(val, [++nCall]); + return nCall === 2; + }; + assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]); + assert.equal(nCall, 2); + }); + it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () { + testHooks.length = 0; + testHooks.push(makeHook(1), makeHook(2), makeHook(3)); + let nCall = 0; + const predicate = (val) => { + assert.deepEqual(val, [++nCall]); + return nCall === 2 ? {} : null; + }; + assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]); + assert.equal(nCall, 2); + }); + it('custom predicate: array value passed unmodified to predicate', async function () { + const want = [0]; + hook.hook_fn = () => want; + const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! + await hooks.aCallFirst(hookName, null, null, predicate); + }); + it('custom predicate: normalized value passed to predicate (undefined)', async function () { + const predicate = (got) => { assert.deepEqual(got, []); }; + await hooks.aCallFirst(hookName, null, null, predicate); + }); + it('custom predicate: normalized value passed to predicate (null)', async function () { + 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 () { + const want = ['val1']; + testHooks.length = 0; + testHooks.push(makeHook(want), makeHook(['val2'])); + assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual! + }); + it('value can be passed via callback', async function () { + const want = {}; + hook.hook_fn = (hn, ctx, cb) => { cb(want); }; + const got = await hooks.aCallFirst(hookName); + assert.deepEqual(got, [want]); + assert.equal(got[0], want); // Note: *NOT* deepEqual! + }); }); - }); }); diff --git a/src/tests/backend/specs/messages.js b/src/tests/backend/specs/messages.js index bccb2584dc0..e79a7e3d08b 100644 --- a/src/tests/backend/specs/messages.js +++ b/src/tests/backend/specs/messages.js @@ -1,171 +1,159 @@ +import assert$0 from "assert"; +import * as common from "../common.js"; +import * as padManager from "../../../node/db/PadManager.js"; +import * as plugins from "../../../static/js/pluginfw/plugin_defs.js"; +import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js"; 'use strict'; - -const assert = require('assert').strict; -const common = require('../common'); -const padManager = require('../../../node/db/PadManager'); -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const readOnlyManager = require('../../../node/db/ReadOnlyManager'); - +const assert = assert$0.strict; describe(__filename, function () { - let agent; - let pad; - let padId; - let roPadId; - let rev; - let socket; - let roSocket; - const backups = {}; - - before(async function () { - agent = await common.init(); - }); - - beforeEach(async function () { - backups.hooks = {handleMessageSecurity: plugins.hooks.handleMessageSecurity}; - plugins.hooks.handleMessageSecurity = []; - padId = common.randomString(); - assert(!await padManager.doesPadExist(padId)); - pad = await padManager.getPad(padId, 'dummy text\n'); - await pad.setText('\n'); // Make sure the pad is created. - assert.equal(pad.text(), '\n'); - let res = await agent.get(`/p/${padId}`).expect(200); - socket = await common.connect(res); - const {type, data: clientVars} = await common.handshake(socket, padId); - assert.equal(type, 'CLIENT_VARS'); - rev = clientVars.collab_client_vars.rev; - - roPadId = await readOnlyManager.getReadOnlyId(padId); - res = await agent.get(`/p/${roPadId}`).expect(200); - roSocket = await common.connect(res); - await common.handshake(roSocket, roPadId); - }); - - afterEach(async function () { - Object.assign(plugins.hooks, backups.hooks); - if (socket != null) socket.close(); - socket = null; - if (roSocket != null) roSocket.close(); - roSocket = null; - if (pad != null) await pad.remove(); - pad = null; - }); - - describe('CHANGESET_REQ', function () { - it('users are unable to read changesets from other pads', async function () { - const otherPadId = `${padId}other`; - assert(!await padManager.doesPadExist(otherPadId)); - const otherPad = await padManager.getPad(otherPadId, 'other text\n'); - try { - await otherPad.setText('other text\n'); - const resP = common.waitForSocketEvent(roSocket, 'message'); - await common.sendMessage(roSocket, { - component: 'pad', - padId: otherPadId, // The server should ignore this. - type: 'CHANGESET_REQ', - data: { - granularity: 1, - start: 0, - requestID: 'requestId', - }, - }); - const res = await resP; - assert.equal(res.type, 'CHANGESET_REQ'); - assert.equal(res.data.requestID, 'requestId'); - // Should match padId's text, not otherPadId's text. - assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/); - } finally { - await otherPad.remove(); - } - }); - }); - - describe('USER_CHANGES', function () { - const sendUserChanges = - async (socket, cs) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs}); - const assertAccepted = async (socket, wantRev) => { - await common.waitForAcceptCommit(socket, wantRev); - rev = wantRev; - }; - const assertRejected = async (socket) => { - const msg = await common.waitForSocketEvent(socket, 'message'); - assert.deepEqual(msg, {disconnect: 'badChangeset'}); - }; - - it('changes are applied', async function () { - await Promise.all([ - assertAccepted(socket, rev + 1), - sendUserChanges(socket, 'Z:1>5+5$hello'), - ]); - assert.equal(pad.text(), 'hello\n'); - }); - - it('bad changeset is rejected', async function () { - await Promise.all([ - assertRejected(socket), - sendUserChanges(socket, 'this is not a valid changeset'), - ]); + let agent; + let pad; + let padId; + let roPadId; + let rev; + let socket; + let roSocket; + const backups = {}; + before(async function () { + agent = await common.init(); }); - - it('retransmission is accepted, has no effect', async function () { - const cs = 'Z:1>5+5$hello'; - await Promise.all([ - assertAccepted(socket, rev + 1), - sendUserChanges(socket, cs), - ]); - --rev; - await Promise.all([ - assertAccepted(socket, rev + 1), - sendUserChanges(socket, cs), - ]); - assert.equal(pad.text(), 'hello\n'); + beforeEach(async function () { + backups.hooks = { handleMessageSecurity: plugins.hooks.handleMessageSecurity }; + plugins.hooks.handleMessageSecurity = []; + padId = common.randomString(); + assert(!await padManager.doesPadExist(padId)); + pad = await padManager.getPad(padId, 'dummy text\n'); + await pad.setText('\n'); // Make sure the pad is created. + assert.equal(pad.text(), '\n'); + let res = await agent.get(`/p/${padId}`).expect(200); + socket = await common.connect(res); + const { type, data: clientVars } = await common.handshake(socket, padId); + assert.equal(type, 'CLIENT_VARS'); + rev = clientVars.collab_client_vars.rev; + roPadId = await readOnlyManager.getReadOnlyId(padId); + res = await agent.get(`/p/${roPadId}`).expect(200); + roSocket = await common.connect(res); + await common.handshake(roSocket, roPadId); }); - - it('identity changeset is accepted, has no effect', async function () { - await Promise.all([ - assertAccepted(socket, rev + 1), - sendUserChanges(socket, 'Z:1>5+5$hello'), - ]); - await Promise.all([ - assertAccepted(socket, rev), - sendUserChanges(socket, 'Z:6>0$'), - ]); - assert.equal(pad.text(), 'hello\n'); + afterEach(async function () { + Object.assign(plugins.hooks, backups.hooks); + if (socket != null) + socket.close(); + socket = null; + if (roSocket != null) + roSocket.close(); + roSocket = null; + if (pad != null) + await pad.remove(); + pad = null; }); - - it('non-identity changeset with no net change is accepted, has no effect', async function () { - await Promise.all([ - assertAccepted(socket, rev + 1), - sendUserChanges(socket, 'Z:1>5+5$hello'), - ]); - await Promise.all([ - assertAccepted(socket, rev), - sendUserChanges(socket, 'Z:6>0-5+5$hello'), - ]); - assert.equal(pad.text(), 'hello\n'); + describe('CHANGESET_REQ', function () { + it('users are unable to read changesets from other pads', async function () { + const otherPadId = `${padId}other`; + assert(!await padManager.doesPadExist(otherPadId)); + const otherPad = await padManager.getPad(otherPadId, 'other text\n'); + try { + await otherPad.setText('other text\n'); + const resP = common.waitForSocketEvent(roSocket, 'message'); + await common.sendMessage(roSocket, { + component: 'pad', + padId: otherPadId, + type: 'CHANGESET_REQ', + data: { + granularity: 1, + start: 0, + requestID: 'requestId', + }, + }); + const res = await resP; + assert.equal(res.type, 'CHANGESET_REQ'); + assert.equal(res.data.requestID, 'requestId'); + // Should match padId's text, not otherPadId's text. + assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/); + } + finally { + await otherPad.remove(); + } + }); }); - - it('handleMessageSecurity can grant one-time write access', async function () { - const cs = 'Z:1>5+5$hello'; - const errRegEx = /write attempt on read-only pad/; - // First try to send a change and verify that it was dropped. - await assert.rejects(sendUserChanges(roSocket, cs), errRegEx); - // sendUserChanges() waits for message ack, so if the message was accepted then head should - // have already incremented by the time we get here. - assert.equal(pad.head, rev); // Not incremented. - - // Now allow the change. - plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'}); - await Promise.all([ - assertAccepted(roSocket, rev + 1), - sendUserChanges(roSocket, cs), - ]); - assert.equal(pad.text(), 'hello\n'); - - // The next change should be dropped. - plugins.hooks.handleMessageSecurity = []; - await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx); - assert.equal(pad.head, rev); // Not incremented. - assert.equal(pad.text(), 'hello\n'); + describe('USER_CHANGES', function () { + const sendUserChanges = async (socket, cs) => await common.sendUserChanges(socket, { baseRev: rev, changeset: cs }); + const assertAccepted = async (socket, wantRev) => { + await common.waitForAcceptCommit(socket, wantRev); + rev = wantRev; + }; + const assertRejected = async (socket) => { + const msg = await common.waitForSocketEvent(socket, 'message'); + assert.deepEqual(msg, { disconnect: 'badChangeset' }); + }; + it('changes are applied', async function () { + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, 'Z:1>5+5$hello'), + ]); + assert.equal(pad.text(), 'hello\n'); + }); + it('bad changeset is rejected', async function () { + await Promise.all([ + assertRejected(socket), + sendUserChanges(socket, 'this is not a valid changeset'), + ]); + }); + it('retransmission is accepted, has no effect', async function () { + const cs = 'Z:1>5+5$hello'; + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, cs), + ]); + --rev; + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, cs), + ]); + assert.equal(pad.text(), 'hello\n'); + }); + it('identity changeset is accepted, has no effect', async function () { + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, 'Z:1>5+5$hello'), + ]); + await Promise.all([ + assertAccepted(socket, rev), + sendUserChanges(socket, 'Z:6>0$'), + ]); + assert.equal(pad.text(), 'hello\n'); + }); + it('non-identity changeset with no net change is accepted, has no effect', async function () { + await Promise.all([ + assertAccepted(socket, rev + 1), + sendUserChanges(socket, 'Z:1>5+5$hello'), + ]); + await Promise.all([ + assertAccepted(socket, rev), + sendUserChanges(socket, 'Z:6>0-5+5$hello'), + ]); + assert.equal(pad.text(), 'hello\n'); + }); + it('handleMessageSecurity can grant one-time write access', async function () { + const cs = 'Z:1>5+5$hello'; + const errRegEx = /write attempt on read-only pad/; + // First try to send a change and verify that it was dropped. + await assert.rejects(sendUserChanges(roSocket, cs), errRegEx); + // sendUserChanges() waits for message ack, so if the message was accepted then head should + // have already incremented by the time we get here. + assert.equal(pad.head, rev); // Not incremented. + // Now allow the change. + plugins.hooks.handleMessageSecurity.push({ hook_fn: () => 'permitOnce' }); + await Promise.all([ + assertAccepted(roSocket, rev + 1), + sendUserChanges(roSocket, cs), + ]); + assert.equal(pad.text(), 'hello\n'); + // The next change should be dropped. + plugins.hooks.handleMessageSecurity = []; + await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx); + assert.equal(pad.head, rev); // Not incremented. + assert.equal(pad.text(), 'hello\n'); + }); }); - }); }); diff --git a/src/tests/backend/specs/pad_utils.js b/src/tests/backend/specs/pad_utils.js index b4e815187b6..b2adae91c8e 100644 --- a/src/tests/backend/specs/pad_utils.js +++ b/src/tests/backend/specs/pad_utils.js @@ -1,43 +1,38 @@ +import assert$0 from "assert"; +import { padutils } from "../../../static/js/pad_utils.js"; 'use strict'; - -const assert = require('assert').strict; -const {padutils} = require('../../../static/js/pad_utils'); - +const assert = assert$0.strict; describe(__filename, function () { - describe('warnDeprecated', function () { - const {warnDeprecated} = padutils; - const backups = {}; - - before(async function () { - backups.logger = warnDeprecated.logger; + describe('warnDeprecated', function () { + const { warnDeprecated } = padutils; + const backups = {}; + before(async function () { + backups.logger = warnDeprecated.logger; + }); + afterEach(async function () { + warnDeprecated.logger = backups.logger; + delete warnDeprecated._rl; // Reset internal rate limiter state. + }); + it('includes the stack', async function () { + let got; + warnDeprecated.logger = { warn: (stack) => got = stack }; + warnDeprecated(); + assert(got.includes(__filename)); + }); + it('rate limited', async function () { + let got = 0; + warnDeprecated.logger = { warn: () => ++got }; + warnDeprecated(); // Initialize internal rate limiter state. + const { period } = warnDeprecated._rl; + got = 0; + const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]]; + for (const [now, want] of testCases) { // In a loop so that the stack trace is the same. + warnDeprecated._rl.now = () => now; + warnDeprecated(); + assert.equal(got, want); + } + warnDeprecated(); // Should have a different stack trace. + assert.equal(got, testCases[testCases.length - 1][1] + 1); + }); }); - - afterEach(async function () { - warnDeprecated.logger = backups.logger; - delete warnDeprecated._rl; // Reset internal rate limiter state. - }); - - it('includes the stack', async function () { - let got; - warnDeprecated.logger = {warn: (stack) => got = stack}; - warnDeprecated(); - assert(got.includes(__filename)); - }); - - it('rate limited', async function () { - let got = 0; - warnDeprecated.logger = {warn: () => ++got}; - warnDeprecated(); // Initialize internal rate limiter state. - const {period} = warnDeprecated._rl; - got = 0; - const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]]; - for (const [now, want] of testCases) { // In a loop so that the stack trace is the same. - warnDeprecated._rl.now = () => now; - warnDeprecated(); - assert.equal(got, want); - } - warnDeprecated(); // Should have a different stack trace. - assert.equal(got, testCases[testCases.length - 1][1] + 1); - }); - }); }); diff --git a/src/tests/backend/specs/pads-with-spaces.js b/src/tests/backend/specs/pads-with-spaces.js index 0db99865b80..f855340ff02 100644 --- a/src/tests/backend/specs/pads-with-spaces.js +++ b/src/tests/backend/specs/pads-with-spaces.js @@ -1,24 +1,20 @@ +import * as common from "../common.js"; +import assertLegacy from "../assert-legacy.js"; 'use strict'; - -const common = require('../common'); -const assert = require('../assert-legacy').strict; - +const assert = assertLegacy.strict; let agent; - describe(__filename, function () { - before(async function () { - agent = await common.init(); - }); - - it('supports pads with spaces, regression test for #4883', async function () { - await agent.get('/p/pads with spaces') - .expect(302) - .expect('location', 'pads_with_spaces'); - }); - - it('supports pads with spaces and query, regression test for #4883', async function () { - await agent.get('/p/pads with spaces?showChat=true&noColors=false') - .expect(302) - .expect('location', 'pads_with_spaces?showChat=true&noColors=false'); - }); + before(async function () { + agent = await common.init(); + }); + it('supports pads with spaces, regression test for #4883', async function () { + await agent.get('/p/pads with spaces') + .expect(302) + .expect('location', 'pads_with_spaces'); + }); + it('supports pads with spaces and query, regression test for #4883', async function () { + await agent.get('/p/pads with spaces?showChat=true&noColors=false') + .expect(302) + .expect('location', 'pads_with_spaces?showChat=true&noColors=false'); + }); }); diff --git a/src/tests/backend/specs/promises.js b/src/tests/backend/specs/promises.js index ad0c1ad9269..45fb135cb9b 100644 --- a/src/tests/backend/specs/promises.js +++ b/src/tests/backend/specs/promises.js @@ -1,85 +1,76 @@ -const assert = require('assert').strict; -const promises = require('../../../node/utils/promises'); - +import assert$0 from "assert"; +import * as promises from "../../../node/utils/promises.js"; +const assert = assert$0.strict; describe(__filename, function () { - describe('promises.timesLimit', function () { - let wantIndex = 0; - const testPromises = []; - const makePromise = (index) => { - // Make sure index increases by one each time. - assert.equal(index, wantIndex++); - // Save the resolve callback (so the test can trigger resolution) - // and the promise itself (to wait for resolve to take effect). - const p = {}; - const promise = new Promise((resolve) => { - p.resolve = resolve; - }); - p.promise = promise; - testPromises.push(p); - return p.promise; - }; - - const total = 11; - const concurrency = 7; - const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); - - it('honors concurrency', async function () { - assert.equal(wantIndex, concurrency); + describe('promises.timesLimit', function () { + let wantIndex = 0; + const testPromises = []; + const makePromise = (index) => { + // Make sure index increases by one each time. + assert.equal(index, wantIndex++); + // Save the resolve callback (so the test can trigger resolution) + // and the promise itself (to wait for resolve to take effect). + const p = {}; + const promise = new Promise((resolve) => { + p.resolve = resolve; + }); + p.promise = promise; + testPromises.push(p); + return p.promise; + }; + const total = 11; + const concurrency = 7; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + it('honors concurrency', async function () { + assert.equal(wantIndex, concurrency); + }); + it('creates another when one completes', async function () { + const { promise, resolve } = testPromises.shift(); + resolve(); + await promise; + assert.equal(wantIndex, concurrency + 1); + }); + it('creates the expected total number of promises', async function () { + while (testPromises.length > 0) { + // Resolve them in random order to ensure that the resolution order doesn't matter. + const i = Math.floor(Math.random() * Math.floor(testPromises.length)); + const { promise, resolve } = testPromises.splice(i, 1)[0]; + resolve(); + await promise; + } + assert.equal(wantIndex, total); + }); + it('resolves', async function () { + await timesLimitPromise; + }); + it('does not create too many promises if total < concurrency', async function () { + wantIndex = 0; + assert.equal(testPromises.length, 0); + const total = 7; + const concurrency = 11; + const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); + while (testPromises.length > 0) { + const { promise, resolve } = testPromises.pop(); + resolve(); + await promise; + } + await timesLimitPromise; + assert.equal(wantIndex, total); + }); + it('accepts total === 0, concurrency > 0', async function () { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, concurrency, makePromise); + assert.equal(wantIndex, 0); + }); + it('accepts total === 0, concurrency === 0', async function () { + wantIndex = 0; + assert.equal(testPromises.length, 0); + await promises.timesLimit(0, 0, makePromise); + assert.equal(wantIndex, 0); + }); + it('rejects total > 0, concurrency === 0', async function () { + await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError); + }); }); - - it('creates another when one completes', async function () { - const {promise, resolve} = testPromises.shift(); - resolve(); - await promise; - assert.equal(wantIndex, concurrency + 1); - }); - - it('creates the expected total number of promises', async function () { - while (testPromises.length > 0) { - // Resolve them in random order to ensure that the resolution order doesn't matter. - const i = Math.floor(Math.random() * Math.floor(testPromises.length)); - const {promise, resolve} = testPromises.splice(i, 1)[0]; - resolve(); - await promise; - } - assert.equal(wantIndex, total); - }); - - it('resolves', async function () { - await timesLimitPromise; - }); - - it('does not create too many promises if total < concurrency', async function () { - wantIndex = 0; - assert.equal(testPromises.length, 0); - const total = 7; - const concurrency = 11; - const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); - while (testPromises.length > 0) { - const {promise, resolve} = testPromises.pop(); - resolve(); - await promise; - } - await timesLimitPromise; - assert.equal(wantIndex, total); - }); - - it('accepts total === 0, concurrency > 0', async function () { - wantIndex = 0; - assert.equal(testPromises.length, 0); - await promises.timesLimit(0, concurrency, makePromise); - assert.equal(wantIndex, 0); - }); - - it('accepts total === 0, concurrency === 0', async function () { - wantIndex = 0; - assert.equal(testPromises.length, 0); - await promises.timesLimit(0, 0, makePromise); - assert.equal(wantIndex, 0); - }); - - it('rejects total > 0, concurrency === 0', async function () { - await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError); - }); - }); }); diff --git a/src/tests/backend/specs/regression-db.js b/src/tests/backend/specs/regression-db.js index 388b8346ab4..aa81bace2db 100644 --- a/src/tests/backend/specs/regression-db.js +++ b/src/tests/backend/specs/regression-db.js @@ -1,30 +1,25 @@ +import * as AuthorManager from "../../../node/db/AuthorManager.js"; +import assert$0 from "assert"; +import * as common from "../common.js"; +import * as db from "../../../node/db/DB.js"; 'use strict'; - -const AuthorManager = require('../../../node/db/AuthorManager'); -const assert = require('assert').strict; -const common = require('../common'); -const db = require('../../../node/db/DB'); - +const assert = assert$0.strict; describe(__filename, function () { - let setBackup; - - before(async function () { - await common.init(); - setBackup = db.set; - - db.set = async (...args) => { - // delay db.set - await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); - return await setBackup.call(db, ...args); - }; - }); - - after(async function () { - db.set = setBackup; - }); - - it('regression test for missing await in createAuthor (#5000)', async function () { - const {authorID} = await AuthorManager.createAuthor(); // Should block until db.set() finishes. - assert(await AuthorManager.doesAuthorExist(authorID)); - }); + let setBackup; + before(async function () { + await common.init(); + setBackup = db.set; + db.set = async (...args) => { + // delay db.set + await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); + return await setBackup.call(db, ...args); + }; + }); + after(async function () { + db.set = setBackup; + }); + it('regression test for missing await in createAuthor (#5000)', async function () { + const { authorID } = await AuthorManager.createAuthor(); // Should block until db.set() finishes. + assert(await AuthorManager.doesAuthorExist(authorID)); + }); }); diff --git a/src/tests/backend/specs/sanitizePathname.js b/src/tests/backend/specs/sanitizePathname.js index 767221920dd..c99f3aafec6 100644 --- a/src/tests/backend/specs/sanitizePathname.js +++ b/src/tests/backend/specs/sanitizePathname.js @@ -1,96 +1,93 @@ +import assert$0 from "assert"; +import path from "path"; +import sanitizePathname from "../../../node/utils/sanitizePathname.js"; 'use strict'; - -const assert = require('assert').strict; -const path = require('path'); -const sanitizePathname = require('../../../node/utils/sanitizePathname'); - +const assert = assert$0.strict; describe(__filename, function () { - describe('absolute paths rejected', function () { - const testCases = [ - ['posix', '/'], - ['posix', '/foo'], - ['win32', '/'], - ['win32', '\\'], - ['win32', 'C:/foo'], - ['win32', 'C:\\foo'], - ['win32', 'c:/foo'], - ['win32', 'c:\\foo'], - ['win32', '/foo'], - ['win32', '\\foo'], - ]; - for (const [platform, p] of testCases) { - it(`${platform} ${p}`, async function () { - assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/}); - }); - } - }); - describe('directory traversal rejected', function () { - const testCases = [ - ['posix', '..'], - ['posix', '../'], - ['posix', '../foo'], - ['posix', 'foo/../..'], - ['win32', '..'], - ['win32', '../'], - ['win32', '..\\'], - ['win32', '../foo'], - ['win32', '..\\foo'], - ['win32', 'foo/../..'], - ['win32', 'foo\\..\\..'], - ]; - for (const [platform, p] of testCases) { - it(`${platform} ${p}`, async function () { - assert.throws(() => sanitizePathname(p, path[platform]), {message: /travers/}); - }); - } - }); - - describe('accepted paths', function () { - const testCases = [ - ['posix', '', '.'], - ['posix', '.'], - ['posix', './'], - ['posix', 'foo'], - ['posix', 'foo/'], - ['posix', 'foo/bar/..', 'foo'], - ['posix', 'foo/bar/../', 'foo/'], - ['posix', './foo', 'foo'], - ['posix', 'foo/bar'], - ['posix', 'foo\\bar'], - ['posix', '\\foo'], - ['posix', '..\\foo'], - ['posix', 'foo/../bar', 'bar'], - ['posix', 'C:/foo'], - ['posix', 'C:\\foo'], - ['win32', '', '.'], - ['win32', '.'], - ['win32', './'], - ['win32', '.\\', './'], - ['win32', 'foo'], - ['win32', 'foo/'], - ['win32', 'foo\\', 'foo/'], - ['win32', 'foo/bar/..', 'foo'], - ['win32', 'foo\\bar\\..', 'foo'], - ['win32', 'foo/bar/../', 'foo/'], - ['win32', 'foo\\bar\\..\\', 'foo/'], - ['win32', './foo', 'foo'], - ['win32', '.\\foo', 'foo'], - ['win32', 'foo/bar'], - ['win32', 'foo\\bar', 'foo/bar'], - ['win32', 'foo/../bar', 'bar'], - ['win32', 'foo\\..\\bar', 'bar'], - ['win32', 'foo/..\\bar', 'bar'], - ['win32', 'foo\\../bar', 'bar'], - ]; - for (const [platform, p, tcWant] of testCases) { - const want = tcWant == null ? p : tcWant; - it(`${platform} ${p || ''} -> ${want}`, async function () { - assert.equal(sanitizePathname(p, path[platform]), want); - }); - } - }); - - it('default path API', async function () { - assert.equal(sanitizePathname('foo'), 'foo'); - }); + describe('absolute paths rejected', function () { + const testCases = [ + ['posix', '/'], + ['posix', '/foo'], + ['win32', '/'], + ['win32', '\\'], + ['win32', 'C:/foo'], + ['win32', 'C:\\foo'], + ['win32', 'c:/foo'], + ['win32', 'c:\\foo'], + ['win32', '/foo'], + ['win32', '\\foo'], + ]; + for (const [platform, p] of testCases) { + it(`${platform} ${p}`, async function () { + assert.throws(() => sanitizePathname(p, path[platform]), { message: /absolute path/ }); + }); + } + }); + describe('directory traversal rejected', function () { + const testCases = [ + ['posix', '..'], + ['posix', '../'], + ['posix', '../foo'], + ['posix', 'foo/../..'], + ['win32', '..'], + ['win32', '../'], + ['win32', '..\\'], + ['win32', '../foo'], + ['win32', '..\\foo'], + ['win32', 'foo/../..'], + ['win32', 'foo\\..\\..'], + ]; + for (const [platform, p] of testCases) { + it(`${platform} ${p}`, async function () { + assert.throws(() => sanitizePathname(p, path[platform]), { message: /travers/ }); + }); + } + }); + describe('accepted paths', function () { + const testCases = [ + ['posix', '', '.'], + ['posix', '.'], + ['posix', './'], + ['posix', 'foo'], + ['posix', 'foo/'], + ['posix', 'foo/bar/..', 'foo'], + ['posix', 'foo/bar/../', 'foo/'], + ['posix', './foo', 'foo'], + ['posix', 'foo/bar'], + ['posix', 'foo\\bar'], + ['posix', '\\foo'], + ['posix', '..\\foo'], + ['posix', 'foo/../bar', 'bar'], + ['posix', 'C:/foo'], + ['posix', 'C:\\foo'], + ['win32', '', '.'], + ['win32', '.'], + ['win32', './'], + ['win32', '.\\', './'], + ['win32', 'foo'], + ['win32', 'foo/'], + ['win32', 'foo\\', 'foo/'], + ['win32', 'foo/bar/..', 'foo'], + ['win32', 'foo\\bar\\..', 'foo'], + ['win32', 'foo/bar/../', 'foo/'], + ['win32', 'foo\\bar\\..\\', 'foo/'], + ['win32', './foo', 'foo'], + ['win32', '.\\foo', 'foo'], + ['win32', 'foo/bar'], + ['win32', 'foo\\bar', 'foo/bar'], + ['win32', 'foo/../bar', 'bar'], + ['win32', 'foo\\..\\bar', 'bar'], + ['win32', 'foo/..\\bar', 'bar'], + ['win32', 'foo\\../bar', 'bar'], + ]; + for (const [platform, p, tcWant] of testCases) { + const want = tcWant == null ? p : tcWant; + it(`${platform} ${p || ''} -> ${want}`, async function () { + assert.equal(sanitizePathname(p, path[platform]), want); + }); + } + }); + it('default path API', async function () { + assert.equal(sanitizePathname('foo'), 'foo'); + }); }); diff --git a/src/tests/backend/specs/settings.js b/src/tests/backend/specs/settings.js index e737f4f3445..6de6fae4110 100644 --- a/src/tests/backend/specs/settings.js +++ b/src/tests/backend/specs/settings.js @@ -1,61 +1,60 @@ +import assert$0 from "assert"; +import { exportedForTestingOnly } from "../../../node/utils/Settings.js"; +import path from "path"; +import process from "process"; 'use strict'; - -const assert = require('assert').strict; -const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly; -const path = require('path'); -const process = require('process'); - +const assert = assert$0.strict; +const { parseSettings } = { exportedForTestingOnly }.exportedForTestingOnly; describe(__filename, function () { - describe('parseSettings', function () { - let settings; - const envVarSubstTestCases = [ - {name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true}, - {name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false}, - {name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null}, - {name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined}, - {name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123}, - {name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'}, - {name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''}, - ]; - - before(async function () { - for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val; - delete process.env.UNSET_VAR; - settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert(settings != null); - }); - - describe('environment variable substitution', function () { - describe('set', function () { - for (const tc of envVarSubstTestCases) { - it(tc.name, async function () { - const obj = settings['environment variable substitution'].set; - if (tc.name === 'undefined') { - assert(!(tc.name in obj)); - } else { - assert.equal(obj[tc.name], tc.want); - } - }); - } - }); - - describe('unset', function () { - it('no default', async function () { - const obj = settings['environment variable substitution'].unset; - assert.equal(obj['no default'], null); + describe('parseSettings', function () { + let settings; + const envVarSubstTestCases = [ + { name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true }, + { name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false }, + { name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null }, + { name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined }, + { name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123 }, + { name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo' }, + { name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: '' }, + ]; + before(async function () { + for (const tc of envVarSubstTestCases) + process.env[tc.var] = tc.val; + delete process.env.UNSET_VAR; + settings = parseSettings(path.join(__dirname, 'settings.json'), true); + assert(settings != null); + }); + describe('environment variable substitution', function () { + describe('set', function () { + for (const tc of envVarSubstTestCases) { + it(tc.name, async function () { + const obj = settings['environment variable substitution'].set; + if (tc.name === 'undefined') { + assert(!(tc.name in obj)); + } + else { + assert.equal(obj[tc.name], tc.want); + } + }); + } + }); + describe('unset', function () { + it('no default', async function () { + const obj = settings['environment variable substitution'].unset; + assert.equal(obj['no default'], null); + }); + for (const tc of envVarSubstTestCases) { + it(tc.name, async function () { + const obj = settings['environment variable substitution'].unset; + if (tc.name === 'undefined') { + assert(!(tc.name in obj)); + } + else { + assert.equal(obj[tc.name], tc.want); + } + }); + } + }); }); - - for (const tc of envVarSubstTestCases) { - it(tc.name, async function () { - const obj = settings['environment variable substitution'].unset; - if (tc.name === 'undefined') { - assert(!(tc.name in obj)); - } else { - assert.equal(obj[tc.name], tc.want); - } - }); - } - }); }); - }); }); diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 15f56177499..7f95d648a61 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -1,426 +1,405 @@ +import assert$0 from "assert"; +import * as common from "../common.js"; +import * as padManager from "../../../node/db/PadManager.js"; +import * as plugins from "../../../static/js/pluginfw/plugin_defs.js"; +import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js"; +import * as settings from "../../../node/utils/Settings.js"; +import * as socketIoRouter from "../../../node/handler/SocketIORouter.js"; 'use strict'; - -const assert = require('assert').strict; -const common = require('../common'); -const padManager = require('../../../node/db/PadManager'); -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const readOnlyManager = require('../../../node/db/ReadOnlyManager'); -const settings = require('../../../node/utils/Settings'); -const socketIoRouter = require('../../../node/handler/SocketIORouter'); - +const assert = assert$0.strict; describe(__filename, function () { - this.timeout(30000); - let agent; - let authorize; - const backups = {}; - const cleanUpPads = async () => { - const padIds = ['pad', 'other-pad', 'päd']; - await Promise.all(padIds.map(async (padId) => { - if (await padManager.doesPadExist(padId)) { - const pad = await padManager.getPad(padId); - await pad.remove(); - } - })); - }; - let socket; - - before(async function () { agent = await common.init(); }); - beforeEach(async function () { - backups.hooks = {}; - for (const hookName of ['preAuthorize', 'authenticate', 'authorize']) { - backups.hooks[hookName] = plugins.hooks[hookName]; - plugins.hooks[hookName] = []; - } - backups.settings = {}; - for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users']) { - backups.settings[setting] = settings[setting]; - } - settings.editOnly = false; - settings.requireAuthentication = false; - settings.requireAuthorization = false; - settings.users = { - admin: {password: 'admin-password', is_admin: true}, - user: {password: 'user-password'}, + this.timeout(30000); + let agent; + let authorize; + const backups = {}; + const cleanUpPads = async () => { + const padIds = ['pad', 'other-pad', 'päd']; + await Promise.all(padIds.map(async (padId) => { + if (await padManager.doesPadExist(padId)) { + const pad = await padManager.getPad(padId); + await pad.remove(); + } + })); }; - assert(socket == null); - authorize = () => true; - plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}]; - await cleanUpPads(); - }); - afterEach(async function () { - if (socket) socket.close(); - socket = null; - await cleanUpPads(); - Object.assign(plugins.hooks, backups.hooks); - Object.assign(settings, backups.settings); - }); - - 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 common.connect(res); - const clientVars = await common.handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - it('!authn !cookie -> ok', async function () { - 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 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 common.connect(res); - const clientVars = await common.handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - - for (const authn of [false, true]) { - const desc = authn ? 'authn user' : '!authn anonymous'; - it(`${desc} read-only /p/pad -> 200, ok`, async function () { - const get = (ep) => { - let res = agent.get(ep); - if (authn) res = res.auth('user', 'user-password'); - return res.expect(200); - }; - settings.requireAuthentication = authn; - let res = await get('/p/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 common.connect(res); - clientVars = await common.handshake(socket, readOnlyId); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, true); - }); - } - - it('authz user /p/pad -> 200, ok', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - 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 () { - 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 - // is currently populated even if requireAuthorization is false, but setting this to true - // ensures the test remains useful if the implementation ever changes. - settings.requireAuthorization = true; - const encodedPadId = encodeURIComponent('päd'); - const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); - socket = await common.connect(res); - const clientVars = await common.handshake(socket, 'päd'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - }); - }); - - describe('Abnormal access attempts', function () { - it('authn anonymous /p/pad -> 401, error', async 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 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 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 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 common.connect(null); - const message = await common.handshake(socket, 'pad'); - assert.equal(message.accessStatus, 'deny'); - }); - it('authorization bypass attempt -> error', async function () { - // Only allowed to access /p/pad. - authorize = (req) => req.path === '/p/pad'; - settings.requireAuthentication = true; - settings.requireAuthorization = true; - // First authenticate and establish a session. - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await common.connect(res); - // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. - const message = await common.handshake(socket, 'other-pad'); - assert.equal(message.accessStatus, 'deny'); - }); - }); - - describe('Authorization levels via authorize hook', function () { + let socket; + before(async function () { agent = await common.init(); }); beforeEach(async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - }); - - it("level='create' -> can create", async function () { - authorize = () => 'create'; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - 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 common.connect(res); - const clientVars = await common.handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, false); - }); - it("level='modify' -> can modify", async 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 common.connect(res); - const clientVars = await common.handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, false); - }); - it("level='create' settings.editOnly=true -> unable to create", async function () { - authorize = () => 'create'; - settings.editOnly = true; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - 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 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 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 common.connect(res); - const clientVars = await common.handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, true); - }); - }); - - describe('Authorization levels via user settings', function () { - beforeEach(async function () { - settings.requireAuthentication = true; - }); - - 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 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 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 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 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 common.connect(res); - const clientVars = await common.handshake(socket, 'pad'); - assert.equal(clientVars.type, 'CLIENT_VARS'); - assert.equal(clientVars.data.readonly, false); - }); - it('user.readOnly = true, user.canCreate = true -> unable to create', async 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 common.connect(res); - const message = await common.handshake(socket, 'pad'); - assert.equal(message.accessStatus, 'deny'); - }); - }); - - describe('Authorization level interaction between authorize hook and user settings', function () { - beforeEach(async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - }); - - it('authorize hook does not elevate level from user settings', async function () { - settings.users.user.readOnly = true; - authorize = () => 'create'; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - 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 () { - settings.users.user.readOnly = false; - settings.users.user.canCreate = true; - authorize = () => 'readOnly'; - const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); - socket = await common.connect(res); - const message = await common.handshake(socket, 'pad'); - 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 common.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(); + backups.hooks = {}; + for (const hookName of ['preAuthorize', 'authenticate', 'authorize']) { + backups.hooks[hookName] = plugins.hooks[hookName]; + plugins.hooks[hookName] = []; } - 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(); + backups.settings = {}; + for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users']) { + backups.settings[setting] = settings[setting]; } - }()); - socket = await common.connect(); - await connected; - socket.close(); - socket = null; - await disconnected; - }); - - it('handleMessage (success)', async function () { - let serverSocket; - const want = { - component: this.test.fullTitle(), - 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 common.connect(); - socket.send(want); - assert.deepEqual(await got, want); - }); - - const tx = async (socket, message = {}) => await new Promise((resolve, reject) => { - 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, msg) { return want; } - }()); - socket = await common.connect(); - const got = await tx(socket, {component: this.test.fullTitle()}); - assert.equal(got, want); + settings.editOnly = false; + settings.requireAuthentication = false; + settings.requireAuthorization = false; + settings.users = { + admin: { password: 'admin-password', is_admin: true }, + user: { password: 'user-password' }, + }; + assert(socket == null); + authorize = () => true; + plugins.hooks.authorize = [{ hook_fn: (hookName, { req }, cb) => cb([authorize(req)]) }]; + await cleanUpPads(); }); - - it('handleMessage with ack (error)', async function () { - const InjectedError = class extends Error { - constructor() { super('injected test error'); this.name = 'InjectedError'; } - }; - socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { - handleMessage(socket, msg) { throw new InjectedError(); } - }()); - socket = await common.connect(); - await assert.rejects(tx(socket, {component: this.test.fullTitle()}), new InjectedError()); + afterEach(async function () { + if (socket) + socket.close(); + socket = null; + await cleanUpPads(); + Object.assign(plugins.hooks, backups.hooks); + Object.assign(settings, backups.settings); + }); + 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 common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + it('!authn !cookie -> ok', async function () { + 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 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 common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + for (const authn of [false, true]) { + const desc = authn ? 'authn user' : '!authn anonymous'; + it(`${desc} read-only /p/pad -> 200, ok`, async function () { + const get = (ep) => { + let res = agent.get(ep); + if (authn) + res = res.auth('user', 'user-password'); + return res.expect(200); + }; + settings.requireAuthentication = authn; + let res = await get('/p/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 common.connect(res); + clientVars = await common.handshake(socket, readOnlyId); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); + } + it('authz user /p/pad -> 200, ok', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + 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 () { + 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 + // is currently populated even if requireAuthorization is false, but setting this to true + // ensures the test remains useful if the implementation ever changes. + settings.requireAuthorization = true; + const encodedPadId = encodeURIComponent('päd'); + const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200); + socket = await common.connect(res); + const clientVars = await common.handshake(socket, 'päd'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + }); + }); + describe('Abnormal access attempts', function () { + it('authn anonymous /p/pad -> 401, error', async 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 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 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 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 common.connect(null); + const message = await common.handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + it('authorization bypass attempt -> error', async function () { + // Only allowed to access /p/pad. + authorize = (req) => req.path === '/p/pad'; + settings.requireAuthentication = true; + settings.requireAuthorization = true; + // First authenticate and establish a session. + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await common.connect(res); + // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad. + const message = await common.handshake(socket, 'other-pad'); + assert.equal(message.accessStatus, 'deny'); + }); + }); + describe('Authorization levels via authorize hook', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + it("level='create' -> can create", async function () { + authorize = () => 'create'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + 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 common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='modify' -> can modify", async 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 common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it("level='create' settings.editOnly=true -> unable to create", async function () { + authorize = () => 'create'; + settings.editOnly = true; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + 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 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 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 common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, true); + }); + }); + describe('Authorization levels via user settings', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + }); + 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 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 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 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 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 common.connect(res); + const clientVars = await common.handshake(socket, 'pad'); + assert.equal(clientVars.type, 'CLIENT_VARS'); + assert.equal(clientVars.data.readonly, false); + }); + it('user.readOnly = true, user.canCreate = true -> unable to create', async 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 common.connect(res); + const message = await common.handshake(socket, 'pad'); + assert.equal(message.accessStatus, 'deny'); + }); + }); + describe('Authorization level interaction between authorize hook and user settings', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + it('authorize hook does not elevate level from user settings', async function () { + settings.users.user.readOnly = true; + authorize = () => 'create'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + 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 () { + settings.users.user.readOnly = false; + settings.users.user.canCreate = true; + authorize = () => 'readOnly'; + const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); + socket = await common.connect(res); + const message = await common.handshake(socket, 'pad'); + 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 common.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 common.connect(); + await connected; + socket.close(); + socket = null; + await disconnected; + }); + it('handleMessage (success)', async function () { + let serverSocket; + const want = { + component: this.test.fullTitle(), + 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 common.connect(); + socket.send(want); + assert.deepEqual(await got, want); + }); + const tx = async (socket, message = {}) => await new Promise((resolve, reject) => { + 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, msg) { return want; } + }()); + socket = await common.connect(); + 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'; } + }; + socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { + handleMessage(socket, msg) { throw new InjectedError(); } + }()); + socket = await common.connect(); + await assert.rejects(tx(socket, { component: this.test.fullTitle() }), new InjectedError()); + }); }); - }); }); diff --git a/src/tests/backend/specs/specialpages.js b/src/tests/backend/specs/specialpages.js index 93c8b3bc4ac..8be1b00fb8f 100644 --- a/src/tests/backend/specs/specialpages.js +++ b/src/tests/backend/specs/specialpages.js @@ -1,28 +1,25 @@ +import * as common from "../common.js"; +import * as settings from "../../../node/utils/Settings.js"; 'use strict'; - -const common = require('../common'); -const settings = require('../../../node/utils/Settings'); - describe(__filename, function () { - this.timeout(30000); - let agent; - const backups = {}; - before(async function () { agent = await common.init(); }); - beforeEach(async function () { - backups.settings = {}; - for (const setting of ['requireAuthentication', 'requireAuthorization']) { - backups.settings[setting] = settings[setting]; - } - settings.requireAuthentication = false; - settings.requireAuthorization = false; - }); - afterEach(async function () { - Object.assign(settings, backups.settings); - }); - - describe('/javascript', function () { - it('/javascript -> 200', async function () { - await agent.get('/javascript').expect(200); + this.timeout(30000); + let agent; + const backups = {}; + before(async function () { agent = await common.init(); }); + beforeEach(async function () { + backups.settings = {}; + for (const setting of ['requireAuthentication', 'requireAuthorization']) { + backups.settings[setting] = settings[setting]; + } + settings.requireAuthentication = false; + settings.requireAuthorization = false; + }); + afterEach(async function () { + Object.assign(settings, backups.settings); + }); + describe('/javascript', function () { + it('/javascript -> 200', async function () { + await agent.get('/javascript').expect(200); + }); }); - }); }); diff --git a/src/tests/backend/specs/webaccess.js b/src/tests/backend/specs/webaccess.js index 23cd2d88968..ced776ca487 100644 --- a/src/tests/backend/specs/webaccess.js +++ b/src/tests/backend/specs/webaccess.js @@ -1,494 +1,478 @@ +import assert$0 from "assert"; +import * as common from "../common.js"; +import * as plugins from "../../../static/js/pluginfw/plugin_defs.js"; +import * as settings from "../../../node/utils/Settings.js"; 'use strict'; - -const assert = require('assert').strict; -const common = require('../common'); -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const settings = require('../../../node/utils/Settings'); - +const assert = assert$0.strict; describe(__filename, function () { - this.timeout(30000); - let agent; - const backups = {}; - const authHookNames = ['preAuthorize', 'authenticate', 'authorize']; - const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure']; - const makeHook = (hookName, hookFn) => ({ - hook_fn: hookFn, - hook_fn_name: `fake_plugin/${hookName}`, - hook_name: hookName, - part: {plugin: 'fake_plugin'}, - }); - - before(async function () { agent = await common.init(); }); - beforeEach(async function () { - backups.hooks = {}; - for (const hookName of authHookNames.concat(failHookNames)) { - backups.hooks[hookName] = plugins.hooks[hookName]; - plugins.hooks[hookName] = []; - } - backups.settings = {}; - for (const setting of ['requireAuthentication', 'requireAuthorization', 'users']) { - backups.settings[setting] = settings[setting]; - } - settings.requireAuthentication = false; - settings.requireAuthorization = false; - settings.users = { - admin: {password: 'admin-password', is_admin: true}, - user: {password: 'user-password'}, - }; - }); - afterEach(async function () { - Object.assign(plugins.hooks, backups.hooks); - Object.assign(settings, backups.settings); - }); - - describe('webaccess: without plugins', function () { - it('!authn !authz anonymous / -> 200', async function () { - settings.requireAuthentication = false; - settings.requireAuthorization = false; - await agent.get('/').expect(200); + this.timeout(30000); + let agent; + const backups = {}; + const authHookNames = ['preAuthorize', 'authenticate', 'authorize']; + const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure']; + const makeHook = (hookName, hookFn) => ({ + hook_fn: hookFn, + hook_fn_name: `fake_plugin/${hookName}`, + hook_name: hookName, + part: { plugin: 'fake_plugin' }, }); - it('!authn !authz anonymous /admin/ -> 401', async function () { - settings.requireAuthentication = false; - settings.requireAuthorization = false; - await agent.get('/admin/').expect(401); - }); - it('authn !authz anonymous / -> 401', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = false; - await agent.get('/').expect(401); - }); - it('authn !authz user / -> 200', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = false; - await agent.get('/').auth('user', 'user-password').expect(200); - }); - it('authn !authz user /admin/ -> 403', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = false; - await agent.get('/admin/').auth('user', 'user-password').expect(403); - }); - it('authn !authz admin / -> 200', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = false; - await agent.get('/').auth('admin', 'admin-password').expect(200); - }); - it('authn !authz admin /admin/ -> 200', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = false; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200); - }); - it('authn authz anonymous /robots.txt -> 200', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - await agent.get('/robots.txt').expect(200); - }); - it('authn authz user / -> 403', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - await agent.get('/').auth('user', 'user-password').expect(403); - }); - it('authn authz user /admin/ -> 403', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - await agent.get('/admin/').auth('user', 'user-password').expect(403); - }); - it('authn authz admin / -> 200', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - await agent.get('/').auth('admin', 'admin-password').expect(200); - }); - it('authn authz admin /admin/ -> 200', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200); - }); - - describe('login fails if password is nullish', function () { - for (const adminPassword of [undefined, null]) { - // https://tools.ietf.org/html/rfc7617 says that the username and password are sent as - // base64(username + ':' + password), but there's nothing stopping a malicious user from - // sending just base64(username) (no colon). The lack of colon could throw off credential - // 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 () { - settings.users.admin.password = adminPassword; - const encCreds = Buffer.from(creds).toString('base64'); - await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401); - }); - } - } - }); - }); - - describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () { - let callOrder; - const Handler = class { - constructor(hookName, suffix) { - this.called = false; - this.hookName = hookName; - this.innerHandle = () => []; - this.id = hookName + suffix; - this.checkContext = () => {}; - } - handle(hookName, context, cb) { - assert.equal(hookName, this.hookName); - assert(context != null); - assert(context.req != null); - assert(context.res != null); - assert(context.next != null); - this.checkContext(context); - assert(!this.called); - this.called = true; - callOrder.push(this.id); - return cb(this.innerHandle(context)); - } - }; - const handlers = {}; - + before(async function () { agent = await common.init(); }); beforeEach(async function () { - callOrder = []; - for (const hookName of authHookNames) { - // Create two handlers for each hook to test deferral to the next function. - const h0 = new Handler(hookName, '_0'); - const h1 = new Handler(hookName, '_1'); - handlers[hookName] = [h0, h1]; - plugins.hooks[hookName] = [ - makeHook(hookName, h0.handle.bind(h0)), - makeHook(hookName, h1.handle.bind(h1)), - ]; - } - }); - - describe('preAuthorize', function () { - beforeEach(async function () { - settings.requireAuthentication = false; - settings.requireAuthorization = false; - }); - - it('defers if it returns []', async function () { - 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 () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - handlers.preAuthorize[0].innerHandle = () => [true]; - await agent.get('/').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0']); - }); - it('bypasses authenticate and authorize hooks when false is returned', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - handlers.preAuthorize[0].innerHandle = () => [false]; - await agent.get('/').expect(403); - assert.deepEqual(callOrder, ['preAuthorize_0']); - }); - it('bypasses authenticate and authorize hooks when next is called', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - handlers.preAuthorize[0].innerHandle = ({next}) => next(); - await agent.get('/').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0']); - }); - it('static content (expressPreSession) bypasses all auth checks', async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - await agent.get('/static/robots.txt').expect(200); - assert.deepEqual(callOrder, []); - }); - it('cannot grant access to /admin', async function () { - handlers.preAuthorize[0].innerHandle = () => [true]; - await agent.get('/admin/').expect(401); - // Notes: - // * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because - // 'true' entries are ignored for /admin/* requests. - // * The authenticate hook always runs for /admin/* requests even if - // settings.requireAuthentication is false. - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('can deny access to /admin', async function () { - 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 () { - handlers.preAuthorize[0].innerHandle = () => [false]; - let called = false; - plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => { - assert.equal(hookName, 'preAuthzFailure'); - assert(req != null); - assert(res != null); - assert(!called); - called = true; - res.status(200).send('injected'); - return cb([true]); - })]; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); - assert(called); - }); - it('returns 500 if an exception is thrown', async function () { - handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; - await agent.get('/').expect(500); - }); - }); - - describe('authenticate', function () { - beforeEach(async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = false; - }); - - it('is not called if !requireAuthentication and not /admin/*', async function () { - settings.requireAuthentication = false; - await agent.get('/').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); - }); - it('is called if !requireAuthentication and /admin/*', async function () { + backups.hooks = {}; + for (const hookName of authHookNames.concat(failHookNames)) { + backups.hooks[hookName] = plugins.hooks[hookName]; + plugins.hooks[hookName] = []; + } + backups.settings = {}; + for (const setting of ['requireAuthentication', 'requireAuthorization', 'users']) { + backups.settings[setting] = settings[setting]; + } settings.requireAuthentication = false; - await agent.get('/admin/').expect(401); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('defers if empty list returned', async function () { - await agent.get('/').expect(401); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('does not defer if return [true], 200', async function () { - 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 () { - handlers.authenticate[0].innerHandle = () => [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 () { - await agent.get('/').auth('user', 'user-password').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('passes settings.users in context', async function () { - handlers.authenticate[0].checkContext = ({users}) => { - assert.equal(users, settings.users); - }; - await agent.get('/').expect(401); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('passes user, password in context if provided', async function () { - handlers.authenticate[0].checkContext = ({username, password}) => { - assert.equal(username, 'user'); - assert.equal(password, 'user-password'); - }; - await agent.get('/').auth('user', 'user-password').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('does not pass user, password in context if not provided', async function () { - handlers.authenticate[0].checkContext = ({username, password}) => { - assert(username == null); - assert(password == null); - }; - await agent.get('/').expect(401); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('errors if req.session.user is not created', async function () { - 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 () { - handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; - await agent.get('/').expect(500); - assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); - }); - }); - - describe('authorize', function () { - beforeEach(async function () { - settings.requireAuthentication = true; - settings.requireAuthorization = true; - }); - - it('is not called if !requireAuthorization (non-/admin)', async function () { settings.requireAuthorization = false; - await agent.get('/').auth('user', 'user-password').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('is not called if !requireAuthorization (/admin)', async function () { - settings.requireAuthorization = false; - await agent.get('/admin/').auth('admin', 'admin-password').expect(200); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1']); - }); - it('defers if empty list returned', async function () { - await agent.get('/').auth('user', 'user-password').expect(403); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1', - 'authorize_0', - 'authorize_1']); - }); - it('does not defer if return [true], 200', async function () { - 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. - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1', - 'authorize_0']); - }); - it('does not defer if return [false], 403', async function () { - handlers.authorize[0].innerHandle = () => [false]; - await agent.get('/').auth('user', 'user-password').expect(403); - // Note: authorize_1 was not called because authorize_0 handled it. - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1', - 'authorize_0']); - }); - it('passes req.path in context', async function () { - handlers.authorize[0].checkContext = ({resource}) => { - assert.equal(resource, '/'); + settings.users = { + admin: { password: 'admin-password', is_admin: true }, + user: { password: 'user-password' }, }; - await agent.get('/').auth('user', 'user-password').expect(403); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1', - 'authorize_0', - 'authorize_1']); - }); - it('returns 500 if an exception is thrown', async function () { - handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; - await agent.get('/').auth('user', 'user-password').expect(500); - assert.deepEqual(callOrder, ['preAuthorize_0', - 'preAuthorize_1', - 'authenticate_0', - 'authenticate_1', - 'authorize_0']); - }); }); - }); - - describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () { - const Handler = class { - constructor(hookName) { - this.hookName = hookName; - this.shouldHandle = false; - this.called = false; - } - handle(hookName, context, cb) { - assert.equal(hookName, this.hookName); - assert(context != null); - assert(context.req != null); - assert(context.res != null); - assert(!this.called); - this.called = true; - if (this.shouldHandle) { - context.res.status(200).send(this.hookName); - return cb([true]); - } - return cb([]); - } - }; - const handlers = {}; - - beforeEach(async function () { - failHookNames.forEach((hookName) => { - const handler = new Handler(hookName); - handlers[hookName] = handler; - plugins.hooks[hookName] = [makeHook(hookName, handler.handle.bind(handler))]; - }); - settings.requireAuthentication = true; - settings.requireAuthorization = true; - }); - - // authn failure tests - it('authn fail, no hooks handle -> 401', async function () { - await agent.get('/').expect(401); - assert(handlers.authnFailure.called); - assert(!handlers.authzFailure.called); - assert(handlers.authFailure.called); - }); - it('authn fail, authnFailure handles', async function () { - handlers.authnFailure.shouldHandle = true; - await agent.get('/').expect(200, 'authnFailure'); - assert(handlers.authnFailure.called); - assert(!handlers.authzFailure.called); - assert(!handlers.authFailure.called); - }); - it('authn fail, authFailure handles', async function () { - handlers.authFailure.shouldHandle = true; - await agent.get('/').expect(200, 'authFailure'); - assert(handlers.authnFailure.called); - assert(!handlers.authzFailure.called); - assert(handlers.authFailure.called); + afterEach(async function () { + Object.assign(plugins.hooks, backups.hooks); + Object.assign(settings, backups.settings); }); - it('authnFailure trumps authFailure', async function () { - handlers.authnFailure.shouldHandle = true; - handlers.authFailure.shouldHandle = true; - await agent.get('/').expect(200, 'authnFailure'); - assert(handlers.authnFailure.called); - assert(!handlers.authFailure.called); + describe('webaccess: without plugins', function () { + it('!authn !authz anonymous / -> 200', async function () { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + await agent.get('/').expect(200); + }); + it('!authn !authz anonymous /admin/ -> 401', async function () { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + await agent.get('/admin/').expect(401); + }); + it('authn !authz anonymous / -> 401', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').expect(401); + }); + it('authn !authz user / -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').auth('user', 'user-password').expect(200); + }); + it('authn !authz user /admin/ -> 403', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/admin/').auth('user', 'user-password').expect(403); + }); + it('authn !authz admin / -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/').auth('admin', 'admin-password').expect(200); + }); + it('authn !authz admin /admin/ -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + }); + it('authn authz anonymous /robots.txt -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/robots.txt').expect(200); + }); + it('authn authz user / -> 403', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/').auth('user', 'user-password').expect(403); + }); + it('authn authz user /admin/ -> 403', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/admin/').auth('user', 'user-password').expect(403); + }); + it('authn authz admin / -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/').auth('admin', 'admin-password').expect(200); + }); + it('authn authz admin /admin/ -> 200', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + }); + describe('login fails if password is nullish', function () { + for (const adminPassword of [undefined, null]) { + // https://tools.ietf.org/html/rfc7617 says that the username and password are sent as + // base64(username + ':' + password), but there's nothing stopping a malicious user from + // sending just base64(username) (no colon). The lack of colon could throw off credential + // 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 () { + settings.users.admin.password = adminPassword; + const encCreds = Buffer.from(creds).toString('base64'); + await agent.get('/admin/').set('Authorization', `Basic ${encCreds}`).expect(401); + }); + } + } + }); }); - - // authz failure tests - it('authz fail, no hooks handle -> 403', async function () { - 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 () { - handlers.authzFailure.shouldHandle = true; - await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); - assert(!handlers.authnFailure.called); - assert(handlers.authzFailure.called); - assert(!handlers.authFailure.called); - }); - it('authz fail, authFailure handles', async function () { - handlers.authFailure.shouldHandle = true; - await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); - assert(!handlers.authnFailure.called); - assert(handlers.authzFailure.called); - assert(handlers.authFailure.called); + describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () { + let callOrder; + const Handler = class { + constructor(hookName, suffix) { + this.called = false; + this.hookName = hookName; + this.innerHandle = () => []; + this.id = hookName + suffix; + this.checkContext = () => { }; + } + handle(hookName, context, cb) { + assert.equal(hookName, this.hookName); + assert(context != null); + assert(context.req != null); + assert(context.res != null); + assert(context.next != null); + this.checkContext(context); + assert(!this.called); + this.called = true; + callOrder.push(this.id); + return cb(this.innerHandle(context)); + } + }; + const handlers = {}; + beforeEach(async function () { + callOrder = []; + for (const hookName of authHookNames) { + // Create two handlers for each hook to test deferral to the next function. + const h0 = new Handler(hookName, '_0'); + const h1 = new Handler(hookName, '_1'); + handlers[hookName] = [h0, h1]; + plugins.hooks[hookName] = [ + makeHook(hookName, h0.handle.bind(h0)), + makeHook(hookName, h1.handle.bind(h1)), + ]; + } + }); + describe('preAuthorize', function () { + beforeEach(async function () { + settings.requireAuthentication = false; + settings.requireAuthorization = false; + }); + it('defers if it returns []', async function () { + 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 () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = () => [true]; + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('bypasses authenticate and authorize hooks when false is returned', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = () => [false]; + await agent.get('/').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('bypasses authenticate and authorize hooks when next is called', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + handlers.preAuthorize[0].innerHandle = ({ next }) => next(); + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0']); + }); + it('static content (expressPreSession) bypasses all auth checks', async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + await agent.get('/static/robots.txt').expect(200); + assert.deepEqual(callOrder, []); + }); + it('cannot grant access to /admin', async function () { + handlers.preAuthorize[0].innerHandle = () => [true]; + await agent.get('/admin/').expect(401); + // Notes: + // * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because + // 'true' entries are ignored for /admin/* requests. + // * The authenticate hook always runs for /admin/* requests even if + // settings.requireAuthentication is false. + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('can deny access to /admin', async function () { + 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 () { + handlers.preAuthorize[0].innerHandle = () => [false]; + let called = false; + plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, { req, res }, cb) => { + assert.equal(hookName, 'preAuthzFailure'); + assert(req != null); + assert(res != null); + assert(!called); + called = true; + res.status(200).send('injected'); + return cb([true]); + })]; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); + assert(called); + }); + it('returns 500 if an exception is thrown', async function () { + handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').expect(500); + }); + }); + describe('authenticate', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = false; + }); + it('is not called if !requireAuthentication and not /admin/*', async function () { + settings.requireAuthentication = false; + await agent.get('/').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']); + }); + it('is called if !requireAuthentication and /admin/*', async function () { + settings.requireAuthentication = false; + await agent.get('/admin/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('defers if empty list returned', async function () { + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('does not defer if return [true], 200', async function () { + 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 () { + handlers.authenticate[0].innerHandle = () => [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 () { + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('passes settings.users in context', async function () { + handlers.authenticate[0].checkContext = ({ users }) => { + assert.equal(users, settings.users); + }; + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('passes user, password in context if provided', async function () { + handlers.authenticate[0].checkContext = ({ username, password }) => { + assert.equal(username, 'user'); + assert.equal(password, 'user-password'); + }; + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('does not pass user, password in context if not provided', async function () { + handlers.authenticate[0].checkContext = ({ username, password }) => { + assert(username == null); + assert(password == null); + }; + await agent.get('/').expect(401); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('errors if req.session.user is not created', async function () { + 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 () { + handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); + }); + }); + describe('authorize', function () { + beforeEach(async function () { + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + it('is not called if !requireAuthorization (non-/admin)', async function () { + settings.requireAuthorization = false; + await agent.get('/').auth('user', 'user-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('is not called if !requireAuthorization (/admin)', async function () { + settings.requireAuthorization = false; + await agent.get('/admin/').auth('admin', 'admin-password').expect(200); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1']); + }); + it('defers if empty list returned', async function () { + await agent.get('/').auth('user', 'user-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0', + 'authorize_1']); + }); + it('does not defer if return [true], 200', async function () { + 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. + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0']); + }); + it('does not defer if return [false], 403', async function () { + handlers.authorize[0].innerHandle = () => [false]; + await agent.get('/').auth('user', 'user-password').expect(403); + // Note: authorize_1 was not called because authorize_0 handled it. + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0']); + }); + it('passes req.path in context', async function () { + handlers.authorize[0].checkContext = ({ resource }) => { + assert.equal(resource, '/'); + }; + await agent.get('/').auth('user', 'user-password').expect(403); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0', + 'authorize_1']); + }); + it('returns 500 if an exception is thrown', async function () { + handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); }; + await agent.get('/').auth('user', 'user-password').expect(500); + assert.deepEqual(callOrder, ['preAuthorize_0', + 'preAuthorize_1', + 'authenticate_0', + 'authenticate_1', + 'authorize_0']); + }); + }); }); - it('authzFailure trumps authFailure', async function () { - handlers.authzFailure.shouldHandle = true; - handlers.authFailure.shouldHandle = true; - await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); - assert(handlers.authzFailure.called); - assert(!handlers.authFailure.called); + describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () { + const Handler = class { + constructor(hookName) { + this.hookName = hookName; + this.shouldHandle = false; + this.called = false; + } + handle(hookName, context, cb) { + assert.equal(hookName, this.hookName); + assert(context != null); + assert(context.req != null); + assert(context.res != null); + assert(!this.called); + this.called = true; + if (this.shouldHandle) { + context.res.status(200).send(this.hookName); + return cb([true]); + } + return cb([]); + } + }; + const handlers = {}; + beforeEach(async function () { + failHookNames.forEach((hookName) => { + const handler = new Handler(hookName); + handlers[hookName] = handler; + plugins.hooks[hookName] = [makeHook(hookName, handler.handle.bind(handler))]; + }); + settings.requireAuthentication = true; + settings.requireAuthorization = true; + }); + // authn failure tests + it('authn fail, no hooks handle -> 401', async function () { + await agent.get('/').expect(401); + assert(handlers.authnFailure.called); + assert(!handlers.authzFailure.called); + assert(handlers.authFailure.called); + }); + it('authn fail, authnFailure handles', async function () { + handlers.authnFailure.shouldHandle = true; + await agent.get('/').expect(200, 'authnFailure'); + assert(handlers.authnFailure.called); + assert(!handlers.authzFailure.called); + assert(!handlers.authFailure.called); + }); + it('authn fail, authFailure handles', async function () { + handlers.authFailure.shouldHandle = true; + await agent.get('/').expect(200, 'authFailure'); + assert(handlers.authnFailure.called); + assert(!handlers.authzFailure.called); + assert(handlers.authFailure.called); + }); + it('authnFailure trumps authFailure', async function () { + handlers.authnFailure.shouldHandle = true; + handlers.authFailure.shouldHandle = true; + await agent.get('/').expect(200, 'authnFailure'); + assert(handlers.authnFailure.called); + assert(!handlers.authFailure.called); + }); + // authz failure tests + it('authz fail, no hooks handle -> 403', async function () { + 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 () { + handlers.authzFailure.shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); + assert(!handlers.authnFailure.called); + assert(handlers.authzFailure.called); + assert(!handlers.authFailure.called); + }); + it('authz fail, authFailure handles', async function () { + handlers.authFailure.shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure'); + assert(!handlers.authnFailure.called); + assert(handlers.authzFailure.called); + assert(handlers.authFailure.called); + }); + it('authzFailure trumps authFailure', async function () { + handlers.authzFailure.shouldHandle = true; + handlers.authFailure.shouldHandle = true; + await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure'); + assert(handlers.authzFailure.called); + assert(!handlers.authFailure.called); + }); }); - }); }); diff --git a/src/tests/frontend/specs/admintroubleshooting.js b/src/tests/frontend/specs/admintroubleshooting.js old mode 100755 new mode 100644 diff --git a/src/tests/frontend/specs/adminupdateplugins.js b/src/tests/frontend/specs/adminupdateplugins.js old mode 100755 new mode 100644 diff --git a/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js old mode 100755 new mode 100644 diff --git a/src/tests/frontend/specs/scrollTo.js b/src/tests/frontend/specs/scrollTo.js old mode 100755 new mode 100644 diff --git a/src/tests/frontend/travis/adminrunner.sh b/src/tests/frontend/travis/adminrunner.sh old mode 100755 new mode 100644 diff --git a/src/tests/frontend/travis/runner.sh b/src/tests/frontend/travis/runner.sh old mode 100755 new mode 100644 diff --git a/src/tests/frontend/travis/runnerBackend.sh b/src/tests/frontend/travis/runnerBackend.sh old mode 100755 new mode 100644 diff --git a/src/tests/frontend/travis/runnerLoadTest.sh b/src/tests/frontend/travis/runnerLoadTest.sh old mode 100755 new mode 100644 diff --git a/src/tests/ratelimit/testlimits.sh b/src/tests/ratelimit/testlimits.sh old mode 100755 new mode 100644 diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 00000000000..a27cc7201d2 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "allowJs": true, + "typeRoots": ["node_modules/@types"], + "types": ["node", "jquery"], + "module": "esnext", + "esModuleInterop": true, + "moduleResolution": "node", + "sourceMap": true, + "target": "ES2015", + "outDir": "dist" + }, + "exclude": ["node_modules"], + "lib": ["es2015"] +} diff --git a/start.bat b/start.bat index 7e9264ee3b6..1bdbf994302 100644 --- a/start.bat +++ b/start.bat @@ -1,11 +1,11 @@ -@echo off -REM Windows and symlinks do not get along with each other, so on Windows -REM `node_modules\ep_etherpad-lite` is sometimes a full copy of `src` not a -REM symlink to `src`. If it is a copy, Node.js sees `src\foo.js` and -REM `node_modules\ep_etherpad-lite\foo.js` as two independent modules with -REM independent state, when they should be treated as the same file. To work -REM around this, everything must consistently use either `src` or -REM `node_modules\ep_etherpad-lite` on Windows. Because some plugins access -REM Etherpad internals via `require('ep_etherpad-lite/foo')`, -REM `node_modules\ep_etherpad-lite` is used here. -node node_modules\ep_etherpad-lite\node\server.js +@echo off +REM Windows and symlinks do not get along with each other, so on Windows +REM `node_modules\ep_etherpad-lite` is sometimes a full copy of `src` not a +REM symlink to `src`. If it is a copy, Node.js sees `src\foo.js` and +REM `node_modules\ep_etherpad-lite\foo.js` as two independent modules with +REM independent state, when they should be treated as the same file. To work +REM around this, everything must consistently use either `src` or +REM `node_modules\ep_etherpad-lite` on Windows. Because some plugins access +REM Etherpad internals via `require('ep_etherpad-lite/foo')`, +REM `node_modules\ep_etherpad-lite` is used here. +node node_modules\ep_etherpad-lite\node\server.js